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 000000000..4d55011da --- /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 000000000..e7896c55f --- /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 192e0a410..297e1ebfe 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 8f665ba8a..6eeac1862 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) diff --git a/pkg/github/tools.go b/pkg/github/tools.go index 95b36f8b9..408429488 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -245,11 +245,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)), @@ -368,7 +368,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)