mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-22 22:44:13 +08:00
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
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { GITHUB_API_URL, GITHUB_SERVER_URL } from "../github/api/config";
|
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";
|
import { Octokit } from "@octokit/rest";
|
||||||
|
|
||||||
type PrepareConfigParams = {
|
type PrepareConfigParams = {
|
||||||
@@ -12,7 +13,7 @@ type PrepareConfigParams = {
|
|||||||
additionalMcpConfig?: string;
|
additionalMcpConfig?: string;
|
||||||
claudeCommentId?: string;
|
claudeCommentId?: string;
|
||||||
allowedTools: string[];
|
allowedTools: string[];
|
||||||
context: ParsedGitHubContext;
|
context: GitHubContext;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function checkActionsReadPermission(
|
async function checkActionsReadPermission(
|
||||||
@@ -67,6 +68,10 @@ export async function prepareMcpConfig(
|
|||||||
const hasGitHubMcpTools = allowedToolsList.some((tool) =>
|
const hasGitHubMcpTools = allowedToolsList.some((tool) =>
|
||||||
tool.startsWith("mcp__github__"),
|
tool.startsWith("mcp__github__"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const hasInlineCommentTools = allowedToolsList.some((tool) =>
|
||||||
|
tool.startsWith("mcp__github_inline_comment__"),
|
||||||
|
);
|
||||||
|
|
||||||
const baseMcpConfig: { mcpServers: Record<string, unknown> } = {
|
const baseMcpConfig: { mcpServers: Record<string, unknown> } = {
|
||||||
mcpServers: {},
|
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
|
// CI server is included when we have a workflow token and context is a PR
|
||||||
const hasWorkflowToken = !!process.env.DEFAULT_WORKFLOW_TOKEN;
|
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
|
// Verify the token actually has actions:read permission
|
||||||
const actuallyHasPermission = await checkActionsReadPermission(
|
const actuallyHasPermission = await checkActionsReadPermission(
|
||||||
process.env.DEFAULT_WORKFLOW_TOKEN || "",
|
process.env.DEFAULT_WORKFLOW_TOKEN || "",
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import * as core from "@actions/core";
|
|||||||
import { mkdir, writeFile } from "fs/promises";
|
import { mkdir, writeFile } from "fs/promises";
|
||||||
import type { Mode, ModeOptions, ModeResult } from "../types";
|
import type { Mode, ModeOptions, ModeResult } from "../types";
|
||||||
import type { PreparedContext } from "../../create-prompt/types";
|
import type { PreparedContext } from "../../create-prompt/types";
|
||||||
|
import { prepareMcpConfig } from "../../mcp/install-mcp-server";
|
||||||
|
import { parseAllowedTools } from "./parse-tools";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Agent mode implementation.
|
* Agent mode implementation.
|
||||||
@@ -39,7 +41,7 @@ export const agentMode: Mode = {
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
async prepare({ context, githubToken, octokit }: ModeOptions): Promise<ModeResult> {
|
async prepare({ context, githubToken }: ModeOptions): Promise<ModeResult> {
|
||||||
// Create prompt directory
|
// Create prompt directory
|
||||||
await mkdir(`${process.env.RUNNER_TEMP}/claude-prompts`, {
|
await mkdir(`${process.env.RUNNER_TEMP}/claude-prompts`, {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
@@ -54,43 +56,43 @@ export const agentMode: Mode = {
|
|||||||
promptContent,
|
promptContent,
|
||||||
);
|
);
|
||||||
|
|
||||||
const mcpConfig: any = {
|
// Parse allowed tools from user's claude_args
|
||||||
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
|
|
||||||
const userClaudeArgs = process.env.CLAUDE_ARGS || "";
|
const userClaudeArgs = process.env.CLAUDE_ARGS || "";
|
||||||
const escapedMcpConfig = JSON.stringify(mcpConfig).replace(/'/g, "'\\''");
|
const allowedTools = parseAllowedTools(userClaudeArgs);
|
||||||
const claudeArgs =
|
|
||||||
`--mcp-config '${escapedMcpConfig}' ${userClaudeArgs}`.trim();
|
// 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);
|
core.setOutput("claude_args", claudeArgs);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
commentId: undefined,
|
commentId: undefined,
|
||||||
branchInfo: {
|
branchInfo: {
|
||||||
baseBranch: "",
|
baseBranch: context.inputs.baseBranch || "main",
|
||||||
currentBranch: "",
|
currentBranch,
|
||||||
claudeBranch: undefined,
|
claudeBranch: undefined,
|
||||||
},
|
},
|
||||||
mcpConfig: JSON.stringify(mcpConfig),
|
mcpConfig,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
22
src/modes/agent/parse-tools.ts
Normal file
22
src/modes/agent/parse-tools.ts
Normal file
@@ -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 [];
|
||||||
|
}
|
||||||
69
test/modes/parse-tools.test.ts
Normal file
69
test/modes/parse-tools.test.ts
Normal file
@@ -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",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user