Skip to content

Commit 2afd7e4

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

File tree

4 files changed

+497
-0
lines changed

4 files changed

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

0 commit comments

Comments
 (0)