1#!/usr/bin/env node
The MCP server Claude Code starts from .mcp.json (nix run .#mcp): MCP
over stdio, newline-delimited JSON-RPC, forwarded message by message to a
jevstrudel MCP endpoint (proxy.mjs). By default that is the dev Worker's
(POST /mcp, worker/src/mcp.ts) that buck2 run //:dev runs under
wrangler dev; the use_environment tool switches to the public site's
hosted MCP, signed in with OAuth (oauth.mjs), and back. It holds no
connection, so the Worker can restart or reload under a running Claude
Code session.
While the dev Worker is down, tools/list answers from worker/src/tools.json and shared-tools.json (the tools both MCPs offer), and tool calls say how to start it. The proxy polls the dev Worker's tool list and sends notifications/tools/list_changed when it differs, so a tool edited in the Worker reaches Claude Code without a restart.
JEVSTRUDEL_MCP_URL overrides the dev endpoint, JEVSTRUDEL_PROD_MCP_URL the prod one (for testing against a local production Worker, never otherwise).
25const worker = new URL('../../worker/', import.meta.url); 26const read = (path) => JSON.parse(readFileSync(new URL(path, worker))); 27const port = read('wrangler.json').dev.port; 28const devEndpoint = process.env.JEVSTRUDEL_MCP_URL ?? `http://localhost:${port}/mcp`; 29const prodEndpoint = process.env.JEVSTRUDEL_PROD_MCP_URL ?? 'https://jevstrudel.deizel.workers.dev/jev/mcp'; 30 31const send = (message) => process.stdout.write(JSON.stringify(message) + '\n');
stdout is the protocol; anything else goes to stderr, which Claude Code logs
33const log = (line) => process.stderr.write(`[jevstrudel-mcp] ${line}\n`);
35const session = oauthSession({ serverUrl: prodEndpoint, openBrowser }); 36const proxy = createProxy({ 37 dev: { 38 endpoint: devEndpoint, 39 // as the dev Worker lists them (worker/src/mcp.ts's DEV_TOOLS) 40 fallbackTools: () => [...read('src/tools.json'), ...read('src/shared-tools.json').map(({ scope: _scope, ...tool }) => tool)], 41 }, 42 prod: { endpoint: prodEndpoint, session }, 43 send, 44 log, 45}); 46 47setInterval(() => proxy.poll(), 3000).unref();
The grant ends with the session: revoke it, so it leaves the user's connected apps, then exit.
61for await (const line of createInterface({ input: process.stdin })) { 62 if (!line.trim()) continue; 63 let message; 64 try { 65 message = JSON.parse(line); 66 } catch { 67 send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'parse error' } }); 68 continue; 69 } 70 if (message.id === undefined) continue; // notifications need no answer 71 proxy.handle(message).then(send); 72} 73await exit();