ysagc-agent 0.9.5 → 0.9.13

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.
Files changed (51) hide show
  1. package/dist/agent.d.ts +1 -1
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +137 -10
  4. package/dist/agent.js.map +1 -1
  5. package/dist/browser.d.ts +31 -0
  6. package/dist/browser.d.ts.map +1 -0
  7. package/dist/browser.js +609 -0
  8. package/dist/browser.js.map +1 -0
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +69 -0
  11. package/dist/cli.js.map +1 -1
  12. package/dist/executor.d.ts +6 -0
  13. package/dist/executor.d.ts.map +1 -1
  14. package/dist/executor.js +46 -0
  15. package/dist/executor.js.map +1 -1
  16. package/dist/hooks.d.ts +29 -0
  17. package/dist/hooks.d.ts.map +1 -0
  18. package/dist/hooks.js +211 -0
  19. package/dist/hooks.js.map +1 -0
  20. package/dist/index.d.ts +3 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +13 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/mcp.d.ts +92 -0
  25. package/dist/mcp.d.ts.map +1 -0
  26. package/dist/mcp.js +746 -0
  27. package/dist/mcp.js.map +1 -0
  28. package/dist/memory.d.ts +14 -0
  29. package/dist/memory.d.ts.map +1 -0
  30. package/dist/memory.js +148 -0
  31. package/dist/memory.js.map +1 -0
  32. package/dist/protocol-handler.d.ts +8 -0
  33. package/dist/protocol-handler.d.ts.map +1 -0
  34. package/dist/protocol-handler.js +127 -0
  35. package/dist/protocol-handler.js.map +1 -0
  36. package/dist/protocol.d.ts +60 -3
  37. package/dist/protocol.d.ts.map +1 -1
  38. package/dist/protocol.js +1 -1
  39. package/dist/protocol.js.map +1 -1
  40. package/dist/screenshot.d.ts +15 -0
  41. package/dist/screenshot.d.ts.map +1 -0
  42. package/dist/screenshot.js +385 -0
  43. package/dist/screenshot.js.map +1 -0
  44. package/dist/security.d.ts.map +1 -1
  45. package/dist/security.js +6 -2
  46. package/dist/security.js.map +1 -1
  47. package/dist/tools.d.ts +22 -1
  48. package/dist/tools.d.ts.map +1 -1
  49. package/dist/tools.js +470 -78
  50. package/dist/tools.js.map +1 -1
  51. package/package.json +1 -1
package/dist/mcp.js ADDED
@@ -0,0 +1,746 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.McpManager = exports.MCP_PREFIX = void 0;
37
+ exports.mcpDisabled = mcpDisabled;
38
+ exports.mcpConfigPath = mcpConfigPath;
39
+ exports.loadMcpConfig = loadMcpConfig;
40
+ exports.loadMcpConfigDetailed = loadMcpConfigDetailed;
41
+ exports.mcpToolName = mcpToolName;
42
+ exports.parseMcpToolName = parseMcpToolName;
43
+ exports.renderMcpResult = renderMcpResult;
44
+ exports.writeSampleMcpConfig = writeSampleMcpConfig;
45
+ // ============================================================================
46
+ // MCP(Model Context Protocol)客户端
47
+ // ----------------------------------------------------------------------------
48
+ // 为什么放在 Agent 侧而不是平台侧:MCP server 是**本机**进程(stdio),
49
+ // 平台在服务器上,够不到用户机器上的 `npx -y @modelcontextprotocol/server-filesystem`。
50
+ // 所以由 Agent 拉起子进程、走 JSON-RPC,再把工具清单报给平台,平台合并进工具表交给模型。
51
+ //
52
+ // 配置:工作区 `.ysagc/mcp.json`,格式与 Claude Desktop / Cursor 一致:
53
+ // { "mcpServers": { "fs": { "command": "npx", "args": ["-y", "..."], "env": {...} } } }
54
+ //
55
+ // 安全:
56
+ // - `YSAGC_MCP=0` 可整体关闭;
57
+ // - 启动命令过一遍 isDangerCommand,命中危险模式(rm -rf / 下载即执行 等)直接拒绝启动;
58
+ // - 只读权限档(read-only)下所有 mcp__* 调用被 preExecute 拦下 —— MCP 工具对我们是黑盒,
59
+ // 无法判断它是否写文件,因此保守地当作写操作。
60
+ const fs = __importStar(require("node:fs"));
61
+ const path = __importStar(require("node:path"));
62
+ const childProcess = __importStar(require("node:child_process"));
63
+ const security_1 = require("./security");
64
+ const MCP_REL = path.join('.ysagc', 'mcp.json');
65
+ /** 单次 JSON-RPC 请求超时(tools/call 可能慢,给足) */
66
+ const CALL_TIMEOUT_MS = Number(process.env.YSAGC_MCP_TIMEOUT || 60000);
67
+ /** 握手/列举超时(server 起不来要快速失败,不要卡住整个任务) */
68
+ const HANDSHAKE_TIMEOUT_MS = Number(process.env.YSAGC_MCP_HANDSHAKE_TIMEOUT || 20000);
69
+ const PROTOCOL_VERSION = '2024-11-05';
70
+ const MAX_TOOLS_PER_SERVER = Number(process.env.YSAGC_MCP_MAX_TOOLS || 64);
71
+ function mcpDisabled() {
72
+ return String(process.env.YSAGC_MCP || '').trim() === '0';
73
+ }
74
+ function mcpConfigPath(root) {
75
+ return path.join(root, MCP_REL);
76
+ }
77
+ /** 读取工作区 MCP 配置;不存在/格式不对返回 null(不抛,避免拖垮 Agent 启动) */
78
+ function loadMcpConfig(root) {
79
+ return loadMcpConfigDetailed(root).config;
80
+ }
81
+ /**
82
+ * 带原因的版本。
83
+ *
84
+ * 为什么需要它:JSON 里写 Windows 路径时 `\m` 这种是**非法转义**,JSON.parse 直接抛,
85
+ * 配置整个作废。以前这种情况只会表现为「一个 MCP 工具都没有」,用户完全看不出是自己
86
+ * 把反斜杠写错了。现在把原因带出来,经 mcp_tools 消息里的 failures 一路报到 UI。
87
+ */
88
+ function loadMcpConfigDetailed(root) {
89
+ if (mcpDisabled())
90
+ return { config: null };
91
+ const p = mcpConfigPath(root);
92
+ try {
93
+ if (!fs.existsSync(p))
94
+ return { config: null };
95
+ let raw;
96
+ try {
97
+ raw = JSON.parse(fs.readFileSync(p, 'utf8'));
98
+ }
99
+ catch (err) {
100
+ return { config: null, error: `${p} 不是合法 JSON:${err?.message || err}(Windows 路径里的反斜杠要写成 \\\\)` };
101
+ }
102
+ const servers = raw?.mcpServers;
103
+ if (!servers || typeof servers !== 'object') {
104
+ return { config: null, error: `${p} 缺少 mcpServers 字段` };
105
+ }
106
+ const out = {};
107
+ for (const [name, cfg] of Object.entries(servers)) {
108
+ if (!cfg || typeof cfg !== 'object')
109
+ continue;
110
+ const command = String(cfg.command || '').trim();
111
+ const url = String(cfg.url || '').trim();
112
+ // 两个都没配 = 这条配置没法用;都配了以 url 为准(HTTP 更明确)
113
+ if (!command && !url)
114
+ continue;
115
+ if (url && !/^https?:\/\//i.test(url)) {
116
+ return { config: null, error: `${p} 里 server "${name}" 的 url 必须是 http/https:${url}` };
117
+ }
118
+ // 云元数据地址(169.254.169.254)是经典的 SSRF 打点目标,任何情况下都不该被连
119
+ if (/^https?:\/\/169\.254\.169\.254/i.test(url)) {
120
+ return { config: null, error: `${p} 里 server "${name}" 的 url 指向云元数据地址,已拒绝` };
121
+ }
122
+ out[name] = {
123
+ ...(url ? { url } : { command }),
124
+ args: Array.isArray(cfg.args) ? cfg.args.map((a) => String(a)) : [],
125
+ env: cfg.env && typeof cfg.env === 'object'
126
+ ? Object.fromEntries(Object.entries(cfg.env).map(([k, v]) => [k, String(v)]))
127
+ : undefined,
128
+ // 请求头支持 ${ENV} 展开:让用户把密钥放环境变量里,而不是提交进 .ysagc/mcp.json
129
+ headers: cfg.headers && typeof cfg.headers === 'object'
130
+ ? Object.fromEntries(Object.entries(cfg.headers).map(([k, v]) => [
131
+ k, String(v).replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_m, env) => process.env[env] || ''),
132
+ ]))
133
+ : undefined,
134
+ cwd: typeof cfg.cwd === 'string' ? cfg.cwd : undefined,
135
+ enabled: cfg.enabled !== false,
136
+ };
137
+ }
138
+ return Object.keys(out).length ? { config: { mcpServers: out } } : { config: null };
139
+ }
140
+ catch (err) {
141
+ return { config: null, error: `读取 ${p} 失败:${err?.message || err}` };
142
+ }
143
+ }
144
+ /** 工具名里的 server/tool 分隔符。用双下划线,和 Claude 的 mcp__server__tool 命名保持一致。 */
145
+ exports.MCP_PREFIX = 'mcp__';
146
+ /** 把 server+tool 拼成平台可见的工具名。名字里不允许再出现分隔符,否则解析会歧义。 */
147
+ function mcpToolName(server, tool) {
148
+ return `${exports.MCP_PREFIX}${server}__${tool}`;
149
+ }
150
+ /** 反向解析;不是 mcp__ 前缀或格式不对返回 null */
151
+ function parseMcpToolName(name) {
152
+ if (!name.startsWith(exports.MCP_PREFIX))
153
+ return null;
154
+ const rest = name.slice(exports.MCP_PREFIX.length);
155
+ const i = rest.indexOf('__');
156
+ if (i <= 0 || i >= rest.length - 2)
157
+ return null;
158
+ return { server: rest.slice(0, i), tool: rest.slice(i + 2) };
159
+ }
160
+ /** 一个 MCP server(stdio 子进程 或 HTTP 端点)+ 它的 JSON-RPC 通道 */
161
+ class McpClient {
162
+ constructor(name, cfg, root) {
163
+ this.child = null;
164
+ this.buf = '';
165
+ this.nextId = 1;
166
+ this.pending = new Map();
167
+ this.ready = null;
168
+ this.tools = [];
169
+ this.closed = false;
170
+ /** HTTP 传输:POST 目标地址(旧版 SSE 握手后会拿到一个独立的 endpoint) */
171
+ this.httpEndpoint = '';
172
+ /** HTTP 传输:正在读的 SSE 响应流(旧版 SSE 用它收所有响应) */
173
+ this.sseReader = null;
174
+ this.sseAbort = null;
175
+ this.name = name;
176
+ this.cfg = cfg;
177
+ this.root = root;
178
+ }
179
+ get isHttp() {
180
+ return !!this.cfg.url;
181
+ }
182
+ get toolDefs() {
183
+ return this.tools;
184
+ }
185
+ get alive() {
186
+ if (this.isHttp)
187
+ return !this.closed;
188
+ return !!this.child && !this.closed && this.child.exitCode === null;
189
+ }
190
+ /** 启动子进程并完成 initialize 握手(幂等:并发调用共享同一个 Promise) */
191
+ start(log) {
192
+ if (this.ready)
193
+ return this.ready;
194
+ this.ready = this.doStart(log).catch((err) => {
195
+ // 失败不缓存,下次调用可以重试(比如用户刚把依赖装上)
196
+ this.ready = null;
197
+ throw err;
198
+ });
199
+ return this.ready;
200
+ }
201
+ async doStart(log) {
202
+ const cfg = this.cfg;
203
+ if (cfg.url)
204
+ return this.doStartHttp(log);
205
+ // 危险命令闸门:MCP server 是本机任意进程,命令串命中危险模式就不启动。
206
+ // 只做「命令串」层面的判断 —— 它拦不住恶意的合法程序,但能挡住最典型的一类配置。
207
+ const cmdline = [cfg.command, ...(cfg.args || [])].join(' ');
208
+ const danger = (0, security_1.isDangerCommand)(cmdline);
209
+ if (danger.danger) {
210
+ throw new Error(`MCP server "${this.name}" 的启动命令被安全策略拒绝(${danger.reason})`);
211
+ }
212
+ // cwd:显式配置按「工作区根」解析相对路径(不是 Agent 进程目录 —— 那是安装目录,
213
+ // server 写相对路径文件会落到 Agent 自己的文件夹里,用户找不到);
214
+ // 没配置就用工作区根,这也符合「MCP 配置属于工作区」的直觉。
215
+ const cwd = cfg.cwd ? path.resolve(this.root, cfg.cwd) : this.root;
216
+ // 走到这里 cfg.command 一定有值(没配 command 的 server 在读取配置时就被过滤掉了),
217
+ // 但 TS 看不到那层约束,显式取一次更清楚。
218
+ const command = String(cfg.command || '');
219
+ // Windows 上 npx/uvx 是 .cmd 批处理,直接 spawn 会 ENOENT;shell 模式让系统自己解析。
220
+ const needShell = process.platform === 'win32' && !/\.(exe|cmd|bat|com)$/i.test(command);
221
+ const child = childProcess.spawn(command, cfg.args || [], {
222
+ cwd,
223
+ shell: needShell,
224
+ env: { ...process.env, ...(cfg.env || {}) },
225
+ stdio: ['pipe', 'pipe', 'pipe'],
226
+ windowsHide: true,
227
+ });
228
+ this.child = child;
229
+ child.stdout?.setEncoding('utf8');
230
+ child.stdout?.on('data', (chunk) => this.onData(chunk));
231
+ child.stderr?.setEncoding('utf8');
232
+ child.stderr?.on('data', (chunk) => {
233
+ const s = String(chunk).trim();
234
+ if (s)
235
+ log?.(`[mcp:${this.name}] ${s.slice(0, 400)}`);
236
+ });
237
+ child.on('error', (err) => this.failAll(new Error(`MCP server "${this.name}" 启动失败:${err.message}`)));
238
+ child.on('exit', (code) => {
239
+ this.closed = true;
240
+ this.failAll(new Error(`MCP server "${this.name}" 已退出(code=${code})`));
241
+ });
242
+ const init = await this.request('initialize', {
243
+ protocolVersion: PROTOCOL_VERSION,
244
+ capabilities: {},
245
+ clientInfo: { name: 'ysagc-agent', version: '1' },
246
+ }, HANDSHAKE_TIMEOUT_MS);
247
+ if (!init || typeof init !== 'object') {
248
+ throw new Error(`MCP server "${this.name}" initialize 返回异常`);
249
+ }
250
+ // 通知(无 id、无响应):告诉 server 客户端准备好了
251
+ this.notify('notifications/initialized', {});
252
+ const listed = await this.request('tools/list', {}, HANDSHAKE_TIMEOUT_MS);
253
+ const arr = Array.isArray(listed?.tools) ? listed.tools : [];
254
+ this.tools = arr.slice(0, MAX_TOOLS_PER_SERVER).map((t) => ({
255
+ name: mcpToolName(this.name, String(t?.name || '')),
256
+ server: this.name,
257
+ tool: String(t?.name || ''),
258
+ description: String(t?.description || '').slice(0, 1000),
259
+ inputSchema: (t?.inputSchema && typeof t.inputSchema === 'object')
260
+ ? t.inputSchema
261
+ : { type: 'object', properties: {} },
262
+ })).filter((t) => t.tool);
263
+ log?.(`[mcp:${this.name}] 已连接,暴露 ${this.tools.length} 个工具`);
264
+ }
265
+ /** 逐行 JSON 解析:MCP 走 stdio 时用换行分隔的 JSON-RPC 帧 */
266
+ onData(chunk) {
267
+ this.buf += chunk;
268
+ let nl = this.buf.indexOf('\n');
269
+ while (nl >= 0) {
270
+ const line = this.buf.slice(0, nl).trim();
271
+ this.buf = this.buf.slice(nl + 1);
272
+ if (line)
273
+ this.onLine(line);
274
+ nl = this.buf.indexOf('\n');
275
+ }
276
+ // 防御:单个超长帧(server 打日志忘了换行)不能把内存吃干
277
+ if (this.buf.length > 4 * 1024 * 1024)
278
+ this.buf = '';
279
+ }
280
+ onLine(line) {
281
+ let msg;
282
+ try {
283
+ msg = JSON.parse(line);
284
+ }
285
+ catch {
286
+ return;
287
+ } // 非 JSON 行 = server 的日志输出,忽略
288
+ if (!msg || typeof msg !== 'object')
289
+ return;
290
+ if (msg.id === undefined || msg.id === null)
291
+ return; // 通知,忽略
292
+ const p = this.pending.get(Number(msg.id));
293
+ if (!p)
294
+ return;
295
+ this.pending.delete(Number(msg.id));
296
+ clearTimeout(p.timer);
297
+ if (msg.error)
298
+ p.reject(new Error(String(msg.error?.message || 'MCP 调用失败')));
299
+ else
300
+ p.resolve(msg.result);
301
+ }
302
+ write(obj) {
303
+ if (!this.child?.stdin || this.closed)
304
+ throw new Error(`MCP server "${this.name}" 未运行`);
305
+ this.child.stdin.write(JSON.stringify(obj) + '\n');
306
+ }
307
+ notify(method, params) {
308
+ if (this.isHttp) {
309
+ void this.httpNotify(method, params);
310
+ return;
311
+ }
312
+ try {
313
+ this.write({ jsonrpc: '2.0', method, params });
314
+ }
315
+ catch { /* 忽略 */ }
316
+ }
317
+ request(method, params, timeoutMs) {
318
+ // 按传输方式分流:HTTP 走 fetch,stdio 走子进程 stdin。
319
+ // (踩过:call() 一律走这里,结果 HTTP server 被当成子进程,报「未运行」。)
320
+ if (this.isHttp)
321
+ return this.httpRequest(method, params, timeoutMs);
322
+ const id = this.nextId++;
323
+ return new Promise((resolve, reject) => {
324
+ const timer = setTimeout(() => {
325
+ this.pending.delete(id);
326
+ reject(new Error(`MCP ${method} 超时(${timeoutMs}ms)`));
327
+ }, timeoutMs);
328
+ timer.unref?.();
329
+ this.pending.set(id, { resolve, reject, timer });
330
+ try {
331
+ this.write({ jsonrpc: '2.0', id, method, params });
332
+ }
333
+ catch (err) {
334
+ clearTimeout(timer);
335
+ this.pending.delete(id);
336
+ reject(err instanceof Error ? err : new Error(String(err)));
337
+ }
338
+ });
339
+ }
340
+ failAll(err) {
341
+ for (const [, p] of this.pending) {
342
+ clearTimeout(p.timer);
343
+ p.reject(err);
344
+ }
345
+ this.pending.clear();
346
+ }
347
+ /** 调用某个工具,返回 MCP 的原始 result(content 数组等) */
348
+ async call(tool, args) {
349
+ await this.start();
350
+ return this.request('tools/call', { name: tool, arguments: args }, CALL_TIMEOUT_MS);
351
+ }
352
+ stop() {
353
+ this.closed = true;
354
+ this.failAll(new Error(`MCP server "${this.name}" 已关闭`));
355
+ try {
356
+ this.child?.kill();
357
+ }
358
+ catch { /* 忽略 */ }
359
+ this.child = null;
360
+ try {
361
+ this.sseAbort?.abort();
362
+ }
363
+ catch { /* 忽略 */ }
364
+ this.sseAbort = null;
365
+ this.sseReader = null;
366
+ }
367
+ // ===== HTTP 传输(Streamable HTTP / 旧版 SSE)=====
368
+ /**
369
+ * 两种 HTTP 形态,靠一次探测自动区分:
370
+ * - **Streamable HTTP**(2025-03-26):所有请求都 POST 到同一个 url,
371
+ * 响应可能是 application/json,也可能是 text/event-stream(SSE 流里带结果)。
372
+ * - **旧版 SSE**(2024-11-05):先 GET url 建一条长连接,服务端先发一个 `endpoint` 事件
373
+ * 告诉客户端「POST 到哪」,之后的响应都从这条 GET 流里回来。
374
+ *
375
+ * 先按 Streamable 试;如果服务端回的是 SSE 且给了 endpoint,就切到旧版 SSE 模式。
376
+ */
377
+ async doStartHttp(log) {
378
+ const url = String(this.cfg.url || '');
379
+ const headers = {
380
+ 'Content-Type': 'application/json',
381
+ Accept: 'application/json, text/event-stream',
382
+ ...(this.cfg.headers || {}),
383
+ };
384
+ this.httpEndpoint = url;
385
+ // 旧版 SSE:先开长连接拿 endpoint(失败不致命,Streamable 可能直接可用)
386
+ await this.openSseIfNeeded(url, headers, log).catch(() => { });
387
+ const init = await this.httpRequest('initialize', {
388
+ protocolVersion: PROTOCOL_VERSION,
389
+ capabilities: {},
390
+ clientInfo: { name: 'ysagc-agent', version: '1' },
391
+ }, HANDSHAKE_TIMEOUT_MS);
392
+ if (!init || typeof init !== 'object') {
393
+ throw new Error(`MCP server "${this.name}" initialize 返回异常`);
394
+ }
395
+ await this.httpNotify('notifications/initialized', {});
396
+ const listed = await this.httpRequest('tools/list', {}, HANDSHAKE_TIMEOUT_MS);
397
+ const arr = Array.isArray(listed?.tools) ? listed.tools : [];
398
+ this.tools = arr.slice(0, MAX_TOOLS_PER_SERVER).map((t) => ({
399
+ name: mcpToolName(this.name, String(t?.name || '')),
400
+ server: this.name,
401
+ tool: String(t?.name || ''),
402
+ description: String(t?.description || '').slice(0, 1000),
403
+ inputSchema: (t?.inputSchema && typeof t.inputSchema === 'object')
404
+ ? t.inputSchema
405
+ : { type: 'object', properties: {} },
406
+ })).filter((t) => t.tool);
407
+ log?.(`[mcp:${this.name}] HTTP 已连接(${this.httpEndpoint === url ? 'Streamable HTTP' : 'SSE'}),暴露 ${this.tools.length} 个工具`);
408
+ }
409
+ /** 探测旧版 SSE:拿到 endpoint 就切过去 */
410
+ async openSseIfNeeded(url, headers, log) {
411
+ const ctrl = new AbortController();
412
+ const res = await fetch(url, {
413
+ method: 'GET',
414
+ headers: { ...headers, Accept: 'text/event-stream' },
415
+ signal: ctrl.signal,
416
+ });
417
+ const ctype = String(res.headers.get('content-type') || '');
418
+ if (!res.ok || !ctype.includes('text/event-stream') || !res.body) {
419
+ ctrl.abort();
420
+ return; // 不是 SSE:交给 Streamable 路径
421
+ }
422
+ this.sseAbort = ctrl;
423
+ this.sseReader = res.body.getReader();
424
+ // 从流里读第一个 endpoint 事件(其余响应交给 pumpSse 一直读)
425
+ const endpoint = await this.readEndpointFromSse(this.sseReader, ctrl.signal);
426
+ if (endpoint) {
427
+ this.httpEndpoint = new URL(endpoint, url).toString();
428
+ log?.(`[mcp:${this.name}] 检测到旧版 SSE 传输,POST 目标 ${this.httpEndpoint}`);
429
+ }
430
+ // 后台持续读这条流:旧版 SSE 的所有响应都从这里回来
431
+ void this.pumpSse();
432
+ }
433
+ /** 读 SSE 流直到出现 endpoint 事件(或超时) */
434
+ async readEndpointFromSse(reader, signal) {
435
+ const dec = new TextDecoder();
436
+ let buf = '';
437
+ const deadline = Date.now() + Math.min(HANDSHAKE_TIMEOUT_MS, 8000);
438
+ while (Date.now() < deadline && !signal.aborted) {
439
+ const { value, done } = await reader.read().catch(() => ({ value: undefined, done: true }));
440
+ if (done || !value)
441
+ return '';
442
+ buf += dec.decode(value, { stream: true });
443
+ const blocks = buf.split('\n\n');
444
+ buf = blocks.pop() || '';
445
+ for (const b of blocks) {
446
+ const ev = /^event:\s*(.+)$/m.exec(b)?.[1]?.trim();
447
+ const data = b.split('\n').filter((l) => l.startsWith('data:')).map((l) => l.slice(5).trim()).join('');
448
+ if (ev === 'endpoint' && data)
449
+ return data;
450
+ }
451
+ }
452
+ return '';
453
+ }
454
+ /** 持续读 SSE 流,把 JSON-RPC 响应分发给等待者 */
455
+ async pumpSse() {
456
+ const reader = this.sseReader;
457
+ if (!reader)
458
+ return;
459
+ const dec = new TextDecoder();
460
+ let buf = '';
461
+ try {
462
+ for (;;) {
463
+ const { value, done } = await reader.read();
464
+ if (done)
465
+ break;
466
+ buf += dec.decode(value, { stream: true });
467
+ const blocks = buf.split('\n\n');
468
+ buf = blocks.pop() || '';
469
+ for (const b of blocks) {
470
+ const data = b.split('\n').filter((l) => l.startsWith('data:')).map((l) => l.slice(5).trim()).join('');
471
+ if (data)
472
+ this.onLine(data);
473
+ }
474
+ }
475
+ }
476
+ catch { /* 流断开:等待者会各自超时 */ }
477
+ }
478
+ /** 发一个 JSON-RPC 请求(HTTP 传输) */
479
+ async httpRequest(method, params, timeoutMs) {
480
+ const id = this.nextId++;
481
+ const body = { jsonrpc: '2.0', id, method, params };
482
+ const res = await fetch(this.httpEndpoint, {
483
+ method: 'POST',
484
+ headers: {
485
+ 'Content-Type': 'application/json',
486
+ Accept: 'application/json, text/event-stream',
487
+ ...(this.cfg.headers || {}),
488
+ },
489
+ body: JSON.stringify(body),
490
+ signal: AbortSignal.timeout(timeoutMs),
491
+ });
492
+ if (res.status === 202 || res.status === 204) {
493
+ // Streamable HTTP 允许「202 Accepted,结果稍后从 SSE 流回来」
494
+ return this.waitPending(id, method, timeoutMs);
495
+ }
496
+ if (!res.ok) {
497
+ throw new Error(`MCP server "${this.name}" 返回 HTTP ${res.status}${await res.text().then((t) => (t ? `:${t.slice(0, 200)}` : '')).catch(() => '')}`);
498
+ }
499
+ const ctype = String(res.headers.get('content-type') || '');
500
+ if (ctype.includes('text/event-stream') && res.body) {
501
+ // 响应本身就是一条 SSE 流:读到我们那个 id 的结果为止
502
+ const dec = new TextDecoder();
503
+ const reader = res.body.getReader();
504
+ let buf = '';
505
+ const deadline = Date.now() + timeoutMs;
506
+ try {
507
+ while (Date.now() < deadline) {
508
+ const { value, done } = await reader.read();
509
+ if (done)
510
+ break;
511
+ buf += dec.decode(value, { stream: true });
512
+ const blocks = buf.split('\n\n');
513
+ buf = blocks.pop() || '';
514
+ for (const b of blocks) {
515
+ const data = b.split('\n').filter((l) => l.startsWith('data:')).map((l) => l.slice(5).trim()).join('');
516
+ if (!data)
517
+ continue;
518
+ let msg;
519
+ try {
520
+ msg = JSON.parse(data);
521
+ }
522
+ catch {
523
+ continue;
524
+ }
525
+ if (Number(msg?.id) === id) {
526
+ if (msg.error)
527
+ throw new Error(String(msg.error?.message || 'MCP 调用失败'));
528
+ return msg.result;
529
+ }
530
+ }
531
+ }
532
+ }
533
+ finally {
534
+ try {
535
+ reader.cancel();
536
+ }
537
+ catch { /* 忽略 */ }
538
+ }
539
+ throw new Error(`MCP ${method} 的 SSE 响应里没有等到结果`);
540
+ }
541
+ const text = await res.text();
542
+ let msg;
543
+ try {
544
+ msg = JSON.parse(text);
545
+ }
546
+ catch {
547
+ throw new Error(`MCP server "${this.name}" 返回了非 JSON 响应:${text.slice(0, 200)}`);
548
+ }
549
+ if (msg?.error)
550
+ throw new Error(String(msg.error?.message || 'MCP 调用失败'));
551
+ return msg?.result;
552
+ }
553
+ /** 等一个已在 pending 里的 id(Streamable HTTP 的 202 场景) */
554
+ waitPending(id, method, timeoutMs) {
555
+ return new Promise((resolve, reject) => {
556
+ const timer = setTimeout(() => {
557
+ this.pending.delete(id);
558
+ reject(new Error(`MCP ${method} 超时(${timeoutMs}ms)`));
559
+ }, timeoutMs);
560
+ timer.unref?.();
561
+ this.pending.set(id, { resolve, reject, timer });
562
+ });
563
+ }
564
+ async httpNotify(method, params) {
565
+ try {
566
+ await fetch(this.httpEndpoint, {
567
+ method: 'POST',
568
+ headers: {
569
+ 'Content-Type': 'application/json',
570
+ Accept: 'application/json, text/event-stream',
571
+ ...(this.cfg.headers || {}),
572
+ },
573
+ body: JSON.stringify({ jsonrpc: '2.0', method, params }),
574
+ signal: AbortSignal.timeout(5000),
575
+ });
576
+ }
577
+ catch { /* 通知发不出去不影响主流程 */ }
578
+ }
579
+ }
580
+ /**
581
+ * 一个工作区对应一个 Manager:懒启动、缓存工具清单。
582
+ * 配置变化(mtime 变了)时整体重启 —— MCP server 不支持热加载,重建是最稳的做法。
583
+ */
584
+ class McpManager {
585
+ constructor(root, log) {
586
+ this.clients = new Map();
587
+ this.errors = new Map();
588
+ this.loadedMtime = -1;
589
+ this.starting = null;
590
+ this.root = root;
591
+ this.log = log;
592
+ }
593
+ configMtime() {
594
+ try {
595
+ return fs.statSync(mcpConfigPath(this.root)).mtimeMs;
596
+ }
597
+ catch {
598
+ return -1;
599
+ }
600
+ }
601
+ /** 确保按当前配置把所有 server 拉起来(幂等) */
602
+ async ensure() {
603
+ if (mcpDisabled())
604
+ return;
605
+ const mtime = this.configMtime();
606
+ if (mtime < 0) {
607
+ // 配置文件被删了:停掉已有连接,清空工具表
608
+ if (this.clients.size)
609
+ this.reset();
610
+ return;
611
+ }
612
+ if (mtime === this.loadedMtime && this.starting)
613
+ return this.starting;
614
+ if (mtime === this.loadedMtime)
615
+ return;
616
+ if (this.starting)
617
+ return this.starting;
618
+ this.loadedMtime = mtime;
619
+ this.starting = this.doEnsure().finally(() => { this.starting = null; });
620
+ return this.starting;
621
+ }
622
+ async doEnsure() {
623
+ const loaded = loadMcpConfigDetailed(this.root);
624
+ // 配置变了 → 全部重启,避免「旧 server 还在跑、新配置没生效」的混乱状态
625
+ this.reset();
626
+ if (loaded.error) {
627
+ // 配置坏了要让人知道:否则表现只是「工具凭空消失」,用户会以为是平台的问题
628
+ this.errors.set('(config)', loaded.error);
629
+ this.log?.(`[mcp] 配置无效:${loaded.error}`);
630
+ return;
631
+ }
632
+ const cfg = loaded.config;
633
+ if (!cfg)
634
+ return;
635
+ const jobs = Object.entries(cfg.mcpServers)
636
+ .filter(([, c]) => c.enabled !== false)
637
+ .map(async ([name, c]) => {
638
+ const client = new McpClient(name, c, this.root);
639
+ this.clients.set(name, client);
640
+ try {
641
+ await client.start(this.log);
642
+ }
643
+ catch (err) {
644
+ const msg = err?.message || String(err);
645
+ this.errors.set(name, msg);
646
+ this.log?.(`[mcp:${name}] 启动失败:${msg}`);
647
+ this.clients.delete(name);
648
+ try {
649
+ client.stop();
650
+ }
651
+ catch { /* 忽略 */ }
652
+ }
653
+ });
654
+ await Promise.all(jobs);
655
+ }
656
+ reset() {
657
+ for (const [, c] of this.clients) {
658
+ try {
659
+ c.stop();
660
+ }
661
+ catch { /* 忽略 */ }
662
+ }
663
+ this.clients.clear();
664
+ this.errors.clear();
665
+ }
666
+ /** 平台可见的工具清单(已带 mcp__server__tool 名字) */
667
+ async listTools() {
668
+ await this.ensure();
669
+ const out = [];
670
+ for (const [, c] of this.clients)
671
+ out.push(...c.toolDefs);
672
+ return out;
673
+ }
674
+ /** 启动失败的 server 及其原因(用于在工具清单里如实说明,而不是静默消失) */
675
+ failures() {
676
+ return [...this.errors.entries()].map(([server, error]) => ({ server, error }));
677
+ }
678
+ /** 按平台工具名分发一次 MCP 调用 */
679
+ async callByName(name, args) {
680
+ const parsed = parseMcpToolName(name);
681
+ if (!parsed)
682
+ throw new Error(`不是合法的 MCP 工具名:${name}`);
683
+ await this.ensure();
684
+ const client = this.clients.get(parsed.server);
685
+ if (!client) {
686
+ const why = this.errors.get(parsed.server);
687
+ throw new Error(why
688
+ ? `MCP server "${parsed.server}" 不可用:${why}`
689
+ : `MCP server "${parsed.server}" 未配置或未启动`);
690
+ }
691
+ return client.call(parsed.tool, args);
692
+ }
693
+ /** 进程退出时收尾(Agent 关闭时调用) */
694
+ shutdown() {
695
+ this.reset();
696
+ }
697
+ }
698
+ exports.McpManager = McpManager;
699
+ /**
700
+ * 把 MCP 的 tools/call 结果压成模型好读的文本。
701
+ * MCP 返回 { content: [{type:'text', text} | {type:'image', data, mimeType}], isError? },
702
+ * 图片没法直接塞进文本结果,用占位说明代替(否则模型会以为拿到了空结果)。
703
+ */
704
+ function renderMcpResult(result) {
705
+ const isError = result?.isError === true;
706
+ const parts = [];
707
+ const content = Array.isArray(result?.content) ? result.content : [];
708
+ for (const c of content) {
709
+ if (!c || typeof c !== 'object')
710
+ continue;
711
+ if (c.type === 'text')
712
+ parts.push(String(c.text ?? ''));
713
+ else if (c.type === 'image')
714
+ parts.push(`[图片内容 ${c.mimeType || 'image'},${Math.round(String(c.data || '').length / 1024)}KB,未在本通道内联展示]`);
715
+ else if (c.type === 'resource')
716
+ parts.push(`[资源 ${c.resource?.uri || ''}]`);
717
+ else
718
+ parts.push(`[${c.type}]`);
719
+ }
720
+ let text = parts.join('\n').trim();
721
+ if (!text) {
722
+ // 有的 server 把结构化数据放在 structuredContent 里
723
+ text = result?.structuredContent ? JSON.stringify(result.structuredContent) : '';
724
+ }
725
+ if (!text)
726
+ text = '(该 MCP 工具没有返回任何内容)';
727
+ return { ok: !isError, text };
728
+ }
729
+ /** 给工作区写一份示例配置(用户点「添加 MCP」时用) */
730
+ function writeSampleMcpConfig(root) {
731
+ const p = mcpConfigPath(root);
732
+ fs.mkdirSync(path.dirname(p), { recursive: true });
733
+ if (!fs.existsSync(p)) {
734
+ fs.writeFileSync(p, JSON.stringify({
735
+ mcpServers: {
736
+ filesystem: {
737
+ command: 'npx',
738
+ args: ['-y', '@modelcontextprotocol/server-filesystem', '.'],
739
+ enabled: false,
740
+ },
741
+ },
742
+ }, null, 2) + '\n', 'utf8');
743
+ }
744
+ return p;
745
+ }
746
+ //# sourceMappingURL=mcp.js.map