Skip to content

Commit 07bb1fa

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

File tree

4 files changed

+471
-0
lines changed

4 files changed

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

0 commit comments

Comments
 (0)