Files
dsh_shenxian/poc/carbon-mcp-probe/stub-mcp-server.mjs
T
admin e6207aa691
build / build-and-scan (push) Waiting to run
chore(仓库对齐): 文档库结构治理 + IM/插件线落地
文档库:目录改为编号制(01-规范/02-架构设计/03-数据库/04-调整方案/
05-交接单/06-ops/07-scripts/08-skills/09-archive),顶层散文件归入 01-规范/;
INDEX.md 与 docs-manifest.json 重刷(档案 146 篇);旧目录名引用全量对齐。

IM 线:src/im/**(SDK / hub / store / presence / ws / gateway-token)、
src/web/routes/im.ts、src/db/plugin-data/**、src/supervisor/plugin-assembly.ts
及对应 test/**。

插件线:poc/{im-agent-bridge,im-connection-gateway,im-conversation-tabs,
business-plugins-im,carbon-mcp-probe}、src/web/routes/{sessions,overlay-device}.ts、
src/net/relay/{device-grant,instance-credential}.ts。

仓库卫生:清出 40 个历史误入库 / 已改名文件(34 个交接单归档 + 6 个旧结构,
本地均有副本);dsh-server-docs/.gitignore 补 tmp/;交接单不入库(政策)。
2026-09-24 07:25:16 +08:00

156 lines
5.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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();
}