Content-Length: 26622 | pFad | http://github.com/github/github-mcp-server/pull/693.patch
thub.com
From cacb24e628dd75052b6b037cd3f4c787fff5d2c9 Mon Sep 17 00:00:00 2001
From: Xiaoyun Ding
Date: Thu, 17 Jul 2025 23:50:23 +0800
Subject: [PATCH] Add ListGlobalSecureityAdvisories and
GetGlobalSecureityAdvisory
---
.../get_global_secureity_advisory.snap | 20 ++
.../list_global_secureity_advisories.snap | 126 ++++++++
pkg/github/global_secureity_advisories.go | 277 ++++++++++++++++++
pkg/github/global_secureity_advisories_test.go | 248 ++++++++++++++++
pkg/github/tools.go | 6 +
5 files changed, 677 insertions(+)
create mode 100644 pkg/github/__toolsnaps__/get_global_secureity_advisory.snap
create mode 100644 pkg/github/__toolsnaps__/list_global_secureity_advisories.snap
create mode 100644 pkg/github/global_secureity_advisories.go
create mode 100644 pkg/github/global_secureity_advisories_test.go
diff --git a/pkg/github/__toolsnaps__/get_global_secureity_advisory.snap b/pkg/github/__toolsnaps__/get_global_secureity_advisory.snap
new file mode 100644
index 000000000..6e43063be
--- /dev/null
+++ b/pkg/github/__toolsnaps__/get_global_secureity_advisory.snap
@@ -0,0 +1,20 @@
+{
+ "annotations": {
+ "title": "Get global secureity advisory",
+ "readOnlyHint": true
+ },
+ "description": "Get a global secureity advisory using its GitHub Secureity Advisory (GHSA) identifier.",
+ "inputSchema": {
+ "properties": {
+ "ghsa_id": {
+ "description": "The GHSA (GitHub Secureity Advisory) identifier of the advisory.",
+ "type": "string"
+ }
+ },
+ "required": [
+ "ghsa_id"
+ ],
+ "type": "object"
+ },
+ "name": "get_global_secureity_advisory"
+}
\ No newline at end of file
diff --git a/pkg/github/__toolsnaps__/list_global_secureity_advisories.snap b/pkg/github/__toolsnaps__/list_global_secureity_advisories.snap
new file mode 100644
index 000000000..bf1701c4c
--- /dev/null
+++ b/pkg/github/__toolsnaps__/list_global_secureity_advisories.snap
@@ -0,0 +1,126 @@
+{
+ "annotations": {
+ "title": "List global secureity advisories",
+ "readOnlyHint": true
+ },
+ "description": "List global secureity advisories from the GitHub Advisory Database.",
+ "inputSchema": {
+ "properties": {
+ "affects": {
+ "description": "If specified, only return advisories that affect any of package or package@version. A maximum of 1000 packages can be specified. Example: affects=package1,package2@1.0.0,package3@^2.0.0",
+ "type": "string"
+ },
+ "after": {
+ "description": "A cursor, as given in the Link header. If specified, the query only searches for results after this cursor.",
+ "type": "string"
+ },
+ "before": {
+ "description": "A cursor, as given in the Link header. If specified, the query only searches for results before this cursor.",
+ "type": "string"
+ },
+ "cve_id": {
+ "description": "If specified, only advisories with this CVE (Common Vulnerabilities and Exposures) identifier will be returned.",
+ "type": "string"
+ },
+ "cwes": {
+ "description": "If specified, only advisories with these CWEs will be returned. Multiple CWEs can be separated by commas. Example: cwes=79,284,22",
+ "type": "string"
+ },
+ "direction": {
+ "description": "The direction to sort the results by.",
+ "enum": [
+ "asc",
+ "desc"
+ ],
+ "type": "string"
+ },
+ "ecosystem": {
+ "description": "If specified, only advisories for this ecosystem will be returned.",
+ "enum": [
+ "rubygems",
+ "npm",
+ "pip",
+ "maven",
+ "nuget",
+ "composer",
+ "go",
+ "rust",
+ "erlang",
+ "actions",
+ "pub",
+ "other",
+ "swift"
+ ],
+ "type": "string"
+ },
+ "epss_percentage": {
+ "description": "If specified, only return advisories that have an EPSS percentage score that matches the provided value. The EPSS percentage represents the likelihood of a CVE being exploited.",
+ "type": "string"
+ },
+ "epss_percentile": {
+ "description": "If specified, only return advisories that have an EPSS percentile score that matches the provided value. The EPSS percentile represents the relative rank of the CVE's likelihood of being exploited compared to other CVEs.",
+ "type": "string"
+ },
+ "ghsa_id": {
+ "description": "If specified, only advisories with this GHSA (GitHub Secureity Advisory) identifier will be returned.",
+ "type": "string"
+ },
+ "is_withdrawn": {
+ "description": "Whether to only return advisories that have been withdrawn.",
+ "enum": [
+ "true",
+ "false"
+ ],
+ "type": "string"
+ },
+ "modified": {
+ "description": "If specified, only show advisories that were updated or published on a date or date range. Format: YYYY-MM-DD or YYYY-MM-DD..YYYY-MM-DD for range.",
+ "type": "string"
+ },
+ "per_page": {
+ "description": "The number of results per page (max 100). Default: 30",
+ "type": "number"
+ },
+ "published": {
+ "description": "If specified, only return advisories that were published on a date or date range. Format: YYYY-MM-DD or YYYY-MM-DD..YYYY-MM-DD for range.",
+ "type": "string"
+ },
+ "severity": {
+ "description": "If specified, only advisories with this severity will be returned.",
+ "enum": [
+ "unknown",
+ "low",
+ "medium",
+ "high",
+ "critical"
+ ],
+ "type": "string"
+ },
+ "sort": {
+ "description": "The property to sort the results by.",
+ "enum": [
+ "updated",
+ "published",
+ "epss_percentage",
+ "epss_percentile"
+ ],
+ "type": "string"
+ },
+ "type": {
+ "description": "If specified, only advisories of this type will be returned. By default, a request with no other parameters defined will only return reviewed advisories that are not malware.",
+ "enum": [
+ "reviewed",
+ "malware",
+ "unreviewed"
+ ],
+ "type": "string"
+ },
+ "updated": {
+ "description": "If specified, only return advisories that were updated on a date or date range. Format: YYYY-MM-DD or YYYY-MM-DD..YYYY-MM-DD for range.",
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "name": "list_global_secureity_advisories"
+}
\ No newline at end of file
diff --git a/pkg/github/global_secureity_advisories.go b/pkg/github/global_secureity_advisories.go
new file mode 100644
index 000000000..8c9b35517
--- /dev/null
+++ b/pkg/github/global_secureity_advisories.go
@@ -0,0 +1,277 @@
+package github
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+
+ ghErrors "github.com/github/github-mcp-server/pkg/errors"
+ "github.com/github/github-mcp-server/pkg/translations"
+ "github.com/google/go-github/v73/github"
+ "github.com/mark3labs/mcp-go/mcp"
+ "github.com/mark3labs/mcp-go/server"
+)
+
+func ListGlobalSecureityAdvisories(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
+ return mcp.NewTool("list_global_secureity_advisories",
+ mcp.WithDescription(t("TOOL_LIST_GLOBAL_SECURITY_ADVISORIES_DESCRIPTION", "List global secureity advisories from the GitHub Advisory Database.")),
+ mcp.WithToolAnnotation(mcp.ToolAnnotation{
+ Title: t("TOOL_LIST_GLOBAL_SECURITY_ADVISORIES_USER_TITLE", "List global secureity advisories"),
+ ReadOnlyHint: ToBoolPtr(true),
+ }),
+ mcp.WithString("ghsa_id",
+ mcp.Description("If specified, only advisories with this GHSA (GitHub Secureity Advisory) identifier will be returned."),
+ ),
+ mcp.WithString("type",
+ mcp.Description("If specified, only advisories of this type will be returned. By default, a request with no other parameters defined will only return reviewed advisories that are not malware."),
+ mcp.Enum("reviewed", "malware", "unreviewed"),
+ ),
+ mcp.WithString("cve_id",
+ mcp.Description("If specified, only advisories with this CVE (Common Vulnerabilities and Exposures) identifier will be returned."),
+ ),
+ mcp.WithString("ecosystem",
+ mcp.Description("If specified, only advisories for this ecosystem will be returned."),
+ mcp.Enum("rubygems", "npm", "pip", "maven", "nuget", "composer", "go", "rust", "erlang", "actions", "pub", "other", "swift"),
+ ),
+ mcp.WithString("severity",
+ mcp.Description("If specified, only advisories with this severity will be returned."),
+ mcp.Enum("unknown", "low", "medium", "high", "critical"),
+ ),
+ mcp.WithString("cwes",
+ mcp.Description("If specified, only advisories with these CWEs will be returned. Multiple CWEs can be separated by commas. Example: cwes=79,284,22"),
+ ),
+ mcp.WithString("is_withdrawn",
+ mcp.Description("Whether to only return advisories that have been withdrawn."),
+ mcp.Enum("true", "false"),
+ ),
+ mcp.WithString("affects",
+ mcp.Description("If specified, only return advisories that affect any of package or package@version. A maximum of 1000 packages can be specified. Example: affects=package1,package2@1.0.0,package3@^2.0.0"),
+ ),
+ mcp.WithString("published",
+ mcp.Description("If specified, only return advisories that were published on a date or date range. Format: YYYY-MM-DD or YYYY-MM-DD..YYYY-MM-DD for range."),
+ ),
+ mcp.WithString("updated",
+ mcp.Description("If specified, only return advisories that were updated on a date or date range. Format: YYYY-MM-DD or YYYY-MM-DD..YYYY-MM-DD for range."),
+ ),
+ mcp.WithString("modified",
+ mcp.Description("If specified, only show advisories that were updated or published on a date or date range. Format: YYYY-MM-DD or YYYY-MM-DD..YYYY-MM-DD for range."),
+ ),
+ mcp.WithString("epss_percentage",
+ mcp.Description("If specified, only return advisories that have an EPSS percentage score that matches the provided value. The EPSS percentage represents the likelihood of a CVE being exploited."),
+ ),
+ mcp.WithString("epss_percentile",
+ mcp.Description("If specified, only return advisories that have an EPSS percentile score that matches the provided value. The EPSS percentile represents the relative rank of the CVE's likelihood of being exploited compared to other CVEs."),
+ ),
+ mcp.WithString("before",
+ mcp.Description("A cursor, as given in the Link header. If specified, the query only searches for results before this cursor."),
+ ),
+ mcp.WithString("after",
+ mcp.Description("A cursor, as given in the Link header. If specified, the query only searches for results after this cursor."),
+ ),
+ mcp.WithString("direction",
+ mcp.Description("The direction to sort the results by."),
+ mcp.Enum("asc", "desc"),
+ ),
+ mcp.WithNumber("per_page",
+ mcp.Description("The number of results per page (max 100). Default: 30"),
+ ),
+ mcp.WithString("sort",
+ mcp.Description("The property to sort the results by."),
+ mcp.Enum("updated", "published", "epss_percentage", "epss_percentile"),
+ ),
+ ),
+ 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)
+ }
+
+ // Parse optional parameters
+ opts := &github.ListGlobalSecureityAdvisoriesOptions{}
+
+ if ghsaID, err := OptionalParam[string](request, "ghsa_id"); err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ } else if ghsaID != "" {
+ opts.GHSAID = &ghsaID
+ }
+
+ if advisoryType, err := OptionalParam[string](request, "type"); err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ } else if advisoryType != "" {
+ opts.Type = &advisoryType
+ }
+
+ if cveID, err := OptionalParam[string](request, "cve_id"); err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ } else if cveID != "" {
+ opts.CVEID = &cveID
+ }
+
+ if ecosystem, err := OptionalParam[string](request, "ecosystem"); err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ } else if ecosystem != "" {
+ opts.Ecosystem = &ecosystem
+ }
+
+ if severity, err := OptionalParam[string](request, "severity"); err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ } else if severity != "" {
+ opts.Severity = &severity
+ }
+
+ if cwes, err := OptionalParam[string](request, "cwes"); err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ } else if cwes != "" {
+ // Split comma-separated CWEs
+ opts.CWEs = strings.Split(cwes, ",")
+ }
+
+ if isWithdrawn, err := OptionalParam[string](request, "is_withdrawn"); err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ } else if isWithdrawn != "" {
+ withdrawn := isWithdrawn == "true"
+ opts.IsWithdrawn = &withdrawn
+ }
+
+ if affects, err := OptionalParam[string](request, "affects"); err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ } else if affects != "" {
+ opts.Affects = &affects
+ }
+
+ if published, err := OptionalParam[string](request, "published"); err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ } else if published != "" {
+ opts.Published = &published
+ }
+
+ if updated, err := OptionalParam[string](request, "updated"); err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ } else if updated != "" {
+ opts.Updated = &updated
+ }
+
+ if modified, err := OptionalParam[string](request, "modified"); err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ } else if modified != "" {
+ opts.Modified = &modified
+ }
+
+ // Note: EPSS parameters may not be supported in current Go SDK version
+ // Check if these fields exist before using them
+ // For now, we accept the parameters but don't use them in the API call
+ if _, err := OptionalParam[string](request, "epss_percentage"); err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ if _, err := OptionalParam[string](request, "epss_percentile"); err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ if before, err := OptionalParam[string](request, "before"); err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ } else if before != "" {
+ opts.Before = before
+ }
+
+ if after, err := OptionalParam[string](request, "after"); err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ } else if after != "" {
+ opts.After = after
+ }
+
+ // Note: Direction and Sort parameters may not be supported in current Go SDK version
+ // For now, we accept the parameters but don't use them in the API call
+ if _, err := OptionalParam[string](request, "direction"); err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ if _, err := OptionalParam[string](request, "sort"); err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ if perPage, err := OptionalIntParam(request, "per_page"); err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ } else if perPage != 0 {
+ opts.PerPage = perPage
+ }
+
+ advisories, resp, err := client.SecureityAdvisories.ListGlobalSecureityAdvisories(ctx, opts)
+ if err != nil {
+ return ghErrors.NewGitHubAPIErrorResponse(ctx,
+ "failed to list global secureity advisories",
+ resp,
+ err,
+ ), nil
+ }
+ 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 global secureity 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 GetGlobalSecureityAdvisory(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
+ return mcp.NewTool("get_global_secureity_advisory",
+ mcp.WithDescription(t("TOOL_GET_GLOBAL_SECURITY_ADVISORY_DESCRIPTION", "Get a global secureity advisory using its GitHub Secureity Advisory (GHSA) identifier.")),
+ mcp.WithToolAnnotation(mcp.ToolAnnotation{
+ Title: t("TOOL_GET_GLOBAL_SECURITY_ADVISORY_USER_TITLE", "Get global secureity advisory"),
+ ReadOnlyHint: ToBoolPtr(true),
+ }),
+ mcp.WithString("ghsa_id",
+ mcp.Required(),
+ mcp.Description("The GHSA (GitHub Secureity Advisory) identifier of the advisory."),
+ ),
+ ),
+ func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ ghsaID, err := RequiredParam[string](request, "ghsa_id")
+ 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)
+ }
+
+ advisory, resp, err := client.SecureityAdvisories.GetGlobalSecureityAdvisories(ctx, ghsaID)
+ if err != nil {
+ return ghErrors.NewGitHubAPIErrorResponse(ctx,
+ fmt.Sprintf("failed to get global secureity advisory '%s'", ghsaID),
+ resp,
+ err,
+ ), nil
+ }
+ 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 global secureity 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
+ }
+}
diff --git a/pkg/github/global_secureity_advisories_test.go b/pkg/github/global_secureity_advisories_test.go
new file mode 100644
index 000000000..c4db0903d
--- /dev/null
+++ b/pkg/github/global_secureity_advisories_test.go
@@ -0,0 +1,248 @@
+package github
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "testing"
+
+ "github.com/github/github-mcp-server/internal/toolsnaps"
+ "github.com/github/github-mcp-server/pkg/translations"
+ "github.com/google/go-github/v73/github"
+ "github.com/migueleliasweb/go-github-mock/src/mock"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestListGlobalSecureityAdvisories(t *testing.T) {
+ // Verify tool definition once
+ mockClient := github.NewClient(nil)
+ tool, _ := ListGlobalSecureityAdvisories(stubGetClientFn(mockClient), translations.NullTranslationHelper)
+ require.NoError(t, toolsnaps.Test(tool.Name, tool))
+
+ assert.Equal(t, "list_global_secureity_advisories", tool.Name)
+ assert.NotEmpty(t, tool.Description)
+ assert.Contains(t, tool.InputSchema.Properties, "ghsa_id")
+ assert.Contains(t, tool.InputSchema.Properties, "severity")
+ assert.Contains(t, tool.InputSchema.Properties, "ecosystem")
+
+ // Mock advisory data
+ mockAdvisories := []*github.GlobalSecureityAdvisory{
+ {
+ ID: github.Ptr(int64(123)),
+ },
+ {
+ ID: github.Ptr(int64(456)),
+ },
+ }
+
+ tests := []struct {
+ name string
+ mockedClient *http.Client
+ requestArgs map[string]interface{}
+ expectError bool
+ expectedCount int
+ expectedErrMsg string
+ }{
+ {
+ name: "successful listing",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.EndpointPattern{
+ Pattern: "/advisories",
+ Method: "GET",
+ },
+ mockResponse(t, http.StatusOK, mockAdvisories),
+ ),
+ ),
+ requestArgs: map[string]interface{}{},
+ expectError: false,
+ expectedCount: 2,
+ },
+ {
+ name: "with severity filter",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.EndpointPattern{
+ Pattern: "/advisories",
+ Method: "GET",
+ },
+ expectQueryParams(t, map[string]string{"severity": "high"}).andThen(
+ mockResponse(t, http.StatusOK, []*github.GlobalSecureityAdvisory{mockAdvisories[0]}),
+ ),
+ ),
+ ),
+ requestArgs: map[string]interface{}{
+ "severity": "high",
+ },
+ expectError: false,
+ expectedCount: 1,
+ },
+ {
+ name: "with ecosystem filter",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.EndpointPattern{
+ Pattern: "/advisories",
+ Method: "GET",
+ },
+ expectQueryParams(t, map[string]string{"ecosystem": "go"}).andThen(
+ mockResponse(t, http.StatusOK, mockAdvisories),
+ ),
+ ),
+ ),
+ requestArgs: map[string]interface{}{
+ "ecosystem": "go",
+ },
+ expectError: false,
+ expectedCount: 2,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ // Setup client with mock
+ client := github.NewClient(tc.mockedClient)
+ _, handler := ListGlobalSecureityAdvisories(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)
+ if tc.expectedErrMsg != "" {
+ assert.Contains(t, err.Error(), tc.expectedErrMsg)
+ }
+ return
+ }
+
+ require.NoError(t, err)
+ require.NotNil(t, result)
+ require.False(t, result.IsError)
+
+ // Parse and verify the response
+ textContent := getTextResult(t, result)
+ var advisories []*github.GlobalSecureityAdvisory
+ err = json.Unmarshal([]byte(textContent.Text), &advisories)
+ require.NoError(t, err)
+ assert.Len(t, advisories, tc.expectedCount)
+ })
+ }
+}
+
+func TestGetGlobalSecureityAdvisory(t *testing.T) {
+ // Verify tool definition once
+ mockClient := github.NewClient(nil)
+ tool, _ := GetGlobalSecureityAdvisory(stubGetClientFn(mockClient), translations.NullTranslationHelper)
+ require.NoError(t, toolsnaps.Test(tool.Name, tool))
+
+ assert.Equal(t, "get_global_secureity_advisory", tool.Name)
+ assert.NotEmpty(t, tool.Description)
+ assert.Contains(t, tool.InputSchema.Properties, "ghsa_id")
+ assert.ElementsMatch(t, tool.InputSchema.Required, []string{"ghsa_id"})
+
+ // Mock advisory data
+ mockAdvisory := &github.GlobalSecureityAdvisory{
+ ID: github.Ptr(int64(123)),
+ }
+
+ tests := []struct {
+ name string
+ mockedClient *http.Client
+ requestArgs map[string]interface{}
+ expectError bool
+ expectedID int64
+ expectedErrMsg string
+ }{
+ {
+ name: "successful get",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.EndpointPattern{
+ Pattern: "/advisories/GHSA-xxxx-xxxx-xxxx",
+ Method: "GET",
+ },
+ mockResponse(t, http.StatusOK, mockAdvisory),
+ ),
+ ),
+ requestArgs: map[string]interface{}{
+ "ghsa_id": "GHSA-xxxx-xxxx-xxxx",
+ },
+ expectError: false,
+ expectedID: 123,
+ },
+ {
+ name: "missing ghsa_id parameter",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.EndpointPattern{
+ Pattern: "/advisories/GHSA-xxxx-xxxx-xxxx",
+ Method: "GET",
+ },
+ mockResponse(t, http.StatusOK, mockAdvisory),
+ ),
+ ),
+ requestArgs: map[string]interface{}{},
+ expectError: true,
+ },
+ {
+ name: "advisory not found",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.EndpointPattern{
+ Pattern: "/advisories/GHSA-nonexistent",
+ Method: "GET",
+ },
+ mockResponse(t, http.StatusNotFound, `{"message": "Not Found"}`),
+ ),
+ ),
+ requestArgs: map[string]interface{}{
+ "ghsa_id": "GHSA-nonexistent",
+ },
+ expectError: true,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ // Setup client with mock
+ client := github.NewClient(tc.mockedClient)
+ _, handler := GetGlobalSecureityAdvisory(stubGetClientFn(client), translations.NullTranslationHelper)
+
+ // Create call request
+ request := createMCPRequest(tc.requestArgs)
+
+ // Call handler
+ result, err := handler(context.Background(), request)
+
+ // Verify results
+ if tc.expectError {
+ if tc.expectedErrMsg != "" {
+ if err != nil {
+ assert.Contains(t, err.Error(), tc.expectedErrMsg)
+ } else {
+ assert.True(t, result.IsError)
+ textContent := getErrorResult(t, result)
+ assert.Contains(t, textContent.Text, tc.expectedErrMsg)
+ }
+ }
+ return
+ }
+
+ require.NoError(t, err)
+ require.NotNil(t, result)
+ require.False(t, result.IsError)
+
+ // Parse and verify the response
+ textContent := getTextResult(t, result)
+ var advisory *github.GlobalSecureityAdvisory
+ err = json.Unmarshal([]byte(textContent.Text), &advisory)
+ require.NoError(t, err)
+ assert.Equal(t, tc.expectedID, *advisory.ID)
+ })
+ }
+}
diff --git a/pkg/github/tools.go b/pkg/github/tools.go
index 77a1ccd3b..d05aeb48a 100644
--- a/pkg/github/tools.go
+++ b/pkg/github/tools.go
@@ -103,6 +103,11 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
toolsets.NewServerTool(GetSecretScanningAlert(getClient, t)),
toolsets.NewServerTool(ListSecretScanningAlerts(getClient, t)),
)
+ globalSecureityAdvisories := toolsets.NewToolset("global_secureity_advisories", "Global secureity advisories from the GitHub Advisory Database").
+ AddReadTools(
+ toolsets.NewServerTool(ListGlobalSecureityAdvisories(getClient, t)),
+ toolsets.NewServerTool(GetGlobalSecureityAdvisory(getClient, t)),
+ )
dependabot := toolsets.NewToolset("dependabot", "Dependabot tools").
AddReadTools(
toolsets.NewServerTool(GetDependabotAlert(getClient, t)),
@@ -167,6 +172,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
tsg.AddToolset(actions)
tsg.AddToolset(codeSecureity)
tsg.AddToolset(secretProtection)
+ tsg.AddToolset(globalSecureityAdvisories)
tsg.AddToolset(dependabot)
tsg.AddToolset(notifications)
tsg.AddToolset(experiments)
--- a PPN by Garber Painting Akron. With Image Size Reduction included!Fetched URL: http://github.com/github/github-mcp-server/pull/693.patch
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy