/** * MNote MCP 扩展 — Pi Rust MCP 工具门面 * * Pi Rust 0.1.21 的异步 pi.exec()/嵌套 HTTP hostcall 在真实网页工具调用中 * 可能卡到扩展任务超时。官方 node:child_process shim 的 execFileSync 通过 * __pi_exec_sync_native 执行,因此这里同步启动相邻 client.mjs。 * * client.mjs 只读取扩展目录相邻的 .pi/mcp.json,并且请求只能选择其中已配置 * 的 server,不接受任意命令或配置路径。 */ import { execFileSync } from "node:child_process"; import path from "node:path"; import { fileURLToPath } from "node:url"; interface ExtensionAPI { registerTool: (spec: Record) => void; } interface ToolResult { content?: Array<{ type: string; text?: string; [key: string]: unknown }>; details?: Record; isError?: boolean; [key: string]: unknown; } const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url)); const MCP_CLIENT_PATH = path.join(EXTENSION_DIR, "client.mjs"); const MCP_REQUEST_MAX_CHARS = 128 * 1024; const MCP_CLIENT_TIMEOUT_MS = 90_000; const MCP_CLIENT_MAX_BUFFER = 2 * 1024 * 1024; const mcpToolParameters = { type: "object", properties: { server: { type: "string", description: "MCP 服务器名称(对应当前 MNote Pi session 的 mcp.json)", }, mode: { type: "string", enum: ["list", "status", "call"], description: "操作模式:list=列出工具,status=检查服务器状态,call=调用工具", }, tool: { type: "string", description: "mode=call 时需要调用的工具名称", }, arguments: { type: "object", description: "mode=call 时传入工具的参数对象", additionalProperties: true, }, }, required: ["server", "mode"], }; function executeMcpRequest(params: { server: string; mode: string; tool?: string; arguments?: Record; }): Record { const requestJson = JSON.stringify({ server: params.server, mode: params.mode, tool: params.tool, arguments: params.arguments || {}, }); if (requestJson.length > MCP_REQUEST_MAX_CHARS) { throw new Error(`MCP 请求超过 ${MCP_REQUEST_MAX_CHARS} 字符限制`); } const stdout = execFileSync("node", [ MCP_CLIENT_PATH, "--request-json", requestJson, ], { cwd: EXTENSION_DIR, timeout: MCP_CLIENT_TIMEOUT_MS, maxBuffer: MCP_CLIENT_MAX_BUFFER, }); const text = String(stdout || "").trim(); if (!text) { throw new Error("MNote MCP client 未返回结果"); } try { const payload = JSON.parse(text); if (!payload || typeof payload !== "object" || Array.isArray(payload)) { throw new Error("返回值不是 JSON 对象"); } return payload as Record; } catch (error) { throw new Error( `MNote MCP client 返回非 JSON: ${text.slice(0, 500)}` + (error instanceof Error ? ` (${error.message})` : ""), ); } } export default function mnoteMcpExtension(pi: ExtensionAPI) { pi.registerTool({ name: "mcp", label: "MNote MCP", description: "通过 Pi Rust 同步本地 client 调用当前 session 配置的 MCP 服务器。" + "支持 list(列出工具)、status(检查连通性)、call(调用工具)三种模式。", parameters: mcpToolParameters, async execute( _toolCallId: string, params: { server: string; mode: string; tool?: string; arguments?: Record }, ): Promise { const { server, mode, tool, arguments: args } = params; if (!["list", "status", "call"].includes(mode)) { return { content: [{ type: "text", text: `无效 mode: "${mode}"。可选值: list, status, call` }], isError: true, }; } if (mode === "call" && !tool) { return { content: [{ type: "text", text: 'mode=call 时缺少必填参数 "tool"' }], isError: true, }; } try { const result = executeMcpRequest({ server, mode, tool, arguments: args }); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: { server, mode, tool: tool || null, result, transport: "pi-rust-sync-client", }, isError: result.ok === false, }; } catch (error) { return { content: [{ type: "text", text: `MCP 调用失败: ${error instanceof Error ? error.message : String(error)}`, }], isError: true, }; } }, }); }