From 952172d4b3acc5a18b1efe1e44dde88a18b08e21 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 11:17:54 +0000 Subject: [PATCH 1/8] Initial plan From 3985dc0fdaac1b14bf162eafe545bd13e1e515d9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 11:28:12 +0000 Subject: [PATCH 2/8] Migrate secret_scanning toolset to modelcontextprotocol/go-sdk Co-authored-by: omgitsads <4619+omgitsads@users.noreply.github.com> --- .../get_secret_scanning_alert.snap | 30 +++ .../list_secret_scanning_alerts.snap | 49 +++++ pkg/github/secret_scanning.go | 176 ++++++++++-------- pkg/github/secret_scanning_test.go | 40 ++-- 4 files changed, 200 insertions(+), 95 deletions(-) create mode 100644 pkg/github/__toolsnaps__/get_secret_scanning_alert.snap create mode 100644 pkg/github/__toolsnaps__/list_secret_scanning_alerts.snap diff --git a/pkg/github/__toolsnaps__/get_secret_scanning_alert.snap b/pkg/github/__toolsnaps__/get_secret_scanning_alert.snap new file mode 100644 index 0000000000..4d55011da3 --- /dev/null +++ b/pkg/github/__toolsnaps__/get_secret_scanning_alert.snap @@ -0,0 +1,30 @@ +{ + "annotations": { + "readOnlyHint": true, + "title": "Get secret scanning alert" + }, + "description": "Get details of a specific secret scanning alert in a GitHub repository.", + "inputSchema": { + "type": "object", + "required": [ + "owner", + "repo", + "alertNumber" + ], + "properties": { + "alertNumber": { + "type": "number", + "description": "The number of the alert." + }, + "owner": { + "type": "string", + "description": "The owner of the repository." + }, + "repo": { + "type": "string", + "description": "The name of the repository." + } + } + }, + "name": "get_secret_scanning_alert" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/list_secret_scanning_alerts.snap b/pkg/github/__toolsnaps__/list_secret_scanning_alerts.snap new file mode 100644 index 0000000000..e7896c55f2 --- /dev/null +++ b/pkg/github/__toolsnaps__/list_secret_scanning_alerts.snap @@ -0,0 +1,49 @@ +{ + "annotations": { + "readOnlyHint": true, + "title": "List secret scanning alerts" + }, + "description": "List secret scanning alerts in a GitHub repository.", + "inputSchema": { + "type": "object", + "required": [ + "owner", + "repo" + ], + "properties": { + "owner": { + "type": "string", + "description": "The owner of the repository." + }, + "repo": { + "type": "string", + "description": "The name of the repository." + }, + "resolution": { + "type": "string", + "description": "Filter by resolution", + "enum": [ + "false_positive", + "wont_fix", + "revoked", + "pattern_edited", + "pattern_deleted", + "used_in_tests" + ] + }, + "secret_type": { + "type": "string", + "description": "A comma-separated list of secret types to return. All default secret patterns are returned. To return generic patterns, pass the token name(s) in the parameter." + }, + "state": { + "type": "string", + "description": "Filter by state", + "enum": [ + "open", + "resolved" + ] + } + } + }, + "name": "list_secret_scanning_alerts" +} \ No newline at end of file diff --git a/pkg/github/secret_scanning.go b/pkg/github/secret_scanning.go index 192e0a4100..297e1ebfea 100644 --- a/pkg/github/secret_scanning.go +++ b/pkg/github/secret_scanning.go @@ -1,5 +1,3 @@ -//go:build ignore - package github import ( @@ -11,49 +9,56 @@ import ( ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/translations" + "github.com/github/github-mcp-server/pkg/utils" "github.com/google/go-github/v79/github" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" ) -func GetSecretScanningAlert(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { - return mcp.NewTool( - "get_secret_scanning_alert", - mcp.WithDescription(t("TOOL_GET_SECRET_SCANNING_ALERT_DESCRIPTION", "Get details of a specific secret scanning alert in a GitHub repository.")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ +func GetSecretScanningAlert(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + return mcp.Tool{ + Name: "get_secret_scanning_alert", + Description: t("TOOL_GET_SECRET_SCANNING_ALERT_DESCRIPTION", "Get details of a specific secret scanning alert in a GitHub repository."), + Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_SECRET_SCANNING_ALERT_USER_TITLE", "Get secret scanning alert"), - ReadOnlyHint: ToBoolPtr(true), - }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("The owner of the repository."), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("The name of the repository."), - ), - mcp.WithNumber("alertNumber", - mcp.Required(), - mcp.Description("The number of the alert."), - ), - ), - func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := RequiredParam[string](request, "owner") + ReadOnlyHint: true, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "The owner of the repository.", + }, + "repo": { + Type: "string", + Description: "The name of the repository.", + }, + "alertNumber": { + Type: "number", + Description: "The number of the alert.", + }, + }, + Required: []string{"owner", "repo", "alertNumber"}, + }, + }, + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - repo, err := RequiredParam[string](request, "repo") + repo, err := RequiredParam[string](args, "repo") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - alertNumber, err := RequiredInt(request, "alertNumber") + alertNumber, err := RequiredInt(args, "alertNumber") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) } alert, resp, err := client.SecretScanning.GetAlert(ctx, owner, repo, int64(alertNumber)) @@ -62,80 +67,89 @@ func GetSecretScanningAlert(getClient GetClientFn, t translations.TranslationHel fmt.Sprintf("failed to get alert with number '%d'", alertNumber), resp, err, - ), nil + ), nil, nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return mcp.NewToolResultError(fmt.Sprintf("failed to get alert: %s", string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to get alert: %s", string(body))), nil, nil } r, err := json.Marshal(alert) if err != nil { - return nil, fmt.Errorf("failed to marshal alert: %w", err) + return nil, nil, fmt.Errorf("failed to marshal alert: %w", err) } - return mcp.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil, nil } } -func ListSecretScanningAlerts(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { - return mcp.NewTool( - "list_secret_scanning_alerts", - mcp.WithDescription(t("TOOL_LIST_SECRET_SCANNING_ALERTS_DESCRIPTION", "List secret scanning alerts in a GitHub repository.")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ +func ListSecretScanningAlerts(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + return mcp.Tool{ + Name: "list_secret_scanning_alerts", + Description: t("TOOL_LIST_SECRET_SCANNING_ALERTS_DESCRIPTION", "List secret scanning alerts in a GitHub repository."), + Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_SECRET_SCANNING_ALERTS_USER_TITLE", "List secret scanning alerts"), - ReadOnlyHint: ToBoolPtr(true), - }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("The owner of the repository."), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("The name of the repository."), - ), - mcp.WithString("state", - mcp.Description("Filter by state"), - mcp.Enum("open", "resolved"), - ), - mcp.WithString("secret_type", - mcp.Description("A comma-separated list of secret types to return. All default secret patterns are returned. To return generic patterns, pass the token name(s) in the parameter."), - ), - mcp.WithString("resolution", - mcp.Description("Filter by resolution"), - mcp.Enum("false_positive", "wont_fix", "revoked", "pattern_edited", "pattern_deleted", "used_in_tests"), - ), - ), - func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := RequiredParam[string](request, "owner") + ReadOnlyHint: true, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "The owner of the repository.", + }, + "repo": { + Type: "string", + Description: "The name of the repository.", + }, + "state": { + Type: "string", + Description: "Filter by state", + Enum: []any{"open", "resolved"}, + }, + "secret_type": { + Type: "string", + Description: "A comma-separated list of secret types to return. All default secret patterns are returned. To return generic patterns, pass the token name(s) in the parameter.", + }, + "resolution": { + Type: "string", + Description: "Filter by resolution", + Enum: []any{"false_positive", "wont_fix", "revoked", "pattern_edited", "pattern_deleted", "used_in_tests"}, + }, + }, + Required: []string{"owner", "repo"}, + }, + }, + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - repo, err := RequiredParam[string](request, "repo") + repo, err := RequiredParam[string](args, "repo") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - state, err := OptionalParam[string](request, "state") + state, err := OptionalParam[string](args, "state") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - secretType, err := OptionalParam[string](request, "secret_type") + secretType, err := OptionalParam[string](args, "secret_type") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - resolution, err := OptionalParam[string](request, "resolution") + resolution, err := OptionalParam[string](args, "resolution") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) } alerts, resp, err := client.SecretScanning.ListAlertsForRepo(ctx, owner, repo, &github.SecretScanningAlertListOptions{State: state, SecretType: secretType, Resolution: resolution}) if err != nil { @@ -143,23 +157,23 @@ func ListSecretScanningAlerts(getClient GetClientFn, t translations.TranslationH fmt.Sprintf("failed to list alerts for repository '%s/%s'", owner, repo), resp, err, - ), nil + ), nil, nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return mcp.NewToolResultError(fmt.Sprintf("failed to list alerts: %s", string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to list alerts: %s", string(body))), nil, nil } r, err := json.Marshal(alerts) if err != nil { - return nil, fmt.Errorf("failed to marshal alerts: %w", err) + return nil, nil, fmt.Errorf("failed to marshal alerts: %w", err) } - return mcp.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil, nil } } diff --git a/pkg/github/secret_scanning_test.go b/pkg/github/secret_scanning_test.go index 8f665ba8a3..6eeac1862c 100644 --- a/pkg/github/secret_scanning_test.go +++ b/pkg/github/secret_scanning_test.go @@ -1,5 +1,3 @@ -//go:build ignore - package github import ( @@ -8,8 +6,10 @@ import ( "net/http" "testing" + "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v79/github" + "github.com/google/jsonschema-go/jsonschema" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -19,12 +19,18 @@ func Test_GetSecretScanningAlert(t *testing.T) { mockClient := github.NewClient(nil) tool, _ := GetSecretScanningAlert(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) + assert.Equal(t, "get_secret_scanning_alert", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "repo") - assert.Contains(t, tool.InputSchema.Properties, "alertNumber") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "alertNumber"}) + + // Verify InputSchema structure + schema, ok := tool.InputSchema.(*jsonschema.Schema) + require.True(t, ok, "InputSchema should be *jsonschema.Schema") + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "repo") + assert.Contains(t, schema.Properties, "alertNumber") + assert.ElementsMatch(t, schema.Required, []string{"owner", "repo", "alertNumber"}) // Setup mock alert for success case mockAlert := &github.SecretScanningAlert{ @@ -88,7 +94,7 @@ func Test_GetSecretScanningAlert(t *testing.T) { request := createMCPRequest(tc.requestArgs) // Call handler - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) // Verify results if tc.expectError { @@ -122,14 +128,20 @@ func Test_ListSecretScanningAlerts(t *testing.T) { mockClient := github.NewClient(nil) tool, _ := ListSecretScanningAlerts(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) + assert.Equal(t, "list_secret_scanning_alerts", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "repo") - assert.Contains(t, tool.InputSchema.Properties, "state") - assert.Contains(t, tool.InputSchema.Properties, "secret_type") - assert.Contains(t, tool.InputSchema.Properties, "resolution") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"}) + + // Verify InputSchema structure + schema, ok := tool.InputSchema.(*jsonschema.Schema) + require.True(t, ok, "InputSchema should be *jsonschema.Schema") + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "repo") + assert.Contains(t, schema.Properties, "state") + assert.Contains(t, schema.Properties, "secret_type") + assert.Contains(t, schema.Properties, "resolution") + assert.ElementsMatch(t, schema.Required, []string{"owner", "repo"}) // Setup mock alerts for success case resolvedAlert := github.SecretScanningAlert{ @@ -219,7 +231,7 @@ func Test_ListSecretScanningAlerts(t *testing.T) { request := createMCPRequest(tc.requestArgs) - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) if tc.expectError { require.NoError(t, err) From 0c9e6bf03f71eeefcc7448c191bda12fdfc2663e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 11:30:58 +0000 Subject: [PATCH 3/8] Enable secret_protection toolset in DefaultToolsetGroup Co-authored-by: omgitsads <4619+omgitsads@users.noreply.github.com> --- pkg/github/tools.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/github/tools.go b/pkg/github/tools.go index c024a31e9f..31dceb8d24 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -244,11 +244,11 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG // toolsets.NewServerTool(GetCodeScanningAlert(getClient, t)), // toolsets.NewServerTool(ListCodeScanningAlerts(getClient, t)), // ) - // secretProtection := toolsets.NewToolset(ToolsetMetadataSecretProtection.ID, ToolsetMetadataSecretProtection.Description). - // AddReadTools( - // toolsets.NewServerTool(GetSecretScanningAlert(getClient, t)), - // toolsets.NewServerTool(ListSecretScanningAlerts(getClient, t)), - // ) + secretProtection := toolsets.NewToolset(ToolsetMetadataSecretProtection.ID, ToolsetMetadataSecretProtection.Description). + AddReadTools( + toolsets.NewServerTool(GetSecretScanningAlert(getClient, t)), + toolsets.NewServerTool(ListSecretScanningAlerts(getClient, t)), + ) // dependabot := toolsets.NewToolset(ToolsetMetadataDependabot.ID, ToolsetMetadataDependabot.Description). // AddReadTools( // toolsets.NewServerTool(GetDependabotAlert(getClient, t)), @@ -367,7 +367,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG // tsg.AddToolset(pullRequests) // tsg.AddToolset(actions) // tsg.AddToolset(codeSecurity) - // tsg.AddToolset(secretProtection) + tsg.AddToolset(secretProtection) // tsg.AddToolset(dependabot) // tsg.AddToolset(notifications) // tsg.AddToolset(experiments) From 7a24f373b0976af49b7de7f9ea8420c0a6b51967 Mon Sep 17 00:00:00 2001 From: Adam Holt Date: Tue, 18 Nov 2025 14:36:39 +0100 Subject: [PATCH 4/8] Don't assert without a testing.T --- pkg/github/helper_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/github/helper_test.go b/pkg/github/helper_test.go index 1e46275440..8a65568e0c 100644 --- a/pkg/github/helper_test.go +++ b/pkg/github/helper_test.go @@ -117,7 +117,9 @@ func createMCPRequest(args any) mcp.CallToolRequest { } argsJSON, err := json.Marshal(argsMap) - require.NoError(nil, err) + if err != nil { + return mcp.CallToolRequest{} + } jsonRawMessage := json.RawMessage(argsJSON) From 2c4d718ad19114f9ab77c66581cc6d57ac5ea51b Mon Sep 17 00:00:00 2001 From: Adam Holt Date: Tue, 18 Nov 2025 14:38:12 +0100 Subject: [PATCH 5/8] just return the tool & handler --- pkg/github/context_tools.go | 99 ++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 51 deletions(-) diff --git a/pkg/github/context_tools.go b/pkg/github/context_tools.go index 5f248934b2..a66902ef24 100644 --- a/pkg/github/context_tools.go +++ b/pkg/github/context_tools.go @@ -36,61 +36,58 @@ type UserDetails struct { // GetMe creates a tool to get details of the authenticated user. func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - tool := mcp.Tool{ - Name: "get_me", - 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."), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_GET_ME_USER_TITLE", "Get my user profile"), - ReadOnlyHint: true, + return mcp.Tool{ + Name: "get_me", + 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."), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_GET_ME_USER_TITLE", "Get my user profile"), + ReadOnlyHint: true, + }, }, - } - - handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, _ map[string]any) (*mcp.CallToolResult, any, error) { - client, err := getClient(ctx) - if err != nil { - return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, err - } - - user, res, err := client.Users.Get(ctx, "") - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to get user", - res, - err, - ), nil, err - } + mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, _ map[string]any) (*mcp.CallToolResult, any, error) { + client, err := getClient(ctx) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, err + } - // Create minimal user representation instead of returning full user object - minimalUser := MinimalUser{ - Login: user.GetLogin(), - ID: user.GetID(), - ProfileURL: user.GetHTMLURL(), - AvatarURL: user.GetAvatarURL(), - Details: &UserDetails{ - Name: user.GetName(), - Company: user.GetCompany(), - Blog: user.GetBlog(), - Location: user.GetLocation(), - Email: user.GetEmail(), - Hireable: user.GetHireable(), - Bio: user.GetBio(), - TwitterUsername: user.GetTwitterUsername(), - PublicRepos: user.GetPublicRepos(), - PublicGists: user.GetPublicGists(), - Followers: user.GetFollowers(), - Following: user.GetFollowing(), - CreatedAt: user.GetCreatedAt().Time, - UpdatedAt: user.GetUpdatedAt().Time, - PrivateGists: user.GetPrivateGists(), - TotalPrivateRepos: user.GetTotalPrivateRepos(), - OwnedPrivateRepos: user.GetOwnedPrivateRepos(), - }, - } + user, res, err := client.Users.Get(ctx, "") + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to get user", + res, + err, + ), nil, err + } - return MarshalledTextResult(minimalUser), nil, nil - }) + // Create minimal user representation instead of returning full user object + minimalUser := MinimalUser{ + Login: user.GetLogin(), + ID: user.GetID(), + ProfileURL: user.GetHTMLURL(), + AvatarURL: user.GetAvatarURL(), + Details: &UserDetails{ + Name: user.GetName(), + Company: user.GetCompany(), + Blog: user.GetBlog(), + Location: user.GetLocation(), + Email: user.GetEmail(), + Hireable: user.GetHireable(), + Bio: user.GetBio(), + TwitterUsername: user.GetTwitterUsername(), + PublicRepos: user.GetPublicRepos(), + PublicGists: user.GetPublicGists(), + Followers: user.GetFollowers(), + Following: user.GetFollowing(), + CreatedAt: user.GetCreatedAt().Time, + UpdatedAt: user.GetUpdatedAt().Time, + PrivateGists: user.GetPrivateGists(), + TotalPrivateRepos: user.GetTotalPrivateRepos(), + OwnedPrivateRepos: user.GetOwnedPrivateRepos(), + }, + } - return tool, handler + return MarshalledTextResult(minimalUser), nil, nil + }) } type TeamInfo struct { From 9fef216f414da6adf118d08bf7d0d15237f94c88 Mon Sep 17 00:00:00 2001 From: Adam Holt Date: Tue, 18 Nov 2025 14:40:39 +0100 Subject: [PATCH 6/8] use lowercase strings for the jsonschema types --- pkg/github/server.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/github/server.go b/pkg/github/server.go index f474d06b4e..270fe93384 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -268,13 +268,13 @@ func OptionalBigIntArrayParam(args map[string]any, p string) ([]int64, error) { // https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api func WithPagination(schema *jsonschema.Schema) *jsonschema.Schema { schema.Properties["page"] = &jsonschema.Schema{ - Type: "Number", + Type: "number", Description: "Page number for pagination (min 1)", Minimum: jsonschema.Ptr(1.0), } schema.Properties["perPage"] = &jsonschema.Schema{ - Type: "Number", + Type: "number", Description: "Results per page for pagination (min 1, max 100)", Minimum: jsonschema.Ptr(1.0), Maximum: jsonschema.Ptr(100.0), @@ -287,20 +287,20 @@ func WithPagination(schema *jsonschema.Schema) *jsonschema.Schema { // GraphQL tools will use this and convert page/perPage to GraphQL cursor parameters internally. func WithUnifiedPagination(schema *jsonschema.Schema) *jsonschema.Schema { schema.Properties["page"] = &jsonschema.Schema{ - Type: "Number", + Type: "number", Description: "Page number for pagination (min 1)", Minimum: jsonschema.Ptr(1.0), } schema.Properties["perPage"] = &jsonschema.Schema{ - Type: "Number", + Type: "number", Description: "Results per page for pagination (min 1, max 100)", Minimum: jsonschema.Ptr(1.0), Maximum: jsonschema.Ptr(100.0), } schema.Properties["after"] = &jsonschema.Schema{ - Type: "String", + Type: "string", Description: "Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs.", } @@ -310,14 +310,14 @@ func WithUnifiedPagination(schema *jsonschema.Schema) *jsonschema.Schema { // WithCursorPagination adds only cursor-based pagination parameters to a tool (no page parameter). func WithCursorPagination(schema *jsonschema.Schema) *jsonschema.Schema { schema.Properties["perPage"] = &jsonschema.Schema{ - Type: "Number", + Type: "number", Description: "Results per page for pagination (min 1, max 100)", Minimum: jsonschema.Ptr(1.0), Maximum: jsonschema.Ptr(100.0), } schema.Properties["after"] = &jsonschema.Schema{ - Type: "String", + Type: "string", Description: "Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs.", } From dc151cf7e832a570ccca49061f18c183020857df Mon Sep 17 00:00:00 2001 From: Adam Holt Date: Tue, 18 Nov 2025 14:52:57 +0100 Subject: [PATCH 7/8] Add Close method to IOLogger to close underlying reader and writer --- pkg/log/io.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pkg/log/io.go b/pkg/log/io.go index 0f034c2a45..deaf4b7eae 100644 --- a/pkg/log/io.go +++ b/pkg/log/io.go @@ -45,3 +45,17 @@ func (l *IOLogger) Write(p []byte) (n int, err error) { l.logger.Info("[stdout]: sending bytes", "count", len(p), "data", string(p)) return l.writer.Write(p) } + +func (l *IOLogger) Close() error { + var errReader, errWriter error + if closer, ok := l.reader.(io.Closer); ok { + errReader = closer.Close() + } + if closer, ok := l.writer.(io.Closer); ok { + errWriter = closer.Close() + } + if errReader != nil { + return errReader + } + return errWriter +} From 2d2b626f9143805a778ce1622d81663dbb995550 Mon Sep 17 00:00:00 2001 From: Adam Holt Date: Tue, 18 Nov 2025 14:42:28 +0100 Subject: [PATCH 8/8] Update cmd/github-mcp-server/generate_docs.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cmd/github-mcp-server/generate_docs.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cmd/github-mcp-server/generate_docs.go b/cmd/github-mcp-server/generate_docs.go index 546bd716b5..d7f87521a3 100644 --- a/cmd/github-mcp-server/generate_docs.go +++ b/cmd/github-mcp-server/generate_docs.go @@ -225,8 +225,12 @@ func generateToolDoc(tool mcp.Tool) string { lines = append(lines, fmt.Sprintf("- **%s** - %s", tool.Name, tool.Annotations.Title)) // Parameters + if tool.InputSchema == nil { + lines = append(lines, " - No parameters required") + return strings.Join(lines, "\n") + } schema, ok := tool.InputSchema.(*jsonschema.Schema) - if !ok { + if !ok || schema == nil { lines = append(lines, " - No parameters required") return strings.Join(lines, "\n") }