diff --git a/README.md b/README.md index 8a0364932..0bb054355 100644 --- a/README.md +++ b/README.md @@ -269,6 +269,7 @@ The following sets of tools are available (all are on by default): | `context` | **Strongly recommended**: Tools that provide context about the current user and GitHub context you are operating in | | `actions` | GitHub Actions workflows and CI/CD operations | | `code_security` | Code security related tools, such as GitHub Code Scanning | +| `dependabot` | Dependabot tools | | `discussions` | GitHub Discussions related tools | | `experiments` | Experimental features that are not considered stable yet | | `issues` | GitHub Issues related tools | @@ -555,6 +556,23 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
+Dependabot + +- **get_dependabot_alert** - Get dependabot alert + - `alertNumber`: The number of the alert. (number, required) + - `owner`: The owner of the repository. (string, required) + - `repo`: The name of the repository. (string, required) + +- **list_dependabot_alerts** - List dependabot alerts + - `owner`: The owner of the repository. (string, required) + - `repo`: The name of the repository. (string, required) + - `severity`: Filter dependabot alerts by severity (string, optional) + - `state`: Filter dependabot alerts by state. Defaults to open (string, optional) + +
+ +
+ Discussions - **get_discussion** - Get discussion diff --git a/docs/remote-server.md b/docs/remote-server.md index 7b5f2c0d4..c36124ecc 100644 --- a/docs/remote-server.md +++ b/docs/remote-server.md @@ -20,6 +20,7 @@ Below is a table of available toolsets for the remote GitHub MCP Server. Each to | all | All available GitHub MCP tools | https://api.githubcopilot.com/mcp/ | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2F%22%7D) | [read-only](https://api.githubcopilot.com/mcp/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Freadonly%22%7D) | | Actions | GitHub Actions workflows and CI/CD operations | https://api.githubcopilot.com/mcp/x/actions | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-actions&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Factions%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/actions/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-actions&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Factions%2Freadonly%22%7D) | | Code Security | Code security related tools, such as GitHub Code Scanning | https://api.githubcopilot.com/mcp/x/code_security | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-code_security&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcode_security%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/code_security/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-code_security&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcode_security%2Freadonly%22%7D) | +| Dependabot | Dependabot tools | https://api.githubcopilot.com/mcp/x/dependabot | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-dependabot&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fdependabot%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/dependabot/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-dependabot&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fdependabot%2Freadonly%22%7D) | | Discussions | GitHub Discussions related tools | https://api.githubcopilot.com/mcp/x/discussions | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-discussions&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fdiscussions%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/discussions/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-discussions&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fdiscussions%2Freadonly%22%7D) | | Experiments | Experimental features that are not considered stable yet | https://api.githubcopilot.com/mcp/x/experiments | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-experiments&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fexperiments%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/experiments/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-experiments&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fexperiments%2Freadonly%22%7D) | | Issues | GitHub Issues related tools | https://api.githubcopilot.com/mcp/x/issues | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-issues&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fissues%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/issues/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-issues&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fissues%2Freadonly%22%7D) | diff --git a/pkg/github/__toolsnaps__/get_dependabot_alert.snap b/pkg/github/__toolsnaps__/get_dependabot_alert.snap new file mode 100644 index 000000000..76b5ef126 --- /dev/null +++ b/pkg/github/__toolsnaps__/get_dependabot_alert.snap @@ -0,0 +1,30 @@ +{ + "annotations": { + "title": "Get dependabot alert", + "readOnlyHint": true + }, + "description": "Get details of a specific dependabot alert in a GitHub repository.", + "inputSchema": { + "properties": { + "alertNumber": { + "description": "The number of the alert.", + "type": "number" + }, + "owner": { + "description": "The owner of the repository.", + "type": "string" + }, + "repo": { + "description": "The name of the repository.", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "alertNumber" + ], + "type": "object" + }, + "name": "get_dependabot_alert" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/list_dependabot_alerts.snap b/pkg/github/__toolsnaps__/list_dependabot_alerts.snap new file mode 100644 index 000000000..681d640b7 --- /dev/null +++ b/pkg/github/__toolsnaps__/list_dependabot_alerts.snap @@ -0,0 +1,46 @@ +{ + "annotations": { + "title": "List dependabot alerts", + "readOnlyHint": true + }, + "description": "List dependabot alerts in a GitHub repository.", + "inputSchema": { + "properties": { + "owner": { + "description": "The owner of the repository.", + "type": "string" + }, + "repo": { + "description": "The name of the repository.", + "type": "string" + }, + "severity": { + "description": "Filter dependabot alerts by severity", + "enum": [ + "low", + "medium", + "high", + "critical" + ], + "type": "string" + }, + "state": { + "default": "open", + "description": "Filter dependabot alerts by state. Defaults to open", + "enum": [ + "open", + "fixed", + "dismissed", + "auto_dismissed" + ], + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_dependabot_alerts" +} \ No newline at end of file diff --git a/pkg/github/dependabot.go b/pkg/github/dependabot.go new file mode 100644 index 000000000..af21b83d1 --- /dev/null +++ b/pkg/github/dependabot.go @@ -0,0 +1,161 @@ +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/v72/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 + } +} diff --git a/pkg/github/dependabot_test.go b/pkg/github/dependabot_test.go new file mode 100644 index 000000000..f7c091981 --- /dev/null +++ b/pkg/github/dependabot_test.go @@ -0,0 +1,276 @@ +package github + +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/v72/github" + "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)) + + // 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"}) + + // 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", + }, + } + + 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 + 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 + } + + require.NoError(t, err) + require.False(t, result.IsError) + + // 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) + }) + } +} + +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) + 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"), + }, + } + + 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) + + request := createMCPRequest(tc.requestArgs) + + result, err := handler(context.Background(), request) + + 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) + + 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) + } + } + }) + } +} diff --git a/pkg/github/tools.go b/pkg/github/tools.go index 9f36cfc3d..a469b7678 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -103,6 +103,11 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG toolsets.NewServerTool(GetSecretScanningAlert(getClient, t)), toolsets.NewServerTool(ListSecretScanningAlerts(getClient, t)), ) + dependabot := toolsets.NewToolset("dependabot", "Dependabot tools"). + AddReadTools( + toolsets.NewServerTool(GetDependabotAlert(getClient, t)), + toolsets.NewServerTool(ListDependabotAlerts(getClient, t)), + ) notifications := toolsets.NewToolset("notifications", "GitHub Notifications related tools"). AddReadTools( @@ -162,6 +167,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG tsg.AddToolset(actions) tsg.AddToolset(codeSecurity) tsg.AddToolset(secretProtection) + tsg.AddToolset(dependabot) tsg.AddToolset(notifications) tsg.AddToolset(experiments) tsg.AddToolset(discussions) @@ -188,3 +194,12 @@ func InitDynamicToolset(s *server.MCPServer, tsg *toolsets.ToolsetGroup, t trans func ToBoolPtr(b bool) *bool { return &b } + +// ToStringPtr converts a string to a *string pointer. +// Returns nil if the string is empty. +func ToStringPtr(s string) *string { + if s == "" { + return nil + } + return &s +}