Files
Mango 935b943b6a 新增 AI Agent 的 MCP 工具,可增删改查快捷命令
通过 uTools 官方的 utools.registerTool 暴露 9 个 qc_* 工具,工具声明写在
plugin.json 的 tools 字段里,外部 Agent 连 uTools 的本地 MCP 服务即可调用。
数据层自包含在 plugin/lib 下(命令 CRUD 不依赖界面是否渲染),权限按
读取/写入/运行 三档控制,默认读开写开、运行关,开关放在 AI 配置界面底部。

顺带把打包后去掉 development 入口的构建钩子带过来,uTools 开发者工具加载
dist 目录需要它,否则会去连 quasar dev 的 8080 端口导致白屏。
2026-10-13 01:21:20 +08:00

154 lines
5.7 KiB
JavaScript

/**
* uTools MCP 服务的端到端自检:打一遍 initialize / tools/list / 增删改查
*
* 用法:node test/mcp-smoke.mjs http://127.0.0.1:3501/mcp <x-mcp-key>
* 端口和 key 在 uTools 设置 → MCP 服务 里。
*/
const [url, key] = process.argv.slice(2);
if (!url || !key) {
console.error("用法:node test/mcp-smoke.mjs <mcp-url> <x-mcp-key>");
process.exit(1);
}
let sessionId = null;
let nextId = 1;
let allTools = [];
const call = async (method, params) => {
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json, text/event-stream",
"x-mcp-key": key,
...(sessionId ? { "mcp-session-id": sessionId } : {}),
},
body: JSON.stringify({ jsonrpc: "2.0", id: nextId++, method, params }),
});
const newSession = res.headers.get("mcp-session-id");
if (newSession) sessionId = newSession;
const text = await res.text();
if (!res.ok) throw new Error(`${method} HTTP ${res.status}: ${text.slice(0, 200)}`);
const payload = text
.split("\n")
.filter((line) => line.startsWith("data:"))
.map((line) => line.slice(5).trim())
.join("");
const parsed = JSON.parse(payload || text);
if (parsed.error) throw new Error(`${method} 返回错误:${JSON.stringify(parsed.error)}`);
return parsed.result;
};
const callTool = async (bareName, args = {}) => {
// uTools 会给工具名加上插件前缀,tools/call 必须用完整名字
const tool = allTools.find((t) => t.name === bareName || t.name.endsWith("_" + bareName));
const result = await call("tools/call", { name: tool ? tool.name : bareName, arguments: args });
const text = (result.content || []).map((c) => c.text || "").join("");
try {
return JSON.parse(text);
} catch (e) {
return { raw: text, isError: result.isError };
}
};
const check = (label, condition, detail) => {
console.log(`${condition ? "✅" : "❌"} ${label}${detail ? " — " + detail : ""}`);
if (!condition) process.exitCode = 1;
};
await call("initialize", {
protocolVersion: "2025-06-18",
capabilities: {},
clientInfo: { name: "quickcommand-smoke", version: "1" },
});
await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json, text/event-stream",
"x-mcp-key": key,
"mcp-session-id": sessionId,
},
body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }),
});
console.log("✅ initialize");
const { tools } = await call("tools/list");
allTools = tools;
const ours = tools.map((t) => t.name).filter((n) => n.includes("qc_"));
check("tools/list 里有 quickcommand 的工具", ours.length > 0, ours.join(", ") || "一个都没有");
if (!ours.length) {
console.log(
"提示:确认 uTools 的 MCP 设置里没有禁用 quickcommand,且插件已重新加载过 plugin.json"
);
process.exit(1);
}
const schema = await callTool("qc_get_schema");
check("qc_get_schema", schema.ok === true && schema.schema.length > 500);
const before = await callTool("qc_list_commands");
check("qc_list_commands", before.ok === true, `当前 ${before.total} 条用户命令`);
const created = await callTool("qc_create_command", {
command: {
program: "quickcommand",
cmd: "console.log('hello from mcp')",
features: { explain: "MCP 自检命令", cmds: ["mcp自检"] },
output: "text",
tags: ["自检"],
},
});
check("qc_create_command", created.ok === true && /^key_/.test(created.code || ""), created.code);
if (!created.ok) {
console.log(JSON.stringify(created, null, 2));
process.exit(1);
}
const code = created.code;
// 故意写错,确认返回可读的校验错误
const bad = await callTool("qc_create_command", {
command: { program: "python3", features: { explain: "", cmds: [{ type: "regexp" }] } },
});
check("参数不合法时返回 errors", bad.ok === false && Array.isArray(bad.errors), `${(bad.errors || []).length} 条`);
const one = await callTool("qc_get_command", { code });
check("qc_get_command", one.ok === true && one.command.cmd.includes("hello from mcp"));
const listed = await callTool("qc_list_commands", { keyword: "自检" });
check("qc_list_commands 能按关键词搜到", (listed.commands || []).some((c) => c.code === code));
const updated = await callTool("qc_update_command", {
code,
command: { ...one.command, cmd: "console.log('updated')" },
});
check("qc_update_command", updated.ok === true && updated.command.cmd.includes("updated"));
const disabled = await callTool("qc_set_enabled", { codes: [code], enabled: false });
const offList = await callTool("qc_list_commands", { enabled: false });
check(
"qc_set_enabled 禁用后列表可见",
disabled.ok === true && offList.commands.some((c) => c.code === code)
);
await callTool("qc_set_enabled", { codes: [code], enabled: true });
const tags = await callTool("qc_list_tags");
check("qc_list_tags", tags.ok === true && tags.tags.some((t) => t.tag === "自检"));
const run = await callTool("qc_run_command", { code });
// 运行档默认关:工具会被反注册,uTools 仍会列出它但调用报「未注册」
const runBlocked = run.ok !== true;
check(
"qc_run_command 的运行权限按开关生效",
runBlocked ? /未注册|权限/.test(run.raw || run.error || "") : run.ok === true,
runBlocked ? "运行档关闭(默认),工具未注册" : JSON.stringify(run.output)
);
const removed = await callTool("qc_delete_command", { code });
check("qc_delete_command", removed.ok === true && removed.command.features.code === code);
const builtin = await callTool("qc_delete_command", { code: "default_textReplacer" });
check("内置命令拒写", builtin.ok === false, builtin.error);
console.log(process.exitCode ? "有失败项" : "全部通过");