c06ace34cc
* Initial plan * Migrate security_advisories toolset to modelcontextprotocol/go-sdk Co-authored-by: omgitsads <4619+omgitsads@users.noreply.github.com> * Add toolsnaps tests and snapshots for security_advisories Co-authored-by: omgitsads <4619+omgitsads@users.noreply.github.com> * Dont bubble up an error for getClient We should do this eventually, but to keep the existing behavior, we just return the error to the client. * re-add security_advisories toolset * Revert this change from the base PR --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: omgitsads <4619+omgitsads@users.noreply.github.com> Co-authored-by: Adam Holt <me@adamholt.co.uk> Co-authored-by: LuluBeatson <lulubeatson@github.com>
459 lines
14 KiB
Go
459 lines
14 KiB
Go
package github
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
|
|
"github.com/github/github-mcp-server/pkg/translations"
|
|
"github.com/github/github-mcp-server/pkg/utils"
|
|
"github.com/google/go-github/v79/github"
|
|
"github.com/google/jsonschema-go/jsonschema"
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
func ListGlobalSecurityAdvisories(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
|
|
tool := mcp.Tool{
|
|
Name: "list_global_security_advisories",
|
|
Description: t("TOOL_LIST_GLOBAL_SECURITY_ADVISORIES_DESCRIPTION", "List global security advisories from GitHub."),
|
|
Annotations: &mcp.ToolAnnotations{
|
|
Title: t("TOOL_LIST_GLOBAL_SECURITY_ADVISORIES_USER_TITLE", "List global security advisories"),
|
|
ReadOnlyHint: true,
|
|
},
|
|
InputSchema: &jsonschema.Schema{
|
|
Type: "object",
|
|
Properties: map[string]*jsonschema.Schema{
|
|
"ghsaId": {
|
|
Type: "string",
|
|
Description: "Filter by GitHub Security Advisory ID (format: GHSA-xxxx-xxxx-xxxx).",
|
|
},
|
|
"type": {
|
|
Type: "string",
|
|
Description: "Advisory type.",
|
|
Enum: []any{"reviewed", "malware", "unreviewed"},
|
|
Default: json.RawMessage(`"reviewed"`),
|
|
},
|
|
"cveId": {
|
|
Type: "string",
|
|
Description: "Filter by CVE ID.",
|
|
},
|
|
"ecosystem": {
|
|
Type: "string",
|
|
Description: "Filter by package ecosystem.",
|
|
Enum: []any{"actions", "composer", "erlang", "go", "maven", "npm", "nuget", "other", "pip", "pub", "rubygems", "rust"},
|
|
},
|
|
"severity": {
|
|
Type: "string",
|
|
Description: "Filter by severity.",
|
|
Enum: []any{"unknown", "low", "medium", "high", "critical"},
|
|
},
|
|
"cwes": {
|
|
Type: "array",
|
|
Description: "Filter by Common Weakness Enumeration IDs (e.g. [\"79\", \"284\", \"22\"]).",
|
|
Items: &jsonschema.Schema{
|
|
Type: "string",
|
|
},
|
|
},
|
|
"isWithdrawn": {
|
|
Type: "boolean",
|
|
Description: "Whether to only return withdrawn advisories.",
|
|
},
|
|
"affects": {
|
|
Type: "string",
|
|
Description: "Filter advisories by affected package or version (e.g. \"package1,package2@1.0.0\").",
|
|
},
|
|
"published": {
|
|
Type: "string",
|
|
Description: "Filter by publish date or date range (ISO 8601 date or range).",
|
|
},
|
|
"updated": {
|
|
Type: "string",
|
|
Description: "Filter by update date or date range (ISO 8601 date or range).",
|
|
},
|
|
"modified": {
|
|
Type: "string",
|
|
Description: "Filter by publish or update date or date range (ISO 8601 date or range).",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
|
|
client, err := getClient(ctx)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
|
|
}
|
|
|
|
ghsaID, err := OptionalParam[string](args, "ghsaId")
|
|
if err != nil {
|
|
return utils.NewToolResultError(fmt.Sprintf("invalid ghsaId: %v", err)), nil, nil
|
|
}
|
|
|
|
typ, err := OptionalParam[string](args, "type")
|
|
if err != nil {
|
|
return utils.NewToolResultError(fmt.Sprintf("invalid type: %v", err)), nil, nil
|
|
}
|
|
|
|
cveID, err := OptionalParam[string](args, "cveId")
|
|
if err != nil {
|
|
return utils.NewToolResultError(fmt.Sprintf("invalid cveId: %v", err)), nil, nil
|
|
}
|
|
|
|
eco, err := OptionalParam[string](args, "ecosystem")
|
|
if err != nil {
|
|
return utils.NewToolResultError(fmt.Sprintf("invalid ecosystem: %v", err)), nil, nil
|
|
}
|
|
|
|
sev, err := OptionalParam[string](args, "severity")
|
|
if err != nil {
|
|
return utils.NewToolResultError(fmt.Sprintf("invalid severity: %v", err)), nil, nil
|
|
}
|
|
|
|
cwes, err := OptionalStringArrayParam(args, "cwes")
|
|
if err != nil {
|
|
return utils.NewToolResultError(fmt.Sprintf("invalid cwes: %v", err)), nil, nil
|
|
}
|
|
|
|
isWithdrawn, err := OptionalParam[bool](args, "isWithdrawn")
|
|
if err != nil {
|
|
return utils.NewToolResultError(fmt.Sprintf("invalid isWithdrawn: %v", err)), nil, nil
|
|
}
|
|
|
|
affects, err := OptionalParam[string](args, "affects")
|
|
if err != nil {
|
|
return utils.NewToolResultError(fmt.Sprintf("invalid affects: %v", err)), nil, nil
|
|
}
|
|
|
|
published, err := OptionalParam[string](args, "published")
|
|
if err != nil {
|
|
return utils.NewToolResultError(fmt.Sprintf("invalid published: %v", err)), nil, nil
|
|
}
|
|
|
|
updated, err := OptionalParam[string](args, "updated")
|
|
if err != nil {
|
|
return utils.NewToolResultError(fmt.Sprintf("invalid updated: %v", err)), nil, nil
|
|
}
|
|
|
|
modified, err := OptionalParam[string](args, "modified")
|
|
if err != nil {
|
|
return utils.NewToolResultError(fmt.Sprintf("invalid modified: %v", err)), nil, 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, 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, nil, fmt.Errorf("failed to read response body: %w", err)
|
|
}
|
|
return utils.NewToolResultError(fmt.Sprintf("failed to list advisories: %s", string(body))), nil, nil
|
|
}
|
|
|
|
r, err := json.Marshal(advisories)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to marshal advisories: %w", err)
|
|
}
|
|
|
|
return utils.NewToolResultText(string(r)), nil, nil
|
|
})
|
|
|
|
return tool, handler
|
|
}
|
|
|
|
func ListRepositorySecurityAdvisories(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
|
|
tool := mcp.Tool{
|
|
Name: "list_repository_security_advisories",
|
|
Description: t("TOOL_LIST_REPOSITORY_SECURITY_ADVISORIES_DESCRIPTION", "List repository security advisories for a GitHub repository."),
|
|
Annotations: &mcp.ToolAnnotations{
|
|
Title: t("TOOL_LIST_REPOSITORY_SECURITY_ADVISORIES_USER_TITLE", "List repository security advisories"),
|
|
ReadOnlyHint: true,
|
|
},
|
|
InputSchema: &jsonschema.Schema{
|
|
Type: "object",
|
|
Properties: map[string]*jsonschema.Schema{
|
|
"owner": {
|
|
Type: "string",
|
|
Description: "The owner of the repository.",
|
|
},
|
|
"repo": {
|
|
Type: "string",
|
|
Description: "The name of the repository.",
|
|
},
|
|
"direction": {
|
|
Type: "string",
|
|
Description: "Sort direction.",
|
|
Enum: []any{"asc", "desc"},
|
|
},
|
|
"sort": {
|
|
Type: "string",
|
|
Description: "Sort field.",
|
|
Enum: []any{"created", "updated", "published"},
|
|
},
|
|
"state": {
|
|
Type: "string",
|
|
Description: "Filter by advisory state.",
|
|
Enum: []any{"triage", "draft", "published", "closed"},
|
|
},
|
|
},
|
|
Required: []string{"owner", "repo"},
|
|
},
|
|
}
|
|
|
|
handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
|
|
owner, err := RequiredParam[string](args, "owner")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
repo, err := RequiredParam[string](args, "repo")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
|
|
direction, err := OptionalParam[string](args, "direction")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
sortField, err := OptionalParam[string](args, "sort")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
state, err := OptionalParam[string](args, "state")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
|
|
client, err := getClient(ctx)
|
|
if err != nil {
|
|
return nil, 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, 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, nil, fmt.Errorf("failed to read response body: %w", err)
|
|
}
|
|
return utils.NewToolResultError(fmt.Sprintf("failed to list repository advisories: %s", string(body))), nil, nil
|
|
}
|
|
|
|
r, err := json.Marshal(advisories)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to marshal advisories: %w", err)
|
|
}
|
|
|
|
return utils.NewToolResultText(string(r)), nil, nil
|
|
})
|
|
|
|
return tool, handler
|
|
}
|
|
|
|
func GetGlobalSecurityAdvisory(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
|
|
tool := mcp.Tool{
|
|
Name: "get_global_security_advisory",
|
|
Description: t("TOOL_GET_GLOBAL_SECURITY_ADVISORY_DESCRIPTION", "Get a global security advisory"),
|
|
Annotations: &mcp.ToolAnnotations{
|
|
Title: t("TOOL_GET_GLOBAL_SECURITY_ADVISORY_USER_TITLE", "Get a global security advisory"),
|
|
ReadOnlyHint: true,
|
|
},
|
|
InputSchema: &jsonschema.Schema{
|
|
Type: "object",
|
|
Properties: map[string]*jsonschema.Schema{
|
|
"ghsaId": {
|
|
Type: "string",
|
|
Description: "GitHub Security Advisory ID (format: GHSA-xxxx-xxxx-xxxx).",
|
|
},
|
|
},
|
|
Required: []string{"ghsaId"},
|
|
},
|
|
}
|
|
|
|
handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
|
|
client, err := getClient(ctx)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
|
|
}
|
|
|
|
ghsaID, err := RequiredParam[string](args, "ghsaId")
|
|
if err != nil {
|
|
return utils.NewToolResultError(fmt.Sprintf("invalid ghsaId: %v", err)), nil, nil
|
|
}
|
|
|
|
advisory, resp, err := client.SecurityAdvisories.GetGlobalSecurityAdvisories(ctx, ghsaID)
|
|
if err != nil {
|
|
return nil, 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, nil, fmt.Errorf("failed to read response body: %w", err)
|
|
}
|
|
return utils.NewToolResultError(fmt.Sprintf("failed to get advisory: %s", string(body))), nil, nil
|
|
}
|
|
|
|
r, err := json.Marshal(advisory)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to marshal advisory: %w", err)
|
|
}
|
|
|
|
return utils.NewToolResultText(string(r)), nil, nil
|
|
})
|
|
|
|
return tool, handler
|
|
}
|
|
|
|
func ListOrgRepositorySecurityAdvisories(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
|
|
tool := mcp.Tool{
|
|
Name: "list_org_repository_security_advisories",
|
|
Description: t("TOOL_LIST_ORG_REPOSITORY_SECURITY_ADVISORIES_DESCRIPTION", "List repository security advisories for a GitHub organization."),
|
|
Annotations: &mcp.ToolAnnotations{
|
|
Title: t("TOOL_LIST_ORG_REPOSITORY_SECURITY_ADVISORIES_USER_TITLE", "List org repository security advisories"),
|
|
ReadOnlyHint: true,
|
|
},
|
|
InputSchema: &jsonschema.Schema{
|
|
Type: "object",
|
|
Properties: map[string]*jsonschema.Schema{
|
|
"org": {
|
|
Type: "string",
|
|
Description: "The organization login.",
|
|
},
|
|
"direction": {
|
|
Type: "string",
|
|
Description: "Sort direction.",
|
|
Enum: []any{"asc", "desc"},
|
|
},
|
|
"sort": {
|
|
Type: "string",
|
|
Description: "Sort field.",
|
|
Enum: []any{"created", "updated", "published"},
|
|
},
|
|
"state": {
|
|
Type: "string",
|
|
Description: "Filter by advisory state.",
|
|
Enum: []any{"triage", "draft", "published", "closed"},
|
|
},
|
|
},
|
|
Required: []string{"org"},
|
|
},
|
|
}
|
|
|
|
handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
|
|
org, err := RequiredParam[string](args, "org")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
direction, err := OptionalParam[string](args, "direction")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
sortField, err := OptionalParam[string](args, "sort")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
state, err := OptionalParam[string](args, "state")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
|
|
client, err := getClient(ctx)
|
|
if err != nil {
|
|
return nil, 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, 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, nil, fmt.Errorf("failed to read response body: %w", err)
|
|
}
|
|
return utils.NewToolResultError(fmt.Sprintf("failed to list organization repository advisories: %s", string(body))), nil, nil
|
|
}
|
|
|
|
r, err := json.Marshal(advisories)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to marshal advisories: %w", err)
|
|
}
|
|
|
|
return utils.NewToolResultText(string(r)), nil, nil
|
|
})
|
|
|
|
return tool, handler
|
|
}
|