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,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
}
|