Files
mnote/packages/pi-mnote/extensions/mnote-mcp/index.ts
T

151 lines
4.6 KiB
TypeScript
Raw Normal View History

2026-07-10 10:54:34 +08:00
/**
* 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<string, unknown>) => void;
}
interface ToolResult {
content?: Array<{ type: string; text?: string; [key: string]: unknown }>;
details?: Record<string, unknown>;
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<string, unknown>;
}): Record<string, unknown> {
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<string, unknown>;
} 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<string, unknown> },
): Promise<ToolResult> {
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,
};
}
},
});
}