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

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