155 lines
5.2 KiB
JavaScript
155 lines
5.2 KiB
JavaScript
#!/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();
|
|||
|
|
}
|