/** * Minimal Model Context Protocol (MCP) server over stdio. * * Implements just enough of the MCP JSON-RPC 2.0 surface to expose * a static list of read-only validation tools to a Claude Code client: * - initialize * - tools/list * - tools/call * * We intentionally avoid the @modelcontextprotocol/sdk dependency here — * the KIS-TOiR optimization policy forbids adding new npm packages for * these lightweight validators, so we speak raw JSON-RPC on stdin/stdout * with Node built-ins only. * * Tool contract: * { * name: string, * description: string, * inputSchema?: JSONSchema, * execute: (args: object) => Promise<{ success: boolean, result?, error? }> * } * * execute() must be read-only, must resolve quickly (<500ms target), and * must NEVER throw — wrap failures in { success: false, error }. */ import readline from "node:readline"; const PROTOCOL_VERSION = "2024-11-05"; // JSON-RPC 2.0 constants const JSONRPC_VERSION = "2.0"; const FIELD_JSONRPC = "jsonrpc"; const FIELD_ID = "id"; const FIELD_METHOD = "method"; const FIELD_PARAMS = "params"; const FIELD_RESULT = "result"; const FIELD_ERROR = "error"; const FIELD_CODE = "code"; const FIELD_MESSAGE = "message"; const FIELD_DATA = "data"; const CONTENT_TYPE_TEXT = "text"; const FIELD_CONTENT = "content"; const FIELD_TYPE = "type"; const FIELD_IS_ERROR = "isError"; function write(obj) { // MCP framing over stdio is line-delimited JSON (one JSON object per line). process.stdout.write(JSON.stringify(obj) + "\n"); } function ok(id, result) { write({ [FIELD_JSONRPC]: JSONRPC_VERSION, [FIELD_ID]: id, [FIELD_RESULT]: result }); } function fail(id, code, message, data) { write({ [FIELD_JSONRPC]: JSONRPC_VERSION, [FIELD_ID]: id, [FIELD_ERROR]: { [FIELD_CODE]: code, [FIELD_MESSAGE]: message, [FIELD_DATA]: data }, }); } /** * Start an MCP server. Blocks on stdin until the parent process closes it. */ export function runMCPServer({ name, version = "0.1.0", tools }) { const byName = new Map(tools.map((t) => [t.name, t])); const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity, }); // Log to stderr so we don't corrupt the stdout JSON-RPC channel. const log = (level, msg) => process.stderr.write(`[${level}] ${name}: ${msg}\n`); log("INFO", `starting (tools=${tools.length})`); rl.on("line", async (line) => { const trimmed = line.trim(); if (!trimmed) return; let req; try { req = JSON.parse(trimmed); } catch (e) { log("WARN", `invalid JSON on stdin: ${e.message}`); return; } const { id, method, params } = req; // Notifications have no id and expect no response. const isNotification = id === undefined || id === null; try { if (method === "initialize") { ok(id, { protocolVersion: PROTOCOL_VERSION, capabilities: { tools: { listChanged: false } }, serverInfo: { name, version }, }); return; } if (method === "tools/list") { ok(id, { tools: tools.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema ?? { [FIELD_TYPE]: "object", properties: {}, additionalProperties: true, }, })), }); return; } if (method === "tools/call") { const toolName = params?.name; const tool = byName.get(toolName); if (!tool) { fail(id, -32601, `Unknown tool: ${toolName}`); return; } let result; try { // Enforce per-tool timeout: tools MUST resolve within 2000ms. // Prevents hung tools from blocking the orchestrator. const toolTimeout = new Promise((_, reject) => setTimeout( () => reject(new Error("Tool execution timeout (>2000ms)")), 2000 ) ); result = await Promise.race([ tool.execute(params?.arguments ?? {}), toolTimeout, ]); } catch (e) { // Capture all errors (timeout, execution, etc.) uniformly. result = { success: false, error: e?.message ?? String(e) }; } ok(id, { [FIELD_CONTENT]: [ { [FIELD_TYPE]: CONTENT_TYPE_TEXT, text: typeof result === "string" ? result : JSON.stringify(result, null, 2), }, ], [FIELD_IS_ERROR]: result?.success === false, }); return; } if (method?.startsWith("notifications/")) { // Client lifecycle notifications (initialized, cancelled, ...). return; } if (!isNotification) { fail(id, -32601, `Method not found: ${method}`); } } catch (e) { log("ERROR", `handler crashed: ${e?.stack ?? e}`); if (!isNotification) { fail(id, -32603, "Internal server error", { message: e?.message }); } } }); rl.on("close", () => { log("INFO", "stdin closed, exiting"); process.exit(0); }); }