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
+}