@agentdevjs/mcp 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1254 @@
1
+ // src/types.ts
2
+ var MCPConnectionState = /* @__PURE__ */ ((MCPConnectionState2) => {
3
+ MCPConnectionState2["Disconnected"] = "disconnected";
4
+ MCPConnectionState2["Connecting"] = "connecting";
5
+ MCPConnectionState2["Connected"] = "connected";
6
+ MCPConnectionState2["Error"] = "error";
7
+ return MCPConnectionState2;
8
+ })(MCPConnectionState || {});
9
+
10
+ // src/connection-manager.ts
11
+ import {
12
+ Client,
13
+ SSEClientTransport,
14
+ StreamableHTTPClientTransport
15
+ } from "@modelcontextprotocol/client";
16
+ import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
17
+ import { createLogger } from "@agentdevjs/core";
18
+ var MCPConnectionManager = class {
19
+ connections = /* @__PURE__ */ new Map();
20
+ reconnectTimers = /* @__PURE__ */ new Map();
21
+ logger = createLogger("mcp.connection");
22
+ /**
23
+ * 连接到 MCP 服务器
24
+ */
25
+ async connectServer(name, config) {
26
+ const existing = this.connections.get(name);
27
+ if (existing?.server && existing.state === "connected" /* Connected */) {
28
+ return existing.server;
29
+ }
30
+ const connection = {
31
+ name,
32
+ config,
33
+ state: "connecting" /* Connecting */,
34
+ reconnectAttempts: 0,
35
+ responseBuffer: "",
36
+ nextRequestId: 1,
37
+ toolCount: 0,
38
+ allowReconnect: true,
39
+ pendingRequests: /* @__PURE__ */ new Map()
40
+ };
41
+ this.connections.set(name, connection);
42
+ try {
43
+ if (config.transport === "stdio") {
44
+ return await this.connectStdio(name, config, connection);
45
+ } else if (config.transport === "sse") {
46
+ return await this.connectSSE(name, config, connection);
47
+ } else {
48
+ return await this.connectHTTP(name, config, connection);
49
+ }
50
+ } catch (error) {
51
+ connection.state = "error" /* Error */;
52
+ connection.lastError = error instanceof Error ? error.message : String(error);
53
+ throw error;
54
+ }
55
+ }
56
+ /**
57
+ * 连接到 stdio MCP 服务器
58
+ */
59
+ async connectStdio(name, config, connection) {
60
+ const client = new Client({
61
+ name: `agentdev-${name}`,
62
+ version: "0.1.0"
63
+ });
64
+ const transport = new StdioClientTransport({
65
+ command: config.command,
66
+ args: config.args,
67
+ env: config.env,
68
+ cwd: config.cwd,
69
+ stderr: "pipe"
70
+ });
71
+ await client.connect(transport);
72
+ client.onerror = (error) => {
73
+ connection.state = "error" /* Error */;
74
+ connection.lastError = error.message;
75
+ this.log("error", `MCP server ${name} client error: ${error.message}`);
76
+ };
77
+ transport.onclose = () => {
78
+ this.log(
79
+ connection.allowReconnect ? "warn" : "info",
80
+ `MCP server ${name} stdio transport closed`
81
+ );
82
+ connection.state = "disconnected" /* Disconnected */;
83
+ this.rejectAllPendingRequests(connection, new Error(`MCP server ${name} disconnected`));
84
+ if (connection.allowReconnect) {
85
+ this.scheduleReconnect(name);
86
+ }
87
+ };
88
+ connection.transport = transport;
89
+ connection.server = client;
90
+ connection.state = "connected" /* Connected */;
91
+ connection.connectedAt = Date.now();
92
+ connection.lastError = void 0;
93
+ connection.reconnectAttempts = 0;
94
+ this.log("info", `Connected to MCP server ${name} (stdio)`);
95
+ return client;
96
+ }
97
+ /**
98
+ * 连接到 HTTP MCP 服务器
99
+ */
100
+ async connectSSE(name, config, connection) {
101
+ const client = new Client({
102
+ name: `agentdev-${name}`,
103
+ version: "0.1.0"
104
+ });
105
+ const transport = config.headers ? new SSEClientTransport(new URL(config.url), {
106
+ eventSourceInit: { headers: config.headers },
107
+ requestInit: { headers: config.headers }
108
+ }) : new SSEClientTransport(new URL(config.url));
109
+ await client.connect(transport);
110
+ client.onerror = (error) => {
111
+ connection.state = "error" /* Error */;
112
+ connection.lastError = error.message;
113
+ this.log("error", `MCP server ${name} client error: ${error.message}`);
114
+ };
115
+ transport.onerror = (error) => {
116
+ connection.state = "error" /* Error */;
117
+ connection.lastError = error.message;
118
+ this.log("error", `MCP server ${name} SSE transport error: ${error.message}`);
119
+ };
120
+ transport.onclose = () => {
121
+ this.log(
122
+ connection.allowReconnect ? "warn" : "info",
123
+ `MCP server ${name} SSE transport closed`
124
+ );
125
+ connection.state = "disconnected" /* Disconnected */;
126
+ this.rejectAllPendingRequests(connection, new Error(`MCP server ${name} disconnected`));
127
+ if (connection.allowReconnect) {
128
+ this.scheduleReconnect(name);
129
+ }
130
+ };
131
+ connection.transport = transport;
132
+ connection.server = client;
133
+ connection.state = "connected" /* Connected */;
134
+ connection.connectedAt = Date.now();
135
+ connection.lastError = void 0;
136
+ connection.reconnectAttempts = 0;
137
+ this.log("info", `Connected to MCP server ${name} (sse)`);
138
+ return client;
139
+ }
140
+ /**
141
+ * 连接到 HTTP MCP 服务器
142
+ */
143
+ async connectHTTP(name, config, connection) {
144
+ const client = new Client({
145
+ name: `agentdev-${name}`,
146
+ version: "0.1.0"
147
+ });
148
+ const transport = new StreamableHTTPClientTransport(new URL(config.url), {
149
+ ...config.headers ? { requestInit: { headers: config.headers } } : {},
150
+ reconnectionOptions: {
151
+ maxRetries: config.retryCount ?? 3,
152
+ initialReconnectionDelay: 1e3,
153
+ maxReconnectionDelay: 3e4,
154
+ reconnectionDelayGrowFactor: 1.5
155
+ }
156
+ });
157
+ await client.connect(transport);
158
+ client.onerror = (error) => {
159
+ connection.state = "error" /* Error */;
160
+ connection.lastError = error.message;
161
+ this.log("error", `MCP server ${name} client error: ${error.message}`);
162
+ };
163
+ transport.onerror = (error) => {
164
+ connection.state = "error" /* Error */;
165
+ connection.lastError = error.message;
166
+ this.log("error", `MCP server ${name} HTTP transport error: ${error.message}`);
167
+ };
168
+ transport.onclose = () => {
169
+ this.log(
170
+ connection.allowReconnect ? "warn" : "info",
171
+ `MCP server ${name} HTTP transport closed`
172
+ );
173
+ connection.state = "disconnected" /* Disconnected */;
174
+ this.rejectAllPendingRequests(connection, new Error(`MCP server ${name} disconnected`));
175
+ if (connection.allowReconnect) {
176
+ this.scheduleReconnect(name);
177
+ }
178
+ };
179
+ connection.transport = transport;
180
+ connection.server = client;
181
+ connection.state = "connected" /* Connected */;
182
+ connection.connectedAt = Date.now();
183
+ connection.lastError = void 0;
184
+ connection.reconnectAttempts = 0;
185
+ this.log("info", `Connected to MCP server ${name} (http)`);
186
+ return client;
187
+ }
188
+ /**
189
+ * 断开服务器连接
190
+ */
191
+ async disconnectServer(name) {
192
+ const connection = this.connections.get(name);
193
+ if (!connection) return;
194
+ connection.allowReconnect = false;
195
+ const timer = this.reconnectTimers.get(name);
196
+ if (timer) {
197
+ clearTimeout(timer);
198
+ this.reconnectTimers.delete(name);
199
+ }
200
+ if (connection.transport?.close) {
201
+ try {
202
+ await connection.transport.close();
203
+ } catch (error) {
204
+ this.log("warn", `Error closing MCP transport ${name}: ${error}`);
205
+ }
206
+ }
207
+ if (connection.process) {
208
+ await this.stopProcess(connection.process);
209
+ }
210
+ this.rejectAllPendingRequests(connection, new Error(`MCP server ${name} disconnected`));
211
+ this.connections.delete(name);
212
+ this.log("info", `Disconnected MCP server ${name}`);
213
+ }
214
+ /**
215
+ * 断开所有连接
216
+ */
217
+ async disconnectAll() {
218
+ const names = Array.from(this.connections.keys());
219
+ await Promise.all(names.map((name) => this.disconnectServer(name)));
220
+ }
221
+ /**
222
+ * 列出服务器的所有工具
223
+ */
224
+ async listTools(name) {
225
+ const connection = this.connections.get(name);
226
+ if (!connection || connection.state !== "connected" /* Connected */) {
227
+ this.log("warn", `[MCP] Cannot list tools: ${name} not connected`);
228
+ return [];
229
+ }
230
+ try {
231
+ const isDirectProcess = connection.process && connection.server && !connection.server.request;
232
+ if (isDirectProcess) {
233
+ this.log("info", `[MCP] Using direct process communication for ${name}`);
234
+ const response2 = await this.sendDirectRequest(connection, "tools/list", {}, 3e5);
235
+ const tools2 = response2.result?.tools || [];
236
+ connection.toolCount = tools2.length;
237
+ this.log("info", `[MCP] Received ${tools2.length} tools`);
238
+ return tools2;
239
+ }
240
+ const response = await connection.server.listTools(
241
+ void 0,
242
+ { timeout: 3e5 }
243
+ );
244
+ const tools = response.tools || [];
245
+ connection.toolCount = tools.length;
246
+ return tools;
247
+ } catch (error) {
248
+ this.log("error", `[MCP] Failed to list tools from ${name}: ${error}`);
249
+ return [];
250
+ }
251
+ }
252
+ async callTool(name, serverName, args) {
253
+ this.log("info", `[MCP] callTool: ${serverName}:${name}`);
254
+ this.log("debug", `[MCP] args: ${JSON.stringify(args)}`);
255
+ const connection = this.connections.get(serverName);
256
+ if (!connection || connection.state !== "connected" /* Connected */) {
257
+ throw new Error(`MCP server ${serverName} not connected`);
258
+ }
259
+ try {
260
+ const isDirectProcess = connection.process && connection.server && !connection.server.request;
261
+ if (isDirectProcess) {
262
+ this.log("info", `[MCP] Using direct process communication for tool call`);
263
+ const response2 = await this.sendDirectRequest(connection, "tools/call", {
264
+ name,
265
+ arguments: args
266
+ }, 3e5);
267
+ const result = this.normalizeToolCallResponse(response2);
268
+ this.log("info", `[MCP] Tool ${name} returned: ${result.isError ? "ERROR" : "OK"}`);
269
+ return result;
270
+ }
271
+ const response = await connection.server.callTool(
272
+ { name, arguments: args },
273
+ { timeout: 3e5 }
274
+ );
275
+ return this.normalizeToolCallResponse(response);
276
+ } catch (error) {
277
+ this.log("error", `[MCP] Failed to call tool ${name}: ${error}`);
278
+ return {
279
+ content: [{
280
+ type: "text",
281
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
282
+ }],
283
+ isError: true
284
+ };
285
+ }
286
+ }
287
+ getConnectionInfo(name) {
288
+ const connection = this.connections.get(name);
289
+ if (!connection) return void 0;
290
+ return {
291
+ name: connection.name,
292
+ state: connection.state,
293
+ connectedAt: connection.connectedAt,
294
+ lastError: connection.lastError,
295
+ toolCount: connection.toolCount
296
+ };
297
+ }
298
+ /**
299
+ * 获取所有连接信息
300
+ */
301
+ getAllConnections() {
302
+ return Array.from(this.connections.keys()).map((name) => this.getConnectionInfo(name)).filter((info) => info !== void 0);
303
+ }
304
+ /**
305
+ * 检查连接状态
306
+ */
307
+ isConnected(name) {
308
+ const connection = this.connections.get(name);
309
+ return connection?.state === "connected" /* Connected */;
310
+ }
311
+ /**
312
+ * 获取服务器实例
313
+ */
314
+ getServer(name) {
315
+ return this.connections.get(name)?.server;
316
+ }
317
+ /**
318
+ * 安排重连
319
+ */
320
+ scheduleReconnect(name) {
321
+ const existingTimer = this.reconnectTimers.get(name);
322
+ if (existingTimer) {
323
+ clearTimeout(existingTimer);
324
+ }
325
+ const timer = setTimeout(async () => {
326
+ const connection = this.connections.get(name);
327
+ if (!connection) return;
328
+ if (connection.reconnectAttempts >= 3) {
329
+ this.log("error", `Max reconnect attempts reached for MCP server ${name}`);
330
+ return;
331
+ }
332
+ connection.reconnectAttempts++;
333
+ this.log("info", `Reconnecting to MCP server ${name} (attempt ${connection.reconnectAttempts})`);
334
+ try {
335
+ await this.connectServer(name, connection.config);
336
+ } catch (error) {
337
+ this.log("error", `Failed to reconnect to MCP server ${name}: ${error}`);
338
+ }
339
+ }, 5e3);
340
+ this.reconnectTimers.set(name, timer);
341
+ }
342
+ /**
343
+ * 日志输出
344
+ */
345
+ log(level, message) {
346
+ this.logger.child({
347
+ tags: ["mcp-connection"]
348
+ })[level](message);
349
+ }
350
+ handleProcessOutput(connection, chunk) {
351
+ connection.responseBuffer += chunk;
352
+ const lines = connection.responseBuffer.split(/\r?\n/);
353
+ connection.responseBuffer = lines.pop() ?? "";
354
+ for (const line of lines) {
355
+ const trimmed = line.trim();
356
+ if (!trimmed) continue;
357
+ try {
358
+ const response = JSON.parse(trimmed);
359
+ this.handleDirectResponse(connection, response);
360
+ } catch {
361
+ connection.responseBuffer = `${trimmed}
362
+ ${connection.responseBuffer}`;
363
+ return;
364
+ }
365
+ }
366
+ }
367
+ handleDirectResponse(connection, response) {
368
+ if (typeof response?.id !== "number") {
369
+ this.log("debug", `[MCP] Ignoring non-response message from ${connection.name}: ${JSON.stringify(response)}`);
370
+ return;
371
+ }
372
+ const pending = connection.pendingRequests.get(response.id);
373
+ if (!pending) {
374
+ this.log("debug", `[MCP] No pending request for response id ${response.id} from ${connection.name}`);
375
+ return;
376
+ }
377
+ clearTimeout(pending.timeout);
378
+ connection.pendingRequests.delete(response.id);
379
+ pending.resolve(response);
380
+ }
381
+ async sendDirectRequest(connection, method, params, timeoutMs) {
382
+ if (!connection.process?.stdin) {
383
+ throw new Error(`MCP server ${connection.name} has no stdin`);
384
+ }
385
+ const requestId = connection.nextRequestId++;
386
+ const request = {
387
+ jsonrpc: "2.0",
388
+ id: requestId,
389
+ method,
390
+ params
391
+ };
392
+ this.log("debug", `[MCP] Sending direct request: ${JSON.stringify(request)}`);
393
+ return await new Promise((resolve2, reject) => {
394
+ const timeout = setTimeout(() => {
395
+ connection.pendingRequests.delete(requestId);
396
+ reject(new Error(`Timeout waiting for ${method} from ${connection.name}`));
397
+ }, timeoutMs);
398
+ connection.pendingRequests.set(requestId, { resolve: resolve2, reject, timeout });
399
+ try {
400
+ connection.process.stdin.write(JSON.stringify(request) + "\n");
401
+ } catch (error) {
402
+ clearTimeout(timeout);
403
+ connection.pendingRequests.delete(requestId);
404
+ reject(error instanceof Error ? error : new Error(String(error)));
405
+ }
406
+ });
407
+ }
408
+ normalizeToolCallResponse(response) {
409
+ if (response.error) {
410
+ return {
411
+ content: [{
412
+ type: "text",
413
+ text: `Error: ${response.error.message || "Unknown error"}`
414
+ }],
415
+ isError: true
416
+ };
417
+ }
418
+ if (response.result) {
419
+ return response.result;
420
+ }
421
+ if (response.content || response.structuredContent || response.toolResult) {
422
+ return response;
423
+ }
424
+ return { content: [] };
425
+ }
426
+ rejectAllPendingRequests(connection, error) {
427
+ for (const [requestId, pending] of connection.pendingRequests) {
428
+ clearTimeout(pending.timeout);
429
+ pending.reject(error);
430
+ connection.pendingRequests.delete(requestId);
431
+ }
432
+ }
433
+ async stopProcess(process2) {
434
+ try {
435
+ process2.stdin?.end();
436
+ } catch {
437
+ }
438
+ try {
439
+ process2.stdout?.destroy();
440
+ } catch {
441
+ }
442
+ if (process2.exitCode !== null || process2.signalCode !== null) {
443
+ return;
444
+ }
445
+ await new Promise((resolve2) => {
446
+ const timeout = setTimeout(resolve2, 1500);
447
+ process2.once("exit", () => {
448
+ clearTimeout(timeout);
449
+ resolve2();
450
+ });
451
+ try {
452
+ process2.kill();
453
+ } catch {
454
+ clearTimeout(timeout);
455
+ resolve2();
456
+ }
457
+ });
458
+ }
459
+ async dispose() {
460
+ await this.disconnectAll();
461
+ for (const timer of this.reconnectTimers.values()) {
462
+ clearTimeout(timer);
463
+ }
464
+ this.reconnectTimers.clear();
465
+ }
466
+ };
467
+
468
+ // src/gateway-client.ts
469
+ async function discoverGatewayServers(origin, timeoutMs = 3e3) {
470
+ const gatewayOrigin = origin || process.env.PROTOCLAW_SERVER_ORIGIN;
471
+ if (!gatewayOrigin) {
472
+ return { origin: "", servers: [] };
473
+ }
474
+ try {
475
+ const controller = new AbortController();
476
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
477
+ const res = await fetch(`${gatewayOrigin}/protoclaw/mcp-gateway/servers`, {
478
+ signal: controller.signal
479
+ });
480
+ clearTimeout(timer);
481
+ if (!res.ok) {
482
+ return { origin: gatewayOrigin, servers: [] };
483
+ }
484
+ const data = await res.json();
485
+ const servers = Array.isArray(data.servers) ? data.servers : [];
486
+ return {
487
+ origin: gatewayOrigin,
488
+ servers: servers.filter((s) => s.toolCount > 0)
489
+ };
490
+ } catch {
491
+ return { origin: gatewayOrigin || "", servers: [] };
492
+ }
493
+ }
494
+ function gatewayServersToConfig(discovery, excludeServers) {
495
+ const servers = {};
496
+ for (const server of discovery.servers) {
497
+ if (excludeServers?.includes(server.id)) {
498
+ continue;
499
+ }
500
+ servers[server.id] = {
501
+ transport: "http",
502
+ url: server.url
503
+ };
504
+ }
505
+ return servers;
506
+ }
507
+
508
+ // src/mcp-adapter.ts
509
+ var MCPToolAdapter = class {
510
+ constructor(registeredTool, config) {
511
+ this.registeredTool = registeredTool;
512
+ this.config = config;
513
+ this.name = registeredTool.name;
514
+ this.description = registeredTool.description || `MCP tool: ${registeredTool.name}`;
515
+ this.parameters = this.extractParameters(registeredTool);
516
+ this.render = {
517
+ call: config.render?.call || "mcp-tool",
518
+ result: config.render?.result || "mcp-result"
519
+ };
520
+ }
521
+ registeredTool;
522
+ config;
523
+ name;
524
+ description;
525
+ parameters;
526
+ render;
527
+ /**
528
+ * 执行 MCP 工具
529
+ */
530
+ async execute(args, context) {
531
+ const startTime = Date.now();
532
+ try {
533
+ const handler = this.registeredTool.handler;
534
+ const result = await handler?.(args, context);
535
+ const duration = Date.now() - startTime;
536
+ return this.formatResult(result, duration);
537
+ } catch (error) {
538
+ return this.formatError(error, Date.now() - startTime);
539
+ }
540
+ }
541
+ /**
542
+ * 格式化 MCP 工具结果
543
+ */
544
+ formatResult(result, duration) {
545
+ if (result && result.isError) {
546
+ return {
547
+ success: false,
548
+ error: (result.content || []).map((c) => c.text || "").join("\n"),
549
+ server: this.config.serverName,
550
+ duration
551
+ };
552
+ }
553
+ const textContent = result.content.filter((c) => c.type === "text").map((c) => c.text || "").join("\n");
554
+ const formatted = {
555
+ content: textContent,
556
+ server: this.config.serverName,
557
+ duration
558
+ };
559
+ if (result.structuredContent) {
560
+ formatted.structuredContent = result.structuredContent;
561
+ }
562
+ const images = result.content.filter((c) => c.type === "image");
563
+ if (images.length > 0) {
564
+ formatted.images = images.map((c) => ({
565
+ data: c.data,
566
+ mimeType: c.mimeType
567
+ }));
568
+ }
569
+ const resources = result.content.filter((c) => c.type === "resource");
570
+ if (resources.length > 0) {
571
+ formatted.resources = resources.map((c) => ({
572
+ uri: c.uri,
573
+ mimeType: c.mimeType,
574
+ text: c.text
575
+ }));
576
+ }
577
+ return formatted;
578
+ }
579
+ /**
580
+ * 格式化错误
581
+ */
582
+ formatError(error, duration) {
583
+ const errorMessage = error instanceof Error ? error.message : String(error);
584
+ return {
585
+ success: false,
586
+ error: `[MCP Error] ${errorMessage}`,
587
+ server: this.config.serverName,
588
+ duration
589
+ };
590
+ }
591
+ /**
592
+ * 从 RegisteredToolLike 提取参数定义
593
+ *
594
+ * MCP 工具的 inputSchema 已经是 JSON Schema 格式
595
+ * 直接返回,不需要转换
596
+ */
597
+ extractParameters(tool) {
598
+ if (!tool.inputSchema) return void 0;
599
+ try {
600
+ const schema = tool.inputSchema;
601
+ if (typeof schema === "object" && schema.type === "object") {
602
+ return schema;
603
+ }
604
+ console.warn(`[MCPToolAdapter] Unexpected inputSchema format for ${tool.name}:`, typeof schema);
605
+ return void 0;
606
+ } catch (error) {
607
+ console.warn(`[MCPToolAdapter] Failed to extract parameters for ${tool.name}:`, error);
608
+ return void 0;
609
+ }
610
+ }
611
+ };
612
+ function createMCPToolAdapters(registeredTools, serverName, config) {
613
+ return registeredTools.map(
614
+ (tool) => new MCPToolAdapter(tool, {
615
+ serverName,
616
+ ...config
617
+ })
618
+ );
619
+ }
620
+
621
+ // src/client.ts
622
+ function createDefaultMCPToolName(serverId, toolName) {
623
+ return `mcp_${serverId}_${toolName}`.replace(/[^a-zA-Z0-9_]/g, "_");
624
+ }
625
+ var MCPClient = class {
626
+ constructor(serverId, config, manager) {
627
+ this.serverId = serverId;
628
+ this.config = config;
629
+ this.manager = manager ?? new MCPConnectionManager();
630
+ }
631
+ serverId;
632
+ config;
633
+ manager;
634
+ connected = false;
635
+ async connect() {
636
+ if (this.connected && this.manager.isConnected(this.serverId)) {
637
+ return;
638
+ }
639
+ await this.manager.connectServer(this.serverId, this.config);
640
+ this.connected = true;
641
+ }
642
+ async listTools() {
643
+ await this.connect();
644
+ return await this.manager.listTools(this.serverId);
645
+ }
646
+ async callTool(name, args) {
647
+ await this.connect();
648
+ return await this.manager.callTool(name, this.serverId, args);
649
+ }
650
+ getConnectionManager() {
651
+ return this.manager;
652
+ }
653
+ async dispose() {
654
+ this.connected = false;
655
+ await this.manager.disconnectServer(this.serverId);
656
+ }
657
+ };
658
+ function createMCPTool(client, tool, options = {}) {
659
+ return new MCPToolAdapter(
660
+ {
661
+ name: options.name ?? createDefaultMCPToolName(client.serverId, tool.name),
662
+ description: options.description ?? (tool.description || `MCP tool: ${tool.name}`),
663
+ inputSchema: tool.inputSchema,
664
+ enabled: true,
665
+ handler: async (args, context) => {
666
+ const finalArgs = options.transformArgs ? options.transformArgs(args, context) : args;
667
+ return await client.callTool(tool.name, finalArgs);
668
+ }
669
+ },
670
+ {
671
+ serverName: client.serverId,
672
+ render: options.render
673
+ }
674
+ );
675
+ }
676
+ async function createMCPToolsFromClient(client, options = {}) {
677
+ const tools = await client.listTools();
678
+ return tools.filter((tool) => options.filter ? options.filter(tool) : true).map((tool) => createMCPTool(client, tool, {
679
+ name: options.mapName?.(tool),
680
+ description: tool.description,
681
+ render: options.render,
682
+ transformArgs: options.transformArgs
683
+ }));
684
+ }
685
+ async function discoverMCPTools(serverId, config, options = {}, manager) {
686
+ const client = new MCPClient(serverId, config, manager);
687
+ const tools = await createMCPToolsFromClient(client, options);
688
+ return { client, tools };
689
+ }
690
+
691
+ // src/config.ts
692
+ import { existsSync, readFileSync, readdirSync } from "fs";
693
+ import { basename } from "path";
694
+ import { cwd } from "process";
695
+ import { isAbsolute, join, resolve } from "path";
696
+ function readConfigFile(configPath) {
697
+ try {
698
+ if (!existsSync(configPath)) {
699
+ return void 0;
700
+ }
701
+ const content = readFileSync(configPath, "utf-8");
702
+ return JSON.parse(content);
703
+ } catch (error) {
704
+ const errorMsg = error instanceof Error ? error.message : String(error);
705
+ console.warn(`[MCP] Failed to load config "${configPath}": ${errorMsg}`);
706
+ return void 0;
707
+ }
708
+ }
709
+ function normalizeServerConfig(server) {
710
+ if (!server.transport && typeof server.type === "string") {
711
+ return { ...server, transport: server.type };
712
+ }
713
+ return server;
714
+ }
715
+ function isMCPServerConfig(value) {
716
+ if (!value || typeof value !== "object") {
717
+ return false;
718
+ }
719
+ const config = normalizeServerConfig(value);
720
+ if (config.transport === "stdio") {
721
+ return typeof config.command === "string" && Array.isArray(config.args);
722
+ }
723
+ if (config.transport === "http") {
724
+ return typeof config.url === "string";
725
+ }
726
+ if (config.transport === "sse") {
727
+ return typeof config.url === "string";
728
+ }
729
+ return false;
730
+ }
731
+ function isMCPConfig(value) {
732
+ if (!value || typeof value !== "object") {
733
+ return false;
734
+ }
735
+ const config = value;
736
+ return !!config.servers && typeof config.servers === "object" || !!config.mcpServers && typeof config.mcpServers === "object";
737
+ }
738
+ function normalizeToMCPConfig(value, fallbackServerId) {
739
+ if (isMCPConfig(value)) {
740
+ const config = value;
741
+ const rawServers = config.servers ?? config.mcpServers;
742
+ if (!rawServers) {
743
+ return void 0;
744
+ }
745
+ const servers = {};
746
+ for (const [id, server] of Object.entries(rawServers)) {
747
+ if (server && typeof server === "object") {
748
+ servers[id] = normalizeServerConfig(server);
749
+ }
750
+ }
751
+ return { servers };
752
+ }
753
+ if (isMCPServerConfig(value)) {
754
+ return {
755
+ servers: {
756
+ [fallbackServerId]: value
757
+ }
758
+ };
759
+ }
760
+ return void 0;
761
+ }
762
+ function resolveMCPConfigInput(input, rootDir = cwd()) {
763
+ let configPath;
764
+ let fallbackServerId;
765
+ if (isAbsolute(input)) {
766
+ configPath = input;
767
+ fallbackServerId = basename(configPath, ".json");
768
+ } else if (input.includes("/") || input.includes("\\")) {
769
+ configPath = resolve(rootDir, input);
770
+ fallbackServerId = basename(configPath, ".json");
771
+ } else {
772
+ configPath = join(getDefaultMCPConfigDir(rootDir), `${input}.json`);
773
+ fallbackServerId = input;
774
+ }
775
+ return { configPath, fallbackServerId };
776
+ }
777
+ function loadMCPConfigFile(configPath, fallbackServerId) {
778
+ const config = readConfigFile(configPath);
779
+ if (!config) {
780
+ return void 0;
781
+ }
782
+ const normalized = normalizeToMCPConfig(config, fallbackServerId);
783
+ if (!normalized || Object.keys(normalized.servers).length === 0) {
784
+ console.warn(`[MCP] Ignoring invalid config file: ${configPath}`);
785
+ return void 0;
786
+ }
787
+ return normalized;
788
+ }
789
+ function getDedupedServerId(serverId, existingServerIds) {
790
+ if (!existingServerIds.has(serverId)) {
791
+ return serverId;
792
+ }
793
+ let suffix = 1;
794
+ let nextServerId = `${serverId} (${suffix})`;
795
+ while (existingServerIds.has(nextServerId)) {
796
+ suffix += 1;
797
+ nextServerId = `${serverId} (${suffix})`;
798
+ }
799
+ return nextServerId;
800
+ }
801
+ function mergeMCPConfig(merged, config, excludedServers) {
802
+ const existingServerIds = new Set(Object.keys(merged.servers));
803
+ for (const [serverId, serverConfig] of Object.entries(config.servers)) {
804
+ if (excludedServers.has(serverId)) {
805
+ continue;
806
+ }
807
+ const dedupedServerId = getDedupedServerId(serverId, existingServerIds);
808
+ merged.servers[dedupedServerId] = serverConfig;
809
+ existingServerIds.add(dedupedServerId);
810
+ }
811
+ }
812
+ function getDefaultMCPConfigDir(rootDir = cwd()) {
813
+ return join(rootDir, ".agentdev", "mcps");
814
+ }
815
+ function loadMCPConfigFromInput(input, rootDir = cwd()) {
816
+ const { configPath, fallbackServerId } = resolveMCPConfigInput(input, rootDir);
817
+ if (!existsSync(configPath)) {
818
+ console.warn(`[MCP] Config file does not exist: ${configPath}`);
819
+ return void 0;
820
+ }
821
+ return loadMCPConfigFile(configPath, fallbackServerId);
822
+ }
823
+ function loadAllMCPConfigs(rootDir = cwd(), options = {}) {
824
+ const shouldLoadDefaultDir = options.loadDefaultDir ?? true;
825
+ const extraConfigFiles = Array.isArray(options.extraConfigFiles) ? options.extraConfigFiles.filter(Boolean) : [];
826
+ const excludedServers = new Set(options.excludeServers ?? []);
827
+ const merged = { servers: {} };
828
+ if (shouldLoadDefaultDir) {
829
+ const configDir = getDefaultMCPConfigDir(rootDir);
830
+ if (existsSync(configDir)) {
831
+ const entries = readdirSync(configDir, { withFileTypes: true });
832
+ for (const entry of entries) {
833
+ if (!entry.isFile() || !entry.name.endsWith(".json")) {
834
+ continue;
835
+ }
836
+ const configPath = join(configDir, entry.name);
837
+ const config = loadMCPConfigFile(configPath, basename(entry.name, ".json"));
838
+ if (!config) {
839
+ continue;
840
+ }
841
+ mergeMCPConfig(merged, config, excludedServers);
842
+ }
843
+ }
844
+ }
845
+ for (const configFile of extraConfigFiles) {
846
+ const config = loadMCPConfigFromInput(configFile, rootDir);
847
+ if (!config) {
848
+ continue;
849
+ }
850
+ mergeMCPConfig(merged, config, excludedServers);
851
+ }
852
+ return Object.keys(merged.servers).length > 0 ? merged : void 0;
853
+ }
854
+
855
+ // src/mount.ts
856
+ function applyStaticPatch(tool, client, options) {
857
+ return {
858
+ enabled: !options.disable?.includes(tool.name),
859
+ name: options.rename?.[tool.name] ?? options.mapName?.(tool, client),
860
+ description: options.describe?.[tool.name],
861
+ render: options.toolRender?.[tool.name] ?? options.render,
862
+ transformArgs: options.transformArgs
863
+ };
864
+ }
865
+ function shouldIncludeTool(tool, options) {
866
+ if (options.include && !options.include.includes(tool.name)) {
867
+ return false;
868
+ }
869
+ if (options.exclude?.includes(tool.name)) {
870
+ return false;
871
+ }
872
+ return true;
873
+ }
874
+ function mergeToolPatch(base, override) {
875
+ if (override === false) {
876
+ return false;
877
+ }
878
+ if (!override) {
879
+ return base;
880
+ }
881
+ return {
882
+ ...base,
883
+ ...override,
884
+ render: override.render ?? base.render,
885
+ transformArgs: override.transformArgs ?? base.transformArgs
886
+ };
887
+ }
888
+ async function createManagedMCPToolsFromClient(client, options = {}) {
889
+ const tools = await client.listTools();
890
+ return tools.flatMap((tool) => {
891
+ if (!shouldIncludeTool(tool, options)) {
892
+ return [];
893
+ }
894
+ const staticPatch = applyStaticPatch(tool, client, options);
895
+ const finalPatch = mergeToolPatch(staticPatch, options.transform?.(tool, client));
896
+ if (finalPatch === false || finalPatch.enabled === false) {
897
+ return [];
898
+ }
899
+ return createMCPTool(
900
+ client,
901
+ {
902
+ ...tool,
903
+ description: finalPatch.description ?? tool.description
904
+ },
905
+ {
906
+ name: finalPatch.name ?? createDefaultMCPToolName(client.serverId, tool.name),
907
+ description: finalPatch.description,
908
+ render: finalPatch.render,
909
+ transformArgs: finalPatch.transformArgs
910
+ }
911
+ );
912
+ });
913
+ }
914
+ async function discoverManagedMCPTools(serverId, config, options = {}, manager) {
915
+ const client = new MCPClient(serverId, config, manager);
916
+ const tools = await createManagedMCPToolsFromClient(client, options);
917
+ return { client, tools };
918
+ }
919
+ async function mountMCPToolsFromConfig(config, options = {}) {
920
+ const clients = options.clients ?? /* @__PURE__ */ new Map();
921
+ const tools = [];
922
+ for (const [serverId, serverConfig] of Object.entries(config.servers)) {
923
+ try {
924
+ const client = clients.get(serverId) ?? new MCPClient(serverId, serverConfig, options.manager);
925
+ clients.set(serverId, client);
926
+ const serverOptions = options.getServerOptions?.(serverId, serverConfig) ?? {};
927
+ tools.push(...await createManagedMCPToolsFromClient(client, serverOptions));
928
+ } catch (error) {
929
+ options.onError?.(serverId, error);
930
+ }
931
+ }
932
+ return { tools, clients };
933
+ }
934
+
935
+ // src/render.ts
936
+ function escapeHtml(text) {
937
+ const str = String(text);
938
+ const map = {
939
+ "&": "&",
940
+ "<": "&lt;",
941
+ ">": "&gt;",
942
+ '"': "&quot;",
943
+ "'": "&#39;"
944
+ };
945
+ return str.replace(/[&<>"']/g, (m) => map[m]);
946
+ }
947
+ var MCP_RENDER_TEMPLATES = {
948
+ /**
949
+ * MCP 工具调用显示
950
+ */
951
+ "mcp-tool": {
952
+ call: (args) => {
953
+ const server = args._server || "unknown";
954
+ const name = args._name || "unknown";
955
+ return `
956
+ <div class="bash-command" style="
957
+ border-left: 3px solid #ff6b6b;
958
+ padding-left: 8px;
959
+ margin: 4px 0;
960
+ ">
961
+ <span style="
962
+ color: #ff6b6b;
963
+ font-weight: bold;
964
+ font-size: 11px;
965
+ text-transform: uppercase;
966
+ ">MCP</span>
967
+ <span class="file-path" style="color: #c068ff;">${escapeHtml(server)}</span>
968
+ <span style="color: #888;">::</span>
969
+ <span style="color: #fff;">${escapeHtml(name)}</span>
970
+ </div>
971
+ `.trim();
972
+ },
973
+ result: (data, success = true) => {
974
+ if (!success || data.error) {
975
+ return `
976
+ <div class="bash-output" style="
977
+ border-left: 3px solid #ff4444;
978
+ padding-left: 8px;
979
+ color: #ff6b6b;
980
+ ">
981
+ <div style="font-weight: bold; margin-bottom: 4px;">MCP Error</div>
982
+ <pre style="margin: 0; white-space: pre-wrap;">${escapeHtml(data.error || "Unknown error")}</pre>
983
+ </div>
984
+ `.trim();
985
+ }
986
+ let content = "";
987
+ if (data.content) {
988
+ content += `<pre class="bash-output" style="max-height: 400px; overflow: auto;">${escapeHtml(data.content)}</pre>`;
989
+ }
990
+ if (data.structuredContent) {
991
+ content += `<details style="margin-top: 8px;">
992
+ <summary style="cursor: pointer; color: var(--accent-color);">Structured Data</summary>
993
+ <pre style="margin: 4px 0; padding: 8px; background: var(--bg-secondary);">${escapeHtml(JSON.stringify(data.structuredContent, null, 2))}</pre>
994
+ </details>`;
995
+ }
996
+ if (data.images && data.images.length > 0) {
997
+ content += `<div style="margin-top: 8px;">`;
998
+ data.images.forEach((img) => {
999
+ content += `<img src="data:${img.mimeType};base64,${img.data}" style="max-width: 100%; border-radius: 4px;" />`;
1000
+ });
1001
+ content += `</div>`;
1002
+ }
1003
+ if (data.resources && data.resources.length > 0) {
1004
+ content += `<div style="margin-top: 8px;">
1005
+ <div style="font-weight: bold; margin-bottom: 4px;">Resources:</div>`;
1006
+ data.resources.forEach((res) => {
1007
+ content += `<div style="padding: 4px; background: var(--bg-secondary); margin: 4px 0;">
1008
+ <div style="font-size: 11px; color: var(--text-secondary);">${escapeHtml(res.uri)}</div>
1009
+ ${res.text ? `<pre style="margin: 4px 0 0 0;">${escapeHtml(res.text)}</pre>` : ""}
1010
+ </div>`;
1011
+ });
1012
+ content += `</div>`;
1013
+ }
1014
+ const meta = `
1015
+ <div style="
1016
+ font-size: 11px;
1017
+ opacity: 0.6;
1018
+ margin-top: 8px;
1019
+ display: flex;
1020
+ gap: 12px;
1021
+ ">
1022
+ <span>Server: ${escapeHtml(data.server)}</span>
1023
+ <span>Duration: ${data.duration}ms</span>
1024
+ </div>
1025
+ `;
1026
+ return content + meta;
1027
+ }
1028
+ },
1029
+ /**
1030
+ * MCP 结果 (简化版)
1031
+ */
1032
+ "mcp-result": {
1033
+ call: () => '<div class="bash-command">MCP Tool Call</div>',
1034
+ result: (data, success = true) => {
1035
+ return MCP_RENDER_TEMPLATES["mcp-tool"].result(data, success);
1036
+ }
1037
+ }
1038
+ };
1039
+ function getMCPRenderTemplate(toolName) {
1040
+ return "mcp-tool";
1041
+ }
1042
+ function renderMCPToolCall(serverName, toolName, args) {
1043
+ const template = MCP_RENDER_TEMPLATES["mcp-tool"].call;
1044
+ if (typeof template === "function") {
1045
+ return template({ _server: serverName, _name: toolName, ...args });
1046
+ }
1047
+ return template;
1048
+ }
1049
+ function renderMCPToolResult(result, success = true) {
1050
+ const template = MCP_RENDER_TEMPLATES["mcp-tool"].result;
1051
+ if (typeof template === "function") {
1052
+ return template(result, success);
1053
+ }
1054
+ return template;
1055
+ }
1056
+
1057
+ // src/feature/index.ts
1058
+ import { fileURLToPath } from "url";
1059
+ import { getPackageInfoFromSource } from "@agentdevjs/core";
1060
+ var __filename = fileURLToPath(import.meta.url);
1061
+ var MCPFeature = class {
1062
+ name = "mcp";
1063
+ source = __filename.replace(/\\/g, "/");
1064
+ description = "\u8FDE\u63A5 MCP \u670D\u52A1\u5668\u5E76\u628A\u53D1\u73B0\u5230\u7684\u8FDC\u7A0B\u80FD\u529B\u6302\u8F7D\u6210\u6807\u51C6\u5DE5\u5177\u3002";
1065
+ manager = new MCPConnectionManager();
1066
+ clients = /* @__PURE__ */ new Map();
1067
+ input;
1068
+ options;
1069
+ config;
1070
+ mcpContext;
1071
+ /**
1072
+ * 缓存包信息
1073
+ */
1074
+ _packageInfo = null;
1075
+ /**
1076
+ * 获取包信息(统一打包方案)
1077
+ */
1078
+ getPackageInfo() {
1079
+ if (!this._packageInfo) {
1080
+ this._packageInfo = getPackageInfoFromSource(this.source);
1081
+ }
1082
+ return this._packageInfo;
1083
+ }
1084
+ /**
1085
+ * 获取模板名称列表(统一打包方案)
1086
+ */
1087
+ getTemplateNames() {
1088
+ return ["mcp-tool"];
1089
+ }
1090
+ constructor(input, options = {}) {
1091
+ this.input = input;
1092
+ this.options = options;
1093
+ if (input && typeof input !== "string") {
1094
+ this.config = input;
1095
+ }
1096
+ }
1097
+ getFeatureManifest() {
1098
+ return {
1099
+ schemaVersion: 1,
1100
+ settings: {
1101
+ properties: {
1102
+ scanAgentdevDir: {
1103
+ type: "boolean",
1104
+ title: "\u52A0\u8F7D .agentdev/mcps",
1105
+ description: "\u662F\u5426\u4ECE\u5DE5\u4F5C\u76EE\u5F55\u7684 .agentdev/mcps/ \u52A0\u8F7D\u7CFB\u7EDF MCP \u914D\u7F6E\u6587\u4EF6\u3002",
1106
+ default: true
1107
+ },
1108
+ extraConfigFiles: {
1109
+ type: "file",
1110
+ title: "\u989D\u5916 MCP \u914D\u7F6E\u6587\u4EF6",
1111
+ description: "\u989D\u5916\u52A0\u8F7D\u591A\u4E2A MCP \u914D\u7F6E\u6587\u4EF6\uFF08\u81F3\u591A 5 \u4E2A\uFF09\uFF0C\u91CD\u540D server/tool \u4F1A\u81EA\u52A8\u52A0\u540E\u7F00\u533A\u5206\u3002",
1112
+ default: [],
1113
+ accept: ".json",
1114
+ maxItems: 5
1115
+ },
1116
+ enableGateway: {
1117
+ type: "boolean",
1118
+ title: "\u542F\u7528 Claw MCP \u7F51\u5173",
1119
+ description: "\u81EA\u52A8\u8FDE\u63A5 Claw \u4E3B\u8FDB\u7A0B\u6258\u7BA1\u7684\u5171\u4EAB MCP \u670D\u52A1\u5668\u3002\u542F\u7528\u540E\uFF0C\u6240\u6709\u4F1A\u8BDD\u5171\u4EAB\u540C\u4E00\u4EFD MCP server \u8FDE\u63A5\uFF0C\u65E0\u9700\u5404\u81EA\u542F\u52A8\u3002",
1120
+ default: true
1121
+ }
1122
+ }
1123
+ }
1124
+ };
1125
+ }
1126
+ resolveFeatureConfig(featureConfig) {
1127
+ if (!featureConfig || typeof featureConfig !== "object") {
1128
+ return {
1129
+ scanAgentdevDir: true,
1130
+ extraConfigFiles: [],
1131
+ enableGateway: true
1132
+ };
1133
+ }
1134
+ const config = featureConfig;
1135
+ return {
1136
+ scanAgentdevDir: config.scanAgentdevDir === void 0 ? true : config.scanAgentdevDir !== false,
1137
+ extraConfigFiles: Array.isArray(config.extraConfigFiles) ? config.extraConfigFiles.filter((value) => typeof value === "string" && value.length > 0) : [],
1138
+ enableGateway: config.enableGateway === void 0 ? true : config.enableGateway !== false
1139
+ };
1140
+ }
1141
+ resolveRuntimeConfig(ctx) {
1142
+ if (this.config) {
1143
+ return this.config;
1144
+ }
1145
+ const rootDir = ctx.config?.workspaceDir;
1146
+ if (typeof this.input === "string") {
1147
+ this.config = loadMCPConfigFromInput(this.input, rootDir);
1148
+ return this.config;
1149
+ }
1150
+ const featureConfig = this.resolveFeatureConfig(ctx.featureConfig);
1151
+ this.config = loadAllMCPConfigs(rootDir, {
1152
+ loadDefaultDir: featureConfig.scanAgentdevDir,
1153
+ extraConfigFiles: featureConfig.extraConfigFiles,
1154
+ excludeServers: this.options.excludeServers
1155
+ });
1156
+ return this.config;
1157
+ }
1158
+ /**
1159
+ * 获取同步工具(无)
1160
+ */
1161
+ getTools() {
1162
+ return [];
1163
+ }
1164
+ /**
1165
+ * 获取异步工具(需要连接 MCP 服务器)
1166
+ */
1167
+ async getAsyncTools(ctx) {
1168
+ const config = this.resolveRuntimeConfig(ctx);
1169
+ const featureConfig = this.resolveFeatureConfig(ctx.featureConfig);
1170
+ let gatewayServers = {};
1171
+ if (featureConfig.enableGateway && !this.input) {
1172
+ const discovery = await discoverGatewayServers();
1173
+ if (discovery.servers.length > 0) {
1174
+ gatewayServers = gatewayServersToConfig(discovery, this.options.excludeServers);
1175
+ }
1176
+ }
1177
+ const hasServers = config?.servers && Object.keys(config.servers).length > 0 || Object.keys(gatewayServers).length > 0;
1178
+ if (!hasServers) {
1179
+ return [];
1180
+ }
1181
+ const mergedConfig = {
1182
+ enabled: true,
1183
+ servers: {
1184
+ ...config?.servers || {},
1185
+ ...gatewayServers
1186
+ }
1187
+ };
1188
+ const result = await mountMCPToolsFromConfig(mergedConfig, {
1189
+ manager: this.manager,
1190
+ clients: this.clients,
1191
+ onError: (serverId, error) => {
1192
+ const errorMsg = error instanceof Error ? error.message : String(error);
1193
+ console.warn(`[MCPFeature] Failed to load tools from "${serverId}": ${errorMsg}`);
1194
+ }
1195
+ });
1196
+ return result.tools;
1197
+ }
1198
+ /**
1199
+ * 声明上下文注入器
1200
+ * 为所有 MCP 工具注入 _mcpContext
1201
+ */
1202
+ getContextInjectors() {
1203
+ return /* @__PURE__ */ new Map([
1204
+ [/^mcp_/, () => ({ _mcpContext: this.mcpContext })]
1205
+ ]);
1206
+ }
1207
+ /**
1208
+ * 清理钩子
1209
+ */
1210
+ async onDestroy() {
1211
+ for (const client of this.clients.values()) {
1212
+ await client.dispose();
1213
+ }
1214
+ this.clients.clear();
1215
+ await this.manager.dispose();
1216
+ }
1217
+ /**
1218
+ * 设置 MCP 上下文(运行时注入)
1219
+ */
1220
+ setMCPContext(context) {
1221
+ this.mcpContext = context;
1222
+ }
1223
+ /**
1224
+ * 获取连接管理器(供外部使用)
1225
+ */
1226
+ getConnectionManager() {
1227
+ return this.manager;
1228
+ }
1229
+ };
1230
+ export {
1231
+ MCPClient,
1232
+ MCPConnectionManager,
1233
+ MCPConnectionState,
1234
+ MCPFeature,
1235
+ MCPToolAdapter,
1236
+ MCP_RENDER_TEMPLATES,
1237
+ createDefaultMCPToolName,
1238
+ createMCPTool,
1239
+ createMCPToolAdapters,
1240
+ createMCPToolsFromClient,
1241
+ createManagedMCPToolsFromClient,
1242
+ discoverGatewayServers,
1243
+ discoverMCPTools,
1244
+ discoverManagedMCPTools,
1245
+ gatewayServersToConfig,
1246
+ getDefaultMCPConfigDir,
1247
+ getMCPRenderTemplate,
1248
+ loadAllMCPConfigs,
1249
+ loadMCPConfigFromInput,
1250
+ mountMCPToolsFromConfig,
1251
+ renderMCPToolCall,
1252
+ renderMCPToolResult
1253
+ };
1254
+ //# sourceMappingURL=index.js.map