@youweichen/pi-web-ui 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +45 -0
- package/bin/pi-web-ui.mjs +1818 -0
- package/deploy/com.xingshuyin.pi-web-ui.plist +48 -0
- package/deploy/nginx-subpath.conf +88 -0
- package/deploy/pi-web-ui-task.xml +71 -0
- package/deploy/pi-web-ui.service +31 -0
- package/dist/server/agent-service.js +2975 -0
- package/dist/server/attachments.js +672 -0
- package/dist/server/bg-servers.js +150 -0
- package/dist/server/client-state.js +234 -0
- package/dist/server/control-socket.js +177 -0
- package/dist/server/ensure-bash.js +80 -0
- package/dist/server/files-service.js +709 -0
- package/dist/server/goal-service.js +869 -0
- package/dist/server/index.js +879 -0
- package/dist/server/mcp-bridge.js +268 -0
- package/dist/server/model-admin.js +821 -0
- package/dist/server/patch-node-pty.js +80 -0
- package/dist/server/plugin-facilities.js +299 -0
- package/dist/server/plugin-updater.js +226 -0
- package/dist/server/plugins.js +893 -0
- package/dist/server/process-utils.js +112 -0
- package/dist/server/protocol-version.js +11 -0
- package/dist/server/protocol.js +6 -0
- package/dist/server/scm.js +298 -0
- package/dist/server/serialize.js +172 -0
- package/dist/server/settings-service.js +395 -0
- package/dist/server/slash-commands.js +263 -0
- package/dist/server/terminals.js +1372 -0
- package/dist/server/text-sniff.js +268 -0
- package/dist/server/uploads.js +107 -0
- package/dist/server/vision-bridge.js +131 -0
- package/dist/server/webui-context.js +222 -0
- package/extensions/webui.ts +192 -0
- package/package.json +99 -0
- package/web/dist/assets/TerminalPanel-6GBZ9nXN.css +32 -0
- package/web/dist/assets/TerminalPanel-D4AktsEn.js +2 -0
- package/web/dist/assets/abnfDiagram-VCTEODGH-Ct5StJwt.js +1 -0
- package/web/dist/assets/arc-pkuxEN8A.js +1 -0
- package/web/dist/assets/architectureDiagram-5GKGNRK7-C6P6yn4y.js +36 -0
- package/web/dist/assets/blockDiagram-I7D4REHJ-BmgfH1jt.js +129 -0
- package/web/dist/assets/c4Diagram-7LVT6UL2-CgmwTFsK.js +38 -0
- package/web/dist/assets/channel-0Y__6sq2.js +1 -0
- package/web/dist/assets/chunk-2Q5K7J3B-DX_NqAiH.js +1 -0
- package/web/dist/assets/chunk-5VM5RSS4-gLeuQuYv.js +15 -0
- package/web/dist/assets/chunk-F27PBJKO-D4WX4mhI.js +1 -0
- package/web/dist/assets/chunk-IMKFNOWR-DBpDxGQD.js +231 -0
- package/web/dist/assets/chunk-JWPE2WC7-DIQ8iZN5.js +1 -0
- package/web/dist/assets/chunk-POPQ4Y6H-F_YTNoGz.js +1 -0
- package/web/dist/assets/chunk-SVP7TREG-CmDIvX-r.js +88 -0
- package/web/dist/assets/chunk-TICWLB2K-BQUzHQyS.js +206 -0
- package/web/dist/assets/chunk-XXDRQBXY-DqR9iffH.js +1 -0
- package/web/dist/assets/classDiagram-ZZMXUADV-BEfGIpzu.js +1 -0
- package/web/dist/assets/classDiagram-v2-VYDZK3BY-BEfGIpzu.js +1 -0
- package/web/dist/assets/cose-bilkent-JH36ORCC-Dl67W4Dx.js +1 -0
- package/web/dist/assets/cynefin-OW5HDTMX-fWdqaCpX.js +178 -0
- package/web/dist/assets/cynefinDiagram-5FMLGOSQ-CSUAXbaP.js +62 -0
- package/web/dist/assets/cytoscape.esm-Bch-eiPH.js +331 -0
- package/web/dist/assets/dagre-GXQ25YYZ-CqUVwu1X.js +4 -0
- package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/web/dist/assets/diagram-S7CK7UJ4-bRce-WYV.js +30 -0
- package/web/dist/assets/diagram-UQ7AKVKN-CiqPtPBE.js +41 -0
- package/web/dist/assets/diagram-VSXAHHWV-CbEA05LX.js +3 -0
- package/web/dist/assets/diagram-VX7I27RA-CRcY9MD-.js +24 -0
- package/web/dist/assets/diagram-Z3DM3KII-DmTwvZdS.js +24 -0
- package/web/dist/assets/ebnfDiagram-PWID7BFC-DbwmiGVj.js +1 -0
- package/web/dist/assets/erDiagram-RLTQ6QDP-Cxiaf87y.js +99 -0
- package/web/dist/assets/flowDiagram-HODETNUW-BQiecP1I.js +168 -0
- package/web/dist/assets/ganttDiagram-EL5Y4UJY-CSr8RRNw.js +292 -0
- package/web/dist/assets/gitGraphDiagram-WWUBYQGX-C4Tdis3g.js +106 -0
- package/web/dist/assets/index-CniHcFxT.js +22 -0
- package/web/dist/assets/index-DYz2iP_B.css +10 -0
- package/web/dist/assets/infoDiagram-27XIBGKW-ij_5A0pY.js +2 -0
- package/web/dist/assets/init-Gi6I4Gst.js +1 -0
- package/web/dist/assets/ishikawaDiagram-5VMMS53U-C8-tm2LV.js +70 -0
- package/web/dist/assets/journeyDiagram-3NMN7TZE-ZJedkNel.js +139 -0
- package/web/dist/assets/kanban-definition-UXKFOSKX-BEuoM53n.js +89 -0
- package/web/dist/assets/katex-C5jXJg4s.js +257 -0
- package/web/dist/assets/layout-zq_9DigZ.js +1 -0
- package/web/dist/assets/linear-D-QXJ5Wc.js +1 -0
- package/web/dist/assets/markdown-CUPbftfV.js +51 -0
- package/web/dist/assets/mermaid.core-CCiG-bOm.js +312 -0
- package/web/dist/assets/mindmap-definition-YA3MSWOX-VaAisUIe.js +96 -0
- package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/web/dist/assets/pegDiagram-XKGWAZYB-D5vmRtkn.js +1 -0
- package/web/dist/assets/pieDiagram-E7YTZNPT-loD6V8fn.js +39 -0
- package/web/dist/assets/quadrantDiagram-AXDQQJYC-B7E4mH1-.js +7 -0
- package/web/dist/assets/railroadDiagram-O6MQD6OU-DTALHvDa.js +1 -0
- package/web/dist/assets/react-ZkKJMDQQ.js +24 -0
- package/web/dist/assets/requirementDiagram-BXWQKSXE-ZjJKuEcR.js +84 -0
- package/web/dist/assets/sankeyDiagram-P5KCCOFB-DufTudDP.js +40 -0
- package/web/dist/assets/sequenceDiagram-WJ2MYXX4-B-Zk4alI.js +162 -0
- package/web/dist/assets/sizeCapture-INFHLROL-B1pjlxXN.js +1 -0
- package/web/dist/assets/stateDiagram-D77RDMKH-BeWg6PIR.js +1 -0
- package/web/dist/assets/stateDiagram-v2-MP3YSRHH-DagWhIFJ.js +1 -0
- package/web/dist/assets/swimlanes-42K2YHIH-4J4jmMmO.js +1 -0
- package/web/dist/assets/swimlanesDiagram-VR7AAH4N-Dsi9x540.js +8 -0
- package/web/dist/assets/timeline-definition-24CTP7MA-ByqLfxqe.js +120 -0
- package/web/dist/assets/vennDiagram-4TSXK5OY-B_EqVW4R.js +34 -0
- package/web/dist/assets/wardleyDiagram-VM6X3IG4-Bu7sA6F8.js +78 -0
- package/web/dist/assets/xterm-D1D2FVe3.js +38 -0
- package/web/dist/assets/xychartDiagram-S5SC5T6Z-w1KPVGKk.js +7 -0
- package/web/dist/favicon.svg +8 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-600-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-600-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-400-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-500-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-600-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-700-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-800-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-ext-400-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-ext-500-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-ext-600-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-ext-700-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-ext-800-normal.woff2 +0 -0
- package/web/dist/icon.ico +0 -0
- package/web/dist/index.html +17 -0
- package/web/public/favicon.svg +8 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-600-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-600-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-400-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-500-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-600-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-700-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-800-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-ext-400-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-ext-500-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-ext-600-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-ext-700-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-ext-800-normal.woff2 +0 -0
- package/web/public/icon.ico +0 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP 工具桥 —— 把外部 Model Context Protocol(stdio)服务器暴露的工具接入
|
|
3
|
+
* pi 会话,让 AI 能调用真实的第三方工具(文件、数据库、GitHub…)。
|
|
4
|
+
*
|
|
5
|
+
* 约定(MCP 规范流式子集):
|
|
6
|
+
* - stdio 传输 = stdin/stdout 上换行分隔的 JSON-RPC 2.0(NDJSON),不依赖任何
|
|
7
|
+
* 第三方包;stderr 是自由日志通道。
|
|
8
|
+
* - 握手:initialize(带 protocolVersion)→ notifications/initialized →
|
|
9
|
+
* tools/list → tools/call。
|
|
10
|
+
* - 工具工具入会:本模块把每个远端工具适配成 PluginAgentTool,经
|
|
11
|
+
* pluginToolsProvider 走与插件工具完全相同的 customTools 管线。
|
|
12
|
+
*
|
|
13
|
+
* 配置:<PI_WEB_DATA_DIR>/mcp.json,形如
|
|
14
|
+
* { "servers": { "gitserv": { "command": "node", "args": ["mcp.js"], "cwd": "/x" } } }
|
|
15
|
+
*/
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
const PROTOCOL_VERSION = "2025-03-26"; // 广泛支持的工具版本
|
|
20
|
+
let rpcSeq = 0;
|
|
21
|
+
/**
|
|
22
|
+
* 单个 MCP 服务器的客户端:管理子进程、请求/响应按 id 关联、握手与工具调用。
|
|
23
|
+
* 线程模型:无需并发控制(MCP 允许乱序 + 我们按请求 id 匹配响应)。
|
|
24
|
+
*/
|
|
25
|
+
export class McpClient {
|
|
26
|
+
spec;
|
|
27
|
+
child = null;
|
|
28
|
+
buffer = "";
|
|
29
|
+
nextId = 1;
|
|
30
|
+
pending = new Map();
|
|
31
|
+
log;
|
|
32
|
+
name;
|
|
33
|
+
/** 已握手的工具列表(tools/list 结果缓存)。 */
|
|
34
|
+
tools = [];
|
|
35
|
+
shuttingDown = false;
|
|
36
|
+
constructor(name, spec, log) {
|
|
37
|
+
this.spec = spec;
|
|
38
|
+
this.name = name;
|
|
39
|
+
this.log = log ?? (() => { });
|
|
40
|
+
}
|
|
41
|
+
/** 启动子进程 + 握手 + 拉取工具列表。 */
|
|
42
|
+
async start(timeoutMs = 8000) {
|
|
43
|
+
if (this.child)
|
|
44
|
+
return;
|
|
45
|
+
const { command, args = [], cwd, env } = this.spec;
|
|
46
|
+
this.log(`[mcp:${this.name}] starting: ${command} ${args.join(" ")}`);
|
|
47
|
+
const child = spawn(command, args, {
|
|
48
|
+
cwd: cwd ?? undefined,
|
|
49
|
+
env: { ...process.env, ...env },
|
|
50
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
51
|
+
windowsHide: true,
|
|
52
|
+
});
|
|
53
|
+
this.child = child;
|
|
54
|
+
child.stderr.on("data", (d) => this.log(`[mcp:${this.name}] stderr:`, d.toString().trimEnd()));
|
|
55
|
+
child.on("error", (err) => this.rejectAll(new Error(`[mcp:${this.name}] spawn error: ${err.message}`)));
|
|
56
|
+
child.on("exit", (code, sig) => {
|
|
57
|
+
this.child = null;
|
|
58
|
+
if (!this.shuttingDown)
|
|
59
|
+
this.rejectAll(new Error(`[mcp:${this.name}] 进程退出 (${sig ?? code})`));
|
|
60
|
+
});
|
|
61
|
+
child.stdout.setEncoding("utf8");
|
|
62
|
+
child.stdout.on("data", (chunk) => this.onData(chunk));
|
|
63
|
+
// 握手
|
|
64
|
+
const handshake = await this.request("initialize", {
|
|
65
|
+
protocolVersion: this.spec.protocolVersion ?? PROTOCOL_VERSION,
|
|
66
|
+
capabilities: {},
|
|
67
|
+
clientInfo: { name: "pi-web-ui", version: "0.41.0" },
|
|
68
|
+
});
|
|
69
|
+
const version = handshake?.protocolVersion ?? this.spec.protocolVersion ?? PROTOCOL_VERSION;
|
|
70
|
+
// 通知 initialized(无 id 的 notification)
|
|
71
|
+
this.send({ jsonrpc: "2.0", method: "notifications/initialized" });
|
|
72
|
+
// 仍以协商协议版本调用 tools(多数服务器对新版本容忍,这里用协商结果)
|
|
73
|
+
void version;
|
|
74
|
+
const listed = (await this.request("tools/list", {}) ?? {});
|
|
75
|
+
this.tools = Array.isArray(listed.tools) ? listed.tools : [];
|
|
76
|
+
this.log(`[mcp:${this.name}] ready, ${this.tools.length} tools`);
|
|
77
|
+
}
|
|
78
|
+
/** 已发现工具。 */
|
|
79
|
+
getTools() {
|
|
80
|
+
return this.tools.map((t) => ({ ...t }));
|
|
81
|
+
}
|
|
82
|
+
/** 调用一个工具,返回结果文本(多 content 拼接为 JSON 字符串保真)。 */
|
|
83
|
+
async call(name, args, timeoutMs = 60000) {
|
|
84
|
+
const res = (await this.request("tools/call", { name, arguments: args }, timeoutMs));
|
|
85
|
+
if (res?.isError) {
|
|
86
|
+
const msg = (res.content ?? []).map((c) => c.text ?? "").join("\n").trim() || "MCP 工具错误";
|
|
87
|
+
throw new Error(msg);
|
|
88
|
+
}
|
|
89
|
+
// 结构化结果优先,其次文本内容。
|
|
90
|
+
if (res?.structuredContent !== undefined)
|
|
91
|
+
return res.structuredContent;
|
|
92
|
+
const text = (res.content ?? []).map((c) => c.text ?? "").filter((x) => x).join("\n");
|
|
93
|
+
return { content: text, isError: !!res.isError };
|
|
94
|
+
}
|
|
95
|
+
/** 关闭:kill 子进程,拒绝所有在途请求。 */
|
|
96
|
+
close() {
|
|
97
|
+
this.shuttingDown = true;
|
|
98
|
+
this.rejectAll(new Error("[mcp] client closed"));
|
|
99
|
+
if (this.child) {
|
|
100
|
+
try {
|
|
101
|
+
this.child.kill();
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
/* 已退出 */
|
|
105
|
+
}
|
|
106
|
+
this.child = null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// -- 内部 -------------------------------------------------------------
|
|
110
|
+
send(msg) {
|
|
111
|
+
const stdin = this.child?.stdin;
|
|
112
|
+
if (!stdin || !stdin.writable)
|
|
113
|
+
return;
|
|
114
|
+
stdin.write(JSON.stringify(msg) + "\n");
|
|
115
|
+
}
|
|
116
|
+
request(method, params, timeoutMs = 8000) {
|
|
117
|
+
const id = (rpcSeq++);
|
|
118
|
+
const outId = String(id);
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
const timer = setTimeout(() => {
|
|
121
|
+
this.pending.delete(outId);
|
|
122
|
+
reject(new Error(`[mcp:${this.name}] ${method} 超时 (${timeoutMs}ms)`));
|
|
123
|
+
}, timeoutMs);
|
|
124
|
+
this.pending.set(outId, { resolve, reject, timer });
|
|
125
|
+
this.send({ jsonrpc: "2.0", id: id, method, params });
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
onData(chunk) {
|
|
129
|
+
this.buffer += chunk;
|
|
130
|
+
let nl;
|
|
131
|
+
while ((nl = this.buffer.indexOf("\n")) >= 0) {
|
|
132
|
+
const line = this.buffer.slice(0, nl).trim();
|
|
133
|
+
this.buffer = this.buffer.slice(nl + 1);
|
|
134
|
+
if (!line)
|
|
135
|
+
continue;
|
|
136
|
+
let msg;
|
|
137
|
+
try {
|
|
138
|
+
msg = JSON.parse(line);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
this.log(`[mcp:${this.name}] 非 JSON 行(忽略):`, line.slice(0, 120));
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
this.handleMessage(msg);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
handleMessage(msg) {
|
|
148
|
+
if (msg.id !== undefined) {
|
|
149
|
+
const pending = this.pending.get(String(msg.id));
|
|
150
|
+
if (!pending) {
|
|
151
|
+
this.log(`[mcp:${this.name}] 未知响应 id=${msg.id}`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
this.pending.delete(String(msg.id));
|
|
155
|
+
clearTimeout(pending.timer);
|
|
156
|
+
if (msg.error)
|
|
157
|
+
pending.reject(new Error(`[mcp:${this.name}] ${msg.error.message ?? "MCP 错误"}`));
|
|
158
|
+
else
|
|
159
|
+
pending.resolve(msg.result);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// 服务端主动通知(log / cancelled 等)——仅记录。
|
|
163
|
+
if (msg.method === "notifications/message") {
|
|
164
|
+
const p = msg.params;
|
|
165
|
+
if (p?.message)
|
|
166
|
+
this.log(`[mcp:${this.name}] ${p.level ?? "message"}:`, p.message);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
rejectAll(err) {
|
|
170
|
+
for (const [, p] of this.pending) {
|
|
171
|
+
clearTimeout(p.timer);
|
|
172
|
+
p.reject(err);
|
|
173
|
+
}
|
|
174
|
+
this.pending.clear();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/** 读取 <dataDir>/mcp.json 里的服务器清单(尽力而为)。 */
|
|
178
|
+
export function readMcpConfig(dataDir) {
|
|
179
|
+
try {
|
|
180
|
+
const raw = JSON.parse(readFileSync(join(dataDir, "mcp.json"), "utf8"));
|
|
181
|
+
const servers = {};
|
|
182
|
+
for (const [name, s] of Object.entries(raw.servers ?? {})) {
|
|
183
|
+
if (!s || typeof s.command !== "string" || !s.command.trim())
|
|
184
|
+
continue;
|
|
185
|
+
servers[name] = {
|
|
186
|
+
command: s.command,
|
|
187
|
+
args: Array.isArray(s.args) ? s.args.map(String) : [],
|
|
188
|
+
cwd: typeof s.cwd === "string" ? s.cwd : undefined,
|
|
189
|
+
env: s.env && typeof s.env === "object" ? s.env : undefined,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return { servers };
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return { servers: {} };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* 整个 MCP 管理器的工具适配:把每个 MCP 工具变成 PluginAgentTool。
|
|
200
|
+
* getAllToolsTool(name, callFn) 生成 execute → 转发到对应 McpClient.call。
|
|
201
|
+
*/
|
|
202
|
+
function adaptMcpTool(serverName, mcpTool, client) {
|
|
203
|
+
const name = sanitizeToolName(mcpTool.name);
|
|
204
|
+
return {
|
|
205
|
+
name,
|
|
206
|
+
label: `${serverName} · ${mcpTool.name}`,
|
|
207
|
+
description: mcpTool.description ?? `从 MCP 服务器「${serverName}」提供的工具 ${mcpTool.name}`,
|
|
208
|
+
parameters: mcpTool.inputSchema ?? {},
|
|
209
|
+
execute: async (_toolCallId, params, _signal) => {
|
|
210
|
+
return client.call(mcpTool.name, params ?? {});
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/** 工具名必须是 [A-Za-z0-9_-]+(与插件工具同规则),MCP 可能含冒号/斜杠 — 归一化。 */
|
|
215
|
+
function sanitizeToolName(name) {
|
|
216
|
+
const cleaned = (name || "").replace(/[^A-Za-z0-9_-]/g, "_");
|
|
217
|
+
return cleaned || "mcp_tool";
|
|
218
|
+
}
|
|
219
|
+
/** MCP 服务器管理器:自管多服务器生命周期 + 聚合工具。 */
|
|
220
|
+
export class McpBridge {
|
|
221
|
+
dataDir;
|
|
222
|
+
log;
|
|
223
|
+
opts;
|
|
224
|
+
clients = [];
|
|
225
|
+
tools = [];
|
|
226
|
+
constructor(dataDir, log = () => { }, opts = {}) {
|
|
227
|
+
this.dataDir = dataDir;
|
|
228
|
+
this.log = log;
|
|
229
|
+
this.opts = opts;
|
|
230
|
+
}
|
|
231
|
+
/** 读取配置并启动全部服务器(顺序 fail-fast:单个失败记日志不拖垮其它)。 */
|
|
232
|
+
async load() {
|
|
233
|
+
const cfg = optsOverrideOrRead(this.opts.specOverride, this.dataDir);
|
|
234
|
+
await Promise.all(Object.entries(cfg.servers).map(async ([name, spec]) => {
|
|
235
|
+
try {
|
|
236
|
+
const client = new McpClient(name, spec, this.log);
|
|
237
|
+
await client.start();
|
|
238
|
+
this.clients.push(client);
|
|
239
|
+
for (const t of client.getTools())
|
|
240
|
+
this.tools.push(adaptMcpTool(name, t, client));
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
this.log(`[mcp] 服务器「${name}」启动失败:`, err instanceof Error ? err.message : err);
|
|
244
|
+
}
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
getTools() {
|
|
248
|
+
return this.tools;
|
|
249
|
+
}
|
|
250
|
+
hasServers() {
|
|
251
|
+
return this.clients.length > 0;
|
|
252
|
+
}
|
|
253
|
+
dispose() {
|
|
254
|
+
for (const c of this.clients)
|
|
255
|
+
c.close();
|
|
256
|
+
this.clients = [];
|
|
257
|
+
this.tools = [];
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function optsOverrideOrRead(specOverride, dataDir) {
|
|
261
|
+
if (specOverride && specOverride.length > 0) {
|
|
262
|
+
const servers = {};
|
|
263
|
+
for (const o of specOverride)
|
|
264
|
+
servers[o.name] = o.spec;
|
|
265
|
+
return { servers };
|
|
266
|
+
}
|
|
267
|
+
return readMcpConfig(dataDir);
|
|
268
|
+
}
|