From e3b569727615099a3786e1a0f4ede7f23155db2b Mon Sep 17 00:00:00 2001 From: km-anthropic Date: Wed, 13 Aug 2025 09:26:04 -0700 Subject: [PATCH] Add GitHub MCP support to agent mode - Parse --allowedTools from claude_args to detect when user wants GitHub MCPs - Wire up github_inline_comment server in prepareMcpConfig for PR contexts - Update agent mode to use prepareMcpConfig instead of manual config - Add comprehensive tests for parseAllowedTools edge cases - Fix TypeScript types to support both entity and automation contexts --- src/mcp/install-mcp-server.ts | 29 ++++++++++++-- src/modes/agent/index.ts | 62 +++++++++++++++--------------- src/modes/agent/parse-tools.ts | 22 +++++++++++ test/modes/parse-tools.test.ts | 69 ++++++++++++++++++++++++++++++++++ 4 files changed, 149 insertions(+), 33 deletions(-) create mode 100644 src/modes/agent/parse-tools.ts create mode 100644 test/modes/parse-tools.test.ts diff --git a/src/mcp/install-mcp-server.ts b/src/mcp/install-mcp-server.ts index 2925d43..f533525 100644 --- a/src/mcp/install-mcp-server.ts +++ b/src/mcp/install-mcp-server.ts @@ -1,6 +1,7 @@ import * as core from "@actions/core"; import { GITHUB_API_URL, GITHUB_SERVER_URL } from "../github/api/config"; -import type { ParsedGitHubContext } from "../github/context"; +import type { GitHubContext } from "../github/context"; +import { isEntityContext } from "../github/context"; import { Octokit } from "@octokit/rest"; type PrepareConfigParams = { @@ -12,7 +13,7 @@ type PrepareConfigParams = { additionalMcpConfig?: string; claudeCommentId?: string; allowedTools: string[]; - context: ParsedGitHubContext; + context: GitHubContext; }; async function checkActionsReadPermission( @@ -67,6 +68,10 @@ export async function prepareMcpConfig( const hasGitHubMcpTools = allowedToolsList.some((tool) => tool.startsWith("mcp__github__"), ); + + const hasInlineCommentTools = allowedToolsList.some((tool) => + tool.startsWith("mcp__github_inline_comment__"), + ); const baseMcpConfig: { mcpServers: Record } = { mcpServers: {}, @@ -110,11 +115,29 @@ export async function prepareMcpConfig( }, }; } + + // Include inline comment server for PRs when requested via allowed tools + if (isEntityContext(context) && context.isPR && (hasGitHubMcpTools || hasInlineCommentTools)) { + baseMcpConfig.mcpServers.github_inline_comment = { + command: "bun", + args: [ + "run", + `${process.env.GITHUB_ACTION_PATH}/src/mcp/github-inline-comment-server.ts`, + ], + env: { + GITHUB_TOKEN: githubToken, + REPO_OWNER: owner, + REPO_NAME: repo, + PR_NUMBER: context.entityNumber?.toString() || "", + GITHUB_API_URL: GITHUB_API_URL, + }, + }; + } // CI server is included when we have a workflow token and context is a PR const hasWorkflowToken = !!process.env.DEFAULT_WORKFLOW_TOKEN; - if (context.isPR && hasWorkflowToken) { + if (isEntityContext(context) && context.isPR && hasWorkflowToken) { // Verify the token actually has actions:read permission const actuallyHasPermission = await checkActionsReadPermission( process.env.DEFAULT_WORKFLOW_TOKEN || "", diff --git a/src/modes/agent/index.ts b/src/modes/agent/index.ts index c9839ad..771d8ed 100644 --- a/src/modes/agent/index.ts +++ b/src/modes/agent/index.ts @@ -2,6 +2,8 @@ import * as core from "@actions/core"; import { mkdir, writeFile } from "fs/promises"; import type { Mode, ModeOptions, ModeResult } from "../types"; import type { PreparedContext } from "../../create-prompt/types"; +import { prepareMcpConfig } from "../../mcp/install-mcp-server"; +import { parseAllowedTools } from "./parse-tools"; /** * Agent mode implementation. @@ -39,7 +41,7 @@ export const agentMode: Mode = { return false; }, - async prepare({ context, githubToken, octokit }: ModeOptions): Promise { + async prepare({ context, githubToken }: ModeOptions): Promise { // Create prompt directory await mkdir(`${process.env.RUNNER_TEMP}/claude-prompts`, { recursive: true, @@ -54,43 +56,43 @@ export const agentMode: Mode = { promptContent, ); - const mcpConfig: any = { - mcpServers: {}, - }; - - // Add user-provided additional MCP config if any - const additionalMcpConfig = process.env.MCP_CONFIG || ""; - if (additionalMcpConfig.trim()) { - try { - const additional = JSON.parse(additionalMcpConfig); - if (additional && typeof additional === "object") { - // Merge mcpServers if both have them - if (additional.mcpServers && mcpConfig.mcpServers) { - Object.assign(mcpConfig.mcpServers, additional.mcpServers); - } else { - Object.assign(mcpConfig, additional); - } - } - } catch (error) { - core.warning(`Failed to parse additional MCP config: ${error}`); - } - } - - // Agent mode: pass through user's claude_args with MCP config + // Parse allowed tools from user's claude_args const userClaudeArgs = process.env.CLAUDE_ARGS || ""; - const escapedMcpConfig = JSON.stringify(mcpConfig).replace(/'/g, "'\\''"); - const claudeArgs = - `--mcp-config '${escapedMcpConfig}' ${userClaudeArgs}`.trim(); + const allowedTools = parseAllowedTools(userClaudeArgs); + + // Detect current branch from GitHub environment + const currentBranch = process.env.GITHUB_HEAD_REF || + process.env.GITHUB_REF_NAME || + "main"; + + // Get MCP configuration with GitHub servers when requested + const additionalMcpConfig = process.env.MCP_CONFIG || ""; + const mcpConfig = await prepareMcpConfig({ + githubToken, + owner: context.repository.owner, + repo: context.repository.repo, + branch: currentBranch, + baseBranch: context.inputs.baseBranch || "main", + additionalMcpConfig, + claudeCommentId: undefined, // No tracking comment in agent mode + allowedTools, + context, + }); + + // Build final claude_args + const escapedMcpConfig = mcpConfig.replace(/'/g, "'\\''"); + const claudeArgs = `--mcp-config '${escapedMcpConfig}' ${userClaudeArgs}`.trim(); + core.setOutput("claude_args", claudeArgs); return { commentId: undefined, branchInfo: { - baseBranch: "", - currentBranch: "", + baseBranch: context.inputs.baseBranch || "main", + currentBranch, claudeBranch: undefined, }, - mcpConfig: JSON.stringify(mcpConfig), + mcpConfig, }; }, diff --git a/src/modes/agent/parse-tools.ts b/src/modes/agent/parse-tools.ts new file mode 100644 index 0000000..57cad49 --- /dev/null +++ b/src/modes/agent/parse-tools.ts @@ -0,0 +1,22 @@ +export function parseAllowedTools(claudeArgs: string): string[] { + // Match --allowedTools followed by the value + // Handle both quoted and unquoted values + const patterns = [ + /--allowedTools\s+"([^"]+)"/, // Double quoted + /--allowedTools\s+'([^']+)'/, // Single quoted + /--allowedTools\s+([^\s]+)/, // Unquoted + ]; + + for (const pattern of patterns) { + const match = claudeArgs.match(pattern); + if (match && match[1]) { + // Don't return if the value starts with -- (another flag) + if (match[1].startsWith('--')) { + return []; + } + return match[1].split(',').map(t => t.trim()); + } + } + + return []; +} \ No newline at end of file diff --git a/test/modes/parse-tools.test.ts b/test/modes/parse-tools.test.ts new file mode 100644 index 0000000..c1c73ff --- /dev/null +++ b/test/modes/parse-tools.test.ts @@ -0,0 +1,69 @@ +import { describe, test, expect } from "bun:test"; +import { parseAllowedTools } from "../../src/modes/agent/parse-tools"; + +describe("parseAllowedTools", () => { + test("parses unquoted tools", () => { + const args = "--allowedTools mcp__github__*,mcp__github_comment__*"; + expect(parseAllowedTools(args)).toEqual([ + "mcp__github__*", + "mcp__github_comment__*", + ]); + }); + + test("parses double-quoted tools", () => { + const args = '--allowedTools "mcp__github__*,mcp__github_comment__*"'; + expect(parseAllowedTools(args)).toEqual([ + "mcp__github__*", + "mcp__github_comment__*", + ]); + }); + + test("parses single-quoted tools", () => { + const args = "--allowedTools 'mcp__github__*,mcp__github_comment__*'"; + expect(parseAllowedTools(args)).toEqual([ + "mcp__github__*", + "mcp__github_comment__*", + ]); + }); + + test("returns empty array when no allowedTools", () => { + const args = "--someOtherFlag value"; + expect(parseAllowedTools(args)).toEqual([]); + }); + + test("handles empty string", () => { + expect(parseAllowedTools("")).toEqual([]); + }); + + test("handles duplicate --allowedTools flags", () => { + const args = "--allowedTools --allowedTools mcp__github__*"; + // Should not match the first one since the value is another flag + expect(parseAllowedTools(args)).toEqual([]); + }); + + test("handles typo --alloedTools", () => { + const args = "--alloedTools mcp__github__*"; + expect(parseAllowedTools(args)).toEqual([]); + }); + + test("handles multiple flags with allowedTools in middle", () => { + const args = '--flag1 value1 --allowedTools "mcp__github__*" --flag2 value2'; + expect(parseAllowedTools(args)).toEqual(["mcp__github__*"]); + }); + + test("trims whitespace from tool names", () => { + const args = "--allowedTools 'mcp__github__* , mcp__github_comment__* '"; + expect(parseAllowedTools(args)).toEqual([ + "mcp__github__*", + "mcp__github_comment__*", + ]); + }); + + test("handles tools with special characters", () => { + const args = '--allowedTools "mcp__github__create_issue,mcp__github_comment__update"'; + expect(parseAllowedTools(args)).toEqual([ + "mcp__github__create_issue", + "mcp__github_comment__update", + ]); + }); +}); \ No newline at end of file