diff --git a/src/mcp/install-mcp-server.ts b/src/mcp/install-mcp-server.ts index 22de611..d5fac34 100644 --- a/src/mcp/install-mcp-server.ts +++ b/src/mcp/install-mcp-server.ts @@ -177,25 +177,27 @@ export async function prepareMcpConfig( if (!actuallyHasPermission) { core.warning( "The github_ci MCP server requires 'actions: read' permission. " + - "Please ensure your GitHub token has this permission. " + + "Skipping CI server installation. " + + "To enable CI status checks, add 'actions: read' to your workflow permissions. " + "See: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token", ); + } else { + baseMcpConfig.mcpServers.github_ci = { + command: "bun", + args: [ + "run", + `${process.env.GITHUB_ACTION_PATH}/src/mcp/github-actions-server.ts`, + ], + env: { + // Use workflow github token, not app token + GITHUB_TOKEN: process.env.DEFAULT_WORKFLOW_TOKEN, + REPO_OWNER: owner, + REPO_NAME: repo, + PR_NUMBER: context.entityNumber?.toString() || "", + RUNNER_TEMP: process.env.RUNNER_TEMP || "/tmp", + }, + }; } - baseMcpConfig.mcpServers.github_ci = { - command: "bun", - args: [ - "run", - `${process.env.GITHUB_ACTION_PATH}/src/mcp/github-actions-server.ts`, - ], - env: { - // Use workflow github token, not app token - GITHUB_TOKEN: process.env.DEFAULT_WORKFLOW_TOKEN, - REPO_OWNER: owner, - REPO_NAME: repo, - PR_NUMBER: context.entityNumber?.toString() || "", - RUNNER_TEMP: process.env.RUNNER_TEMP || "/tmp", - }, - }; } if (hasGitHubMcpTools) { diff --git a/test/install-mcp-server.test.ts b/test/install-mcp-server.test.ts index 8de7b5c..152d2be 100644 --- a/test/install-mcp-server.test.ts +++ b/test/install-mcp-server.test.ts @@ -9,6 +9,7 @@ describe("prepareMcpConfig", () => { let consoleWarningSpy: any; let setFailedSpy: any; let processExitSpy: any; + let fetchSpy: any; // Create a mock context for tests const mockContext: ParsedGitHubContext = { @@ -66,6 +67,10 @@ describe("prepareMcpConfig", () => { processExitSpy = spyOn(process, "exit").mockImplementation(() => { throw new Error("Process exit"); }); + // Mock fetch so checkActionsReadPermission succeeds (returns 200 for actions API) + fetchSpy = spyOn(global, "fetch").mockResolvedValue( + new Response(JSON.stringify({ workflow_runs: [] }), { status: 200 }), + ); // Set up required environment variables if (!process.env.GITHUB_ACTION_PATH) { @@ -78,6 +83,7 @@ describe("prepareMcpConfig", () => { consoleWarningSpy.mockRestore(); setFailedSpy.mockRestore(); processExitSpy.mockRestore(); + fetchSpy.mockRestore(); }); test("should return comment server when commit signing is disabled", async () => { @@ -263,6 +269,33 @@ describe("prepareMcpConfig", () => { expect(parsed.mcpServers.github_ci).not.toBeDefined(); }); + test("should not include github_ci server when actions:read permission is missing", async () => { + process.env.DEFAULT_WORKFLOW_TOKEN = "workflow-token"; + // Simulate 403 from actions API + fetchSpy.mockResolvedValue( + new Response( + JSON.stringify({ message: "Resource not accessible by integration" }), + { status: 403 }, + ), + ); + + const result = await prepareMcpConfig({ + githubToken: "test-token", + owner: "test-owner", + repo: "test-repo", + branch: "test-branch", + baseBranch: "main", + allowedTools: [], + mode: "tag", + context: mockPRContext, + }); + + const parsed = JSON.parse(result); + expect(parsed.mcpServers.github_ci).not.toBeDefined(); + + delete process.env.DEFAULT_WORKFLOW_TOKEN; + }); + test("should not include github_ci server when DEFAULT_WORKFLOW_TOKEN is missing", async () => { delete process.env.DEFAULT_WORKFLOW_TOKEN;