Skip to content

Commit 0a76d95

Browse files
feat: wire up GetMe and ForkRepository to use typed inputs
- GetMe: GetMeInput (empty struct for tool with no parameters) - ForkRepository: ForkRepositoryInput Updated tests and toolsnaps for both tools. Co-authored-by: SamMorrowDrums <[email protected]>
1 parent ad64c47 commit 0a76d95

File tree

6 files changed

+69
-106
lines changed

6 files changed

+69
-106
lines changed

pkg/github/__toolsnaps__/fork_repository.snap

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"properties": {
1313
"organization": {
1414
"type": "string",
15-
"description": "Organization to fork to"
15+
"description": "Organization to fork to (defaults to authenticated user)"
1616
},
1717
"owner": {
1818
"type": "string",
@@ -22,7 +22,8 @@
2222
"type": "string",
2323
"description": "Repository name"
2424
}
25-
}
25+
},
26+
"additionalProperties": false
2627
},
2728
"name": "fork_repository"
2829
}

pkg/github/__toolsnaps__/get_me.snap

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
"title": "Get my user profile"
55
},
66
"description": "Get details of the authenticated GitHub user. Use this when a request is about the user's own profile for GitHub. Or when information is missing to build other tool calls.",
7-
"inputSchema": null,
7+
"inputSchema": {
8+
"type": "object",
9+
"additionalProperties": false
10+
},
811
"name": "get_me"
912
}

pkg/github/context_tools.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,17 @@ type UserDetails struct {
3535
}
3636

3737
// GetMe creates a tool to get details of the authenticated user.
38-
func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
38+
func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[GetMeInput, any]) {
3939
return mcp.Tool{
4040
Name: "get_me",
4141
Description: t("TOOL_GET_ME_DESCRIPTION", "Get details of the authenticated GitHub user. Use this when a request is about the user's own profile for GitHub. Or when information is missing to build other tool calls."),
4242
Annotations: &mcp.ToolAnnotations{
4343
Title: t("TOOL_GET_ME_USER_TITLE", "Get my user profile"),
4444
ReadOnlyHint: true,
4545
},
46+
InputSchema: GetMeInput{}.MCPSchema(),
4647
},
47-
mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, _ map[string]any) (*mcp.CallToolResult, any, error) {
48+
mcp.ToolHandlerFor[GetMeInput, any](func(ctx context.Context, _ *mcp.CallToolRequest, _ GetMeInput) (*mcp.CallToolResult, any, error) {
4849
client, err := getClient(ctx)
4950
if err != nil {
5051
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil

pkg/github/context_tools_test.go

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ func Test_GetMe(t *testing.T) {
4848
tests := []struct {
4949
name string
5050
stubbedGetClientFn GetClientFn
51-
requestArgs map[string]any
5251
expectToolError bool
5352
expectedUser *github.User
5453
expectedToolErrMsg string
@@ -63,7 +62,6 @@ func Test_GetMe(t *testing.T) {
6362
),
6463
),
6564
),
66-
requestArgs: map[string]any{},
6765
expectToolError: false,
6866
expectedUser: mockUser,
6967
},
@@ -77,16 +75,12 @@ func Test_GetMe(t *testing.T) {
7775
),
7876
),
7977
),
80-
requestArgs: map[string]any{
81-
"reason": "Testing API",
82-
},
8378
expectToolError: false,
8479
expectedUser: mockUser,
8580
},
8681
{
8782
name: "getting client fails",
8883
stubbedGetClientFn: stubGetClientFnErr("expected test error"),
89-
requestArgs: map[string]any{},
9084
expectToolError: true,
9185
expectedToolErrMsg: "failed to get GitHub client: expected test error",
9286
},
@@ -100,7 +94,6 @@ func Test_GetMe(t *testing.T) {
10094
),
10195
),
10296
),
103-
requestArgs: map[string]any{},
10497
expectToolError: true,
10598
expectedToolErrMsg: "expected test failure",
10699
},
@@ -110,8 +103,8 @@ func Test_GetMe(t *testing.T) {
110103
t.Run(tc.name, func(t *testing.T) {
111104
_, handler := GetMe(tc.stubbedGetClientFn, translations.NullTranslationHelper)
112105

113-
request := createMCPRequest(tc.requestArgs)
114-
result, _, _ := handler(context.Background(), &request, tc.requestArgs)
106+
// Call handler with empty typed input
107+
result, _, _ := handler(context.Background(), nil, GetMeInput{})
115108
textContent := getTextResult(t, result)
116109

117110
if tc.expectToolError {

pkg/github/repositories.go

Lines changed: 45 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -755,95 +755,62 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t
755755
}
756756

757757
// ForkRepository creates a tool to fork a repository.
758-
func ForkRepository(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
759-
tool := mcp.Tool{
760-
Name: "fork_repository",
761-
Description: t("TOOL_FORK_REPOSITORY_DESCRIPTION", "Fork a GitHub repository to your account or specified organization"),
762-
Annotations: &mcp.ToolAnnotations{
763-
Title: t("TOOL_FORK_REPOSITORY_USER_TITLE", "Fork repository"),
764-
ReadOnlyHint: false,
765-
},
766-
InputSchema: &jsonschema.Schema{
767-
Type: "object",
768-
Properties: map[string]*jsonschema.Schema{
769-
"owner": {
770-
Type: "string",
771-
Description: "Repository owner",
772-
},
773-
"repo": {
774-
Type: "string",
775-
Description: "Repository name",
776-
},
777-
"organization": {
778-
Type: "string",
779-
Description: "Organization to fork to",
780-
},
758+
func ForkRepository(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[ForkRepositoryInput, any]) {
759+
return mcp.Tool{
760+
Name: "fork_repository",
761+
Description: t("TOOL_FORK_REPOSITORY_DESCRIPTION", "Fork a GitHub repository to your account or specified organization"),
762+
Annotations: &mcp.ToolAnnotations{
763+
Title: t("TOOL_FORK_REPOSITORY_USER_TITLE", "Fork repository"),
764+
ReadOnlyHint: false,
781765
},
782-
Required: []string{"owner", "repo"},
766+
InputSchema: ForkRepositoryInput{}.MCPSchema(),
783767
},
784-
}
768+
func(ctx context.Context, _ *mcp.CallToolRequest, input ForkRepositoryInput) (*mcp.CallToolResult, any, error) {
769+
opts := &github.RepositoryCreateForkOptions{}
770+
if input.Organization != "" {
771+
opts.Organization = input.Organization
772+
}
785773

786-
handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
787-
owner, err := RequiredParam[string](args, "owner")
788-
if err != nil {
789-
return utils.NewToolResultError(err.Error()), nil, nil
790-
}
791-
repo, err := RequiredParam[string](args, "repo")
792-
if err != nil {
793-
return utils.NewToolResultError(err.Error()), nil, nil
794-
}
795-
org, err := OptionalParam[string](args, "organization")
796-
if err != nil {
797-
return utils.NewToolResultError(err.Error()), nil, nil
798-
}
774+
client, err := getClient(ctx)
775+
if err != nil {
776+
return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
777+
}
778+
forkedRepo, resp, err := client.Repositories.CreateFork(ctx, input.Owner, input.Repo, opts)
779+
if err != nil {
780+
// Check if it's an acceptedError. An acceptedError indicates that the update is in progress,
781+
// and it's not a real error.
782+
if resp != nil && resp.StatusCode == http.StatusAccepted && isAcceptedError(err) {
783+
return utils.NewToolResultText("Fork is in progress"), nil, nil
784+
}
785+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
786+
"failed to fork repository",
787+
resp,
788+
err,
789+
), nil, nil
790+
}
791+
defer func() { _ = resp.Body.Close() }()
799792

800-
opts := &github.RepositoryCreateForkOptions{}
801-
if org != "" {
802-
opts.Organization = org
803-
}
793+
if resp.StatusCode != http.StatusAccepted {
794+
body, err := io.ReadAll(resp.Body)
795+
if err != nil {
796+
return nil, nil, fmt.Errorf("failed to read response body: %w", err)
797+
}
798+
return utils.NewToolResultError(fmt.Sprintf("failed to fork repository: %s", string(body))), nil, nil
799+
}
804800

805-
client, err := getClient(ctx)
806-
if err != nil {
807-
return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
808-
}
809-
forkedRepo, resp, err := client.Repositories.CreateFork(ctx, owner, repo, opts)
810-
if err != nil {
811-
// Check if it's an acceptedError. An acceptedError indicates that the update is in progress,
812-
// and it's not a real error.
813-
if resp != nil && resp.StatusCode == http.StatusAccepted && isAcceptedError(err) {
814-
return utils.NewToolResultText("Fork is in progress"), nil, nil
801+
// Return minimal response with just essential information
802+
minimalResponse := MinimalResponse{
803+
ID: fmt.Sprintf("%d", forkedRepo.GetID()),
804+
URL: forkedRepo.GetHTMLURL(),
815805
}
816-
return ghErrors.NewGitHubAPIErrorResponse(ctx,
817-
"failed to fork repository",
818-
resp,
819-
err,
820-
), nil, nil
821-
}
822-
defer func() { _ = resp.Body.Close() }()
823806

824-
if resp.StatusCode != http.StatusAccepted {
825-
body, err := io.ReadAll(resp.Body)
807+
r, err := json.Marshal(minimalResponse)
826808
if err != nil {
827-
return nil, nil, fmt.Errorf("failed to read response body: %w", err)
809+
return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
828810
}
829-
return utils.NewToolResultError(fmt.Sprintf("failed to fork repository: %s", string(body))), nil, nil
830-
}
831-
832-
// Return minimal response with just essential information
833-
minimalResponse := MinimalResponse{
834-
ID: fmt.Sprintf("%d", forkedRepo.GetID()),
835-
URL: forkedRepo.GetHTMLURL(),
836-
}
837811

838-
r, err := json.Marshal(minimalResponse)
839-
if err != nil {
840-
return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
812+
return utils.NewToolResultText(string(r)), nil, nil
841813
}
842-
843-
return utils.NewToolResultText(string(r)), nil, nil
844-
})
845-
846-
return tool, handler
847814
}
848815

849816
// DeleteFile creates a tool to delete a file in a GitHub repository.

pkg/github/repositories_test.go

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -335,8 +335,9 @@ func Test_ForkRepository(t *testing.T) {
335335
tool, _ := ForkRepository(stubGetClientFn(mockClient), translations.NullTranslationHelper)
336336
require.NoError(t, toolsnaps.Test(tool.Name, tool))
337337

338-
schema, ok := tool.InputSchema.(*jsonschema.Schema)
339-
require.True(t, ok, "InputSchema should be *jsonschema.Schema")
338+
// Get schema from the input type's MCPSchema() method
339+
schema := ForkRepositoryInput{}.MCPSchema()
340+
require.NotNil(t, schema, "MCPSchema should return a schema")
340341

341342
assert.Equal(t, "fork_repository", tool.Name)
342343
assert.NotEmpty(t, tool.Description)
@@ -362,7 +363,7 @@ func Test_ForkRepository(t *testing.T) {
362363
tests := []struct {
363364
name string
364365
mockedClient *http.Client
365-
requestArgs map[string]interface{}
366+
input ForkRepositoryInput
366367
expectError bool
367368
expectedRepo *github.Repository
368369
expectedErrMsg string
@@ -375,9 +376,9 @@ func Test_ForkRepository(t *testing.T) {
375376
mockResponse(t, http.StatusAccepted, mockForkedRepo),
376377
),
377378
),
378-
requestArgs: map[string]interface{}{
379-
"owner": "owner",
380-
"repo": "repo",
379+
input: ForkRepositoryInput{
380+
Owner: "owner",
381+
Repo: "repo",
381382
},
382383
expectError: false,
383384
expectedRepo: mockForkedRepo,
@@ -393,9 +394,9 @@ func Test_ForkRepository(t *testing.T) {
393394
}),
394395
),
395396
),
396-
requestArgs: map[string]interface{}{
397-
"owner": "owner",
398-
"repo": "repo",
397+
input: ForkRepositoryInput{
398+
Owner: "owner",
399+
Repo: "repo",
399400
},
400401
expectError: true,
401402
expectedErrMsg: "failed to fork repository",
@@ -408,11 +409,8 @@ func Test_ForkRepository(t *testing.T) {
408409
client := github.NewClient(tc.mockedClient)
409410
_, handler := ForkRepository(stubGetClientFn(client), translations.NullTranslationHelper)
410411

411-
// Create call request
412-
request := createMCPRequest(tc.requestArgs)
413-
414-
// Call handler
415-
result, _, err := handler(context.Background(), &request, tc.requestArgs)
412+
// Call handler with typed input
413+
result, _, err := handler(context.Background(), nil, tc.input)
416414

417415
// Verify results
418416
if tc.expectError {

0 commit comments

Comments
 (0)