Skip to content

Commit 41fb3da

Browse files
Consolidate CRUD operations into unified tools with operation parameters (#1)
* Initial plan * Initial analysis and planning for CRUD tool consolidation Co-authored-by: mattdholloway <[email protected]> * Implement consolidated manage_gist tool with CRUD operations Co-authored-by: mattdholloway <[email protected]> * Complete phases 2-3: Consolidated code scanning and secret scanning alert tools Co-authored-by: mattdholloway <[email protected]> * Consolidate issue, pull request, and repository operations with manage_* tools Co-authored-by: mattdholloway <[email protected]> * Remove old individual tools replaced by consolidated manage_* tools Co-authored-by: mattdholloway <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: mattdholloway <[email protected]>
1 parent 0418808 commit 41fb3da

File tree

11 files changed

+2000
-22
lines changed

11 files changed

+2000
-22
lines changed

github-mcp-server

1.22 MB
Binary file not shown.

pkg/github/code_scanning.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,166 @@ import (
1414
"github.com/mark3labs/mcp-go/server"
1515
)
1616

17+
// ManageCodeScanningAlerts creates a consolidated tool to perform operations on code scanning alerts
18+
func ManageCodeScanningAlerts(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
19+
return mcp.NewTool("manage_code_scanning_alerts",
20+
mcp.WithDescription(t("TOOL_MANAGE_CODE_SCANNING_ALERTS_DESCRIPTION", "Manage code scanning alerts with various operations: list, get")),
21+
mcp.WithToolAnnotation(mcp.ToolAnnotation{
22+
Title: t("TOOL_MANAGE_CODE_SCANNING_ALERTS", "Manage Code Scanning Alerts"),
23+
ReadOnlyHint: ToBoolPtr(true),
24+
}),
25+
mcp.WithString("operation",
26+
mcp.Required(),
27+
mcp.Description("Operation to perform: 'list', 'get'"),
28+
mcp.Enum("list", "get"),
29+
),
30+
mcp.WithString("owner",
31+
mcp.Required(),
32+
mcp.Description("The owner of the repository"),
33+
),
34+
mcp.WithString("repo",
35+
mcp.Required(),
36+
mcp.Description("The name of the repository"),
37+
),
38+
// Parameters for get operation
39+
mcp.WithNumber("alertNumber",
40+
mcp.Description("The number of the alert (required for 'get' operation)"),
41+
),
42+
// Parameters for list operation
43+
mcp.WithString("state",
44+
mcp.Description("Filter code scanning alerts by state (used for 'list' operation)"),
45+
mcp.DefaultString("open"),
46+
mcp.Enum("open", "closed", "dismissed", "fixed"),
47+
),
48+
mcp.WithString("ref",
49+
mcp.Description("The Git reference for the results you want to list (used for 'list' operation)"),
50+
),
51+
mcp.WithString("severity",
52+
mcp.Description("Filter code scanning alerts by severity (used for 'list' operation)"),
53+
mcp.Enum("critical", "high", "medium", "low", "warning", "note", "error"),
54+
),
55+
mcp.WithString("tool_name",
56+
mcp.Description("The name of the tool used for code scanning (used for 'list' operation)"),
57+
),
58+
),
59+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
60+
operation, err := RequiredParam[string](request, "operation")
61+
if err != nil {
62+
return mcp.NewToolResultError(err.Error()), nil
63+
}
64+
65+
switch operation {
66+
case "list":
67+
return handleListCodeScanningAlerts(ctx, getClient, request)
68+
case "get":
69+
return handleGetCodeScanningAlert(ctx, getClient, request)
70+
default:
71+
return mcp.NewToolResultError(fmt.Sprintf("unsupported operation: %s", operation)), nil
72+
}
73+
}
74+
}
75+
76+
func handleGetCodeScanningAlert(ctx context.Context, getClient GetClientFn, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
77+
owner, err := RequiredParam[string](request, "owner")
78+
if err != nil {
79+
return mcp.NewToolResultError(err.Error()), nil
80+
}
81+
repo, err := RequiredParam[string](request, "repo")
82+
if err != nil {
83+
return mcp.NewToolResultError(err.Error()), nil
84+
}
85+
alertNumber, err := RequiredInt(request, "alertNumber")
86+
if err != nil {
87+
return mcp.NewToolResultError(err.Error()), nil
88+
}
89+
90+
client, err := getClient(ctx)
91+
if err != nil {
92+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
93+
}
94+
95+
alert, resp, err := client.CodeScanning.GetAlert(ctx, owner, repo, int64(alertNumber))
96+
if err != nil {
97+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
98+
"failed to get alert",
99+
resp,
100+
err,
101+
), nil
102+
}
103+
defer func() { _ = resp.Body.Close() }()
104+
105+
if resp.StatusCode != http.StatusOK {
106+
body, err := io.ReadAll(resp.Body)
107+
if err != nil {
108+
return nil, fmt.Errorf("failed to read response body: %w", err)
109+
}
110+
return mcp.NewToolResultError(fmt.Sprintf("failed to get alert: %s", string(body))), nil
111+
}
112+
113+
r, err := json.Marshal(alert)
114+
if err != nil {
115+
return nil, fmt.Errorf("failed to marshal alert: %w", err)
116+
}
117+
118+
return mcp.NewToolResultText(string(r)), nil
119+
}
120+
121+
func handleListCodeScanningAlerts(ctx context.Context, getClient GetClientFn, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
122+
owner, err := RequiredParam[string](request, "owner")
123+
if err != nil {
124+
return mcp.NewToolResultError(err.Error()), nil
125+
}
126+
repo, err := RequiredParam[string](request, "repo")
127+
if err != nil {
128+
return mcp.NewToolResultError(err.Error()), nil
129+
}
130+
ref, err := OptionalParam[string](request, "ref")
131+
if err != nil {
132+
return mcp.NewToolResultError(err.Error()), nil
133+
}
134+
state, err := OptionalParam[string](request, "state")
135+
if err != nil {
136+
return mcp.NewToolResultError(err.Error()), nil
137+
}
138+
severity, err := OptionalParam[string](request, "severity")
139+
if err != nil {
140+
return mcp.NewToolResultError(err.Error()), nil
141+
}
142+
toolName, err := OptionalParam[string](request, "tool_name")
143+
if err != nil {
144+
return mcp.NewToolResultError(err.Error()), nil
145+
}
146+
147+
client, err := getClient(ctx)
148+
if err != nil {
149+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
150+
}
151+
alerts, resp, err := client.CodeScanning.ListAlertsForRepo(ctx, owner, repo, &github.AlertListOptions{Ref: ref, State: state, Severity: severity, ToolName: toolName})
152+
if err != nil {
153+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
154+
"failed to list alerts",
155+
resp,
156+
err,
157+
), nil
158+
}
159+
defer func() { _ = resp.Body.Close() }()
160+
161+
if resp.StatusCode != http.StatusOK {
162+
body, err := io.ReadAll(resp.Body)
163+
if err != nil {
164+
return nil, fmt.Errorf("failed to read response body: %w", err)
165+
}
166+
return mcp.NewToolResultError(fmt.Sprintf("failed to list alerts: %s", string(body))), nil
167+
}
168+
169+
r, err := json.Marshal(alerts)
170+
if err != nil {
171+
return nil, fmt.Errorf("failed to marshal alerts: %w", err)
172+
}
173+
174+
return mcp.NewToolResultText(string(r)), nil
175+
}
176+
17177
func GetCodeScanningAlert(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
18178
return mcp.NewTool("get_code_scanning_alert",
19179
mcp.WithDescription(t("TOOL_GET_CODE_SCANNING_ALERT_DESCRIPTION", "Get details of a specific code scanning alert in a GitHub repository.")),

pkg/github/code_scanning_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,3 +247,70 @@ func Test_ListCodeScanningAlerts(t *testing.T) {
247247
})
248248
}
249249
}
250+
251+
func Test_ManageCodeScanningAlerts(t *testing.T) {
252+
// Verify tool definition
253+
mockClient := github.NewClient(nil)
254+
tool, _ := ManageCodeScanningAlerts(stubGetClientFn(mockClient), translations.NullTranslationHelper)
255+
256+
assert.Equal(t, "manage_code_scanning_alerts", tool.Name)
257+
assert.NotEmpty(t, tool.Description)
258+
assert.Contains(t, tool.InputSchema.Properties, "operation")
259+
assert.Contains(t, tool.InputSchema.Required, "operation")
260+
261+
t.Run("list operation", func(t *testing.T) {
262+
// Setup mock alerts for success case
263+
mockAlerts := []*github.Alert{
264+
{
265+
Number: github.Ptr(1),
266+
Rule: &github.Rule{
267+
ID: github.Ptr("rule1"),
268+
Severity: github.Ptr("error"),
269+
Description: github.Ptr("Test rule"),
270+
},
271+
State: github.Ptr("open"),
272+
},
273+
}
274+
275+
mockedHTTPClient := mock.NewMockedHTTPClient(
276+
mock.WithRequestMatch(mock.GetReposCodeScanningAlertsByOwnerByRepo, mockAlerts),
277+
)
278+
client := github.NewClient(mockedHTTPClient)
279+
_, handler := ManageCodeScanningAlerts(stubGetClientFn(client), translations.NullTranslationHelper)
280+
281+
request := createMCPRequest(map[string]interface{}{
282+
"operation": "list",
283+
"owner": "testowner",
284+
"repo": "testrepo",
285+
})
286+
287+
result, err := handler(context.Background(), request)
288+
require.NoError(t, err)
289+
assert.False(t, result.IsError)
290+
291+
var alerts []*github.Alert
292+
textContent := getTextResult(t, result)
293+
err = json.Unmarshal([]byte(textContent.Text), &alerts)
294+
require.NoError(t, err)
295+
assert.Len(t, alerts, 1)
296+
assert.Equal(t, 1, *alerts[0].Number)
297+
})
298+
299+
t.Run("unsupported operation", func(t *testing.T) {
300+
mockedHTTPClient := mock.NewMockedHTTPClient()
301+
client := github.NewClient(mockedHTTPClient)
302+
_, handler := ManageCodeScanningAlerts(stubGetClientFn(client), translations.NullTranslationHelper)
303+
304+
request := createMCPRequest(map[string]interface{}{
305+
"operation": "delete",
306+
"owner": "testowner",
307+
"repo": "testrepo",
308+
})
309+
310+
result, err := handler(context.Background(), request)
311+
require.NoError(t, err)
312+
assert.True(t, result.IsError)
313+
textContent := getTextResult(t, result)
314+
assert.Contains(t, textContent.Text, "unsupported operation: delete")
315+
})
316+
}

0 commit comments

Comments
 (0)