diff --git a/README.md b/README.md
index b4168a136..a6e740e66 100644
--- a/README.md
+++ b/README.md
@@ -36,7 +36,7 @@ Alternatively, to manually configure VS Code, choose the appropriate JSON block
-
+
```json
{
"servers": {
@@ -130,7 +130,7 @@ To keep your GitHub PAT secure and reusable across different MCP hosts:
```bash
# CLI usage
claude mcp update github -e GITHUB_PERSONAL_ACCESS_TOKEN=$GITHUB_PAT
-
+
# In config files (where supported)
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "$GITHUB_PAT"
@@ -241,10 +241,10 @@ For other MCP host applications, please refer to our installation guides:
- **[GitHub Copilot in other IDEs](/docs/installation-guides/install-other-copilot-ides.md)** - Installation for JetBrains, Visual Studio, Eclipse, and Xcode with GitHub Copilot
- **[Claude Code & Claude Desktop](docs/installation-guides/install-claude.md)** - Installation guide for Claude Code and Claude Desktop
-- **[Cursor](docs/installation-guides/install-cursor.md)** - Installation guide for Cursor IDE
+- **[Cursor](docs/installation-guides/install-cursor.md)** - Installation guide for Cursor IDE
- **[Windsurf](docs/installation-guides/install-windsurf.md)** - Installation guide for Windsurf IDE
-For a complete overview of all installation options, see our **[Installation Guides Index](docs/installation-guides/installation-guides.md)**.
+For a complete overview of all installation options, see our **[Installation Guides Index](docs/installation-guides)**.
> **Note:** Any host application that supports local MCP servers should be able to access the local GitHub MCP server. However, the specific configuration process, syntax and stability of the integration will vary by host application. While many may follow a similar format to the examples above, this is not guaranteed. Please refer to your host application's documentation for the correct MCP configuration syntax and setup process.
@@ -295,6 +295,7 @@ The following sets of tools are available (all are on by default):
| `pull_requests` | GitHub Pull Request related tools |
| `repos` | GitHub Repository related tools |
| `secret_protection` | Secret protection related tools, such as GitHub Secret Scanning |
+| `security_advisories` | Security advisories related tools |
| `users` | GitHub User related tools |
@@ -923,6 +924,41 @@ The following sets of tools are available (all are on by default):
+Security Advisories
+
+- **get_global_security_advisory** - Get a global security advisory
+ - `ghsaId`: GitHub Security Advisory ID (format: GHSA-xxxx-xxxx-xxxx). (string, required)
+
+- **list_global_security_advisories** - List global security advisories
+ - `affects`: Filter advisories by affected package or version (e.g. "package1,package2@1.0.0"). (string, optional)
+ - `cveId`: Filter by CVE ID. (string, optional)
+ - `cwes`: Filter by Common Weakness Enumeration IDs (e.g. ["79", "284", "22"]). (string[], optional)
+ - `ecosystem`: Filter by package ecosystem. (string, optional)
+ - `ghsaId`: Filter by GitHub Security Advisory ID (format: GHSA-xxxx-xxxx-xxxx). (string, optional)
+ - `isWithdrawn`: Whether to only return withdrawn advisories. (boolean, optional)
+ - `modified`: Filter by publish or update date or date range (ISO 8601 date or range). (string, optional)
+ - `published`: Filter by publish date or date range (ISO 8601 date or range). (string, optional)
+ - `severity`: Filter by severity. (string, optional)
+ - `type`: Advisory type. (string, optional)
+ - `updated`: Filter by update date or date range (ISO 8601 date or range). (string, optional)
+
+- **list_org_repository_security_advisories** - List org repository security advisories
+ - `direction`: Sort direction. (string, optional)
+ - `org`: The organization login. (string, required)
+ - `sort`: Sort field. (string, optional)
+ - `state`: Filter by advisory state. (string, optional)
+
+- **list_repository_security_advisories** - List repository security advisories
+ - `direction`: Sort direction. (string, optional)
+ - `owner`: The owner of the repository. (string, required)
+ - `repo`: The name of the repository. (string, required)
+ - `sort`: Sort field. (string, optional)
+ - `state`: Filter by advisory state. (string, optional)
+
+
+
+
+
Users
- **search_users** - Search users
diff --git a/docs/installation-guides/install-cursor.md b/docs/installation-guides/install-cursor.md
index b069addd3..654f0a788 100644
--- a/docs/installation-guides/install-cursor.md
+++ b/docs/installation-guides/install-cursor.md
@@ -1,17 +1,19 @@
# Install GitHub MCP Server in Cursor
## Prerequisites
+
1. Cursor IDE installed (latest version)
2. [GitHub Personal Access Token](https://github.com/settings/personal-access-tokens/new) with appropriate scopes
3. For local installation: [Docker](https://www.docker.com/) installed and running
## Remote Server Setup (Recommended)
-[](https://cursor.com/install-mcp?name=github&config=eyJ1cmwiOiJodHRwczovL2FwaS5naXRodWJjb3BpbG90LmNvbS9tY3AvIiwiaGVhZGVycyI6eyJBdXRob3JpemF0aW9uIjoiQmVhcmVyIFlPVVJfR0lUSFVCX1BBVCJ9LCJ0eXBlIjoiaHR0cCJ9)
+[](https://cursor.com/en/install-mcp?name=github&config=eyJ1cmwiOiJodHRwczovL2FwaS5naXRodWJjb3BpbG90LmNvbS9tY3AvIiwiaGVhZGVycyI6eyJBdXRob3JpemF0aW9uIjoiQmVhcmVyIFlPVVJfR0lUSFVCX1BBVCJ9fQ%3D%3D)
Uses GitHub's hosted server at https://api.githubcopilot.com/mcp/. Requires Cursor v0.48.0+ for Streamable HTTP support. While Cursor supports OAuth for some MCP servers, the GitHub server currently requires a Personal Access Token.
### Install steps
+
1. Click the install button above and follow the flow, or go directly to your global MCP configuration file at `~/.cursor/mcp.json` and enter the code block below
2. In Tools & Integrations > MCP tools, click the pencil icon next to "github"
3. Replace `YOUR_GITHUB_PAT` with your actual [GitHub Personal Access Token](https://github.com/settings/tokens)
@@ -35,11 +37,12 @@ Uses GitHub's hosted server at https://api.githubcopilot.com/mcp/. Requires Curs
## Local Server Setup
-[](https://cursor.com/install-mcp?name=github&config=eyJjb21tYW5kIjoiZG9ja2VyIiwiYXJncyI6WyJydW4iLCItaSIsIi0tcm0iLCItZSIsIkdJVEhVQl9QRVJTT05BTF9BQ0NFU1NfVE9LRU4iLCJnaGNyLmlvL2dpdGh1Yi9naXRodWItbWNwLXNlcnZlciJdLCJlbnYiOnsiR0lUSFVCX1BFUlNPTkFMX0FDQ0VTU19UT0tFTiI6IllPVVJfR0lUSFVCX1BHVCJ9fQ==)
+[](https://cursor.com/en/install-mcp?name=github&config=eyJjb21tYW5kIjoiZG9ja2VyIHJ1biAtaSAtLXJtIC1lIEdJVEhVQl9QRVJTT05BTF9BQ0NFU1NfVE9LRU4gZ2hjci5pby9naXRodWIvZ2l0aHViLW1jcC1zZXJ2ZXIiLCJlbnYiOnsiR0lUSFVCX1BFUlNPTkFMX0FDQ0VTU19UT0tFTiI6IllPVVJfR0lUSFVCX1BBVCJ9fQ%3D%3D)
The local GitHub MCP server runs via Docker and requires Docker Desktop to be installed and running.
### Install steps
+
1. Click the install button above and follow the flow, or go directly to your global MCP configuration file at `~/.cursor/mcp.json` and enter the code block below
2. In Tools & Integrations > MCP tools, click the pencil icon next to "github"
3. Replace `YOUR_GITHUB_PAT` with your actual [GitHub Personal Access Token](https://github.com/settings/tokens)
@@ -77,6 +80,7 @@ The local GitHub MCP server runs via Docker and requires Docker Desktop to be in
- **Project-specific**: `.cursor/mcp.json` in project root
## Verify Installation
+
1. Restart Cursor completely
2. Check for green dot in Settings → Tools & Integrations → MCP Tools
3. In chat/composer, check "Available Tools"
@@ -85,16 +89,19 @@ The local GitHub MCP server runs via Docker and requires Docker Desktop to be in
## Troubleshooting
### Remote Server Issues
+
- **Streamable HTTP not working**: Ensure you're using Cursor v0.48.0 or later
- **Authentication failures**: Verify PAT has correct scopes
- **Connection errors**: Check firewall/proxy settings
### Local Server Issues
+
- **Docker errors**: Ensure Docker Desktop is running
- **Image pull failures**: Try `docker logout ghcr.io` then retry
- **Docker not found**: Install Docker Desktop and ensure it's running
### General Issues
+
- **MCP not loading**: Restart Cursor completely after configuration
- **Invalid JSON**: Validate that json format is correct
- **Tools not appearing**: Check server shows green dot in MCP settings
diff --git a/docs/remote-server.md b/docs/remote-server.md
index 5f57f4961..b6f7fa61d 100644
--- a/docs/remote-server.md
+++ b/docs/remote-server.md
@@ -32,6 +32,7 @@ Below is a table of available toolsets for the remote GitHub MCP Server. Each to
| Pull Requests | GitHub Pull Request related tools | https://api.githubcopilot.com/mcp/x/pull_requests | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-pull_requests&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fpull_requests%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/pull_requests/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-pull_requests&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fpull_requests%2Freadonly%22%7D) |
| Repositories | GitHub Repository related tools | https://api.githubcopilot.com/mcp/x/repos | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-repos&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Frepos%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/repos/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-repos&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Frepos%2Freadonly%22%7D) |
| Secret Protection | Secret protection related tools, such as GitHub Secret Scanning | https://api.githubcopilot.com/mcp/x/secret_protection | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-secret_protection&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fsecret_protection%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/secret_protection/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-secret_protection&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fsecret_protection%2Freadonly%22%7D) |
+| Security Advisories | Security advisories related tools | https://api.githubcopilot.com/mcp/x/security_advisories | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-security_advisories&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fsecurity_advisories%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/security_advisories/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-security_advisories&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fsecurity_advisories%2Freadonly%22%7D) |
| Users | GitHub User related tools | https://api.githubcopilot.com/mcp/x/users | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-users&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fusers%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/users/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-users&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fusers%2Freadonly%22%7D) |
diff --git a/pkg/github/security_advisories.go b/pkg/github/security_advisories.go
new file mode 100644
index 000000000..6eaeebe47
--- /dev/null
+++ b/pkg/github/security_advisories.go
@@ -0,0 +1,397 @@
+package github
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/github/github-mcp-server/pkg/translations"
+ "github.com/google/go-github/v74/github"
+ "github.com/mark3labs/mcp-go/mcp"
+ "github.com/mark3labs/mcp-go/server"
+)
+
+func ListGlobalSecurityAdvisories(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
+ return mcp.NewTool("list_global_security_advisories",
+ mcp.WithDescription(t("TOOL_LIST_GLOBAL_SECURITY_ADVISORIES_DESCRIPTION", "List global security advisories from GitHub.")),
+ mcp.WithToolAnnotation(mcp.ToolAnnotation{
+ Title: t("TOOL_LIST_GLOBAL_SECURITY_ADVISORIES_USER_TITLE", "List global security advisories"),
+ ReadOnlyHint: ToBoolPtr(true),
+ }),
+ mcp.WithString("ghsaId",
+ mcp.Description("Filter by GitHub Security Advisory ID (format: GHSA-xxxx-xxxx-xxxx)."),
+ ),
+ mcp.WithString("type",
+ mcp.Description("Advisory type."),
+ mcp.Enum("reviewed", "malware", "unreviewed"),
+ mcp.DefaultString("reviewed"),
+ ),
+ mcp.WithString("cveId",
+ mcp.Description("Filter by CVE ID."),
+ ),
+ mcp.WithString("ecosystem",
+ mcp.Description("Filter by package ecosystem."),
+ mcp.Enum("actions", "composer", "erlang", "go", "maven", "npm", "nuget", "other", "pip", "pub", "rubygems", "rust"),
+ ),
+ mcp.WithString("severity",
+ mcp.Description("Filter by severity."),
+ mcp.Enum("unknown", "low", "medium", "high", "critical"),
+ ),
+ mcp.WithArray("cwes",
+ mcp.Description("Filter by Common Weakness Enumeration IDs (e.g. [\"79\", \"284\", \"22\"])."),
+ mcp.Items(map[string]any{
+ "type": "string",
+ }),
+ ),
+ mcp.WithBoolean("isWithdrawn",
+ mcp.Description("Whether to only return withdrawn advisories."),
+ ),
+ mcp.WithString("affects",
+ mcp.Description("Filter advisories by affected package or version (e.g. \"package1,package2@1.0.0\")."),
+ ),
+ mcp.WithString("published",
+ mcp.Description("Filter by publish date or date range (ISO 8601 date or range)."),
+ ),
+ mcp.WithString("updated",
+ mcp.Description("Filter by update date or date range (ISO 8601 date or range)."),
+ ),
+ mcp.WithString("modified",
+ mcp.Description("Filter by publish or update date or date range (ISO 8601 date or range)."),
+ ),
+ ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ client, err := getClient(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+ }
+
+ ghsaID, err := OptionalParam[string](request, "ghsaId")
+ if err != nil {
+ return mcp.NewToolResultError(fmt.Sprintf("invalid ghsaId: %v", err)), nil
+ }
+
+ typ, err := OptionalParam[string](request, "type")
+ if err != nil {
+ return mcp.NewToolResultError(fmt.Sprintf("invalid type: %v", err)), nil
+ }
+
+ cveID, err := OptionalParam[string](request, "cveId")
+ if err != nil {
+ return mcp.NewToolResultError(fmt.Sprintf("invalid cveId: %v", err)), nil
+ }
+
+ eco, err := OptionalParam[string](request, "ecosystem")
+ if err != nil {
+ return mcp.NewToolResultError(fmt.Sprintf("invalid ecosystem: %v", err)), nil
+ }
+
+ sev, err := OptionalParam[string](request, "severity")
+ if err != nil {
+ return mcp.NewToolResultError(fmt.Sprintf("invalid severity: %v", err)), nil
+ }
+
+ cwes, err := OptionalParam[[]string](request, "cwes")
+ if err != nil {
+ return mcp.NewToolResultError(fmt.Sprintf("invalid cwes: %v", err)), nil
+ }
+
+ isWithdrawn, err := OptionalParam[bool](request, "isWithdrawn")
+ if err != nil {
+ return mcp.NewToolResultError(fmt.Sprintf("invalid isWithdrawn: %v", err)), nil
+ }
+
+ affects, err := OptionalParam[string](request, "affects")
+ if err != nil {
+ return mcp.NewToolResultError(fmt.Sprintf("invalid affects: %v", err)), nil
+ }
+
+ published, err := OptionalParam[string](request, "published")
+ if err != nil {
+ return mcp.NewToolResultError(fmt.Sprintf("invalid published: %v", err)), nil
+ }
+
+ updated, err := OptionalParam[string](request, "updated")
+ if err != nil {
+ return mcp.NewToolResultError(fmt.Sprintf("invalid updated: %v", err)), nil
+ }
+
+ modified, err := OptionalParam[string](request, "modified")
+ if err != nil {
+ return mcp.NewToolResultError(fmt.Sprintf("invalid modified: %v", err)), nil
+ }
+
+ opts := &github.ListGlobalSecurityAdvisoriesOptions{}
+
+ if ghsaID != "" {
+ opts.GHSAID = &ghsaID
+ }
+ if typ != "" {
+ opts.Type = &typ
+ }
+ if cveID != "" {
+ opts.CVEID = &cveID
+ }
+ if eco != "" {
+ opts.Ecosystem = &eco
+ }
+ if sev != "" {
+ opts.Severity = &sev
+ }
+ if len(cwes) > 0 {
+ opts.CWEs = cwes
+ }
+
+ if isWithdrawn {
+ opts.IsWithdrawn = &isWithdrawn
+ }
+
+ if affects != "" {
+ opts.Affects = &affects
+ }
+ if published != "" {
+ opts.Published = &published
+ }
+ if updated != "" {
+ opts.Updated = &updated
+ }
+ if modified != "" {
+ opts.Modified = &modified
+ }
+
+ advisories, resp, err := client.SecurityAdvisories.ListGlobalSecurityAdvisories(ctx, opts)
+ if err != nil {
+ return nil, fmt.Errorf("failed to list global security advisories: %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 advisories: %s", string(body))), nil
+ }
+
+ r, err := json.Marshal(advisories)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal advisories: %w", err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+ }
+}
+
+func ListRepositorySecurityAdvisories(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
+ return mcp.NewTool("list_repository_security_advisories",
+ mcp.WithDescription(t("TOOL_LIST_REPOSITORY_SECURITY_ADVISORIES_DESCRIPTION", "List repository security advisories for a GitHub repository.")),
+ mcp.WithToolAnnotation(mcp.ToolAnnotation{
+ Title: t("TOOL_LIST_REPOSITORY_SECURITY_ADVISORIES_USER_TITLE", "List repository security advisories"),
+ 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("direction",
+ mcp.Description("Sort direction."),
+ mcp.Enum("asc", "desc"),
+ ),
+ mcp.WithString("sort",
+ mcp.Description("Sort field."),
+ mcp.Enum("created", "updated", "published"),
+ ),
+ mcp.WithString("state",
+ mcp.Description("Filter by advisory state."),
+ mcp.Enum("triage", "draft", "published", "closed"),
+ ),
+ ), 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
+ }
+
+ direction, err := OptionalParam[string](request, "direction")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ sortField, err := OptionalParam[string](request, "sort")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ state, err := OptionalParam[string](request, "state")
+ 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)
+ }
+
+ opts := &github.ListRepositorySecurityAdvisoriesOptions{}
+ if direction != "" {
+ opts.Direction = direction
+ }
+ if sortField != "" {
+ opts.Sort = sortField
+ }
+ if state != "" {
+ opts.State = state
+ }
+
+ advisories, resp, err := client.SecurityAdvisories.ListRepositorySecurityAdvisories(ctx, owner, repo, opts)
+ if err != nil {
+ return nil, fmt.Errorf("failed to list repository security advisories: %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 repository advisories: %s", string(body))), nil
+ }
+
+ r, err := json.Marshal(advisories)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal advisories: %w", err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+ }
+}
+
+func GetGlobalSecurityAdvisory(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
+ return mcp.NewTool("get_global_security_advisory",
+ mcp.WithDescription(t("TOOL_GET_GLOBAL_SECURITY_ADVISORY_DESCRIPTION", "Get a global security advisory")),
+ mcp.WithToolAnnotation(mcp.ToolAnnotation{
+ Title: t("TOOL_GET_GLOBAL_SECURITY_ADVISORY_USER_TITLE", "Get a global security advisory"),
+ ReadOnlyHint: ToBoolPtr(true),
+ }),
+ mcp.WithString("ghsaId",
+ mcp.Description("GitHub Security Advisory ID (format: GHSA-xxxx-xxxx-xxxx)."),
+ mcp.Required(),
+ ),
+ ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ client, err := getClient(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+ }
+
+ ghsaID, err := RequiredParam[string](request, "ghsaId")
+ if err != nil {
+ return mcp.NewToolResultError(fmt.Sprintf("invalid ghsaId: %v", err)), nil
+ }
+
+ advisory, resp, err := client.SecurityAdvisories.GetGlobalSecurityAdvisories(ctx, ghsaID)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get advisory: %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 advisory: %s", string(body))), nil
+ }
+
+ r, err := json.Marshal(advisory)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal advisory: %w", err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+ }
+}
+
+func ListOrgRepositorySecurityAdvisories(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
+ return mcp.NewTool("list_org_repository_security_advisories",
+ mcp.WithDescription(t("TOOL_LIST_ORG_REPOSITORY_SECURITY_ADVISORIES_DESCRIPTION", "List repository security advisories for a GitHub organization.")),
+ mcp.WithToolAnnotation(mcp.ToolAnnotation{
+ Title: t("TOOL_LIST_ORG_REPOSITORY_SECURITY_ADVISORIES_USER_TITLE", "List org repository security advisories"),
+ ReadOnlyHint: ToBoolPtr(true),
+ }),
+ mcp.WithString("org",
+ mcp.Required(),
+ mcp.Description("The organization login."),
+ ),
+ mcp.WithString("direction",
+ mcp.Description("Sort direction."),
+ mcp.Enum("asc", "desc"),
+ ),
+ mcp.WithString("sort",
+ mcp.Description("Sort field."),
+ mcp.Enum("created", "updated", "published"),
+ ),
+ mcp.WithString("state",
+ mcp.Description("Filter by advisory state."),
+ mcp.Enum("triage", "draft", "published", "closed"),
+ ),
+ ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ org, err := RequiredParam[string](request, "org")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ direction, err := OptionalParam[string](request, "direction")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ sortField, err := OptionalParam[string](request, "sort")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ state, err := OptionalParam[string](request, "state")
+ 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)
+ }
+
+ opts := &github.ListRepositorySecurityAdvisoriesOptions{}
+ if direction != "" {
+ opts.Direction = direction
+ }
+ if sortField != "" {
+ opts.Sort = sortField
+ }
+ if state != "" {
+ opts.State = state
+ }
+
+ advisories, resp, err := client.SecurityAdvisories.ListRepositorySecurityAdvisoriesForOrg(ctx, org, opts)
+ if err != nil {
+ return nil, fmt.Errorf("failed to list organization repository security advisories: %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 organization repository advisories: %s", string(body))), nil
+ }
+
+ r, err := json.Marshal(advisories)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal advisories: %w", err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+ }
+}
diff --git a/pkg/github/security_advisories_test.go b/pkg/github/security_advisories_test.go
new file mode 100644
index 000000000..0640f917d
--- /dev/null
+++ b/pkg/github/security_advisories_test.go
@@ -0,0 +1,526 @@
+package github
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "testing"
+
+ "github.com/github/github-mcp-server/pkg/translations"
+ "github.com/google/go-github/v74/github"
+ "github.com/migueleliasweb/go-github-mock/src/mock"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func Test_ListGlobalSecurityAdvisories(t *testing.T) {
+ mockClient := github.NewClient(nil)
+ tool, _ := ListGlobalSecurityAdvisories(stubGetClientFn(mockClient), translations.NullTranslationHelper)
+
+ assert.Equal(t, "list_global_security_advisories", tool.Name)
+ assert.NotEmpty(t, tool.Description)
+ assert.Contains(t, tool.InputSchema.Properties, "ecosystem")
+ assert.Contains(t, tool.InputSchema.Properties, "severity")
+ assert.Contains(t, tool.InputSchema.Properties, "ghsaId")
+ assert.ElementsMatch(t, tool.InputSchema.Required, []string{})
+
+ // Setup mock advisory for success case
+ mockAdvisory := &github.GlobalSecurityAdvisory{
+ SecurityAdvisory: github.SecurityAdvisory{
+ GHSAID: github.Ptr("GHSA-xxxx-xxxx-xxxx"),
+ Summary: github.Ptr("Test advisory"),
+ Description: github.Ptr("This is a test advisory."),
+ Severity: github.Ptr("high"),
+ },
+ }
+
+ tests := []struct {
+ name string
+ mockedClient *http.Client
+ requestArgs map[string]interface{}
+ expectError bool
+ expectedAdvisories []*github.GlobalSecurityAdvisory
+ expectedErrMsg string
+ }{
+ {
+ name: "successful advisory fetch",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatch(
+ mock.GetAdvisories,
+ []*github.GlobalSecurityAdvisory{mockAdvisory},
+ ),
+ ),
+ requestArgs: map[string]interface{}{
+ "type": "reviewed",
+ "ecosystem": "npm",
+ "severity": "high",
+ },
+ expectError: false,
+ expectedAdvisories: []*github.GlobalSecurityAdvisory{mockAdvisory},
+ },
+ {
+ name: "invalid severity value",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.GetAdvisories,
+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusBadRequest)
+ _, _ = w.Write([]byte(`{"message": "Bad Request"}`))
+ }),
+ ),
+ ),
+ requestArgs: map[string]interface{}{
+ "type": "reviewed",
+ "severity": "extreme",
+ },
+ expectError: true,
+ expectedErrMsg: "failed to list global security advisories",
+ },
+ {
+ name: "API error handling",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.GetAdvisories,
+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusInternalServerError)
+ _, _ = w.Write([]byte(`{"message": "Internal Server Error"}`))
+ }),
+ ),
+ ),
+ requestArgs: map[string]interface{}{},
+ expectError: true,
+ expectedErrMsg: "failed to list global security advisories",
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ // Setup client with mock
+ client := github.NewClient(tc.mockedClient)
+ _, handler := ListGlobalSecurityAdvisories(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 returnedAdvisories []*github.GlobalSecurityAdvisory
+ err = json.Unmarshal([]byte(textContent.Text), &returnedAdvisories)
+ assert.NoError(t, err)
+ assert.Len(t, returnedAdvisories, len(tc.expectedAdvisories))
+ for i, advisory := range returnedAdvisories {
+ assert.Equal(t, *tc.expectedAdvisories[i].GHSAID, *advisory.GHSAID)
+ assert.Equal(t, *tc.expectedAdvisories[i].Summary, *advisory.Summary)
+ assert.Equal(t, *tc.expectedAdvisories[i].Description, *advisory.Description)
+ assert.Equal(t, *tc.expectedAdvisories[i].Severity, *advisory.Severity)
+ }
+ })
+ }
+}
+
+func Test_GetGlobalSecurityAdvisory(t *testing.T) {
+ mockClient := github.NewClient(nil)
+ tool, _ := GetGlobalSecurityAdvisory(stubGetClientFn(mockClient), translations.NullTranslationHelper)
+
+ assert.Equal(t, "get_global_security_advisory", tool.Name)
+ assert.NotEmpty(t, tool.Description)
+ assert.Contains(t, tool.InputSchema.Properties, "ghsaId")
+ assert.ElementsMatch(t, tool.InputSchema.Required, []string{"ghsaId"})
+
+ // Setup mock advisory for success case
+ mockAdvisory := &github.GlobalSecurityAdvisory{
+ SecurityAdvisory: github.SecurityAdvisory{
+ GHSAID: github.Ptr("GHSA-xxxx-xxxx-xxxx"),
+ Summary: github.Ptr("Test advisory"),
+ Description: github.Ptr("This is a test advisory."),
+ Severity: github.Ptr("high"),
+ },
+ }
+
+ tests := []struct {
+ name string
+ mockedClient *http.Client
+ requestArgs map[string]interface{}
+ expectError bool
+ expectedAdvisory *github.GlobalSecurityAdvisory
+ expectedErrMsg string
+ }{
+ {
+ name: "successful advisory fetch",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatch(
+ mock.GetAdvisoriesByGhsaId,
+ mockAdvisory,
+ ),
+ ),
+ requestArgs: map[string]interface{}{
+ "ghsaId": "GHSA-xxxx-xxxx-xxxx",
+ },
+ expectError: false,
+ expectedAdvisory: mockAdvisory,
+ },
+ {
+ name: "invalid ghsaId format",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.GetAdvisoriesByGhsaId,
+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusBadRequest)
+ _, _ = w.Write([]byte(`{"message": "Bad Request"}`))
+ }),
+ ),
+ ),
+ requestArgs: map[string]interface{}{
+ "ghsaId": "invalid-ghsa-id",
+ },
+ expectError: true,
+ expectedErrMsg: "failed to get advisory",
+ },
+ {
+ name: "advisory not found",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.GetAdvisoriesByGhsaId,
+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusNotFound)
+ _, _ = w.Write([]byte(`{"message": "Not Found"}`))
+ }),
+ ),
+ ),
+ requestArgs: map[string]interface{}{
+ "ghsaId": "GHSA-xxxx-xxxx-xxxx",
+ },
+ expectError: true,
+ expectedErrMsg: "failed to get advisory",
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ // Setup client with mock
+ client := github.NewClient(tc.mockedClient)
+ _, handler := GetGlobalSecurityAdvisory(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)
+
+ // Verify the result
+ assert.Contains(t, textContent.Text, *tc.expectedAdvisory.GHSAID)
+ assert.Contains(t, textContent.Text, *tc.expectedAdvisory.Summary)
+ assert.Contains(t, textContent.Text, *tc.expectedAdvisory.Description)
+ assert.Contains(t, textContent.Text, *tc.expectedAdvisory.Severity)
+ })
+ }
+}
+
+func Test_ListRepositorySecurityAdvisories(t *testing.T) {
+ // Verify tool definition once
+ mockClient := github.NewClient(nil)
+ tool, _ := ListRepositorySecurityAdvisories(stubGetClientFn(mockClient), translations.NullTranslationHelper)
+
+ assert.Equal(t, "list_repository_security_advisories", 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, "direction")
+ assert.Contains(t, tool.InputSchema.Properties, "sort")
+ assert.Contains(t, tool.InputSchema.Properties, "state")
+ assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"})
+
+ // Local endpoint pattern for repository security advisories
+ var GetReposSecurityAdvisoriesByOwnerByRepo = mock.EndpointPattern{
+ Pattern: "/repos/{owner}/{repo}/security-advisories",
+ Method: "GET",
+ }
+
+ // Setup mock advisories for success cases
+ adv1 := &github.SecurityAdvisory{
+ GHSAID: github.Ptr("GHSA-1111-1111-1111"),
+ Summary: github.Ptr("Repo advisory one"),
+ Description: github.Ptr("First repo advisory."),
+ Severity: github.Ptr("high"),
+ }
+ adv2 := &github.SecurityAdvisory{
+ GHSAID: github.Ptr("GHSA-2222-2222-2222"),
+ Summary: github.Ptr("Repo advisory two"),
+ Description: github.Ptr("Second repo advisory."),
+ Severity: github.Ptr("medium"),
+ }
+
+ tests := []struct {
+ name string
+ mockedClient *http.Client
+ requestArgs map[string]interface{}
+ expectError bool
+ expectedAdvisories []*github.SecurityAdvisory
+ expectedErrMsg string
+ }{
+ {
+ name: "successful advisories listing (no filters)",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ GetReposSecurityAdvisoriesByOwnerByRepo,
+ expect(t, expectations{
+ path: "/repos/owner/repo/security-advisories",
+ queryParams: map[string]string{},
+ }).andThen(
+ mockResponse(t, http.StatusOK, []*github.SecurityAdvisory{adv1, adv2}),
+ ),
+ ),
+ ),
+ requestArgs: map[string]interface{}{
+ "owner": "owner",
+ "repo": "repo",
+ },
+ expectError: false,
+ expectedAdvisories: []*github.SecurityAdvisory{adv1, adv2},
+ },
+ {
+ name: "successful advisories listing with filters",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ GetReposSecurityAdvisoriesByOwnerByRepo,
+ expect(t, expectations{
+ path: "/repos/octo/hello-world/security-advisories",
+ queryParams: map[string]string{
+ "direction": "desc",
+ "sort": "updated",
+ "state": "published",
+ },
+ }).andThen(
+ mockResponse(t, http.StatusOK, []*github.SecurityAdvisory{adv1}),
+ ),
+ ),
+ ),
+ requestArgs: map[string]interface{}{
+ "owner": "octo",
+ "repo": "hello-world",
+ "direction": "desc",
+ "sort": "updated",
+ "state": "published",
+ },
+ expectError: false,
+ expectedAdvisories: []*github.SecurityAdvisory{adv1},
+ },
+ {
+ name: "advisories listing fails",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ GetReposSecurityAdvisoriesByOwnerByRepo,
+ expect(t, expectations{
+ path: "/repos/owner/repo/security-advisories",
+ queryParams: map[string]string{},
+ }).andThen(
+ mockResponse(t, http.StatusInternalServerError, map[string]string{"message": "Internal Server Error"}),
+ ),
+ ),
+ ),
+ requestArgs: map[string]interface{}{
+ "owner": "owner",
+ "repo": "repo",
+ },
+ expectError: true,
+ expectedErrMsg: "failed to list repository security advisories",
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ client := github.NewClient(tc.mockedClient)
+ _, handler := ListRepositorySecurityAdvisories(stubGetClientFn(client), translations.NullTranslationHelper)
+
+ request := createMCPRequest(tc.requestArgs)
+
+ result, err := handler(context.Background(), request)
+
+ if tc.expectError {
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), tc.expectedErrMsg)
+ return
+ }
+
+ require.NoError(t, err)
+
+ textContent := getTextResult(t, result)
+
+ var returnedAdvisories []*github.SecurityAdvisory
+ err = json.Unmarshal([]byte(textContent.Text), &returnedAdvisories)
+ assert.NoError(t, err)
+ assert.Len(t, returnedAdvisories, len(tc.expectedAdvisories))
+ for i, advisory := range returnedAdvisories {
+ assert.Equal(t, *tc.expectedAdvisories[i].GHSAID, *advisory.GHSAID)
+ assert.Equal(t, *tc.expectedAdvisories[i].Summary, *advisory.Summary)
+ assert.Equal(t, *tc.expectedAdvisories[i].Description, *advisory.Description)
+ assert.Equal(t, *tc.expectedAdvisories[i].Severity, *advisory.Severity)
+ }
+ })
+ }
+}
+
+func Test_ListOrgRepositorySecurityAdvisories(t *testing.T) {
+ // Verify tool definition once
+ mockClient := github.NewClient(nil)
+ tool, _ := ListOrgRepositorySecurityAdvisories(stubGetClientFn(mockClient), translations.NullTranslationHelper)
+
+ assert.Equal(t, "list_org_repository_security_advisories", tool.Name)
+ assert.NotEmpty(t, tool.Description)
+ assert.Contains(t, tool.InputSchema.Properties, "org")
+ assert.Contains(t, tool.InputSchema.Properties, "direction")
+ assert.Contains(t, tool.InputSchema.Properties, "sort")
+ assert.Contains(t, tool.InputSchema.Properties, "state")
+ assert.ElementsMatch(t, tool.InputSchema.Required, []string{"org"})
+
+ // Endpoint pattern for org repository security advisories
+ var GetOrgsSecurityAdvisoriesByOrg = mock.EndpointPattern{
+ Pattern: "/orgs/{org}/security-advisories",
+ Method: "GET",
+ }
+
+ adv1 := &github.SecurityAdvisory{
+ GHSAID: github.Ptr("GHSA-aaaa-bbbb-cccc"),
+ Summary: github.Ptr("Org repo advisory 1"),
+ Description: github.Ptr("First advisory"),
+ Severity: github.Ptr("low"),
+ }
+ adv2 := &github.SecurityAdvisory{
+ GHSAID: github.Ptr("GHSA-dddd-eeee-ffff"),
+ Summary: github.Ptr("Org repo advisory 2"),
+ Description: github.Ptr("Second advisory"),
+ Severity: github.Ptr("critical"),
+ }
+
+ tests := []struct {
+ name string
+ mockedClient *http.Client
+ requestArgs map[string]interface{}
+ expectError bool
+ expectedAdvisories []*github.SecurityAdvisory
+ expectedErrMsg string
+ }{
+ {
+ name: "successful listing (no filters)",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ GetOrgsSecurityAdvisoriesByOrg,
+ expect(t, expectations{
+ path: "/orgs/octo/security-advisories",
+ queryParams: map[string]string{},
+ }).andThen(
+ mockResponse(t, http.StatusOK, []*github.SecurityAdvisory{adv1, adv2}),
+ ),
+ ),
+ ),
+ requestArgs: map[string]interface{}{
+ "org": "octo",
+ },
+ expectError: false,
+ expectedAdvisories: []*github.SecurityAdvisory{adv1, adv2},
+ },
+ {
+ name: "successful listing with filters",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ GetOrgsSecurityAdvisoriesByOrg,
+ expect(t, expectations{
+ path: "/orgs/octo/security-advisories",
+ queryParams: map[string]string{
+ "direction": "asc",
+ "sort": "created",
+ "state": "triage",
+ },
+ }).andThen(
+ mockResponse(t, http.StatusOK, []*github.SecurityAdvisory{adv1}),
+ ),
+ ),
+ ),
+ requestArgs: map[string]interface{}{
+ "org": "octo",
+ "direction": "asc",
+ "sort": "created",
+ "state": "triage",
+ },
+ expectError: false,
+ expectedAdvisories: []*github.SecurityAdvisory{adv1},
+ },
+ {
+ name: "listing fails",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ GetOrgsSecurityAdvisoriesByOrg,
+ expect(t, expectations{
+ path: "/orgs/octo/security-advisories",
+ queryParams: map[string]string{},
+ }).andThen(
+ mockResponse(t, http.StatusForbidden, map[string]string{"message": "Forbidden"}),
+ ),
+ ),
+ ),
+ requestArgs: map[string]interface{}{
+ "org": "octo",
+ },
+ expectError: true,
+ expectedErrMsg: "failed to list organization repository security advisories",
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ client := github.NewClient(tc.mockedClient)
+ _, handler := ListOrgRepositorySecurityAdvisories(stubGetClientFn(client), translations.NullTranslationHelper)
+
+ request := createMCPRequest(tc.requestArgs)
+
+ result, err := handler(context.Background(), request)
+
+ if tc.expectError {
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), tc.expectedErrMsg)
+ return
+ }
+
+ require.NoError(t, err)
+
+ textContent := getTextResult(t, result)
+
+ var returnedAdvisories []*github.SecurityAdvisory
+ err = json.Unmarshal([]byte(textContent.Text), &returnedAdvisories)
+ assert.NoError(t, err)
+ assert.Len(t, returnedAdvisories, len(tc.expectedAdvisories))
+ for i, advisory := range returnedAdvisories {
+ assert.Equal(t, *tc.expectedAdvisories[i].GHSAID, *advisory.GHSAID)
+ assert.Equal(t, *tc.expectedAdvisories[i].Summary, *advisory.Summary)
+ assert.Equal(t, *tc.expectedAdvisories[i].Description, *advisory.Description)
+ assert.Equal(t, *tc.expectedAdvisories[i].Severity, *advisory.Severity)
+ }
+ })
+ }
+}
diff --git a/pkg/github/tools.go b/pkg/github/tools.go
index 513b93e42..728d78097 100644
--- a/pkg/github/tools.go
+++ b/pkg/github/tools.go
@@ -160,6 +160,14 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
toolsets.NewServerTool(DeleteWorkflowRunLogs(getClient, t)),
)
+ securityAdvisories := toolsets.NewToolset("security_advisories", "Security advisories related tools").
+ AddReadTools(
+ toolsets.NewServerTool(ListGlobalSecurityAdvisories(getClient, t)),
+ toolsets.NewServerTool(GetGlobalSecurityAdvisory(getClient, t)),
+ toolsets.NewServerTool(ListRepositorySecurityAdvisories(getClient, t)),
+ toolsets.NewServerTool(ListOrgRepositorySecurityAdvisories(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")
@@ -194,6 +202,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
tsg.AddToolset(experiments)
tsg.AddToolset(discussions)
tsg.AddToolset(gists)
+ tsg.AddToolset(securityAdvisories)
return tsg
}
|