From 5144f559607c8605e71c22c5abadbb3c9acb1d20 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Nov 2025 17:08:41 +0000 Subject: [PATCH 1/4] Initial plan From 7988cb07ec43c35af14f41550bbc40b97b2c71b9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Nov 2025 17:19:15 +0000 Subject: [PATCH 2/4] Migrate dependabot toolset to modelcontextprotocol/go-sdk Co-authored-by: omgitsads <4619+omgitsads@users.noreply.github.com> --- pkg/github/dependabot.go | 339 +++++++++++----------- pkg/github/dependabot_test.go | 511 +++++++++++++++++----------------- 2 files changed, 438 insertions(+), 412 deletions(-) diff --git a/pkg/github/dependabot.go b/pkg/github/dependabot.go index f43da8287..f16ec14a0 100644 --- a/pkg/github/dependabot.go +++ b/pkg/github/dependabot.go @@ -1,161 +1,182 @@ package github -// import ( -// "context" -// "encoding/json" -// "fmt" -// "io" -// "net/http" - -// ghErrors "github.com/github/github-mcp-server/pkg/errors" -// "github.com/github/github-mcp-server/pkg/translations" -// "github.com/google/go-github/v77/github" -// "github.com/mark3labs/mcp-go/mcp" -// "github.com/mark3labs/mcp-go/server" -// ) - -// func GetDependabotAlert(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { -// return mcp.NewTool( -// "get_dependabot_alert", -// mcp.WithDescription(t("TOOL_GET_DEPENDABOT_ALERT_DESCRIPTION", "Get details of a specific dependabot alert in a GitHub repository.")), -// mcp.WithToolAnnotation(mcp.ToolAnnotation{ -// Title: t("TOOL_GET_DEPENDABOT_ALERT_USER_TITLE", "Get dependabot 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") -// 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.Dependabot.GetRepoAlert(ctx, owner, repo, alertNumber) -// if err != nil { -// return ghErrors.NewGitHubAPIErrorResponse(ctx, -// fmt.Sprintf("failed to get alert with number '%d'", alertNumber), -// resp, -// err, -// ), 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 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 ListDependabotAlerts(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { -// return mcp.NewTool( -// "list_dependabot_alerts", -// mcp.WithDescription(t("TOOL_LIST_DEPENDABOT_ALERTS_DESCRIPTION", "List dependabot alerts in a GitHub repository.")), -// mcp.WithToolAnnotation(mcp.ToolAnnotation{ -// Title: t("TOOL_LIST_DEPENDABOT_ALERTS_USER_TITLE", "List dependabot 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 dependabot alerts by state. Defaults to open"), -// mcp.DefaultString("open"), -// mcp.Enum("open", "fixed", "dismissed", "auto_dismissed"), -// ), -// mcp.WithString("severity", -// mcp.Description("Filter dependabot alerts by severity"), -// mcp.Enum("low", "medium", "high", "critical"), -// ), -// ), -// 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 -// } -// severity, err := OptionalParam[string](request, "severity") -// 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.Dependabot.ListRepoAlerts(ctx, owner, repo, &github.ListAlertsOptions{ -// State: ToStringPtr(state), -// Severity: ToStringPtr(severity), -// }) -// if err != nil { -// return ghErrors.NewGitHubAPIErrorResponse(ctx, -// fmt.Sprintf("failed to list alerts for repository '%s/%s'", owner, repo), -// resp, -// err, -// ), 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 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 -// } -// } +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + 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/v77/github" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +func GetDependabotAlert(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + tool := mcp.Tool{ + Name: "get_dependabot_alert", + Description: t("TOOL_GET_DEPENDABOT_ALERT_DESCRIPTION", "Get details of a specific dependabot alert in a GitHub repository."), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_GET_DEPENDABOT_ALERT_USER_TITLE", "Get dependabot alert"), + 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"}, + }, + } + + handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + alertNumber, err := RequiredInt(args, "alertNumber") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + + client, err := getClient(ctx) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil + } + + alert, resp, err := client.Dependabot.GetRepoAlert(ctx, owner, repo, alertNumber) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + fmt.Sprintf("failed to get alert with number '%d'", alertNumber), + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil + } + return utils.NewToolResultError(fmt.Sprintf("failed to get alert: %s", string(body))), nil, nil + } + + r, err := json.Marshal(alert) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to marshal alert", err), nil, nil + } + + return utils.NewToolResultText(string(r)), nil, nil + }) + + return tool, handler +} + +func ListDependabotAlerts(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + tool := mcp.Tool{ + Name: "list_dependabot_alerts", + Description: t("TOOL_LIST_DEPENDABOT_ALERTS_DESCRIPTION", "List dependabot alerts in a GitHub repository."), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_LIST_DEPENDABOT_ALERTS_USER_TITLE", "List dependabot alerts"), + 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 dependabot alerts by state. Defaults to open", + Enum: []any{"open", "fixed", "dismissed", "auto_dismissed"}, + Default: json.RawMessage(`"open"`), + }, + "severity": { + Type: "string", + Description: "Filter dependabot alerts by severity", + Enum: []any{"low", "medium", "high", "critical"}, + }, + }, + Required: []string{"owner", "repo"}, + }, + } + + handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + state, err := OptionalParam[string](args, "state") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + severity, err := OptionalParam[string](args, "severity") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + + client, err := getClient(ctx) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil + } + + alerts, resp, err := client.Dependabot.ListRepoAlerts(ctx, owner, repo, &github.ListAlertsOptions{ + State: ToStringPtr(state), + Severity: ToStringPtr(severity), + }) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + fmt.Sprintf("failed to list alerts for repository '%s/%s'", owner, repo), + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil + } + return utils.NewToolResultError(fmt.Sprintf("failed to list alerts: %s", string(body))), nil, nil + } + + r, err := json.Marshal(alerts) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to marshal alerts", err), nil, nil + } + + return utils.NewToolResultText(string(r)), nil, nil + }) + + return tool, handler +} diff --git a/pkg/github/dependabot_test.go b/pkg/github/dependabot_test.go index ab879ace1..8668b55b8 100644 --- a/pkg/github/dependabot_test.go +++ b/pkg/github/dependabot_test.go @@ -1,276 +1,281 @@ package github -// import ( -// "context" -// "encoding/json" -// "net/http" -// "testing" +import ( + "context" + "encoding/json" + "net/http" + "testing" -// "github.com/github/github-mcp-server/internal/toolsnaps" -// "github.com/github/github-mcp-server/pkg/translations" -// "github.com/google/go-github/v77/github" -// "github.com/migueleliasweb/go-github-mock/src/mock" -// "github.com/stretchr/testify/assert" -// "github.com/stretchr/testify/require" -// ) + "github.com/github/github-mcp-server/internal/toolsnaps" + "github.com/github/github-mcp-server/pkg/translations" + "github.com/google/go-github/v77/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" +) -// func Test_GetDependabotAlert(t *testing.T) { -// // Verify tool definition -// mockClient := github.NewClient(nil) -// tool, _ := GetDependabotAlert(stubGetClientFn(mockClient), translations.NullTranslationHelper) -// require.NoError(t, toolsnaps.Test(tool.Name, tool)) +func Test_GetDependabotAlert(t *testing.T) { + // Verify tool definition + mockClient := github.NewClient(nil) + tool, _ := GetDependabotAlert(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) -// // Validate tool schema -// assert.Equal(t, "get_dependabot_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"}) + // Validate tool schema + assert.Equal(t, "get_dependabot_alert", tool.Name) + assert.NotEmpty(t, tool.Description) + + // Cast InputSchema to *jsonschema.Schema to access properties + schema, ok := tool.InputSchema.(*jsonschema.Schema) + require.True(t, ok, "expected InputSchema to be of type *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.DependabotAlert{ -// Number: github.Ptr(42), -// State: github.Ptr("open"), -// HTMLURL: github.Ptr("https://github.com/owner/repo/security/dependabot/42"), -// } + // Setup mock alert for success case + mockAlert := &github.DependabotAlert{ + Number: github.Ptr(42), + State: github.Ptr("open"), + HTMLURL: github.Ptr("https://github.com/owner/repo/security/dependabot/42"), + } -// tests := []struct { -// name string -// mockedClient *http.Client -// requestArgs map[string]interface{} -// expectError bool -// expectedAlert *github.DependabotAlert -// expectedErrMsg string -// }{ -// { -// name: "successful alert fetch", -// mockedClient: mock.NewMockedHTTPClient( -// mock.WithRequestMatch( -// mock.GetReposDependabotAlertsByOwnerByRepoByAlertNumber, -// 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.GetReposDependabotAlertsByOwnerByRepoByAlertNumber, -// 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", -// }, -// } + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedAlert *github.DependabotAlert + expectedErrMsg string + }{ + { + name: "successful alert fetch", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetReposDependabotAlertsByOwnerByRepoByAlertNumber, + 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.GetReposDependabotAlertsByOwnerByRepoByAlertNumber, + 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 := GetDependabotAlert(stubGetClientFn(client), translations.NullTranslationHelper) + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := GetDependabotAlert(stubGetClientFn(client), translations.NullTranslationHelper) -// // Create call request -// request := createMCPRequest(tc.requestArgs) + // Call handler with args directly + result, _, err := handler(context.Background(), nil, tc.requestArgs) -// // Call handler -// result, err := handler(context.Background(), request) + // Verify results + if tc.expectError { + require.NoError(t, err) + require.True(t, result.IsError) + errorContent := getErrorResult(t, result) + assert.Contains(t, errorContent.Text, tc.expectedErrMsg) + return + } -// // Verify results -// if tc.expectError { -// require.NoError(t, err) -// require.True(t, result.IsError) -// errorContent := getErrorResult(t, result) -// assert.Contains(t, errorContent.Text, tc.expectedErrMsg) -// return -// } + require.NoError(t, err) + require.False(t, result.IsError) -// require.NoError(t, err) -// require.False(t, result.IsError) + // Parse the result and get the text content if no error + textContent := getTextResult(t, result) -// // Parse the result and get the text content if no error -// textContent := getTextResult(t, result) + // Unmarshal and verify the result + var returnedAlert github.DependabotAlert + 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) + }) + } +} -// // Unmarshal and verify the result -// var returnedAlert github.DependabotAlert -// 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_ListDependabotAlerts(t *testing.T) { + // Verify tool definition once + mockClient := github.NewClient(nil) + tool, _ := ListDependabotAlerts(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) -// func Test_ListDependabotAlerts(t *testing.T) { -// // Verify tool definition once -// mockClient := github.NewClient(nil) -// tool, _ := ListDependabotAlerts(stubGetClientFn(mockClient), translations.NullTranslationHelper) -// require.NoError(t, toolsnaps.Test(tool.Name, tool)) + assert.Equal(t, "list_dependabot_alerts", tool.Name) + assert.NotEmpty(t, tool.Description) + + // Cast InputSchema to *jsonschema.Schema to access properties + schema, ok := tool.InputSchema.(*jsonschema.Schema) + require.True(t, ok, "expected InputSchema to be of type *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, "severity") + assert.ElementsMatch(t, schema.Required, []string{"owner", "repo"}) -// assert.Equal(t, "list_dependabot_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, "severity") -// assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"}) + // Setup mock alerts for success case + criticalAlert := github.DependabotAlert{ + Number: github.Ptr(1), + HTMLURL: github.Ptr("https://github.com/owner/repo/security/dependabot/1"), + State: github.Ptr("open"), + SecurityAdvisory: &github.DependabotSecurityAdvisory{ + Severity: github.Ptr("critical"), + }, + } + highSeverityAlert := github.DependabotAlert{ + Number: github.Ptr(2), + HTMLURL: github.Ptr("https://github.com/owner/repo/security/dependabot/2"), + State: github.Ptr("fixed"), + SecurityAdvisory: &github.DependabotSecurityAdvisory{ + Severity: github.Ptr("high"), + }, + } -// // Setup mock alerts for success case -// criticalAlert := github.DependabotAlert{ -// Number: github.Ptr(1), -// HTMLURL: github.Ptr("https://github.com/owner/repo/security/dependabot/1"), -// State: github.Ptr("open"), -// SecurityAdvisory: &github.DependabotSecurityAdvisory{ -// Severity: github.Ptr("critical"), -// }, -// } -// highSeverityAlert := github.DependabotAlert{ -// Number: github.Ptr(2), -// HTMLURL: github.Ptr("https://github.com/owner/repo/security/dependabot/2"), -// State: github.Ptr("fixed"), -// SecurityAdvisory: &github.DependabotSecurityAdvisory{ -// Severity: github.Ptr("high"), -// }, -// } + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedAlerts []*github.DependabotAlert + expectedErrMsg string + }{ + { + name: "successful open alerts listing", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposDependabotAlertsByOwnerByRepo, + expectQueryParams(t, map[string]string{ + "state": "open", + }).andThen( + mockResponse(t, http.StatusOK, []*github.DependabotAlert{&criticalAlert}), + ), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "state": "open", + }, + expectError: false, + expectedAlerts: []*github.DependabotAlert{&criticalAlert}, + }, + { + name: "successful severity filtered listing", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposDependabotAlertsByOwnerByRepo, + expectQueryParams(t, map[string]string{ + "severity": "high", + }).andThen( + mockResponse(t, http.StatusOK, []*github.DependabotAlert{&highSeverityAlert}), + ), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "severity": "high", + }, + expectError: false, + expectedAlerts: []*github.DependabotAlert{&highSeverityAlert}, + }, + { + name: "successful all alerts listing", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposDependabotAlertsByOwnerByRepo, + expectQueryParams(t, map[string]string{}).andThen( + mockResponse(t, http.StatusOK, []*github.DependabotAlert{&criticalAlert, &highSeverityAlert}), + ), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + }, + expectError: false, + expectedAlerts: []*github.DependabotAlert{&criticalAlert, &highSeverityAlert}, + }, + { + name: "alerts listing fails", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposDependabotAlertsByOwnerByRepo, + 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", + }, + } -// tests := []struct { -// name string -// mockedClient *http.Client -// requestArgs map[string]interface{} -// expectError bool -// expectedAlerts []*github.DependabotAlert -// expectedErrMsg string -// }{ -// { -// name: "successful open alerts listing", -// mockedClient: mock.NewMockedHTTPClient( -// mock.WithRequestMatchHandler( -// mock.GetReposDependabotAlertsByOwnerByRepo, -// expectQueryParams(t, map[string]string{ -// "state": "open", -// }).andThen( -// mockResponse(t, http.StatusOK, []*github.DependabotAlert{&criticalAlert}), -// ), -// ), -// ), -// requestArgs: map[string]interface{}{ -// "owner": "owner", -// "repo": "repo", -// "state": "open", -// }, -// expectError: false, -// expectedAlerts: []*github.DependabotAlert{&criticalAlert}, -// }, -// { -// name: "successful severity filtered listing", -// mockedClient: mock.NewMockedHTTPClient( -// mock.WithRequestMatchHandler( -// mock.GetReposDependabotAlertsByOwnerByRepo, -// expectQueryParams(t, map[string]string{ -// "severity": "high", -// }).andThen( -// mockResponse(t, http.StatusOK, []*github.DependabotAlert{&highSeverityAlert}), -// ), -// ), -// ), -// requestArgs: map[string]interface{}{ -// "owner": "owner", -// "repo": "repo", -// "severity": "high", -// }, -// expectError: false, -// expectedAlerts: []*github.DependabotAlert{&highSeverityAlert}, -// }, -// { -// name: "successful all alerts listing", -// mockedClient: mock.NewMockedHTTPClient( -// mock.WithRequestMatchHandler( -// mock.GetReposDependabotAlertsByOwnerByRepo, -// expectQueryParams(t, map[string]string{}).andThen( -// mockResponse(t, http.StatusOK, []*github.DependabotAlert{&criticalAlert, &highSeverityAlert}), -// ), -// ), -// ), -// requestArgs: map[string]interface{}{ -// "owner": "owner", -// "repo": "repo", -// }, -// expectError: false, -// expectedAlerts: []*github.DependabotAlert{&criticalAlert, &highSeverityAlert}, -// }, -// { -// name: "alerts listing fails", -// mockedClient: mock.NewMockedHTTPClient( -// mock.WithRequestMatchHandler( -// mock.GetReposDependabotAlertsByOwnerByRepo, -// 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) { + client := github.NewClient(tc.mockedClient) + _, handler := ListDependabotAlerts(stubGetClientFn(client), translations.NullTranslationHelper) -// for _, tc := range tests { -// t.Run(tc.name, func(t *testing.T) { -// client := github.NewClient(tc.mockedClient) -// _, handler := ListDependabotAlerts(stubGetClientFn(client), translations.NullTranslationHelper) + // Call handler with args directly + result, _, err := handler(context.Background(), nil, tc.requestArgs) -// request := createMCPRequest(tc.requestArgs) + if tc.expectError { + require.NoError(t, err) + require.True(t, result.IsError) + errorContent := getErrorResult(t, result) + assert.Contains(t, errorContent.Text, tc.expectedErrMsg) + return + } -// result, err := handler(context.Background(), request) + require.NoError(t, err) + require.False(t, result.IsError) -// if tc.expectError { -// require.NoError(t, err) -// require.True(t, result.IsError) -// errorContent := getErrorResult(t, result) -// assert.Contains(t, errorContent.Text, tc.expectedErrMsg) -// return -// } + textContent := getTextResult(t, result) -// require.NoError(t, err) -// require.False(t, result.IsError) - -// textContent := getTextResult(t, result) - -// // Unmarshal and verify the result -// var returnedAlerts []*github.DependabotAlert -// 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].HTMLURL, *alert.HTMLURL) -// assert.Equal(t, *tc.expectedAlerts[i].State, *alert.State) -// if tc.expectedAlerts[i].SecurityAdvisory != nil && tc.expectedAlerts[i].SecurityAdvisory.Severity != nil && -// alert.SecurityAdvisory != nil && alert.SecurityAdvisory.Severity != nil { -// assert.Equal(t, *tc.expectedAlerts[i].SecurityAdvisory.Severity, *alert.SecurityAdvisory.Severity) -// } -// } -// }) -// } -// } + // Unmarshal and verify the result + var returnedAlerts []*github.DependabotAlert + 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].HTMLURL, *alert.HTMLURL) + assert.Equal(t, *tc.expectedAlerts[i].State, *alert.State) + if tc.expectedAlerts[i].SecurityAdvisory != nil && tc.expectedAlerts[i].SecurityAdvisory.Severity != nil && + alert.SecurityAdvisory != nil && alert.SecurityAdvisory.Severity != nil { + assert.Equal(t, *tc.expectedAlerts[i].SecurityAdvisory.Severity, *alert.SecurityAdvisory.Severity) + } + } + }) + } +} From cd8ab0a083d8700b64606c3103d43b040f680db3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Nov 2025 17:21:16 +0000 Subject: [PATCH 3/4] Register dependabot 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 b334929a0..54715ecf2 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -249,11 +249,11 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG // toolsets.NewServerTool(GetSecretScanningAlert(getClient, t)), // toolsets.NewServerTool(ListSecretScanningAlerts(getClient, t)), // ) - // dependabot := toolsets.NewToolset(ToolsetMetadataDependabot.ID, ToolsetMetadataDependabot.Description). - // AddReadTools( - // toolsets.NewServerTool(GetDependabotAlert(getClient, t)), - // toolsets.NewServerTool(ListDependabotAlerts(getClient, t)), - // ) + dependabot := toolsets.NewToolset(ToolsetMetadataDependabot.ID, ToolsetMetadataDependabot.Description). + AddReadTools( + toolsets.NewServerTool(GetDependabotAlert(getClient, t)), + toolsets.NewServerTool(ListDependabotAlerts(getClient, t)), + ) // notifications := toolsets.NewToolset(ToolsetMetadataNotifications.ID, ToolsetMetadataNotifications.Description). // AddReadTools( @@ -369,7 +369,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG // tsg.AddToolset(actions) // tsg.AddToolset(codeSecurity) // tsg.AddToolset(secretProtection) - // tsg.AddToolset(dependabot) + tsg.AddToolset(dependabot) // tsg.AddToolset(notifications) // tsg.AddToolset(experiments) // tsg.AddToolset(discussions) From d045ddc901f0313905761cc9ba1e90c2e3e1bc9c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Nov 2025 17:23:57 +0000 Subject: [PATCH 4/4] Final verification and testing complete Co-authored-by: omgitsads <4619+omgitsads@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 008787299..a29865f84 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.24.0 require ( github.com/google/go-github/v77 v77.0.0 + github.com/google/jsonschema-go v0.3.0 github.com/josephburnett/jd v1.9.2 github.com/mark3labs/mcp-go v0.36.0 github.com/microcosm-cc/bluemonday v1.0.27 @@ -20,7 +21,6 @@ require ( github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/swag v0.21.1 // indirect github.com/google/go-github/v71 v71.0.0 // indirect - github.com/google/jsonschema-go v0.3.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/mux v1.8.0 // indirect github.com/invopop/jsonschema v0.13.0 // indirect @@ -37,8 +37,8 @@ require ( require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 - github.com/google/go-querystring v1.1.0 + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/modelcontextprotocol/go-sdk v1.1.0 diff --git a/go.sum b/go.sum index cd568cb51..184b2a617 100644 --- a/go.sum +++ b/go.sum @@ -114,8 +114,6 @@ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= -golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= @@ -124,6 +122,8 @@ golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=