-
Notifications
You must be signed in to change notification settings - Fork 371
Add checks as a first-class MCP tool to the gh-aw MCP server
#24757
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,6 +8,7 @@ import ( | |||||||||||||||||||||||||||||||||||
| "os" | ||||||||||||||||||||||||||||||||||||
| "os/exec" | ||||||||||||||||||||||||||||||||||||
| "path/filepath" | ||||||||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||||||||
| "testing" | ||||||||||||||||||||||||||||||||||||
| "time" | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
@@ -367,6 +368,90 @@ func TestMCPServer_LogsToolReturnsValidJSON(t *testing.T) { | |||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // TestMCPServer_ChecksToolReturnsValidJSON tests that the checks tool returns valid JSON | ||||||||||||||||||||||||||||||||||||
| // (or a well-formed MCP error when GitHub credentials are unavailable in test environments). | ||||||||||||||||||||||||||||||||||||
| func TestMCPServer_ChecksToolReturnsValidJSON(t *testing.T) { | ||||||||||||||||||||||||||||||||||||
| // Skip if the binary doesn't exist | ||||||||||||||||||||||||||||||||||||
| binaryPath := "../../gh-aw" | ||||||||||||||||||||||||||||||||||||
| if _, err := os.Stat(binaryPath); os.IsNotExist(err) { | ||||||||||||||||||||||||||||||||||||
| t.Skip("Skipping test: gh-aw binary not found. Run 'make build' first.") | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| session, _, ctx, cancel := setupMCPServerTest(t, binaryPath) | ||||||||||||||||||||||||||||||||||||
| defer cancel() | ||||||||||||||||||||||||||||||||||||
| defer session.Close() | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| t.Run("missing pr_number returns MCP error", func(t *testing.T) { | ||||||||||||||||||||||||||||||||||||
| params := &mcp.CallToolParams{ | ||||||||||||||||||||||||||||||||||||
| Name: "checks", | ||||||||||||||||||||||||||||||||||||
| Arguments: map[string]any{}, | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| _, err := session.CallTool(ctx, params) | ||||||||||||||||||||||||||||||||||||
| if err == nil { | ||||||||||||||||||||||||||||||||||||
| t.Error("Expected MCP error when pr_number is missing") | ||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||
| t.Logf("Checks tool correctly returned error for missing pr_number: %v", err) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| t.Run("valid pr_number returns JSON or auth error", func(t *testing.T) { | ||||||||||||||||||||||||||||||||||||
| params := &mcp.CallToolParams{ | ||||||||||||||||||||||||||||||||||||
| Name: "checks", | ||||||||||||||||||||||||||||||||||||
| Arguments: map[string]any{ | ||||||||||||||||||||||||||||||||||||
| "pr_number": "1", | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
| "pr_number": "1", | |
| "pr_number": "1", | |
| "repo": "cli/cli", |
Copilot
AI
Apr 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After session.CallTool succeeds, this test checks for an "Error:" prefix in the returned content. But the checks tool returns failures as an MCP (jsonrpc) error (i.e., via the err return), so this branch should never be hit and may confuse future readers. Consider removing it or adjusting it to match the tool’s actual error-returning behavior.
| if textContent.Text == "" { | |
| t.Fatal("Expected non-empty text content from checks tool") | |
| } | |
| // In test environments without GitHub credentials, an error message is returned | |
| if strings.HasPrefix(textContent.Text, "Error:") { | |
| t.Logf("Checks tool returned error message (expected in test environment without GitHub credentials)") | |
| return | |
| } | |
| // If credentials are available, verify JSON structure | |
| if strings.TrimSpace(textContent.Text) == "" { | |
| t.Fatal("Expected non-empty text content from checks tool") | |
| } | |
| // On success, the checks tool should return structured output; failures are | |
| // reported by session.CallTool via err above. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -299,6 +299,76 @@ Returns formatted text output showing: | |
| }) | ||
| } | ||
|
|
||
| // registerChecksTool registers the checks tool with the MCP server. | ||
| // The checks tool is read-only and idempotent. | ||
| func registerChecksTool(server *mcp.Server) { | ||
| type checksArgs struct { | ||
| PRNumber string `json:"pr_number" jsonschema:"Pull request number to classify CI checks for"` | ||
| Repo string `json:"repo,omitempty" jsonschema:"Repository in owner/repo format (defaults to current repository)"` | ||
| } | ||
|
|
||
| mcp.AddTool(server, &mcp.Tool{ | ||
| Name: "checks", | ||
| Annotations: &mcp.ToolAnnotations{ | ||
| ReadOnlyHint: true, | ||
| IdempotentHint: true, | ||
| OpenWorldHint: boolPtr(true), | ||
| }, | ||
| Description: `Classify CI check state for a pull request and return a normalized result. | ||
|
|
||
| Maps PR check rollups to one of the following normalized states: | ||
| success - all checks passed | ||
| failed - one or more checks failed | ||
| pending - checks are still running or queued | ||
| no_checks - no checks configured or triggered | ||
| policy_blocked - policy or account gates are blocking the PR | ||
|
|
||
| Returns JSON with two state fields: | ||
| state - aggregate state across all check runs and commit statuses | ||
| required_state - state derived from check runs and policy commit statuses only; | ||
| ignores optional third-party commit statuses (e.g. Vercel, | ||
| Netlify deployments) but still surfaces policy_blocked when | ||
| branch-protection or account-gate statuses fail | ||
|
|
||
| Use required_state as the authoritative CI verdict in repos that have optional | ||
| deployment integrations posting commit statuses alongside required CI checks. | ||
|
|
||
| Also returns pr_number, head_sha, check_runs, statuses, and total_count.`, | ||
| Icons: []mcp.Icon{ | ||
| {Source: "✅"}, | ||
| }, | ||
| }, func(ctx context.Context, req *mcp.CallToolRequest, args checksArgs) (*mcp.CallToolResult, any, error) { | ||
| // Check for cancellation before starting | ||
| select { | ||
| case <-ctx.Done(): | ||
| return nil, nil, newMCPError(jsonrpc.CodeInternalError, "request cancelled", ctx.Err().Error()) | ||
| default: | ||
| } | ||
|
|
||
| if args.PRNumber == "" { | ||
| return nil, nil, newMCPError(jsonrpc.CodeInvalidParams, "missing required parameter: pr_number", nil) | ||
| } | ||
|
|
||
| mcpLog.Printf("Executing checks tool: pr_number=%s, repo=%s", args.PRNumber, args.Repo) | ||
|
|
||
| result, err := FetchChecksResult(args.Repo, args.PRNumber) | ||
| if err != nil { | ||
| return nil, nil, newMCPError(jsonrpc.CodeInternalError, "failed to fetch checks", map[string]any{"error": err.Error()}) | ||
|
Comment on lines
+352
to
+356
|
||
| } | ||
|
|
||
| jsonBytes, err := json.Marshal(result) | ||
| if err != nil { | ||
| return nil, nil, newMCPError(jsonrpc.CodeInternalError, "failed to marshal checks result", map[string]any{"error": err.Error()}) | ||
| } | ||
|
|
||
| return &mcp.CallToolResult{ | ||
| Content: []mcp.Content{ | ||
| &mcp.TextContent{Text: string(jsonBytes)}, | ||
| }, | ||
| }, nil, nil | ||
| }) | ||
| } | ||
|
|
||
| // buildDockerErrorResults builds a []ValidationResult with a config_error for each target | ||
| // workflow. It is used when Docker is unavailable so the compile tool returns consistent | ||
| // structured JSON instead of a protocol-level error. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In createMCPServer, checks is registered after logs/audit registration. Since createMCPServer returns early if registerLogsTool/registerAuditTool fail (e.g., schema generation errors), this read-only tool may never be available even though it doesn’t depend on those privileged tools. Consider registering checks alongside the other read-only tools before any early-return points.