Skip to content

Commit 1d83838

Browse files
committed
Add Global Security Advisories Toolset
1 parent c2d5b43 commit 1d83838

File tree

4 files changed

+468
-0
lines changed

4 files changed

+468
-0
lines changed

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,29 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
666666
- `repo`: Repository name (string, required)
667667
- `prNumber`: Pull request number (string, required)
668668
- `path`: File or directory path (string, optional)
669+
670+
## Security Advisories
671+
672+
- **`list_global_security_advisories`**
673+
List global security advisories
674+
675+
- **Parameters**:
676+
- * `ghsaId`: Filter by GitHub Security Advisory ID (string, optional – format: `GHSA-xxxx-xxxx-xxxx`)
677+
- * `type`: Advisory type (string, optional – one of `reviewed`, `malware`, `unreviewed`)
678+
- * `cveId`: Filter by CVE ID (string, optional)
679+
- * `ecosystem`: Filter by package ecosystem (string, optional – one of `actions`, `composer`, `erlang`, `go`, `maven`, `npm`, `nuget`, `other`, `pip`, `pub`, `rubygems`, `rust`)
680+
- * `severity`: Filter by severity (string, optional – one of `unknown`, `low`, `medium`, `high`, `critical`)
681+
- * `cwes`: Filter by Common Weakness Enumeration IDs (array of strings, optional – e.g. `["79", "284", "22"]`)
682+
- * `isWithdrawn`: Whether to only return withdrawn advisories (boolean, optional)
683+
- * `affects`: Filter advisories by affected package or version (string, optional – e.g. `"package1,package2@1.0.0"`)
684+
- * `published`: Filter by publish date or date range (string, optional – ISO 8601 date or range)
685+
- * `updated`: Filter by update date or date range (string, optional – ISO 8601 date or range)
686+
- * `modified`: Filter by publish or update date or date range (string, optional – ISO 8601 date or range)
687+
688+
- **`get_global_security_advisory`**
689+
Get a global security advisory
690+
691+
- **Template**: `advisories/{ghsaId}`
669692

670693
## Library Usage
671694

pkg/github/security_advisories.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
10+
"github.com/github/github-mcp-server/pkg/translations"
11+
"github.com/google/go-github/v72/github"
12+
"github.com/mark3labs/mcp-go/mcp"
13+
"github.com/mark3labs/mcp-go/server"
14+
)
15+
16+
func ListGlobalSecurityAdvisories(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
17+
return mcp.NewTool("list_global_security_advisories",
18+
mcp.WithDescription(t("TOOL_LIST_GLOBAL_SECURITY_ADVISORIES_DESCRIPTION", "List global security advisories from GitHub.")),
19+
mcp.WithToolAnnotation(mcp.ToolAnnotation{
20+
Title: t("TOOL_LIST_GLOBAL_SECURITY_ADVISORIES_USER_TITLE", "List global security advisories"),
21+
ReadOnlyHint: toBoolPtr(true),
22+
}),
23+
mcp.WithString("ghsaId",
24+
mcp.Description("Filter by GitHub Security Advisory ID (format: GHSA-xxxx-xxxx-xxxx)."),
25+
),
26+
mcp.WithString("type",
27+
mcp.Description("Advisory type."),
28+
mcp.Enum("reviewed", "malware", "unreviewed"),
29+
),
30+
mcp.WithString("cveId",
31+
mcp.Description("Filter by CVE ID."),
32+
),
33+
mcp.WithString("ecosystem",
34+
mcp.Description("Filter by package ecosystem."),
35+
mcp.Enum("actions", "composer", "erlang", "go", "maven", "npm", "nuget", "other", "pip", "pub", "rubygems", "rust"),
36+
),
37+
mcp.WithString("severity",
38+
mcp.Description("Filter by severity."),
39+
mcp.Enum("unknown", "low", "medium", "high", "critical"),
40+
),
41+
mcp.WithArray("cwes",
42+
mcp.Description("Filter by Common Weakness Enumeration IDs (e.g. [\"79\", \"284\", \"22\"])."),
43+
),
44+
mcp.WithBoolean("isWithdrawn",
45+
mcp.Description("Whether to only return withdrawn advisories."),
46+
),
47+
mcp.WithString("affects",
48+
mcp.Description("Filter advisories by affected package or version (e.g. \"package1,package2@1.0.0\")."),
49+
),
50+
mcp.WithString("published",
51+
mcp.Description("Filter by publish date or date range (ISO 8601 date or range)."),
52+
),
53+
mcp.WithString("updated",
54+
mcp.Description("Filter by update date or date range (ISO 8601 date or range)."),
55+
),
56+
mcp.WithString("modified",
57+
mcp.Description("Filter by publish or update date or date range (ISO 8601 date or range)."),
58+
),
59+
), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
60+
client, err := getClient(ctx)
61+
if err != nil {
62+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
63+
}
64+
65+
ghsaID, err := OptionalParam[string](request, "ghsaId")
66+
if err != nil {
67+
return mcp.NewToolResultError(fmt.Sprintf("invalid ghsaId: %v", err)), nil
68+
}
69+
70+
typ, err := OptionalParam[string](request, "type")
71+
if err != nil {
72+
return mcp.NewToolResultError(fmt.Sprintf("invalid type: %v", err)), nil
73+
}
74+
75+
cveID, err := OptionalParam[string](request, "cveId")
76+
if err != nil {
77+
return mcp.NewToolResultError(fmt.Sprintf("invalid cveId: %v", err)), nil
78+
}
79+
80+
eco, err := OptionalParam[string](request, "ecosystem")
81+
if err != nil {
82+
return mcp.NewToolResultError(fmt.Sprintf("invalid ecosystem: %v", err)), nil
83+
}
84+
85+
sev, err := OptionalParam[string](request, "severity")
86+
if err != nil {
87+
return mcp.NewToolResultError(fmt.Sprintf("invalid severity: %v", err)), nil
88+
}
89+
90+
cwes, err := OptionalParam[[]string](request, "cwes")
91+
if err != nil {
92+
return mcp.NewToolResultError(fmt.Sprintf("invalid cwes: %v", err)), nil
93+
}
94+
95+
isWithdrawn, err := OptionalParam[bool](request, "isWithdrawn")
96+
if err != nil {
97+
return mcp.NewToolResultError(fmt.Sprintf("invalid isWithdrawn: %v", err)), nil
98+
}
99+
100+
affects, err := OptionalParam[string](request, "affects")
101+
if err != nil {
102+
return mcp.NewToolResultError(fmt.Sprintf("invalid affects: %v", err)), nil
103+
}
104+
105+
published, err := OptionalParam[string](request, "published")
106+
if err != nil {
107+
return mcp.NewToolResultError(fmt.Sprintf("invalid published: %v", err)), nil
108+
}
109+
110+
updated, err := OptionalParam[string](request, "updated")
111+
if err != nil {
112+
return mcp.NewToolResultError(fmt.Sprintf("invalid updated: %v", err)), nil
113+
}
114+
115+
modified, err := OptionalParam[string](request, "modified")
116+
if err != nil {
117+
return mcp.NewToolResultError(fmt.Sprintf("invalid modified: %v", err)), nil
118+
}
119+
120+
advisories, resp, err := client.SecurityAdvisories.ListGlobalSecurityAdvisories(ctx, &github.ListGlobalSecurityAdvisoriesOptions{
121+
GHSAID: &ghsaID,
122+
Type: &typ,
123+
CVEID: &cveID,
124+
Ecosystem: &eco,
125+
Severity: &sev,
126+
CWEs: cwes,
127+
IsWithdrawn: &isWithdrawn,
128+
Affects: &affects,
129+
Published: &published,
130+
Updated: &updated,
131+
Modified: &modified,
132+
})
133+
if err != nil {
134+
return nil, fmt.Errorf("failed to list global security advisories: %w", err)
135+
}
136+
defer func() { _ = resp.Body.Close() }()
137+
138+
if resp.StatusCode != http.StatusOK {
139+
body, err := io.ReadAll(resp.Body)
140+
if err != nil {
141+
return nil, fmt.Errorf("failed to read response body: %w", err)
142+
}
143+
return mcp.NewToolResultError(fmt.Sprintf("failed to list advisories: %s", string(body))), nil
144+
}
145+
146+
r, err := json.Marshal(advisories)
147+
if err != nil {
148+
return nil, fmt.Errorf("failed to marshal advisories: %w", err)
149+
}
150+
151+
return mcp.NewToolResultText(string(r)), nil
152+
}
153+
}
154+
155+
func GetGlobalSecurityAdvisory(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
156+
return mcp.NewTool("get_global_security_advisory",
157+
mcp.WithDescription(t("TOOL_GET_GLOBAL_SECURITY_ADVISORY_DESCRIPTION", "Get a global security advisory")),
158+
mcp.WithToolAnnotation(mcp.ToolAnnotation{
159+
Title: t("TOOL_GET_GLOBAL_SECURITY_ADVISORY_USER_TITLE", "Get a global security advisory"),
160+
ReadOnlyHint: toBoolPtr(true),
161+
}),
162+
mcp.WithString("ghsaId",
163+
mcp.Description("GitHub Security Advisory ID (format: GHSA-xxxx-xxxx-xxxx)."),
164+
mcp.Required(),
165+
),
166+
), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
167+
client, err := getClient(ctx)
168+
if err != nil {
169+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
170+
}
171+
172+
ghsaID, err := requiredParam[string](request, "ghsaId")
173+
if err != nil {
174+
return mcp.NewToolResultError(fmt.Sprintf("invalid ghsaId: %v", err)), nil
175+
}
176+
177+
advisory, resp, err := client.SecurityAdvisories.GetGlobalSecurityAdvisories(ctx, ghsaID)
178+
if err != nil {
179+
return nil, fmt.Errorf("failed to get advisory: %w", err)
180+
}
181+
defer func() { _ = resp.Body.Close() }()
182+
183+
if resp.StatusCode != http.StatusOK {
184+
body, err := io.ReadAll(resp.Body)
185+
if err != nil {
186+
return nil, fmt.Errorf("failed to read response body: %w", err)
187+
}
188+
return mcp.NewToolResultError(fmt.Sprintf("failed to get advisory: %s", string(body))), nil
189+
}
190+
191+
r, err := json.Marshal(advisory)
192+
if err != nil {
193+
return nil, fmt.Errorf("failed to marshal advisory: %w", err)
194+
}
195+
196+
return mcp.NewToolResultText(string(r)), nil
197+
}
198+
}

0 commit comments

Comments
 (0)