151 lines
4.6 KiB
TypeScript
151 lines
4.6 KiB
TypeScript
/**
|
||
* 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,
|
||
};
|
||
}
|
||
},
|
||
});
|
||
}
|