build / build-and-scan (push) Waiting to run
文档库:目录改为编号制(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/;交接单不入库(政策)。
156 lines
5.2 KiB
JavaScript
156 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();
|
||
}
|