feat(prd-1): implement M6 - MCP server platform routing
- Add platform detection to prepareMcpConfig() via detectPlatform() - Skip GitHub public MCP server (ghcr.io/github/github-mcp-server) on Forgejo - Skip checkActionsReadPermission on Forgejo (workflow token has full access) - Reuse existing github-actions-server.ts for both platforms (no duplication) - Add 6 Forgejo-specific tests to install-mcp-server.test.ts - Updated PRD checkboxes for M6 Progress: 67% complete (6/9 milestones) - M7 now unblocked Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
0802ac18eb
commit
ce1459fe7f
3 files changed
+208
-17
No files matched your search
@@ -270,11 +270,11 @@ The upstream repo has 13 workflows in `.github/workflows/`. Strategy:
|
|||||||
|
|
||||||
### Phase 3 - Integration (wiring it together)
|
### Phase 3 - Integration (wiring it together)
|
||||||
|
|
||||||
- [ ] **M6: MCP server platform routing** — Modify `install-mcp-server.ts` to select GitHub or Forgejo server implementations based on platform. Create `src/mcp/forgejo-actions-server.ts` for CI status (may be a subset of GitHub's if some Actions API endpoints don't exist in Forgejo). Keep MCP server names identical (`github_comment`, `github_ci`, etc.) to avoid touching tool name lists. For comment/file-ops/inline-comment servers, the existing GitHub implementations likely work via Octokit REST — verify and use directly.
|
- [x] **M6: MCP server platform routing** — Modified `install-mcp-server.ts` to add platform detection. Skips GitHub public MCP server (`ghcr.io/github/github-mcp-server`) on Forgejo. Skips `checkActionsReadPermission` on Forgejo (workflow token has full repo access). Did NOT create `src/mcp/forgejo-actions-server.ts` — existing `github-actions-server.ts` works on Forgejo via Octokit REST + `GITHUB_API_URL` (zero duplication, easier maintenance). Server names remain `github_comment`, `github_ci`, etc.
|
||||||
|
|
||||||
- Files: `src/mcp/install-mcp-server.ts` (MODIFIED), `src/mcp/forgejo-actions-server.ts` (NEW, possibly)
|
- Files: `src/mcp/install-mcp-server.ts` (MODIFIED), `test/install-mcp-server.test.ts` (MODIFIED — 6 new Forgejo tests)
|
||||||
- Depends on: M4b, M5
|
- Depends on: M4b, M5
|
||||||
- Success: Claude can update comments, query CI status on Forgejo. MCP tools work end-to-end.
|
- Success: All MCP servers route correctly on both platforms. 772 tests pass.
|
||||||
|
|
||||||
- [ ] **M7: Entry point + mode handler integration** — Wire platform detection into mode handlers. In `tag/index.ts`: swap `fetchGitHubData` for Forgejo fetcher + comment logic. In `agent/index.ts`: swap token setup. Keep changes surgical — platform branch at the top of functions, existing flow untouched.
|
- [ ] **M7: Entry point + mode handler integration** — Wire platform detection into mode handlers. In `tag/index.ts`: swap `fetchGitHubData` for Forgejo fetcher + comment logic. In `agent/index.ts`: swap token setup. Keep changes surgical — platform branch at the top of functions, existing flow untouched.
|
||||||
- Files: `src/modes/tag/index.ts` (MODIFIED), `src/modes/agent/index.ts` (MODIFIED)
|
- Files: `src/modes/tag/index.ts` (MODIFIED), `src/modes/agent/index.ts` (MODIFIED)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { GitHubContext } from "../github/context";
|
|||||||
import { isEntityContext } from "../github/context";
|
import { isEntityContext } from "../github/context";
|
||||||
import { Octokit } from "@octokit/rest";
|
import { Octokit } from "@octokit/rest";
|
||||||
import type { AutoDetectedMode } from "../modes/detector";
|
import type { AutoDetectedMode } from "../modes/detector";
|
||||||
|
import { detectPlatform } from "../platform/detector";
|
||||||
|
|
||||||
type PrepareConfigParams = {
|
type PrepareConfigParams = {
|
||||||
githubToken: string;
|
githubToken: string;
|
||||||
@@ -66,6 +67,10 @@ export async function prepareMcpConfig(
|
|||||||
try {
|
try {
|
||||||
const allowedToolsList = allowedTools || [];
|
const allowedToolsList = allowedTools || [];
|
||||||
|
|
||||||
|
// Detect platform (github vs forgejo)
|
||||||
|
const platform = detectPlatform();
|
||||||
|
const isForgejo = platform === "forgejo";
|
||||||
|
|
||||||
// Detect if we're in agent mode (explicit prompt provided)
|
// Detect if we're in agent mode (explicit prompt provided)
|
||||||
const isAgentMode = mode === "agent";
|
const isAgentMode = mode === "agent";
|
||||||
|
|
||||||
@@ -167,21 +172,31 @@ export async function prepareMcpConfig(
|
|||||||
hasWorkflowToken;
|
hasWorkflowToken;
|
||||||
|
|
||||||
if (shouldIncludeCIServer) {
|
if (shouldIncludeCIServer) {
|
||||||
// Verify the token actually has actions:read permission
|
// On Forgejo, the workflow token has full repo access — skip the
|
||||||
const actuallyHasPermission = await checkActionsReadPermission(
|
// GitHub-specific permission probe which checks for 'actions: read'.
|
||||||
process.env.DEFAULT_WORKFLOW_TOKEN || "",
|
// On GitHub, verify the token actually has actions:read permission.
|
||||||
owner,
|
let includeCIServer = isForgejo;
|
||||||
repo,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!actuallyHasPermission) {
|
if (!includeCIServer) {
|
||||||
core.warning(
|
const actuallyHasPermission = await checkActionsReadPermission(
|
||||||
"The github_ci MCP server requires 'actions: read' permission. " +
|
process.env.DEFAULT_WORKFLOW_TOKEN || "",
|
||||||
"Skipping CI server installation. " +
|
owner,
|
||||||
"To enable CI status checks, add 'actions: read' to your workflow permissions. " +
|
repo,
|
||||||
"See: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token",
|
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
|
if (!actuallyHasPermission) {
|
||||||
|
core.warning(
|
||||||
|
"The github_ci MCP server requires 'actions: read' 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 {
|
||||||
|
includeCIServer = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeCIServer) {
|
||||||
baseMcpConfig.mcpServers.github_ci = {
|
baseMcpConfig.mcpServers.github_ci = {
|
||||||
command: "bun",
|
command: "bun",
|
||||||
args: [
|
args: [
|
||||||
@@ -200,7 +215,9 @@ export async function prepareMcpConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasGitHubMcpTools) {
|
// The GitHub public MCP server (ghcr.io/github/github-mcp-server) is
|
||||||
|
// GitHub-only — it doesn't work with Forgejo instances.
|
||||||
|
if (hasGitHubMcpTools && !isForgejo) {
|
||||||
baseMcpConfig.mcpServers.github = {
|
baseMcpConfig.mcpServers.github = {
|
||||||
command: "docker",
|
command: "docker",
|
||||||
args: [
|
args: [
|
||||||
|
|||||||
@@ -313,4 +313,178 @@ describe("prepareMcpConfig", () => {
|
|||||||
const parsed = JSON.parse(result);
|
const parsed = JSON.parse(result);
|
||||||
expect(parsed.mcpServers.github_ci).not.toBeDefined();
|
expect(parsed.mcpServers.github_ci).not.toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Forgejo platform routing", () => {
|
||||||
|
let savedPlatform: string | undefined;
|
||||||
|
let savedForgejo: string | undefined;
|
||||||
|
let savedServerUrl: string | undefined;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
savedPlatform = process.env.PLATFORM;
|
||||||
|
savedForgejo = process.env.FORGEJO;
|
||||||
|
savedServerUrl = process.env.GITHUB_SERVER_URL;
|
||||||
|
// Set platform to forgejo for all tests in this block
|
||||||
|
process.env.PLATFORM = "forgejo";
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (savedPlatform !== undefined) {
|
||||||
|
process.env.PLATFORM = savedPlatform;
|
||||||
|
} else {
|
||||||
|
delete process.env.PLATFORM;
|
||||||
|
}
|
||||||
|
if (savedForgejo !== undefined) {
|
||||||
|
process.env.FORGEJO = savedForgejo;
|
||||||
|
} else {
|
||||||
|
delete process.env.FORGEJO;
|
||||||
|
}
|
||||||
|
if (savedServerUrl !== undefined) {
|
||||||
|
process.env.GITHUB_SERVER_URL = savedServerUrl;
|
||||||
|
} else {
|
||||||
|
delete process.env.GITHUB_SERVER_URL;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should skip GitHub public MCP server on Forgejo", async () => {
|
||||||
|
const result = await prepareMcpConfig({
|
||||||
|
githubToken: "test-token",
|
||||||
|
owner: "test-owner",
|
||||||
|
repo: "test-repo",
|
||||||
|
branch: "test-branch",
|
||||||
|
baseBranch: "main",
|
||||||
|
allowedTools: [
|
||||||
|
"mcp__github__create_issue",
|
||||||
|
"mcp__github__create_pr",
|
||||||
|
],
|
||||||
|
mode: "tag",
|
||||||
|
context: mockContext,
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
// The Docker-based GitHub MCP server should NOT be included on Forgejo
|
||||||
|
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should include CI server on Forgejo without permission check", async () => {
|
||||||
|
process.env.DEFAULT_WORKFLOW_TOKEN = "forgejo-workflow-token";
|
||||||
|
// Simulate a 403 from the permission check — on Forgejo this should be
|
||||||
|
// skipped entirely, so the CI server is still included.
|
||||||
|
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);
|
||||||
|
// CI server should be included on Forgejo even though the permission
|
||||||
|
// check would fail — the check is skipped entirely.
|
||||||
|
expect(parsed.mcpServers.github_ci).toBeDefined();
|
||||||
|
expect(parsed.mcpServers.github_ci.env.GITHUB_TOKEN).toBe(
|
||||||
|
"forgejo-workflow-token",
|
||||||
|
);
|
||||||
|
|
||||||
|
delete process.env.DEFAULT_WORKFLOW_TOKEN;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should not call checkActionsReadPermission on Forgejo", async () => {
|
||||||
|
process.env.DEFAULT_WORKFLOW_TOKEN = "forgejo-workflow-token";
|
||||||
|
fetchSpy.mockClear();
|
||||||
|
|
||||||
|
await prepareMcpConfig({
|
||||||
|
githubToken: "test-token",
|
||||||
|
owner: "test-owner",
|
||||||
|
repo: "test-repo",
|
||||||
|
branch: "test-branch",
|
||||||
|
baseBranch: "main",
|
||||||
|
allowedTools: [],
|
||||||
|
mode: "tag",
|
||||||
|
context: mockPRContext,
|
||||||
|
});
|
||||||
|
|
||||||
|
// fetch should not have been called — the permission check is skipped
|
||||||
|
expect(fetchSpy).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
delete process.env.DEFAULT_WORKFLOW_TOKEN;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should keep server names as github_* on Forgejo", async () => {
|
||||||
|
process.env.DEFAULT_WORKFLOW_TOKEN = "forgejo-workflow-token";
|
||||||
|
|
||||||
|
const result = await prepareMcpConfig({
|
||||||
|
githubToken: "test-token",
|
||||||
|
owner: "test-owner",
|
||||||
|
repo: "test-repo",
|
||||||
|
branch: "test-branch",
|
||||||
|
baseBranch: "main",
|
||||||
|
allowedTools: [],
|
||||||
|
mode: "tag",
|
||||||
|
context: {
|
||||||
|
...mockPRContext,
|
||||||
|
inputs: { ...mockPRContext.inputs, useCommitSigning: true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
// Server names must stay as github_* to match hardcoded tool names
|
||||||
|
expect(parsed.mcpServers.github_comment).toBeDefined();
|
||||||
|
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||||
|
expect(parsed.mcpServers.github_ci).toBeDefined();
|
||||||
|
|
||||||
|
delete process.env.DEFAULT_WORKFLOW_TOKEN;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should include comment server on Forgejo in tag mode", async () => {
|
||||||
|
const result = await prepareMcpConfig({
|
||||||
|
githubToken: "test-token",
|
||||||
|
owner: "test-owner",
|
||||||
|
repo: "test-repo",
|
||||||
|
branch: "test-branch",
|
||||||
|
baseBranch: "main",
|
||||||
|
allowedTools: [],
|
||||||
|
mode: "tag",
|
||||||
|
context: mockContext,
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
expect(parsed.mcpServers.github_comment).toBeDefined();
|
||||||
|
expect(parsed.mcpServers.github_comment.env.GITHUB_TOKEN).toBe(
|
||||||
|
"test-token",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should include inline comment server on Forgejo for PRs", async () => {
|
||||||
|
const result = await prepareMcpConfig({
|
||||||
|
githubToken: "test-token",
|
||||||
|
owner: "test-owner",
|
||||||
|
repo: "test-repo",
|
||||||
|
branch: "test-branch",
|
||||||
|
baseBranch: "main",
|
||||||
|
allowedTools: [
|
||||||
|
"mcp__github_inline_comment__create_inline_comment",
|
||||||
|
],
|
||||||
|
mode: "tag",
|
||||||
|
context: mockPRContext,
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
expect(parsed.mcpServers.github_inline_comment).toBeDefined();
|
||||||
|
expect(
|
||||||
|
parsed.mcpServers.github_inline_comment.env.GITHUB_TOKEN,
|
||||||
|
).toBe("test-token");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user