Files
dsh_shenxian/poc/carbon-mcp-probe/stub-mcp-server.mjs
T

155 lines
5.2 KiB
JavaScript
Raw Normal View History

#!/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();
}