import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; import { mkdtemp, rm, mkdir } from "node:fs/promises"; import path from "node:path"; const execFileAsync = promisify(execFile); const WORKSPACE_ROOT = "/workspace"; // Runs the real Claude Code CLI — billed against the Claude Pro/Max subscription // (CLAUDE_CODE_OAUTH_TOKEN), not per-token API billing. This only works because it's // the actual `claude` binary making the request: Anthropic rejects the same OAuth token // used by any other HTTP client (proven earlier — direct curl replicating the same // request shape gets rejected). Read-only: no git/file-write tools, since this is a // quick-answer bridge, not a repo-editing agent (claude-agent's own webhook flow already // owns that for PRs). async function askClaudeSubscription(prompt) { await mkdir(WORKSPACE_ROOT, { recursive: true }); const dir = await mkdtemp(path.join(WORKSPACE_ROOT, "mcp-")); try { const { stdout } = await execFileAsync( "claude", [ "-p", prompt, "--output-format", "text", "--permission-mode", "bypassPermissions", "--disallowedTools", "Bash(git push:*),Bash(git commit:*),Edit,Write,NotebookEdit", ], { cwd: dir, maxBuffer: 1024 * 1024 * 32 } ); return stdout; } finally { await rm(dir, { recursive: true, force: true }); } } // A fresh McpServer per request (stateless transport) — cheap, and avoids any // cross-request state for what's a single-tool, single-shot bridge. export function createMcpServer() { const server = new McpServer({ name: "claude-code-bridge", version: "1.0.0" }); server.registerTool( "ask_claude_code", { description: "Ask the real Claude Code CLI a question or reasoning task, billed against the " + "Claude Pro/Max subscription rather than per-token API credits. Use this when " + "you specifically want Claude's own model rather than whatever the default " + "routed model provides. Read-only — cannot edit files, push, or commit.", inputSchema: { prompt: z.string().describe("The question or task to ask Claude") }, }, async ({ prompt }) => { try { const text = await askClaudeSubscription(prompt); return { content: [{ type: "text", text }] }; } catch (err) { return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true }; } } ); return server; } export function mcpAuthMiddleware(req, res, next) { const key = process.env.MCP_BRIDGE_KEY; if (!key) return res.status(500).send("MCP_BRIDGE_KEY not configured"); if (req.get("Authorization") !== `Bearer ${key}`) return res.status(401).send("unauthorized"); next(); }