feat: integrate pi rust lab runtime
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* 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 { existsSync } from "node:fs";
|
||||
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> {
|
||||
if (!existsSync(MCP_CLIENT_PATH)) {
|
||||
throw new Error(`MNote MCP client 不存在: ${MCP_CLIENT_PATH}`);
|
||||
}
|
||||
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,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user