From f14f0b3964c54fceb984e70d03b3d565ead7d31a Mon Sep 17 00:00:00 2001 From: tonytrg Date: Tue, 15 Apr 2025 14:08:14 +0200 Subject: [PATCH 1/7] Adding GetSecretScanningAlert and ListSecretScanningAlerts --- .gitignore | 1 + pkg/github/secret_scanning.go | 162 +++++++++++++++++++++++++++++ pkg/github/secret_scanning_test.go | 1 + 3 files changed, 164 insertions(+) create mode 100644 pkg/github/secret_scanning.go create mode 100644 pkg/github/secret_scanning_test.go diff --git a/.gitignore b/.gitignore index 9fb1dca9..4f5ec6ac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .idea cmd/github-mcp-server/github-mcp-server +.vscode/mcp.json # Added by goreleaser init: dist/ __debug_bin* \ No newline at end of file diff --git a/pkg/github/secret_scanning.go b/pkg/github/secret_scanning.go new file mode 100644 index 00000000..cb202d95 --- /dev/null +++ b/pkg/github/secret_scanning.go @@ -0,0 +1,162 @@ +package github + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/github/github-mcp-server/pkg/translations" + "github.com/google/go-github/v69/github" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +func GetSecretScanningAlert(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool( + "get_secret_scanning_alert", + 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") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + repo, err := requiredParam[string](request, "repo") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + alertNumber, err := RequiredInt(request, "alertNumber") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + alert, resp, err := client.SecretScanning.GetAlert(ctx, owner, repo, int64(alertNumber)) + if err != nil { + return nil, fmt.Errorf("failed to get alert: %w", err) + } + 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 mcp.NewToolResultError(fmt.Sprintf("failed to get alert: %s", string(body))), nil + } + + r, err := json.Marshal(alert) + if err != nil { + return nil, fmt.Errorf("failed to marshal alert: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + +func ListSecretScanningAlerts(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool( + "list_secret_scanning_alerts", + 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 code scanning alerts by state ('open', 'resolved')"), + 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 code scanning alerts by resolution ('false_positive', 'wont_fix', 'revoked', 'pattern_edited', 'pattern_deleted', 'used_in_tests')"), + mcp.Enum("false_positive", "wont_fix", "revoked", "pattern_edited", "pattern_deleted", "used_in_tests"), + ), + mcp.WithString("sort", + mcp.Description("Filter code scanning alerts by sort ('created', 'updated') Default: created"), + mcp.DefaultString("created"), + mcp.Enum("created", "updated"), + ), + mcp.WithString("direction", + mcp.Description("Filter code scanning alerts by direction ('desc', 'asc') Default: desc"), + mcp.DefaultString("desc"), + mcp.Enum("desc", "asc"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + owner, err := requiredParam[string](request, "owner") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + repo, err := requiredParam[string](request, "repo") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + state, err := OptionalParam[string](request, "state") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + secretType, err := OptionalParam[string](request, "secret_type") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + resolution, err := OptionalParam[string](request, "resolution") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + sort, err := OptionalParam[string](request, "sort") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + direction, err := OptionalParam[string](request, "direction") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + client, err := getClient(ctx) + if err != nil { + return 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, Sort: sort, Direction: direction}) + if err != nil { + return nil, fmt.Errorf("failed to list alerts: %w", err) + } + 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 mcp.NewToolResultError(fmt.Sprintf("failed to list alerts: %s", string(body))), nil + } + + r, err := json.Marshal(alerts) + if err != nil { + return nil, fmt.Errorf("failed to marshal alerts: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} diff --git a/pkg/github/secret_scanning_test.go b/pkg/github/secret_scanning_test.go new file mode 100644 index 00000000..d2e73c26 --- /dev/null +++ b/pkg/github/secret_scanning_test.go @@ -0,0 +1 @@ +package github From 5dbfaa8e42352c98f5dd3df7ed9e77451a540856 Mon Sep 17 00:00:00 2001 From: tonytrg Date: Tue, 15 Apr 2025 15:33:57 +0200 Subject: [PATCH 2/7] adding wip for tests --- .gitignore | 8 +- pkg/github/secret_scanning.go | 2 + pkg/github/secret_scanning_test.go | 232 +++++++++++++++++++++++++++++ pkg/github/tools.go | 6 + 4 files changed, 247 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4f5ec6ac..a9550aa8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,12 @@ .idea cmd/github-mcp-server/github-mcp-server + +# VSCode .vscode/mcp.json + # Added by goreleaser init: dist/ -__debug_bin* \ No newline at end of file +__debug_bin* + +# Go +vendor \ No newline at end of file diff --git a/pkg/github/secret_scanning.go b/pkg/github/secret_scanning.go index cb202d95..859c80f5 100644 --- a/pkg/github/secret_scanning.go +++ b/pkg/github/secret_scanning.go @@ -16,6 +16,7 @@ import ( 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.WithString("owner", mcp.Required(), mcp.Description("The owner of the repository."), @@ -74,6 +75,7 @@ func GetSecretScanningAlert(getClient GetClientFn, t translations.TranslationHel 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.WithString("owner", mcp.Required(), mcp.Description("The owner of the repository."), diff --git a/pkg/github/secret_scanning_test.go b/pkg/github/secret_scanning_test.go index d2e73c26..044f1612 100644 --- a/pkg/github/secret_scanning_test.go +++ b/pkg/github/secret_scanning_test.go @@ -1 +1,233 @@ package github + +import ( + "context" + "encoding/json" + "net/http" + "testing" + + "github.com/github/github-mcp-server/pkg/translations" + "github.com/google/go-github/v69/github" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_GetSecretScanningAlert(t *testing.T) { + mockClient := github.NewClient(nil) + tool, _ := GetSecretScanningAlert(stubGetClientFn(mockClient), translations.NullTranslationHelper) + + 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"}) + + // Setup mock alert for success case + mockAlert := &github.SecretScanningAlert{ + Number: github.Ptr(42), + State: github.Ptr("open"), + HTMLURL: github.Ptr("https://github.com/owner/private-repo/security/secret-scanning/42"), + } + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedAlert *github.SecretScanningAlert + expectedErrMsg string + }{ + { + name: "successful alert fetch", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetReposSecretScanningAlertsByOwnerByRepoByAlertNumber, + mockAlert, + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "alertNumber": float64(42), + }, + expectError: false, + expectedAlert: mockAlert, + }, + { + name: "alert fetch fails", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposSecretScanningAlertsByOwnerByRepoByAlertNumber, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message": "Not Found"}`)) + }), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "alertNumber": float64(9999), + }, + expectError: true, + expectedErrMsg: "failed to get alert", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := GetSecretScanningAlert(stubGetClientFn(client), translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + // Verify results + if tc.expectError { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedErrMsg) + return + } + + require.NoError(t, err) + + // Parse the result and get the text content if no error + textContent := getTextResult(t, result) + + // Unmarshal and verify the result + var returnedAlert github.Alert + err = json.Unmarshal([]byte(textContent.Text), &returnedAlert) + assert.NoError(t, err) + assert.Equal(t, *tc.expectedAlert.Number, *returnedAlert.Number) + assert.Equal(t, *tc.expectedAlert.State, *returnedAlert.State) + assert.Equal(t, *tc.expectedAlert.HTMLURL, *returnedAlert.HTMLURL) + + }) + } +} + +func Test_ListSecretScanningAlerts(t *testing.T) { + // Verify tool definition once + mockClient := github.NewClient(nil) + tool, _ := ListSecretScanningAlerts(stubGetClientFn(mockClient), translations.NullTranslationHelper) + + 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.Contains(t, tool.InputSchema.Properties, "sort") + assert.Contains(t, tool.InputSchema.Properties, "direction") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"}) + + // Setup mock alerts for success case + mockAlerts := []*github.SecretScanningAlert{ + { + Number: github.Ptr(42), + State: github.Ptr("open"), + HTMLURL: github.Ptr("https://github.com/owner/repo/security/code-scanning/42"), + }, + { + Number: github.Ptr(43), + State: github.Ptr("fixed"), + HTMLURL: github.Ptr("https://github.com/owner/repo/security/code-scanning/43"), + }, + } + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedAlerts []*github.SecretScanningAlert + expectedErrMsg string + }{ + { + name: "successful alerts listing", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposSecretScanningAlertsByOwnerByRepoByAlertNumber, + expectQueryParams(t, map[string]string{ + "ref": "main", + "state": "open", + "severity": "high", + }).andThen( + mockResponse(t, http.StatusOK, mockAlerts), + ), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "ref": "main", + "state": "open", + "severity": "high", + }, + expectError: false, + expectedAlerts: mockAlerts, + }, + { + name: "alerts listing fails", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposSecretScanningAlertsByOwnerByRepo, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"message": "Unauthorized access"}`)) + }), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + }, + expectError: true, + expectedErrMsg: "failed to list alerts", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := ListSecretScanningAlerts(stubGetClientFn(client), translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + // Verify results + if tc.expectError { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedErrMsg) + return + } + + require.NoError(t, err) + + // Parse the result and get the text content if no error + textContent := getTextResult(t, result) + + // Unmarshal and verify the result + var returnedAlerts []*github.Alert + err = json.Unmarshal([]byte(textContent.Text), &returnedAlerts) + assert.NoError(t, err) + assert.Len(t, returnedAlerts, len(tc.expectedAlerts)) + for i, alert := range returnedAlerts { + assert.Equal(t, *tc.expectedAlerts[i].Number, *alert.Number) + assert.Equal(t, *tc.expectedAlerts[i].State, *alert.State) + assert.Equal(t, *tc.expectedAlerts[i].HTMLURL, *alert.HTMLURL) + } + }) + } +} diff --git a/pkg/github/tools.go b/pkg/github/tools.go index ce10c4ad..1267b1ab 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -73,6 +73,11 @@ func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn, toolsets.NewServerTool(GetCodeScanningAlert(getClient, t)), toolsets.NewServerTool(ListCodeScanningAlerts(getClient, t)), ) + secretSecurity := toolsets.NewToolset("secret_security", "Secret security related tools, such as GitHub Secret Scanning"). + AddReadTools( + toolsets.NewServerTool(GetSecretScanningAlert(getClient, t)), + toolsets.NewServerTool(ListSecretScanningAlerts(getClient, t)), + ) // Keep experiments alive so the system doesn't error out when it's always enabled experiments := toolsets.NewToolset("experiments", "Experimental features that are not considered stable yet") @@ -82,6 +87,7 @@ func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn, tsg.AddToolset(users) tsg.AddToolset(pullRequests) tsg.AddToolset(codeSecurity) + tsg.AddToolset(secretSecurity) tsg.AddToolset(experiments) // Enable the requested features From 47fdf2ea9cb20ed257345ba849cb52ed7c4d2fd0 Mon Sep 17 00:00:00 2001 From: tonytrg Date: Wed, 16 Apr 2025 15:18:15 +0200 Subject: [PATCH 3/7] fix tests --- pkg/github/secret_scanning.go | 24 ++------- pkg/github/secret_scanning_test.go | 78 +++++++++++++++++------------- 2 files changed, 47 insertions(+), 55 deletions(-) diff --git a/pkg/github/secret_scanning.go b/pkg/github/secret_scanning.go index 859c80f5..ee344061 100644 --- a/pkg/github/secret_scanning.go +++ b/pkg/github/secret_scanning.go @@ -85,26 +85,16 @@ func ListSecretScanningAlerts(getClient GetClientFn, t translations.TranslationH mcp.Description("The name of the repository."), ), mcp.WithString("state", - mcp.Description("Filter code scanning alerts by state ('open', 'resolved')"), + 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 code scanning alerts by resolution ('false_positive', 'wont_fix', 'revoked', 'pattern_edited', 'pattern_deleted', 'used_in_tests')"), + mcp.Description("Filter by resolution"), mcp.Enum("false_positive", "wont_fix", "revoked", "pattern_edited", "pattern_deleted", "used_in_tests"), ), - mcp.WithString("sort", - mcp.Description("Filter code scanning alerts by sort ('created', 'updated') Default: created"), - mcp.DefaultString("created"), - mcp.Enum("created", "updated"), - ), - mcp.WithString("direction", - mcp.Description("Filter code scanning alerts by direction ('desc', 'asc') Default: desc"), - mcp.DefaultString("desc"), - mcp.Enum("desc", "asc"), - ), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := requiredParam[string](request, "owner") @@ -127,20 +117,12 @@ func ListSecretScanningAlerts(getClient GetClientFn, t translations.TranslationH if err != nil { return mcp.NewToolResultError(err.Error()), nil } - sort, err := OptionalParam[string](request, "sort") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - direction, err := OptionalParam[string](request, "direction") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } client, err := getClient(ctx) if err != nil { return 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, Sort: sort, Direction: direction}) + alerts, resp, err := client.SecretScanning.ListAlertsForRepo(ctx, owner, repo, &github.SecretScanningAlertListOptions{State: state, SecretType: secretType, Resolution: resolution}) if err != nil { return nil, fmt.Errorf("failed to list alerts: %w", err) } diff --git a/pkg/github/secret_scanning_test.go b/pkg/github/secret_scanning_test.go index 044f1612..d32cbca9 100644 --- a/pkg/github/secret_scanning_test.go +++ b/pkg/github/secret_scanning_test.go @@ -26,8 +26,8 @@ func Test_GetSecretScanningAlert(t *testing.T) { // Setup mock alert for success case mockAlert := &github.SecretScanningAlert{ - Number: github.Ptr(42), - State: github.Ptr("open"), + Number: github.Ptr(42), + State: github.Ptr("open"), HTMLURL: github.Ptr("https://github.com/owner/private-repo/security/secret-scanning/42"), } @@ -124,22 +124,22 @@ func Test_ListSecretScanningAlerts(t *testing.T) { assert.Contains(t, tool.InputSchema.Properties, "state") assert.Contains(t, tool.InputSchema.Properties, "secret_type") assert.Contains(t, tool.InputSchema.Properties, "resolution") - assert.Contains(t, tool.InputSchema.Properties, "sort") - assert.Contains(t, tool.InputSchema.Properties, "direction") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"}) // Setup mock alerts for success case - mockAlerts := []*github.SecretScanningAlert{ - { - Number: github.Ptr(42), - State: github.Ptr("open"), - HTMLURL: github.Ptr("https://github.com/owner/repo/security/code-scanning/42"), - }, - { - Number: github.Ptr(43), - State: github.Ptr("fixed"), - HTMLURL: github.Ptr("https://github.com/owner/repo/security/code-scanning/43"), - }, + resolvedAlert := github.SecretScanningAlert{ + Number: github.Ptr(2), + HTMLURL: github.Ptr("https://github.com/owner/private-repo/security/secret-scanning/2"), + State: github.Ptr("resolved"), + Resolution: github.Ptr("false_positive"), + SecretType: github.Ptr("adafruit_io_key"), + } + openAlert := github.SecretScanningAlert{ + Number: github.Ptr(2), + HTMLURL: github.Ptr("https://github.com/owner/private-repo/security/secret-scanning/3"), + State: github.Ptr("open"), + Resolution: github.Ptr("false_positive"), + SecretType: github.Ptr("adafruit_io_key"), } tests := []struct { @@ -151,28 +151,41 @@ func Test_ListSecretScanningAlerts(t *testing.T) { expectedErrMsg string }{ { - name: "successful alerts listing", + name: "successful resolved alerts listing", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( - mock.GetReposSecretScanningAlertsByOwnerByRepoByAlertNumber, + mock.GetReposSecretScanningAlertsByOwnerByRepo, expectQueryParams(t, map[string]string{ - "ref": "main", - "state": "open", - "severity": "high", + "state": "resolved", }).andThen( - mockResponse(t, http.StatusOK, mockAlerts), + mockResponse(t, http.StatusOK, []*github.SecretScanningAlert{&resolvedAlert}), ), ), ), requestArgs: map[string]interface{}{ - "owner": "owner", - "repo": "repo", - "ref": "main", - "state": "open", - "severity": "high", + "owner": "owner", + "repo": "repo", + "state": "resolved", }, expectError: false, - expectedAlerts: mockAlerts, + expectedAlerts: []*github.SecretScanningAlert{&resolvedAlert}, + }, + { + name: "successful alerts listing", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposSecretScanningAlertsByOwnerByRepo, + expectQueryParams(t, map[string]string{}).andThen( + mockResponse(t, http.StatusOK, []*github.SecretScanningAlert{&resolvedAlert, &openAlert}), + ), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + }, + expectError: false, + expectedAlerts: []*github.SecretScanningAlert{&resolvedAlert, &openAlert}, }, { name: "alerts listing fails", @@ -196,17 +209,13 @@ func Test_ListSecretScanningAlerts(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - // Setup client with mock client := github.NewClient(tc.mockedClient) _, handler := ListSecretScanningAlerts(stubGetClientFn(client), translations.NullTranslationHelper) - // Create call request request := createMCPRequest(tc.requestArgs) - // Call handler result, err := handler(context.Background(), request) - // Verify results if tc.expectError { require.Error(t, err) assert.Contains(t, err.Error(), tc.expectedErrMsg) @@ -215,18 +224,19 @@ func Test_ListSecretScanningAlerts(t *testing.T) { require.NoError(t, err) - // Parse the result and get the text content if no error textContent := getTextResult(t, result) // Unmarshal and verify the result - var returnedAlerts []*github.Alert + var returnedAlerts []*github.SecretScanningAlert err = json.Unmarshal([]byte(textContent.Text), &returnedAlerts) assert.NoError(t, err) assert.Len(t, returnedAlerts, len(tc.expectedAlerts)) for i, alert := range returnedAlerts { assert.Equal(t, *tc.expectedAlerts[i].Number, *alert.Number) - assert.Equal(t, *tc.expectedAlerts[i].State, *alert.State) assert.Equal(t, *tc.expectedAlerts[i].HTMLURL, *alert.HTMLURL) + assert.Equal(t, *tc.expectedAlerts[i].State, *alert.State) + assert.Equal(t, *tc.expectedAlerts[i].Resolution, *alert.Resolution) + assert.Equal(t, *tc.expectedAlerts[i].SecretType, *alert.SecretType) } }) } From 6c01bf954e01cb1f846e318edc074a543479f46c Mon Sep 17 00:00:00 2001 From: tonytrg Date: Wed, 16 Apr 2025 16:15:51 +0200 Subject: [PATCH 4/7] add readme section --- README.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 288d7548..f16c6e71 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,6 @@ automation and interaction capabilities for developers and tools. 3. Lastly you will need to [Create a GitHub Personal Access Token](https://github.com/settings/personal-access-tokens/new). The MCP server can use many of the GitHub APIs, so enable the permissions that you feel comfortable granting your AI tools (to learn more about access tokens, please check out the [documentation](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)). - - ## Installation ### Usage with VS Code @@ -438,6 +436,21 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `severity`: Alert severity (string, optional) - `tool_name`: The name of the tool used for code scanning (string, optional) +### Secret Scanning + +- **get_secret_scanning_alert** - Get a secret scanning alert + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `alertNumber`: Alert number (number, required) + +- **list_secret_scanning_alerts** - List secret scanning alerts for a repository + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `state`: Alert state (string, optional) + - `secret_type`: The secret types to be filtered for in a comma-seperated list (string, optional) + - `resolution`: The resolution status (string, optional) + ## Resources ### Repository Content From bab315fca1459b0c5206c4459da75a0675929a30 Mon Sep 17 00:00:00 2001 From: Tony Truong Date: Wed, 16 Apr 2025 16:28:45 +0200 Subject: [PATCH 5/7] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f16c6e71..8a5ee539 100644 --- a/README.md +++ b/README.md @@ -448,7 +448,7 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) - `state`: Alert state (string, optional) - - `secret_type`: The secret types to be filtered for in a comma-seperated list (string, optional) + - `secret_type`: The secret types to be filtered for in a comma-separated list (string, optional) - `resolution`: The resolution status (string, optional) ## Resources From 5e2fa398d9161be12b517a9fb5a0d93061ec2bb8 Mon Sep 17 00:00:00 2001 From: Tony Truong Date: Thu, 17 Apr 2025 10:11:17 +0200 Subject: [PATCH 6/7] Update .gitignore Co-authored-by: Sam Morrow --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a9550aa8..9371be3e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,4 @@ dist/ __debug_bin* # Go -vendor \ No newline at end of file +vendor From 7a4b0e92405a44ad9aab2a3f7b27d8fcc39d03c7 Mon Sep 17 00:00:00 2001 From: tonytrg Date: Thu, 17 Apr 2025 10:13:06 +0200 Subject: [PATCH 7/7] fix product name --- pkg/github/tools.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/github/tools.go b/pkg/github/tools.go index 1267b1ab..35dabaef 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -73,7 +73,7 @@ func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn, toolsets.NewServerTool(GetCodeScanningAlert(getClient, t)), toolsets.NewServerTool(ListCodeScanningAlerts(getClient, t)), ) - secretSecurity := toolsets.NewToolset("secret_security", "Secret security related tools, such as GitHub Secret Scanning"). + secretProtection := toolsets.NewToolset("secret_protection", "Secret protection related tools, such as GitHub Secret Scanning"). AddReadTools( toolsets.NewServerTool(GetSecretScanningAlert(getClient, t)), toolsets.NewServerTool(ListSecretScanningAlerts(getClient, t)), @@ -87,7 +87,7 @@ func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn, tsg.AddToolset(users) tsg.AddToolset(pullRequests) tsg.AddToolset(codeSecurity) - tsg.AddToolset(secretSecurity) + tsg.AddToolset(secretProtection) tsg.AddToolset(experiments) // Enable the requested features