Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 78 additions & 141 deletions README.md

Large diffs are not rendered by default.

Binary file modified github-mcp-server
Binary file not shown.
160 changes: 160 additions & 0 deletions pkg/github/code_scanning.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,166 @@ import (
"github.com/mark3labs/mcp-go/server"
)

// ManageCodeScanningAlerts creates a consolidated tool to perform operations on code scanning alerts
func ManageCodeScanningAlerts(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("manage_code_scanning_alerts",
mcp.WithDescription(t("TOOL_MANAGE_CODE_SCANNING_ALERTS_DESCRIPTION", "Manage code scanning alerts with various operations: list, get")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: t("TOOL_MANAGE_CODE_SCANNING_ALERTS", "Manage Code Scanning Alerts"),
ReadOnlyHint: ToBoolPtr(true),
}),
mcp.WithString("operation",
mcp.Required(),
mcp.Description("Operation to perform: 'list', 'get'"),
mcp.Enum("list", "get"),
),
mcp.WithString("owner",
mcp.Required(),
mcp.Description("The owner of the repository"),
),
mcp.WithString("repo",
mcp.Required(),
mcp.Description("The name of the repository"),
),
// Parameters for get operation
mcp.WithNumber("alertNumber",
mcp.Description("The number of the alert (required for 'get' operation)"),
),
// Parameters for list operation
mcp.WithString("state",
mcp.Description("Filter code scanning alerts by state (used for 'list' operation)"),
mcp.DefaultString("open"),
mcp.Enum("open", "closed", "dismissed", "fixed"),
),
mcp.WithString("ref",
mcp.Description("The Git reference for the results you want to list (used for 'list' operation)"),
),
mcp.WithString("severity",
mcp.Description("Filter code scanning alerts by severity (used for 'list' operation)"),
mcp.Enum("critical", "high", "medium", "low", "warning", "note", "error"),
),
mcp.WithString("tool_name",
mcp.Description("The name of the tool used for code scanning (used for 'list' operation)"),
),
),
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
operation, err := RequiredParam[string](request, "operation")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

switch operation {
case "list":
return handleListCodeScanningAlerts(ctx, getClient, request)
case "get":
return handleGetCodeScanningAlert(ctx, getClient, request)
default:
return mcp.NewToolResultError(fmt.Sprintf("unsupported operation: %s", operation)), nil
}
}
}

func handleGetCodeScanningAlert(ctx context.Context, getClient GetClientFn, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
owner, err := RequiredParam[string](request, "owner")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
repo, err := RequiredParam[string](request, "repo")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
alertNumber, err := RequiredInt(request, "alertNumber")
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)
}

alert, resp, err := client.CodeScanning.GetAlert(ctx, owner, repo, int64(alertNumber))
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
"failed to get alert",
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 alert: %s", string(body))), nil
}

r, err := json.Marshal(alert)
if err != nil {
return nil, fmt.Errorf("failed to marshal alert: %w", err)
}

return mcp.NewToolResultText(string(r)), nil
}

func handleListCodeScanningAlerts(ctx context.Context, getClient GetClientFn, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
owner, err := RequiredParam[string](request, "owner")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
repo, err := RequiredParam[string](request, "repo")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
ref, err := OptionalParam[string](request, "ref")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
state, err := OptionalParam[string](request, "state")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
severity, err := OptionalParam[string](request, "severity")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
toolName, err := OptionalParam[string](request, "tool_name")
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)
}
alerts, resp, err := client.CodeScanning.ListAlertsForRepo(ctx, owner, repo, &github.AlertListOptions{Ref: ref, State: state, Severity: severity, ToolName: toolName})
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
"failed to list alerts",
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 alerts: %s", string(body))), nil
}

r, err := json.Marshal(alerts)
if err != nil {
return nil, fmt.Errorf("failed to marshal alerts: %w", err)
}

return mcp.NewToolResultText(string(r)), nil
}

func GetCodeScanningAlert(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("get_code_scanning_alert",
mcp.WithDescription(t("TOOL_GET_CODE_SCANNING_ALERT_DESCRIPTION", "Get details of a specific code scanning alert in a GitHub repository.")),
Expand Down
67 changes: 67 additions & 0 deletions pkg/github/code_scanning_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,3 +247,70 @@ func Test_ListCodeScanningAlerts(t *testing.T) {
})
}
}

func Test_ManageCodeScanningAlerts(t *testing.T) {
// Verify tool definition
mockClient := github.NewClient(nil)
Comment on lines +251 to +253
Copy link

Copilot AI Aug 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing blank line after function declaration. Add a blank line between the function signature and the first comment.

Copilot uses AI. Check for mistakes.
tool, _ := ManageCodeScanningAlerts(stubGetClientFn(mockClient), translations.NullTranslationHelper)

assert.Equal(t, "manage_code_scanning_alerts", tool.Name)
assert.NotEmpty(t, tool.Description)
assert.Contains(t, tool.InputSchema.Properties, "operation")
assert.Contains(t, tool.InputSchema.Required, "operation")

t.Run("list operation", func(t *testing.T) {
// Setup mock alerts for success case
mockAlerts := []*github.Alert{
{
Number: github.Ptr(1),
Rule: &github.Rule{
ID: github.Ptr("rule1"),
Severity: github.Ptr("error"),
Description: github.Ptr("Test rule"),
},
State: github.Ptr("open"),
},
}

mockedHTTPClient := mock.NewMockedHTTPClient(
mock.WithRequestMatch(mock.GetReposCodeScanningAlertsByOwnerByRepo, mockAlerts),
)
client := github.NewClient(mockedHTTPClient)
_, handler := ManageCodeScanningAlerts(stubGetClientFn(client), translations.NullTranslationHelper)

request := createMCPRequest(map[string]interface{}{
"operation": "list",
"owner": "testowner",
"repo": "testrepo",
})

result, err := handler(context.Background(), request)
require.NoError(t, err)
assert.False(t, result.IsError)

var alerts []*github.Alert
textContent := getTextResult(t, result)
err = json.Unmarshal([]byte(textContent.Text), &alerts)
require.NoError(t, err)
assert.Len(t, alerts, 1)
assert.Equal(t, 1, *alerts[0].Number)
})

t.Run("unsupported operation", func(t *testing.T) {
mockedHTTPClient := mock.NewMockedHTTPClient()
client := github.NewClient(mockedHTTPClient)
_, handler := ManageCodeScanningAlerts(stubGetClientFn(client), translations.NullTranslationHelper)

request := createMCPRequest(map[string]interface{}{
"operation": "delete",
"owner": "testowner",
"repo": "testrepo",
})

result, err := handler(context.Background(), request)
require.NoError(t, err)
assert.True(t, result.IsError)
textContent := getTextResult(t, result)
assert.Contains(t, textContent.Text, "unsupported operation: delete")
})
}
Loading
Loading