#!/usr/bin/env node /** * carbon MCP probe — stub MCP server(零依赖单文件) * * 用途:只回答「dsh 的 mcp-client 能否连上并同步工具」这一个问题。 * 不实现任何业务语义,不做任何外部访问。 * * 用法: * node stub-mcp-server.mjs # stdio(默认) * node stub-mcp-server.mjs --http 39877 # Streamable HTTP * * 环境变量: * PROBE_LOG 请求日志落点(默认 ./stub-mcp.log) */ import http from "node:http"; import { appendFileSync, writeFileSync } from "node:fs"; const LOG = process.env.PROBE_LOG || "./stub-mcp.log"; const TOOL_NAME = "ping"; const PONG_PREFIX = "pong-carbon-probe"; try { writeFileSync(LOG, ""); } catch { /* 日志不可写不致命 */ } function log(line) { const row = `${new Date().toISOString()} ${line}\n`; try { appendFileSync(LOG, row); } catch { /* ignore */ } process.stderr.write(row); } const PING_TOOL = { name: TOOL_NAME, description: "carbon MCP probe stub — 返回固定串与服务端时间", inputSchema: { type: "object", properties: {}, additionalProperties: false }, }; function callTool(params) { if (!params || params.name !== TOOL_NAME) { return { content: [{ type: "text", text: `unknown tool: ${params && params.name}` }], isError: true }; } const ts = new Date().toISOString(); return { content: [{ type: "text", text: `${PONG_PREFIX} ${ts}` }] }; } /** 处理一条 JSON-RPC message,返回 response 或 null(notification)。 */ function handle(msg) { const method = msg && msg.method; const id = msg && msg.id; log(`rpc method=${method} id=${id === undefined ? "-" : id}`); if (method === "initialize") { return { jsonrpc: "2.0", id, result: { protocolVersion: msg.params && msg.params.protocolVersion ? msg.params.protocolVersion : "2025-06-18", capabilities: { tools: { listChanged: false } }, serverInfo: { name: "carbon-probe-stub", version: "0.0.1" }, }, }; } if (method === "notifications/initialized" || String(method || "").startsWith("notifications/")) return null; if (method === "ping") return { jsonrpc: "2.0", id, result: {} }; if (method === "tools/list") return { jsonrpc: "2.0", id, result: { tools: [PING_TOOL] } }; if (method === "tools/call") return { jsonrpc: "2.0", id, result: callTool(msg.params) }; if (id === undefined) return null; return { jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${method}` } }; } function dispatch(payload) { const msgs = Array.isArray(payload) ? payload : [payload]; const out = []; for (const m of msgs) { const r = handle(m); if (r !== null) out.push(r); } return out; } /* ---------------- Streamable HTTP ---------------- */ function startHttp(port) { const server = http.createServer((req, res) => { if (req.method === "GET" && req.url.startsWith("/health")) { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true, server: "carbon-probe-stub" })); return; } if (req.method !== "POST") { res.writeHead(405).end(); return; } let body = ""; req.on("data", (c) => { body += c.toString("utf8"); }); req.on("end", () => { let payload; try { payload = JSON.parse(body); } catch (e) { log(`http parse-error ${e.message} raw=${body.slice(0, 200)}`); res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "parse error" } })); return; } const out = dispatch(payload); if (out.length === 0) { log("http -> 202 (notification only)"); res.writeHead(202, { "Mcp-Session-Id": "probe-session-1" }); res.end(); return; } const payloadOut = out.length === 1 ? out[0] : out; log(`http -> 200 ${JSON.stringify(payloadOut).slice(0, 300)}`); res.writeHead(200, { "Content-Type": "application/json", "Mcp-Session-Id": "probe-session-1", }); res.end(JSON.stringify(payloadOut)); }); }); server.listen(port, "127.0.0.1", () => { log(`stub MCP server listening on http://127.0.0.1:${port}/mcp (transport=streamable-http)`); }); server.on("error", (e) => { log(`listen error ${e.message}`); process.exit(1); }); } /* ---------------- stdio ---------------- */ function startStdio() { log("stub MCP server on stdio (transport=stdio)"); let buf = ""; process.stdin.setEncoding("utf8"); process.stdin.on("data", (chunk) => { buf += chunk; let idx; while ((idx = buf.indexOf("\n")) >= 0) { const line = buf.slice(0, idx).trim(); buf = buf.slice(idx + 1); if (!line) continue; let msg; try { msg = JSON.parse(line); } catch (e) { log(`stdio parse-error ${e.message}`); continue; } const out = dispatch(msg); for (const r of out) process.stdout.write(JSON.stringify(r) + "\n"); } }); process.stdin.on("end", () => process.exit(0)); } const argv = process.argv.slice(2); const httpFlag = argv.indexOf("--http"); if (httpFlag >= 0) { const port = Number(argv[httpFlag + 1] || 39877); startHttp(port); } else { startStdio(); }