foliko 1.1.93 → 2.0.1
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/.claude/settings.local.json +2 -1
- package/CLAUDE.md +56 -30
- package/REFACTORING_PLAN.md +645 -0
- package/docs/architecture.md +131 -0
- package/docs/migration.md +57 -0
- package/docs/public-api.md +138 -0
- package/docs/usage.md +385 -0
- package/examples/ambient-example.js +20 -137
- package/examples/basic.js +21 -48
- package/examples/bootstrap.js +16 -74
- package/examples/mcp-example.js +6 -29
- package/examples/skill-example.js +6 -19
- package/examples/workflow.js +8 -56
- package/package.json +8 -4
- package/plugins/README.md +49 -0
- package/plugins/{ambient-agent → ambient}/EventWatcher.js +1 -1
- package/plugins/{ambient-agent → ambient}/ExplorerLoop.js +3 -3
- package/plugins/{ambient-agent → ambient}/GoalManager.js +2 -2
- package/plugins/ambient/README.md +14 -0
- package/plugins/{ambient-agent → ambient}/Reflector.js +1 -1
- package/plugins/{ambient-agent → ambient}/StateStore.js +1 -1
- package/plugins/{ambient-agent → ambient}/index.js +2 -2
- package/plugins/{ai-plugin.js → core/ai/index.js} +14 -30
- package/plugins/{audit-plugin.js → core/audit/index.js} +3 -30
- package/plugins/{coordinator-plugin.js → core/coordinator/index.js} +3 -35
- package/plugins/core/default/bootstrap.js +224 -0
- package/plugins/core/default/config.js +222 -0
- package/plugins/core/default/index.js +58 -0
- package/plugins/core/mcp/index.js +1 -0
- package/plugins/{python-plugin-loader.js → core/python-loader/index.js} +7 -187
- package/plugins/{rules-plugin.js → core/rules/index.js} +121 -64
- package/plugins/{scheduler-plugin.js → core/scheduler/index.js} +12 -114
- package/plugins/{session-plugin.js → core/session/index.js} +9 -73
- package/{src/capabilities/skill-manager.js → plugins/core/skill-manager/index.js} +64 -18
- package/plugins/{storage-plugin.js → core/storage/index.js} +5 -29
- package/plugins/{subagent-plugin.js → core/sub-agent/index.js} +10 -171
- package/plugins/{think-plugin.js → core/think/index.js} +24 -91
- package/{src/capabilities/workflow-engine.js → plugins/core/workflow/index.js} +87 -85
- package/plugins/default-plugins.js +6 -720
- package/plugins/{data-splitter-plugin.js → executors/data-splitter/index.js} +9 -83
- package/plugins/{extension-executor-plugin.js → executors/extension/index.js} +13 -97
- package/plugins/{python-executor-plugin.js → executors/python/index.js} +6 -31
- package/plugins/{shell-executor-plugin.js → executors/shell/index.js} +2 -5
- package/plugins/install/README.md +9 -0
- package/plugins/{install-plugin.js → install/index.js} +3 -3
- package/plugins/{file-system-plugin.js → io/file-system/index.js} +34 -236
- package/plugins/{web-plugin.js → io/web/index.js} +11 -113
- package/plugins/memory/README.md +13 -0
- package/plugins/{memory-plugin.js → memory/index.js} +4 -18
- package/plugins/messaging/email/README.md +19 -0
- package/plugins/{email → messaging/email}/index.js +3 -3
- package/plugins/{feishu-plugin.js → messaging/feishu/index.js} +4 -4
- package/plugins/{qq-plugin.js → messaging/qq/index.js} +6 -17
- package/plugins/{telegram-plugin.js → messaging/telegram/index.js} +4 -4
- package/plugins/{weixin-plugin.js → messaging/weixin/index.js} +16 -16
- package/plugins/{plugin-manager-plugin.js → plugin-manager/index.js} +36 -180
- package/plugins/{tools-plugin.js → tools/index.js} +68 -116
- package/plugins/trading/README.md +15 -0
- package/plugins/{gate-trading.js → trading/index.js} +8 -8
- package/{examples → sandbox}/test-concurrent-chat.js +2 -2
- package/{examples → sandbox}/test-long-chat.js +2 -2
- package/{examples → sandbox}/test-session-chat.js +2 -2
- package/{examples → sandbox}/test-web-plugin.js +1 -1
- package/{examples → sandbox}/test-weixin-feishu.js +2 -2
- package/src/agent/base.js +56 -0
- package/src/{core/agent-chat.js → agent/chat.js} +11 -11
- package/src/{core/coordinator-manager.js → agent/coordinator.js} +3 -3
- package/src/agent/index.js +111 -0
- package/src/agent/main.js +337 -0
- package/src/agent/prompt.js +78 -0
- package/src/agent/sub.js +198 -0
- package/src/agent/worker.js +104 -0
- package/{cli/bin/foliko.js → src/cli/bin.js} +1 -1
- package/{cli/src → src/cli}/commands/chat.js +25 -21
- package/{cli/src → src/cli}/index.js +1 -0
- package/{cli/src → src/cli}/ui/chat-ui-old.js +40 -178
- package/{cli/src → src/cli}/ui/chat-ui.js +3 -3
- package/{cli/src → src/cli}/ui/components/footer-bar.js +1 -1
- package/src/common/errors.js +402 -0
- package/src/{utils → common}/logger.js +33 -0
- package/src/{utils/chat-queue.js → common/queue.js} +2 -2
- package/src/config/plugin-config.js +50 -0
- package/src/context/agent.js +32 -0
- package/src/context/compaction-prompts.js +170 -0
- package/src/context/compaction-utils.js +191 -0
- package/src/context/compressor.js +413 -0
- package/src/context/index.js +9 -0
- package/src/{core/context-manager.js → context/manager.js} +1 -1
- package/src/context/request.js +50 -0
- package/src/context/session.js +33 -0
- package/src/context/storage.js +30 -0
- package/src/executors/mcp-client.js +153 -0
- package/src/executors/mcp-desc.js +236 -0
- package/src/executors/mcp-executor.js +91 -956
- package/src/{core → framework}/command-registry.js +1 -1
- package/src/framework/framework.js +300 -0
- package/src/framework/index.js +18 -0
- package/src/framework/lifecycle.js +203 -0
- package/src/framework/loader.js +78 -0
- package/src/framework/registry.js +86 -0
- package/src/{core/ui-extension-context.js → framework/ui-extension.js} +1 -1
- package/src/index.js +130 -15
- package/src/llm/index.js +26 -0
- package/src/llm/provider.js +212 -0
- package/src/llm/registry.js +11 -0
- package/src/{core/token-counter.js → llm/tokens.js} +4 -37
- package/src/{core/plugin-base.js → plugin/base.js} +10 -136
- package/src/plugin/index.js +14 -0
- package/src/plugin/loader.js +101 -0
- package/src/plugin/manager.js +484 -0
- package/src/{core → session}/branch-summary-auto.js +2 -2
- package/src/{core/chat-session.js → session/chat.js} +2 -2
- package/src/session/index.js +7 -0
- package/src/{core/session-manager.js → session/session.js} +2 -2
- package/src/session/ttl.js +92 -0
- package/src/{core/jsonl-storage.js → storage/jsonl.js} +1 -1
- package/src/tool/executor.js +85 -0
- package/src/tool/index.js +15 -0
- package/src/tool/registry.js +143 -0
- package/src/{core/tool-router.js → tool/router.js} +17 -124
- package/src/tool/schema.js +108 -0
- package/src/utils/data-splitter.js +1 -1
- package/src/utils/download.js +1 -1
- package/src/utils/index.js +6 -6
- package/src/utils/message-validator.js +1 -1
- package/tests/core/context-storage.test.js +46 -0
- package/tests/core/llm.test.js +54 -0
- package/tests/core/plugin.test.js +42 -0
- package/tests/core/tool.test.js +60 -0
- package/tests/setup.js +10 -0
- package/tests/smoke.test.js +58 -0
- package/vitest.config.js +9 -0
- package/cli/src/daemon.js +0 -149
- package/docs/CONTEXT_DESIGN.md +0 -1596
- package/docs/ai-sdk-optimization.md +0 -655
- package/docs/features.md +0 -120
- package/docs/qq-bot.md +0 -976
- package/docs/quick-reference.md +0 -160
- package/docs/user-manual.md +0 -1391
- package/images/geometric_shapes.jpg +0 -0
- package/images/sunset_mountain_lake.jpg +0 -0
- package/skills/poster-guide/SKILL.md +0 -792
- package/src/capabilities/index.js +0 -11
- package/src/core/agent.js +0 -808
- package/src/core/context-compressor.js +0 -959
- package/src/core/enhanced-context-compressor.js +0 -210
- package/src/core/framework.js +0 -1422
- package/src/core/index.js +0 -30
- package/src/core/plugin-manager.js +0 -961
- package/src/core/provider-registry.js +0 -159
- package/src/core/provider.js +0 -156
- package/src/core/request-context.js +0 -98
- package/src/core/subagent.js +0 -442
- package/src/core/system-prompt-builder.js +0 -120
- package/src/core/tool-executor.js +0 -202
- package/src/core/tool-registry.js +0 -517
- package/src/core/worker-agent.js +0 -192
- package/src/executors/executor-base.js +0 -58
- package/src/utils/error-boundary.js +0 -363
- package/src/utils/error.js +0 -374
- package/system.md +0 -1645
- package/website_v2/README.md +0 -57
- package/website_v2/SPEC.md +0 -1
- package/website_v2/docs/api.html +0 -128
- package/website_v2/docs/configuration.html +0 -147
- package/website_v2/docs/plugin-development.html +0 -129
- package/website_v2/docs/project-structure.html +0 -89
- package/website_v2/docs/skill-development.html +0 -85
- package/website_v2/index.html +0 -489
- package/website_v2/scripts/main.js +0 -93
- package/website_v2/styles/animations.css +0 -8
- package/website_v2/styles/docs.css +0 -83
- package/website_v2/styles/main.css +0 -417
- package/xhs_auth.json +0 -268
- package//346/265/267/346/212/245/346/217/222/344/273/266.md +0 -621
- /package/plugins/{ambient-agent → ambient}/constants.js +0 -0
- /package/plugins/{email → messaging/email}/constants.js +0 -0
- /package/plugins/{email → messaging/email}/handlers.js +0 -0
- /package/plugins/{email → messaging/email}/monitor.js +0 -0
- /package/plugins/{email → messaging/email}/parser.js +0 -0
- /package/plugins/{email → messaging/email}/reply.js +0 -0
- /package/plugins/{email → messaging/email}/utils.js +0 -0
- /package/{examples → sandbox}/test-chat.js +0 -0
- /package/{examples → sandbox}/test-mcp.js +0 -0
- /package/{examples → sandbox}/test-reload.js +0 -0
- /package/{examples → sandbox}/test-telegram.js +0 -0
- /package/{examples → sandbox}/test-tg-bot.js +0 -0
- /package/{examples → sandbox}/test-tg-simple.js +0 -0
- /package/{examples → sandbox}/test-tg.js +0 -0
- /package/{examples → sandbox}/test-think.js +0 -0
- /package/src/{core/sub-agent-config.js → agent/sub-config.js} +0 -0
- /package/{cli/src → src/cli}/commands/daemon.js +0 -0
- /package/{cli/src → src/cli}/commands/list.js +0 -0
- /package/{cli/src → src/cli}/commands/plugin.js +0 -0
- /package/{cli/src → src/cli}/ui/components/agent-mention-provider.js +0 -0
- /package/{cli/src → src/cli}/ui/components/chained-autocomplete-provider.js +0 -0
- /package/{cli/src → src/cli}/ui/components/message-bubble.js +0 -0
- /package/{cli/src → src/cli}/ui/components/status-bar.js +0 -0
- /package/{cli/src → src/cli}/utils/ansi.js +0 -0
- /package/{cli/src → src/cli}/utils/config.js +0 -0
- /package/{cli/src → src/cli}/utils/markdown.js +0 -0
- /package/{cli/src → src/cli}/utils/plugin-config.js +0 -0
- /package/{cli/src → src/cli}/utils/render-diff.js +0 -0
- /package/src/{utils/circuit-breaker.js → common/circuit.js} +0 -0
- /package/src/{core → common}/constants.js +0 -0
- /package/src/{utils/edit-diff.js → common/diff.js} +0 -0
- /package/src/{utils/event-emitter.js → common/events.js} +0 -0
- /package/src/{utils → common}/id.js +0 -0
- /package/src/{utils → common}/retry.js +0 -0
- /package/src/{core/notification-manager.js → notification/manager.js} +0 -0
- /package/src/{core/session-entry.js → session/entry.js} +0 -0
- /package/src/{core/storage-manager.js → storage/manager.js} +0 -0
|
@@ -1,208 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* MCPExecutor MCP 执行器
|
|
3
5
|
* 使用 @ai-sdk/mcp 连接到 MCP 服务器
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
|
-
const { Plugin } = require('../
|
|
7
|
-
const { spawn } = require('child_process');
|
|
8
|
+
const { Plugin } = require('../plugin/base');
|
|
8
9
|
const fs = require('fs');
|
|
9
10
|
const path = require('path');
|
|
10
11
|
const { z } = require('zod');
|
|
11
12
|
const { createMCPClient } = require('@ai-sdk/mcp');
|
|
12
13
|
const { StdioClientTransport } = require('@modelcontextprotocol/sdk/client/stdio.js');
|
|
13
14
|
const { zodSchemaToMarkdown } = require('@chnak/zod-to-markdown');
|
|
14
|
-
const { logger } = require('../
|
|
15
|
+
const { logger } = require('../common/logger');
|
|
16
|
+
const { MCPClientWrapper } = require('./mcp-client');
|
|
17
|
+
const { extractSchema, buildMCPServersDescription, bindMcpParamsDesc } = require('./mcp-desc');
|
|
18
|
+
const { jsonSchemaToZod } = require('../tool/schema');
|
|
15
19
|
|
|
16
20
|
const log = logger.child('MCPExecutor');
|
|
17
21
|
|
|
18
|
-
/**
|
|
19
|
-
* MCP 客户端包装器
|
|
20
|
-
*/
|
|
21
|
-
class MCPClientWrapper {
|
|
22
|
-
constructor(options = {}) {
|
|
23
|
-
this.serverName = options.serverName || 'unknown';
|
|
24
|
-
this.command = options.command;
|
|
25
|
-
this.args = options.args || [];
|
|
26
|
-
this.env = options.env || {};
|
|
27
|
-
this.timeout = options.timeout || 30000;
|
|
28
|
-
this.priority = 11;
|
|
29
|
-
this.client = null;
|
|
30
|
-
this.connected = false;
|
|
31
|
-
this.tools = [];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* 连接 MCP 服务器
|
|
36
|
-
*/
|
|
37
|
-
async connect() {
|
|
38
|
-
if (this.connected) return;
|
|
39
|
-
|
|
40
|
-
const controller = new AbortController();
|
|
41
|
-
const timeoutMs = this.timeout || 30000;
|
|
42
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
// 动态导入 @ai-sdk/mcp
|
|
46
|
-
let createMCPClient;
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
const mcpModule = require('@ai-sdk/mcp');
|
|
50
|
-
createMCPClient = mcpModule.createMCPClient || mcpModule.experimental_createMCPClient;
|
|
51
|
-
} catch (err) {
|
|
52
|
-
throw new Error('@ai-sdk/mcp not installed. Please run: npm install @ai-sdk/mcp');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (!createMCPClient) {
|
|
56
|
-
throw new Error('createMCPClient not available');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// 解析命令路径
|
|
60
|
-
const { which, shellResolve } = this._resolveCommand(this.command);
|
|
61
|
-
|
|
62
|
-
// 使用正确的子路径导入
|
|
63
|
-
try {
|
|
64
|
-
const { Experimental_StdioMCPTransport } = require('@ai-sdk/mcp/mcp-stdio');
|
|
65
|
-
const transport = new Experimental_StdioMCPTransport({
|
|
66
|
-
command: which || this.command,
|
|
67
|
-
args: shellResolve ? [shellResolve, ...this.args] : this.args,
|
|
68
|
-
env: { ...process.env, ...this.env },
|
|
69
|
-
});
|
|
70
|
-
this.client = await createMCPClient({
|
|
71
|
-
transport,
|
|
72
|
-
signal: controller.signal,
|
|
73
|
-
});
|
|
74
|
-
} catch (transportErr) {
|
|
75
|
-
log.error(` Transport error:`, transportErr.message);
|
|
76
|
-
throw transportErr;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// 获取工具列表
|
|
80
|
-
// 新版 API: tools() 是函数
|
|
81
|
-
if (typeof this.client.tools === 'function') {
|
|
82
|
-
const toolsObject = await this.client.tools();
|
|
83
|
-
this.tools = Object.entries(toolsObject)
|
|
84
|
-
.filter(([, tool]) => tool != null)
|
|
85
|
-
.map(([name, tool]) => ({ ...tool, name }));
|
|
86
|
-
} else if (this.client.tools && typeof this.client.tools.list === 'function') {
|
|
87
|
-
// 旧版 API: tools.list()
|
|
88
|
-
const listResult = await this.client.tools.list();
|
|
89
|
-
this.tools = listResult.tools || [];
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
this.connected = true;
|
|
93
|
-
// log.info(` Connected to ${this.serverName} with ${this.tools.length} tools`);
|
|
94
|
-
} catch (err) {
|
|
95
|
-
if (controller.signal.aborted) {
|
|
96
|
-
log.error(` Connection timeout (${timeoutMs}ms) for ${this.serverName}`);
|
|
97
|
-
throw new Error(`连接超时 (${timeoutMs}ms): ${this.serverName}`);
|
|
98
|
-
}
|
|
99
|
-
log.error(` Failed to connect to ${this.serverName}:`, err.message);
|
|
100
|
-
// 不抛出错误,让框架继续运行
|
|
101
|
-
} finally {
|
|
102
|
-
clearTimeout(timeoutId);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* 解析命令路径
|
|
108
|
-
*/
|
|
109
|
-
_resolveCommand(cmd) {
|
|
110
|
-
const isWindows = process.platform === 'win32';
|
|
111
|
-
const ext = isWindows ? '.cmd' : '';
|
|
112
|
-
const withExt = cmd + ext;
|
|
113
|
-
|
|
114
|
-
// 检查命令是否直接存在
|
|
115
|
-
try {
|
|
116
|
-
const result = spawn.sync(withExt, ['--version'], {
|
|
117
|
-
stdio: 'ignore',
|
|
118
|
-
timeout: 2000,
|
|
119
|
-
});
|
|
120
|
-
if (result.error?.code !== 'ENOENT') {
|
|
121
|
-
return { which: withExt, shellResolve: null };
|
|
122
|
-
}
|
|
123
|
-
} catch (e) {
|
|
124
|
-
// ignore
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// 尝试解析 npm 全局 bin 目录
|
|
128
|
-
const globalCommands = isWindows
|
|
129
|
-
? [
|
|
130
|
-
path.join(process.env.APPDATA || '', 'npm', withExt),
|
|
131
|
-
path.join(process.env.PROGRAMDATA || 'C:\\ProgramData', 'npm', withExt),
|
|
132
|
-
]
|
|
133
|
-
: ['/usr/local/bin/' + cmd, '/usr/bin/' + cmd];
|
|
134
|
-
|
|
135
|
-
for (const cmdPath of globalCommands) {
|
|
136
|
-
try {
|
|
137
|
-
if (fs.existsSync(cmdPath)) {
|
|
138
|
-
return { which: cmdPath, shellResolve: null };
|
|
139
|
-
}
|
|
140
|
-
} catch (e) {
|
|
141
|
-
// ignore
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return { which: cmd, shellResolve: null };
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* 调用工具
|
|
150
|
-
*/
|
|
151
|
-
async callTool(toolName, args) {
|
|
152
|
-
if (!this.connected) {
|
|
153
|
-
await this.connect();
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (!this.client) {
|
|
157
|
-
throw new Error('MCP client not initialized');
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
try {
|
|
161
|
-
// 新版 API: tools() 返回对象
|
|
162
|
-
let toolsObject;
|
|
163
|
-
if (typeof this.client.tools === 'function') {
|
|
164
|
-
toolsObject = await this.client.tools();
|
|
165
|
-
} else if (this.client.tools && typeof this.client.tools.list === 'function') {
|
|
166
|
-
// 旧版 API
|
|
167
|
-
const listResult = await this.client.tools.list();
|
|
168
|
-
toolsObject = listResult.tools || {};
|
|
169
|
-
} else {
|
|
170
|
-
throw new Error('Cannot get tools from MCP client');
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const tool = toolsObject[toolName];
|
|
174
|
-
if (!tool) {
|
|
175
|
-
throw new Error(`Tool not found: ${toolName}`);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const result = await tool.execute(args);
|
|
179
|
-
return result;
|
|
180
|
-
} catch (err) {
|
|
181
|
-
log.error(` Tool call failed: ${toolName}:`, err.message);
|
|
182
|
-
throw err;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* 断开连接
|
|
188
|
-
*/
|
|
189
|
-
async disconnect() {
|
|
190
|
-
if (this.client) {
|
|
191
|
-
try {
|
|
192
|
-
await this.client.destroy();
|
|
193
|
-
} catch (e) {
|
|
194
|
-
// ignore
|
|
195
|
-
}
|
|
196
|
-
this.client = null;
|
|
197
|
-
this.connected = false;
|
|
198
|
-
this.tools = [];
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* MCPExecutorPlugin
|
|
205
|
-
*/
|
|
206
22
|
class MCPExecutorPlugin extends Plugin {
|
|
207
23
|
constructor(config = {}) {
|
|
208
24
|
super();
|
|
@@ -213,13 +29,9 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
213
29
|
this.system = true;
|
|
214
30
|
|
|
215
31
|
this._framework = null;
|
|
216
|
-
// serverName -> { client, tools: [{ name, description, inputSchema }], toolObjects: { toolName: actualTool } }
|
|
217
32
|
this._clients = new Map();
|
|
218
|
-
// 保存服务器配置,用于重新启用
|
|
219
33
|
this._serverConfigs = new Map();
|
|
220
34
|
this._config = config;
|
|
221
|
-
// 注册的工具对象(extension-executor 会扫描这个)
|
|
222
|
-
// 格式: { 'server_toolname': { name, description, inputSchema, execute } }
|
|
223
35
|
this.tools = {};
|
|
224
36
|
}
|
|
225
37
|
|
|
@@ -229,23 +41,18 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
229
41
|
}
|
|
230
42
|
|
|
231
43
|
async start(framework) {
|
|
232
|
-
// 异步加载所有 MCP 服务器,不阻塞框架启动
|
|
233
44
|
if (this._config.servers) {
|
|
234
45
|
for (const serverConfig of this._config.servers) {
|
|
235
46
|
if (serverConfig.enabled === false) {
|
|
236
|
-
// log.info(` Skipping disabled server: ${serverConfig.name}`);
|
|
237
|
-
// 保存完整配置,以便后续动态启用
|
|
238
47
|
this._serverConfigs.set(serverConfig.name, { ...serverConfig });
|
|
239
48
|
continue;
|
|
240
49
|
}
|
|
241
|
-
// 异步连接,不 await
|
|
242
50
|
this.addServer(serverConfig).catch(err => {
|
|
243
51
|
log.error(`[MCP] Async server connection failed for ${serverConfig.name}:`, err.message);
|
|
244
52
|
});
|
|
245
53
|
}
|
|
246
54
|
}
|
|
247
55
|
|
|
248
|
-
// 注册 mcp_tool_schema 工具 - 直接返回可用的调用示例
|
|
249
56
|
framework.registerTool({
|
|
250
57
|
name: 'mcp_tool_schema',
|
|
251
58
|
description: '查询工具的调用示例,直接复制 example 或 fullExample 字段使用',
|
|
@@ -256,42 +63,31 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
256
63
|
execute: async (args) => {
|
|
257
64
|
const { server, tool } = args;
|
|
258
65
|
const clientInfo = this._clients.get(server);
|
|
259
|
-
if (!clientInfo) {
|
|
260
|
-
return { success: false, error: `MCP server '${server}' not found` };
|
|
261
|
-
}
|
|
66
|
+
if (!clientInfo) return { success: false, error: `MCP server '${server}' not found` };
|
|
262
67
|
|
|
263
68
|
const toolInfo = clientInfo.tools.find((t) => t.name === tool);
|
|
264
|
-
if (!toolInfo) {
|
|
265
|
-
return { success: false, error: `Tool '${tool}' not found` };
|
|
266
|
-
}
|
|
69
|
+
if (!toolInfo) return { success: false, error: `Tool '${tool}' not found` };
|
|
267
70
|
|
|
268
|
-
const schema =
|
|
71
|
+
const schema = extractSchema(toolInfo.inputSchema);
|
|
269
72
|
const props = schema.properties || {};
|
|
270
73
|
const required = schema.required || [];
|
|
271
74
|
|
|
272
|
-
// 生成示例值的辅助函数
|
|
273
75
|
const generateExample = (prop, depth = 0) => {
|
|
274
76
|
if (!prop) return null;
|
|
275
77
|
const type = prop.type || (prop.properties ? 'object' : 'string');
|
|
276
|
-
|
|
277
|
-
if (type === '
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
return prop.default || 0;
|
|
281
|
-
} else if (type === 'boolean') {
|
|
282
|
-
return false;
|
|
283
|
-
} else if (type === 'array') {
|
|
284
|
-
// 数组类型:提供单个示例元素
|
|
78
|
+
if (type === 'string') return prop.description?.split('.')[0] || `${prop.title || '值'}`;
|
|
79
|
+
if (type === 'number' || type === 'integer') return prop.default || 0;
|
|
80
|
+
if (type === 'boolean') return false;
|
|
81
|
+
if (type === 'array') {
|
|
285
82
|
if (prop.items) {
|
|
286
83
|
const itemExample = generateExample(prop.items, depth + 1);
|
|
287
84
|
return depth === 0 ? [itemExample] : itemExample;
|
|
288
85
|
}
|
|
289
86
|
return ['示例'];
|
|
290
|
-
}
|
|
291
|
-
|
|
87
|
+
}
|
|
88
|
+
if (type === 'object' || prop.properties) {
|
|
292
89
|
const obj = {};
|
|
293
|
-
const
|
|
294
|
-
for (const [key, val] of Object.entries(objProps)) {
|
|
90
|
+
for (const [key, val] of Object.entries(prop.properties || {})) {
|
|
295
91
|
obj[key] = generateExample(val, depth + 1);
|
|
296
92
|
}
|
|
297
93
|
return obj;
|
|
@@ -299,33 +95,23 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
299
95
|
return null;
|
|
300
96
|
};
|
|
301
97
|
|
|
302
|
-
// 生成可直接使用的调用示例
|
|
303
98
|
const exampleArgs = {};
|
|
304
99
|
for (const key of required) {
|
|
305
100
|
const prop = props[key];
|
|
306
|
-
if (prop)
|
|
307
|
-
exampleArgs[key] = generateExample(prop);
|
|
308
|
-
}
|
|
101
|
+
if (prop) exampleArgs[key] = generateExample(prop);
|
|
309
102
|
}
|
|
310
103
|
|
|
311
104
|
return {
|
|
312
105
|
success: true,
|
|
313
106
|
data: {
|
|
314
|
-
name: toolInfo.name,
|
|
315
|
-
description: toolInfo.description,
|
|
316
|
-
required,
|
|
317
|
-
// 各参数的详细说明
|
|
107
|
+
name: toolInfo.name, description: toolInfo.description, required,
|
|
318
108
|
parameters: Object.fromEntries(
|
|
319
|
-
required.map((key) => [
|
|
320
|
-
key,
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
example: generateExample(props[key]),
|
|
325
|
-
},
|
|
326
|
-
])
|
|
109
|
+
required.map((key) => [key, {
|
|
110
|
+
type: props[key]?.type || 'string',
|
|
111
|
+
description: props[key]?.description || '',
|
|
112
|
+
example: generateExample(props[key]),
|
|
113
|
+
}])
|
|
327
114
|
),
|
|
328
|
-
// 直接返回可以复制使用的调用示例
|
|
329
115
|
example: JSON.stringify(exampleArgs, null, 2),
|
|
330
116
|
fullExample: `mcp_call({ server: "${server}", tool: "${tool}", args_json: ${JSON.stringify(JSON.stringify(exampleArgs))} })`,
|
|
331
117
|
},
|
|
@@ -333,162 +119,81 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
333
119
|
},
|
|
334
120
|
});
|
|
335
121
|
|
|
336
|
-
// 注册 mcp_call 工具
|
|
337
122
|
framework.registerTool({
|
|
338
123
|
name: 'mcp_call',
|
|
339
|
-
description:
|
|
340
|
-
'调用 MCP 服务器工具。args_json 是包含实际参数的 JSON 字符串,如 \'{"url": "https://news.baidu.com"}\'',
|
|
124
|
+
description: '调用 MCP 服务器工具。args_json 是包含实际参数的 JSON 字符串',
|
|
341
125
|
inputSchema: z.object({
|
|
342
126
|
server: z.string().describe('MCP 服务器名称'),
|
|
343
127
|
tool: z.string().describe('工具名称'),
|
|
344
|
-
args_json: z
|
|
345
|
-
.string()
|
|
346
|
-
.describe('参数 JSON 字符串,如 {"url": "https://..."},fetch 工具必填 url'),
|
|
128
|
+
args_json: z.string().describe('参数 JSON 字符串'),
|
|
347
129
|
}),
|
|
348
130
|
execute: async (args) => {
|
|
349
131
|
const { server, tool, args_json } = args;
|
|
350
|
-
// log.info(` mcp_call: server=${server}, tool=${tool}, args_json=${args_json}`);
|
|
351
|
-
|
|
352
132
|
const clientInfo = this._clients.get(server);
|
|
353
|
-
if (!clientInfo) {
|
|
354
|
-
return { success: false, error: `MCP server '${server}' not found` };
|
|
355
|
-
}
|
|
133
|
+
if (!clientInfo) return { success: false, error: `MCP server '${server}' not found` };
|
|
356
134
|
|
|
357
|
-
// 解析 args_json
|
|
358
135
|
let finalArgs = {};
|
|
359
136
|
if (args_json) {
|
|
360
|
-
try {
|
|
361
|
-
|
|
362
|
-
} catch (e) {
|
|
363
|
-
return { success: false, error: `args_json 格式错误,不是有效的 JSON: ${args_json}` };
|
|
364
|
-
}
|
|
137
|
+
try { finalArgs = JSON.parse(args_json); }
|
|
138
|
+
catch (e) { return { success: false, error: `args_json 格式错误: ${args_json}` }; }
|
|
365
139
|
}
|
|
366
140
|
|
|
367
|
-
// 检查参数是否为空
|
|
368
141
|
if (!finalArgs || Object.keys(finalArgs).length === 0) {
|
|
369
142
|
const toolInfo = clientInfo.tools.find((t) => t.name === tool);
|
|
370
143
|
if (toolInfo) {
|
|
371
|
-
const schema =
|
|
144
|
+
const schema = extractSchema(toolInfo.inputSchema);
|
|
372
145
|
const required = schema.required || [];
|
|
373
|
-
return {
|
|
374
|
-
success: false,
|
|
375
|
-
error: `参数为空!必须提供: ${required.join(', ')}`,
|
|
376
|
-
hint: `正确格式: {"${required[0]}": "具体值"}`,
|
|
377
|
-
};
|
|
146
|
+
return { success: false, error: `参数为空!必须提供: ${required.join(', ')}`, hint: `正确格式: {"${required[0]}": "具体值"}` };
|
|
378
147
|
}
|
|
379
148
|
}
|
|
380
149
|
|
|
381
150
|
try {
|
|
382
|
-
// 使用缓存的工具对象
|
|
383
151
|
const mcpTool = clientInfo.toolObjects?.[tool];
|
|
384
|
-
if (!mcpTool) {
|
|
385
|
-
return { success: false, error: `Tool '${tool}' not found on server '${server}'` };
|
|
386
|
-
}
|
|
152
|
+
if (!mcpTool) return { success: false, error: `Tool '${tool}' not found on server '${server}'` };
|
|
387
153
|
|
|
388
|
-
// 触发 MCP 工具开始事件
|
|
389
154
|
const mcpToolName = `${server}:${tool}`;
|
|
390
|
-
framework.emit('tool:call', {
|
|
391
|
-
|
|
392
|
-
args: finalArgs,
|
|
393
|
-
source: 'mcp',
|
|
394
|
-
});
|
|
395
|
-
framework.emit('tool-call', {
|
|
396
|
-
name: mcpToolName,
|
|
397
|
-
args: finalArgs,
|
|
398
|
-
source: 'mcp',
|
|
399
|
-
});
|
|
155
|
+
framework.emit('tool:call', { name: mcpToolName, args: finalArgs, source: 'mcp' });
|
|
156
|
+
framework.emit('tool-call', { name: mcpToolName, args: finalArgs, source: 'mcp' });
|
|
400
157
|
|
|
401
158
|
const execResult = await mcpTool.execute(finalArgs);
|
|
402
159
|
|
|
403
|
-
|
|
404
|
-
framework.emit('tool:result', {
|
|
405
|
-
name: mcpToolName,
|
|
406
|
-
args: finalArgs,
|
|
407
|
-
result: execResult,
|
|
408
|
-
source: 'mcp',
|
|
409
|
-
});
|
|
410
|
-
|
|
160
|
+
framework.emit('tool:result', { name: mcpToolName, args: finalArgs, result: execResult, source: 'mcp' });
|
|
411
161
|
return { success: true, data: execResult };
|
|
412
162
|
} catch (err) {
|
|
413
163
|
log.error(` Tool '${tool}' failed:`, err.message);
|
|
414
|
-
|
|
415
|
-
// 触发 MCP 工具错误事件
|
|
416
|
-
const mcpToolName = `${server}:${tool}`;
|
|
417
|
-
framework.emit('tool:error', {
|
|
418
|
-
name: mcpToolName,
|
|
419
|
-
args: finalArgs,
|
|
420
|
-
error: err.message,
|
|
421
|
-
source: 'mcp',
|
|
422
|
-
});
|
|
423
|
-
|
|
164
|
+
framework.emit('tool:error', { name: `${server}:${tool}`, args: finalArgs, error: err.message, source: 'mcp' });
|
|
424
165
|
return { success: false, error: err.message };
|
|
425
166
|
}
|
|
426
167
|
},
|
|
427
168
|
});
|
|
428
169
|
|
|
429
|
-
// 注册 mcp_list_servers 工具
|
|
430
170
|
framework.registerTool({
|
|
431
171
|
name: 'mcp_list_servers',
|
|
432
172
|
description: '列出已连接的 MCP 服务器及其工具',
|
|
433
173
|
inputSchema: z.object({}),
|
|
434
174
|
execute: async () => {
|
|
435
175
|
const servers = [];
|
|
436
|
-
// 已连接的服务器
|
|
437
176
|
for (const [name, info] of this._clients) {
|
|
438
|
-
servers.push({
|
|
439
|
-
name,
|
|
440
|
-
enabled: true,
|
|
441
|
-
connected: true,
|
|
442
|
-
tools: info.tools.map((t) => ({ name: t.name, description: t.description })),
|
|
443
|
-
});
|
|
177
|
+
servers.push({ name, enabled: true, connected: true, tools: info.tools.map((t) => ({ name: t.name, description: t.description })) });
|
|
444
178
|
}
|
|
445
|
-
// 未连接的服务器(配置中存在但未启用)
|
|
446
179
|
for (const [name, config] of this._serverConfigs) {
|
|
447
|
-
if (!this._clients.has(name)) {
|
|
448
|
-
servers.push({
|
|
449
|
-
name,
|
|
450
|
-
enabled: false,
|
|
451
|
-
connected: false,
|
|
452
|
-
tools: [],
|
|
453
|
-
});
|
|
454
|
-
}
|
|
180
|
+
if (!this._clients.has(name)) servers.push({ name, enabled: false, connected: false, tools: [] });
|
|
455
181
|
}
|
|
456
182
|
return { success: true, data: servers };
|
|
457
183
|
},
|
|
458
184
|
});
|
|
459
185
|
|
|
460
|
-
// 注册 mcp_reload 工具
|
|
461
186
|
framework.registerTool({
|
|
462
187
|
name: 'mcp_reload',
|
|
463
|
-
description:
|
|
464
|
-
'重新加载 MCP 服务器配置。当配置文件 .foliko/mcp_config.json 修改后使用此工具重载配置',
|
|
188
|
+
description: '重新加载 MCP 服务器配置。当配置文件 .foliko/mcp_config.json 修改后使用此工具重载配置',
|
|
465
189
|
inputSchema: z.object({}),
|
|
466
190
|
execute: async () => {
|
|
467
|
-
// log.info(' mcp_reload called');
|
|
468
191
|
try {
|
|
469
|
-
const fs = require('fs');
|
|
470
|
-
const path = require('path');
|
|
471
192
|
const configPath = path.resolve('.foliko/mcp_config.json');
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
if (!fs.existsSync(configPath)) {
|
|
475
|
-
return { success: false, error: `配置文件不存在: ${configPath}` };
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
479
|
-
const config = JSON.parse(configContent);
|
|
480
|
-
// log.info(' Config loaded, reloading...');
|
|
481
|
-
|
|
193
|
+
if (!fs.existsSync(configPath)) return { success: false, error: `配置文件不存在: ${configPath}` };
|
|
194
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
482
195
|
await this.reloadConfig(config);
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
return {
|
|
486
|
-
success: true,
|
|
487
|
-
data: 'MCP 配置已重载',
|
|
488
|
-
metadata: {
|
|
489
|
-
servers: Object.keys(config.mcpServers || {}),
|
|
490
|
-
},
|
|
491
|
-
};
|
|
196
|
+
return { success: true, data: 'MCP 配置已重载', metadata: { servers: Object.keys(config.mcpServers || {}) } };
|
|
492
197
|
} catch (err) {
|
|
493
198
|
log.error(' Reload error:', err);
|
|
494
199
|
return { success: false, error: `重载失败: ${err.message}` };
|
|
@@ -496,7 +201,6 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
496
201
|
},
|
|
497
202
|
});
|
|
498
203
|
|
|
499
|
-
// 注册 mcp_set_enabled 工具 - 动态开启/关闭 MCP 服务器
|
|
500
204
|
framework.registerTool({
|
|
501
205
|
name: 'mcp_set_enabled',
|
|
502
206
|
description: '动态开启或关闭某个 MCP 服务器',
|
|
@@ -510,70 +214,42 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
510
214
|
const serverConfig = this._serverConfigs.get(server);
|
|
511
215
|
|
|
512
216
|
if (enabled) {
|
|
513
|
-
|
|
514
|
-
if (clientInfo && clientInfo.enabled) {
|
|
515
|
-
return { success: true, data: `MCP 服务器 '${server}' 已经是开启状态` };
|
|
516
|
-
}
|
|
517
|
-
// 如果服务器从未连接过,也检查配置中的 enabled 状态
|
|
518
|
-
if (!clientInfo && serverConfig && serverConfig.enabled === false) {
|
|
519
|
-
// 服务器是被禁用的,可以尝试启用
|
|
520
|
-
}
|
|
521
|
-
// log.info(`[MCP] Enabling server: ${server}, clientInfo=${!!clientInfo}, serverConfig.enabled=${serverConfig?.enabled}`);
|
|
217
|
+
if (clientInfo && clientInfo.enabled) return { success: true, data: `MCP 服务器 '${server}' 已经是开启状态` };
|
|
522
218
|
try {
|
|
523
219
|
if (serverConfig) {
|
|
524
220
|
const result = await this.addServer({ ...serverConfig, enabled: true });
|
|
525
|
-
// log.info(`[MCP] addServer result:`, JSON.stringify(result));
|
|
526
221
|
if (!result || result.success === false) {
|
|
527
222
|
this._refreshAllAgentsMCPPrompt(this._framework);
|
|
528
223
|
return result;
|
|
529
224
|
}
|
|
530
|
-
// 保存 enabled 状态到配置文件
|
|
531
225
|
await this._saveMCPServerEnabled(server, true);
|
|
532
226
|
this._refreshAllAgentsMCPPrompt(this._framework);
|
|
533
227
|
return { success: true, data: `MCP 服务器 '${server}' 已开启` };
|
|
534
|
-
} else {
|
|
535
|
-
return { success: false, error: '服务器配置不存在,需要重载配置' };
|
|
536
228
|
}
|
|
229
|
+
return { success: false, error: '服务器配置不存在,需要重载配置' };
|
|
537
230
|
} catch (err) {
|
|
538
231
|
log.error(`[MCP] Enabling failed:`, err.message);
|
|
539
232
|
return { success: false, error: `开启失败: ${err.message}` };
|
|
540
233
|
}
|
|
541
234
|
} else {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
return { success: false, error: `MCP server '${server}' not found` };
|
|
545
|
-
}
|
|
546
|
-
// log.info(` Disabling MCP server: ${server}`);
|
|
547
|
-
try {
|
|
548
|
-
clientInfo.client.close?.();
|
|
549
|
-
} catch (e) {}
|
|
550
|
-
// 从 clients 中移除
|
|
235
|
+
if (!clientInfo) return { success: false, error: `MCP server '${server}' not found` };
|
|
236
|
+
try { clientInfo.client.close?.(); } catch (e) { /* ignore */ }
|
|
551
237
|
this._clients.delete(server);
|
|
552
|
-
// 移除该服务器注册的工具
|
|
553
238
|
for (const key of Object.keys(this.tools)) {
|
|
554
|
-
if (key.startsWith(`${server}_`))
|
|
555
|
-
delete this.tools[key];
|
|
556
|
-
}
|
|
239
|
+
if (key.startsWith(`${server}_`)) delete this.tools[key];
|
|
557
240
|
}
|
|
558
|
-
// 保存 enabled 状态到配置文件
|
|
559
241
|
await this._saveMCPServerEnabled(server, false);
|
|
560
242
|
this._refreshAllAgentsMCPPrompt(this._framework);
|
|
561
243
|
return { success: true, data: `MCP 服务器 '${server}' 已关闭` };
|
|
562
244
|
}
|
|
563
|
-
|
|
564
|
-
return { success: true, data: `MCP 服务器 '${server}' 状态未变化` };
|
|
565
245
|
},
|
|
566
246
|
});
|
|
567
247
|
|
|
568
|
-
// 监听 agent 创建事件,附加 MCP 信息到系统提示词
|
|
569
248
|
framework.on('agent:created', (agent) => {
|
|
570
|
-
if (!this._mainAgent)
|
|
571
|
-
this._mainAgent = agent;
|
|
572
|
-
}
|
|
249
|
+
if (!this._mainAgent) this._mainAgent = agent;
|
|
573
250
|
this._refreshAgentMCPPrompt(agent);
|
|
574
251
|
});
|
|
575
252
|
|
|
576
|
-
// 等待框架就绪后,刷新所有已有 agent 的 MCP 提示词
|
|
577
253
|
if (framework._ready) {
|
|
578
254
|
this._refreshAllAgentsMCPPrompt(framework);
|
|
579
255
|
} else {
|
|
@@ -585,515 +261,64 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
585
261
|
return this;
|
|
586
262
|
}
|
|
587
263
|
|
|
588
|
-
/**
|
|
589
|
-
* 刷新所有 agent 的 MCP 提示词(包括子 agent)
|
|
590
|
-
*/
|
|
591
264
|
_refreshAllAgentsMCPPrompt(framework) {
|
|
592
265
|
const visited = new Set();
|
|
593
|
-
|
|
594
266
|
const traverse = (agent) => {
|
|
595
267
|
if (!agent || visited.has(agent)) return;
|
|
596
268
|
visited.add(agent);
|
|
597
|
-
//this._refreshAgentMCPPrompt(agent);
|
|
598
|
-
|
|
599
|
-
// 递归处理子 agent
|
|
600
269
|
const subAgents = agent.getSubAgents?.() || agent._subAgents || new Map();
|
|
601
|
-
for (const [
|
|
602
|
-
traverse(subAgentInfo.agent);
|
|
603
|
-
}
|
|
270
|
+
for (const [, subAgentInfo] of subAgents) traverse(subAgentInfo.agent);
|
|
604
271
|
};
|
|
605
|
-
|
|
606
|
-
for (const agent of framework._agents || []) {
|
|
607
|
-
traverse(agent);
|
|
608
|
-
}
|
|
272
|
+
for (const agent of framework._agents || []) traverse(agent);
|
|
609
273
|
}
|
|
610
274
|
|
|
611
|
-
/**
|
|
612
|
-
* 刷新单个 agent 的 MCP 提示词
|
|
613
|
-
*/
|
|
614
275
|
_refreshAgentMCPPrompt(agent) {
|
|
615
|
-
// 检查是否已刷新过(通过检查系统提示词是否已包含 MCP 描述)
|
|
616
276
|
const existingPrompt = (typeof agent.getOriginalPrompt === 'function' ? agent.getOriginalPrompt() : agent._originalPrompt) || '';
|
|
617
|
-
if (existingPrompt.includes('【MCP Servers】'))
|
|
618
|
-
return;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// const mcpDesc = this._buildMCPServersDescription();
|
|
622
|
-
// if (!mcpDesc) return;
|
|
623
|
-
|
|
624
|
-
// 将 MCP 描述追加到系统提示词
|
|
625
|
-
//agent.setSystemPrompt(existingPrompt + '\n\n' + mcpDesc);
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
/**
|
|
629
|
-
* 从 inputSchema 中提取实际 schema
|
|
630
|
-
*/
|
|
631
|
-
_extractSchema(inputSchema) {
|
|
632
|
-
if (!inputSchema) return {};
|
|
633
|
-
// 处理 {jsonSchema: {...}} 格式
|
|
634
|
-
if (inputSchema.jsonSchema) return inputSchema.jsonSchema;
|
|
635
|
-
// 处理 {inputSchema: {...}} 格式 (某些 MCP 服务器返回这种格式)
|
|
636
|
-
if (inputSchema.inputSchema) return inputSchema.inputSchema;
|
|
637
|
-
// 处理直接是 schema 的格式
|
|
638
|
-
return inputSchema;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
/**
|
|
642
|
-
* 构建 MCP 服务器描述
|
|
643
|
-
*/
|
|
644
|
-
_buildMCPServersDescription() {
|
|
645
|
-
if (this._clients.size === 0) {
|
|
646
|
-
return '';
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
let desc = '## 【MCP Servers】 工具合集\n\n';
|
|
650
|
-
desc += 'MCP 服务器工具已注册为 `服务器_工具名` 格式(如 github_search)。\n';
|
|
651
|
-
desc += '通过 `ext_call` 调用:\n\n';
|
|
652
|
-
|
|
653
|
-
for (const [serverName, info] of this._clients) {
|
|
654
|
-
desc += `### ${serverName}\n\n`;
|
|
655
|
-
for (const tool of info.tools) {
|
|
656
|
-
const fullName = `${serverName}_${tool.name}`;
|
|
657
|
-
desc += `- **${fullName}**: ${tool.description || '无描述'}\n`;
|
|
658
|
-
|
|
659
|
-
// 使用 zodSchemaToMarkdown 生成参数文档
|
|
660
|
-
const rawSchema = tool.inputSchema;
|
|
661
|
-
const schema = this._extractSchema(rawSchema);
|
|
662
|
-
if (schema) {
|
|
663
|
-
try {
|
|
664
|
-
let schemaResult = null;
|
|
665
|
-
let zodSchemaForFallback = null; // 保存 Zod schema 供 fallback 使用
|
|
666
|
-
// 检查是否是 Zod schema (有 shape 方法或 _def 属性)
|
|
667
|
-
const isZodSchema =
|
|
668
|
-
typeof schema.shape === 'function' || (schema._def && schema._def.typeName);
|
|
669
|
-
if (isZodSchema) {
|
|
670
|
-
// Zod schema 直接转换
|
|
671
|
-
schemaResult = zodSchemaToMarkdown(schema);
|
|
672
|
-
zodSchemaForFallback = schema;
|
|
673
|
-
} else if (schema.jsonSchema) {
|
|
674
|
-
// JSON Schema 格式,尝试转换为 Zod schema
|
|
675
|
-
const zodSchema = this._jsonSchemaToZod(schema.jsonSchema || schema);
|
|
676
|
-
if (zodSchema) {
|
|
677
|
-
schemaResult = zodSchemaToMarkdown(zodSchema);
|
|
678
|
-
zodSchemaForFallback = zodSchema;
|
|
679
|
-
}
|
|
680
|
-
} else if (schema.properties) {
|
|
681
|
-
// 已经是对象格式
|
|
682
|
-
const zodSchema = this._jsonSchemaToZod(schema);
|
|
683
|
-
if (zodSchema) {
|
|
684
|
-
schemaResult = zodSchemaToMarkdown(zodSchema);
|
|
685
|
-
zodSchemaForFallback = zodSchema;
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// 检查是否包含未解析的原始 Zod 类型名或只有类型信息没有参数
|
|
690
|
-
if (schemaResult) {
|
|
691
|
-
const hasUnparsedTypes = /\|.*\bZod\w+\b.*\|/.test(schemaResult);
|
|
692
|
-
// 检查是否只有类型信息而没有实际参数(如 "Type: Object" 或 "Type: ZodAny")
|
|
693
|
-
const hasOnlyType =
|
|
694
|
-
/^-\s+\w+:\s+.+\n?$/.test(schemaResult.trim()) && !schemaResult.includes('`');
|
|
695
|
-
if (hasUnparsedTypes || hasOnlyType) {
|
|
696
|
-
// zodSchemaToMarkdown 未正确解析,使用 fallback
|
|
697
|
-
desc += this._fallbackParamsDesc(zodSchemaForFallback || schema);
|
|
698
|
-
} else {
|
|
699
|
-
desc += `**参数:**\n\n`;
|
|
700
|
-
desc += schemaResult + '\n\n';
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
} catch (e) {
|
|
704
|
-
// 如果转换失败,使用 fallback
|
|
705
|
-
try {
|
|
706
|
-
desc += this._fallbackParamsDesc(zodSchemaForFallback || schema);
|
|
707
|
-
} catch (e2) {
|
|
708
|
-
// fallback 也失败,忽略参数描述
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
desc += '\n';
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
desc += '**调用格式:**\n';
|
|
717
|
-
desc += '```\next_call({ plugin: "mcp", tool: "服务器_工具名", args: {参数} })\n';
|
|
718
|
-
desc += '```\n';
|
|
719
|
-
return desc.trim();
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
_bindMcpParamsDesc(rawSchema) {
|
|
723
|
-
let desc = '';
|
|
724
|
-
const schema = this._extractSchema(rawSchema);
|
|
725
|
-
if (!schema) return desc;
|
|
726
|
-
try {
|
|
727
|
-
let schemaResult = null;
|
|
728
|
-
let zodSchemaForFallback = null; // 保存 Zod schema 供 fallback 使用
|
|
729
|
-
// 检查是否是 Zod schema (有 shape 方法或 _def 属性)
|
|
730
|
-
const isZodSchema =
|
|
731
|
-
typeof schema.shape === 'function' || (schema._def && schema._def.typeName);
|
|
732
|
-
if (isZodSchema) {
|
|
733
|
-
// Zod schema 直接转换
|
|
734
|
-
schemaResult = zodSchemaToMarkdown(schema);
|
|
735
|
-
zodSchemaForFallback = schema;
|
|
736
|
-
} else if (schema.jsonSchema) {
|
|
737
|
-
// JSON Schema 格式,尝试转换为 Zod schema
|
|
738
|
-
const zodSchema = this._jsonSchemaToZod(schema.jsonSchema || schema);
|
|
739
|
-
if (zodSchema) {
|
|
740
|
-
schemaResult = zodSchemaToMarkdown(zodSchema);
|
|
741
|
-
zodSchemaForFallback = zodSchema;
|
|
742
|
-
}
|
|
743
|
-
} else if (schema.properties) {
|
|
744
|
-
// 已经是对象格式
|
|
745
|
-
const zodSchema = this._jsonSchemaToZod(schema);
|
|
746
|
-
if (zodSchema) {
|
|
747
|
-
schemaResult = zodSchemaToMarkdown(zodSchema);
|
|
748
|
-
zodSchemaForFallback = zodSchema;
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
// 检查是否包含未解析的原始 Zod 类型名或只有类型信息没有参数
|
|
753
|
-
if (schemaResult) {
|
|
754
|
-
const hasUnparsedTypes = /\|.*\bZod\w+\b.*\|/.test(schemaResult);
|
|
755
|
-
// 检查是否只有类型信息而没有实际参数(如 "Type: Object" 或 "Type: ZodAny")
|
|
756
|
-
const hasOnlyType =
|
|
757
|
-
/^-\s+\w+:\s+.+\n?$/.test(schemaResult.trim()) && !schemaResult.includes('`');
|
|
758
|
-
if (hasUnparsedTypes || hasOnlyType) {
|
|
759
|
-
// zodSchemaToMarkdown 未正确解析,使用 fallback
|
|
760
|
-
desc += this._fallbackParamsDesc(zodSchemaForFallback || schema);
|
|
761
|
-
} else {
|
|
762
|
-
desc += `**参数:**\n\n`;
|
|
763
|
-
desc += schemaResult + '\n\n';
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
} catch (e) {
|
|
767
|
-
// 如果转换失败,使用 fallback
|
|
768
|
-
try {
|
|
769
|
-
desc += this._fallbackParamsDesc(zodSchemaForFallback || schema);
|
|
770
|
-
} catch (e2) {
|
|
771
|
-
// fallback 也失败,忽略参数描述
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
return desc;
|
|
277
|
+
if (existingPrompt.includes('【MCP Servers】')) return;
|
|
775
278
|
}
|
|
776
279
|
|
|
777
|
-
/**
|
|
778
|
-
* 将 JSON Schema 转换为 Zod schema
|
|
779
|
-
*/
|
|
780
280
|
_jsonSchemaToZod(jsonSchema) {
|
|
781
|
-
|
|
782
|
-
return null;
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
try {
|
|
786
|
-
const shape = {};
|
|
787
|
-
const properties = jsonSchema.properties;
|
|
788
|
-
const required = jsonSchema.required || [];
|
|
789
|
-
|
|
790
|
-
for (const [key, prop] of Object.entries(properties)) {
|
|
791
|
-
shape[key] = this._jsonSchemaPropToZod(prop, required.includes(key));
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
return z.object(shape);
|
|
795
|
-
} catch (e) {
|
|
796
|
-
return null;
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
/**
|
|
801
|
-
* 将 JSON Schema 属性转换为 Zod 类型
|
|
802
|
-
*/
|
|
803
|
-
_jsonSchemaPropToZod(prop, isRequired) {
|
|
804
|
-
if (prop.$ref) {
|
|
805
|
-
return isRequired ? z.any() : z.any().optional();
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
if (prop.enum) {
|
|
809
|
-
let zodType = z.string().enum(prop.enum);
|
|
810
|
-
if (prop.nullable) {
|
|
811
|
-
zodType = zodType.nullable();
|
|
812
|
-
}
|
|
813
|
-
return isRequired ? zodType : zodType.optional();
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
const type = prop.type || 'string';
|
|
817
|
-
|
|
818
|
-
switch (type) {
|
|
819
|
-
case 'string':
|
|
820
|
-
return isRequired ? z.string() : z.string().optional();
|
|
821
|
-
case 'number':
|
|
822
|
-
case 'integer':
|
|
823
|
-
return isRequired ? z.number() : z.number().optional();
|
|
824
|
-
case 'boolean':
|
|
825
|
-
return isRequired ? z.boolean() : z.boolean().optional();
|
|
826
|
-
case 'array':
|
|
827
|
-
return isRequired ? z.array(z.any()) : z.array(z.any()).optional();
|
|
828
|
-
case 'object':
|
|
829
|
-
if (prop.properties) {
|
|
830
|
-
const nested = {};
|
|
831
|
-
for (const [k, v] of Object.entries(prop.properties)) {
|
|
832
|
-
nested[k] = this._jsonSchemaPropToZod(v, prop.required?.includes(k) || false);
|
|
833
|
-
}
|
|
834
|
-
return isRequired ? z.object(nested) : z.object(nested).optional();
|
|
835
|
-
}
|
|
836
|
-
return isRequired ? z.record(z.any()) : z.record(z.any()).optional();
|
|
837
|
-
default:
|
|
838
|
-
return isRequired ? z.any() : z.any().optional();
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
/**
|
|
843
|
-
* 回退的参数描述(当 zodSchemaToMarkdown 失败时)
|
|
844
|
-
*/
|
|
845
|
-
_fallbackParamsDesc(schema) {
|
|
846
|
-
// 优先尝试 JSON Schema 格式
|
|
847
|
-
const props = schema.properties || {};
|
|
848
|
-
const required = schema.required || [];
|
|
849
|
-
|
|
850
|
-
if (Object.keys(props).length > 0) {
|
|
851
|
-
let desc = '**参数:**\n\n';
|
|
852
|
-
for (const [key, prop] of Object.entries(props)) {
|
|
853
|
-
const isRequired = required.includes(key);
|
|
854
|
-
const type = prop.type || 'any';
|
|
855
|
-
const desc_text = prop.description || '';
|
|
856
|
-
desc += `- \`${key}\`${isRequired ? ' (必填)' : ''}: ${type} ${desc_text}\n`;
|
|
857
|
-
}
|
|
858
|
-
return desc + '\n';
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
// 尝试 Zod schema 格式
|
|
862
|
-
if (typeof schema.shape === 'function' || (schema._def && schema._def.shape)) {
|
|
863
|
-
const shape = typeof schema.shape === 'function' ? schema.shape() : schema._def.shape();
|
|
864
|
-
if (shape && typeof shape === 'object') {
|
|
865
|
-
let desc = '**参数:**\n\n';
|
|
866
|
-
for (const [key, field] of Object.entries(shape)) {
|
|
867
|
-
const typeInfo = this._extractZodFieldType(field);
|
|
868
|
-
const isOptional = typeInfo.isOptional ? ' (可选)' : ' (必填)';
|
|
869
|
-
desc += `- \`${key}\`${isOptional}: ${typeInfo.type}\n`;
|
|
870
|
-
}
|
|
871
|
-
return desc + '\n';
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
return '';
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
/**
|
|
879
|
-
* 从 Zod schema 字段提取类型信息
|
|
880
|
-
*/
|
|
881
|
-
_extractZodFieldType(field) {
|
|
882
|
-
if (!field) {
|
|
883
|
-
return { type: 'unknown', isOptional: false, description: '' };
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
const typeName = field._def?.typeName || field.constructor?.name || '';
|
|
887
|
-
const description = field.description || field._def?.description || '';
|
|
888
|
-
|
|
889
|
-
switch (typeName) {
|
|
890
|
-
case 'ZodOptional': {
|
|
891
|
-
const inner = field._def?.innerType;
|
|
892
|
-
if (!inner) return { type: 'optional', isOptional: true, description };
|
|
893
|
-
return this._extractZodFieldType(inner);
|
|
894
|
-
}
|
|
895
|
-
case 'ZodArray': {
|
|
896
|
-
const inner = field._def?.type;
|
|
897
|
-
if (!inner) return { type: 'array', isOptional: false, description };
|
|
898
|
-
const innerInfo = this._extractZodFieldType(inner);
|
|
899
|
-
return { type: `${innerInfo.type}[]`, isOptional: false, description };
|
|
900
|
-
}
|
|
901
|
-
case 'ZodObject': {
|
|
902
|
-
const shapeFn = field._def?.shape;
|
|
903
|
-
if (typeof shapeFn !== 'function')
|
|
904
|
-
return { type: 'object', isOptional: false, description };
|
|
905
|
-
const shape = shapeFn();
|
|
906
|
-
if (!shape) return { type: 'object', isOptional: false, description };
|
|
907
|
-
const props = Object.keys(shape).join(', ');
|
|
908
|
-
return { type: `object{${props}}`, isOptional: false, description };
|
|
909
|
-
}
|
|
910
|
-
case 'ZodString':
|
|
911
|
-
return { type: 'string', isOptional: false, description };
|
|
912
|
-
case 'ZodNumber':
|
|
913
|
-
return { type: 'number', isOptional: false, description };
|
|
914
|
-
case 'ZodBoolean':
|
|
915
|
-
return { type: 'boolean', isOptional: false, description };
|
|
916
|
-
case 'ZodEnum': {
|
|
917
|
-
const values = field._def?.values;
|
|
918
|
-
if (!values) return { type: 'enum', isOptional: false, description };
|
|
919
|
-
return {
|
|
920
|
-
type: values.map((v) => `"${v}"`).join(' | '),
|
|
921
|
-
isOptional: false,
|
|
922
|
-
description,
|
|
923
|
-
};
|
|
924
|
-
}
|
|
925
|
-
case 'ZodUnion': {
|
|
926
|
-
const options = field._def?.options;
|
|
927
|
-
if (!options) return { type: 'union', isOptional: false, description };
|
|
928
|
-
const types = options.map((opt) => this._extractZodFieldType(opt).type);
|
|
929
|
-
return { type: `union(${types.join(' | ')})`, isOptional: false, description };
|
|
930
|
-
}
|
|
931
|
-
case 'ZodLiteral':
|
|
932
|
-
return { type: JSON.stringify(field._def?.value), isOptional: false, description };
|
|
933
|
-
case 'ZodRecord': {
|
|
934
|
-
const keyType = this._extractZodFieldType(field._def?.keyType);
|
|
935
|
-
const valueType = this._extractZodFieldType(field._def?.valueType);
|
|
936
|
-
return {
|
|
937
|
-
type: `record<${keyType?.type || 'any'}, ${valueType?.type || 'any'}>`,
|
|
938
|
-
isOptional: false,
|
|
939
|
-
description,
|
|
940
|
-
};
|
|
941
|
-
}
|
|
942
|
-
case 'ZodMap': {
|
|
943
|
-
const keyType = this._extractZodFieldType(field._def?.keyType);
|
|
944
|
-
const valueType = this._extractZodFieldType(field._def?.valueType);
|
|
945
|
-
return {
|
|
946
|
-
type: `map<${keyType?.type || 'any'}, ${valueType?.type || 'any'}>`,
|
|
947
|
-
isOptional: false,
|
|
948
|
-
description,
|
|
949
|
-
};
|
|
950
|
-
}
|
|
951
|
-
case 'ZodSet': {
|
|
952
|
-
const valueType = this._extractZodFieldType(field._def?.valueType);
|
|
953
|
-
return { type: `set<${valueType?.type || 'any'}>`, isOptional: false, description };
|
|
954
|
-
}
|
|
955
|
-
case 'ZodAny':
|
|
956
|
-
return { type: 'any', isOptional: false, description };
|
|
957
|
-
case 'ZodUnknown':
|
|
958
|
-
return { type: 'unknown', isOptional: false, description };
|
|
959
|
-
case 'ZodNever':
|
|
960
|
-
return { type: 'never', isOptional: false, description };
|
|
961
|
-
case 'ZodNull':
|
|
962
|
-
return { type: 'null', isOptional: false, description };
|
|
963
|
-
case 'ZodUndefined':
|
|
964
|
-
return { type: 'undefined', isOptional: false, description };
|
|
965
|
-
case 'ZodDate':
|
|
966
|
-
return { type: 'date', isOptional: false, description };
|
|
967
|
-
case 'ZodBigInt':
|
|
968
|
-
return { type: 'bigint', isOptional: false, description };
|
|
969
|
-
case 'ZodFunction':
|
|
970
|
-
return { type: 'function', isOptional: false, description };
|
|
971
|
-
case 'ZodLazy':
|
|
972
|
-
return { type: 'lazy', isOptional: false, description };
|
|
973
|
-
case 'ZodNativeEnum':
|
|
974
|
-
return { type: 'nativeEnum', isOptional: false, description };
|
|
975
|
-
case 'ZodDefault': {
|
|
976
|
-
const inner = field._def?.innerType;
|
|
977
|
-
if (!inner) return { type: 'default', isOptional: false, description };
|
|
978
|
-
const innerInfo = this._extractZodFieldType(inner);
|
|
979
|
-
return { type: innerInfo.type, isOptional: false, description };
|
|
980
|
-
}
|
|
981
|
-
case 'ZodEffects':
|
|
982
|
-
return { type: 'effects', isOptional: false, description };
|
|
983
|
-
case 'ZodPipeline':
|
|
984
|
-
return { type: 'pipeline', isOptional: false, description };
|
|
985
|
-
default:
|
|
986
|
-
return {
|
|
987
|
-
type: typeName.replace('Zod', '').toLowerCase() || 'any',
|
|
988
|
-
isOptional: false,
|
|
989
|
-
description,
|
|
990
|
-
};
|
|
991
|
-
}
|
|
281
|
+
return jsonSchemaToZod(jsonSchema);
|
|
992
282
|
}
|
|
993
283
|
|
|
994
284
|
_getParentAgent() {
|
|
995
|
-
|
|
996
|
-
if (this._mainAgent) {
|
|
997
|
-
return this._mainAgent;
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
// 尝试从 framework 获取
|
|
285
|
+
if (this._mainAgent) return this._mainAgent;
|
|
1001
286
|
if (this._framework._mainAgent) {
|
|
1002
287
|
this._mainAgent = this._framework._mainAgent;
|
|
1003
288
|
return this._mainAgent;
|
|
1004
289
|
}
|
|
1005
|
-
|
|
1006
|
-
// 查找最后一个创建的 agent 作为父 agent
|
|
1007
290
|
const agents = this._framework._agents || [];
|
|
1008
291
|
if (agents.length > 0) {
|
|
1009
292
|
this._mainAgent = agents[agents.length - 1];
|
|
1010
293
|
return this._mainAgent;
|
|
1011
294
|
}
|
|
1012
|
-
|
|
1013
295
|
return null;
|
|
1014
296
|
}
|
|
1015
297
|
|
|
1016
|
-
/**
|
|
1017
|
-
* 带超时的 Promise
|
|
1018
|
-
*/
|
|
1019
|
-
_withTimeout(promise, ms, name) {
|
|
1020
|
-
return Promise.race([
|
|
1021
|
-
promise,
|
|
1022
|
-
new Promise((_, reject) =>
|
|
1023
|
-
setTimeout(() => reject(new Error(`${name} timeout (${ms}ms)`)), ms)
|
|
1024
|
-
),
|
|
1025
|
-
]);
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
/**
|
|
1029
|
-
* 添加 MCP 服务器
|
|
1030
|
-
*/
|
|
1031
298
|
async addServer(config) {
|
|
1032
|
-
const {
|
|
1033
|
-
name,
|
|
1034
|
-
command,
|
|
1035
|
-
args = [],
|
|
1036
|
-
env,
|
|
1037
|
-
url,
|
|
1038
|
-
type,
|
|
1039
|
-
headers = {},
|
|
1040
|
-
authProvider,
|
|
1041
|
-
enabled,
|
|
1042
|
-
} = config;
|
|
299
|
+
const { name, command, args = [], env, url, type, headers = {}, authProvider, enabled } = config;
|
|
1043
300
|
log.debug(`[MCP] addServer called: ${name}, enabled=${enabled}, hasClient=${this._clients.has(name)}`);
|
|
1044
301
|
|
|
1045
|
-
if (this._clients.has(name)) {
|
|
1046
|
-
|
|
1047
|
-
return { success: false, error: `Server '${name}' already connected` };
|
|
1048
|
-
}
|
|
302
|
+
if (this._clients.has(name)) return { success: false, error: `Server '${name}' already connected` };
|
|
303
|
+
if (enabled === false) return { success: false, error: `Server '${name}' is disabled` };
|
|
1049
304
|
|
|
1050
|
-
|
|
1051
|
-
if (enabled === false) {
|
|
1052
|
-
// log.info(` Server '${name}' is disabled, skipping connection`);
|
|
1053
|
-
return { success: false, error: `Server '${name}' is disabled` };
|
|
1054
|
-
}
|
|
305
|
+
this._serverConfigs.set(name, { name, command, args, env, url, type, headers, authProvider, enabled });
|
|
1055
306
|
|
|
1056
|
-
// 保存配置,用于重新启用
|
|
1057
|
-
this._serverConfigs.set(name, {
|
|
1058
|
-
name,
|
|
1059
|
-
command,
|
|
1060
|
-
args,
|
|
1061
|
-
env,
|
|
1062
|
-
url,
|
|
1063
|
-
type,
|
|
1064
|
-
headers,
|
|
1065
|
-
authProvider,
|
|
1066
|
-
enabled,
|
|
1067
|
-
});
|
|
1068
|
-
// log.info(` Connecting to ${name}...`);
|
|
1069
307
|
let client;
|
|
1070
308
|
try {
|
|
1071
309
|
if (['sse', 'http'].includes(type)) {
|
|
1072
|
-
client = await
|
|
310
|
+
client = await withTimeout(
|
|
1073
311
|
createMCPClient({
|
|
1074
|
-
transport: {
|
|
1075
|
-
type: type,
|
|
1076
|
-
url: url,
|
|
1077
|
-
headers: headers,
|
|
1078
|
-
authProvider: authProvider,
|
|
1079
|
-
redirect: 'error',
|
|
1080
|
-
env: env,
|
|
1081
|
-
},
|
|
312
|
+
transport: { type, url, headers, authProvider, redirect: 'error', env },
|
|
1082
313
|
}),
|
|
1083
|
-
10000,
|
|
1084
|
-
`createMCPClient ${name}`
|
|
314
|
+
10000, `createMCPClient ${name}`
|
|
1085
315
|
);
|
|
1086
316
|
} else {
|
|
1087
|
-
client = await
|
|
317
|
+
client = await withTimeout(
|
|
1088
318
|
createMCPClient({
|
|
1089
|
-
transport: new StdioClientTransport({
|
|
1090
|
-
command: command,
|
|
1091
|
-
args: args,
|
|
1092
|
-
env: env,
|
|
1093
|
-
}),
|
|
319
|
+
transport: new StdioClientTransport({ command, args, env }),
|
|
1094
320
|
}),
|
|
1095
|
-
10000,
|
|
1096
|
-
`createMCPClient ${name}`
|
|
321
|
+
10000, `createMCPClient ${name}`
|
|
1097
322
|
);
|
|
1098
323
|
}
|
|
1099
324
|
} catch (err) {
|
|
@@ -1101,188 +326,97 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
1101
326
|
return { success: false, error: `创建客户端失败: ${err.message}` };
|
|
1102
327
|
}
|
|
1103
328
|
|
|
1104
|
-
// 获取 MCP 服务器的工具列表
|
|
1105
329
|
let mcpTools;
|
|
1106
330
|
try {
|
|
1107
|
-
mcpTools = await
|
|
331
|
+
mcpTools = await withTimeout(client.tools(), 10000, `client.tools ${name}`);
|
|
1108
332
|
} catch (err) {
|
|
1109
333
|
log.error(` Failed to get tools from '${name}':`, err.message);
|
|
1110
334
|
client.close?.();
|
|
1111
335
|
return { success: false, error: `获取工具列表失败: ${err.message}` };
|
|
1112
336
|
}
|
|
1113
337
|
|
|
1114
|
-
// 提取工具信息和实际工具对象
|
|
1115
338
|
const toolsInfo = [];
|
|
1116
|
-
const toolObjects = {};
|
|
339
|
+
const toolObjects = {};
|
|
1117
340
|
if (mcpTools && typeof mcpTools === 'object') {
|
|
1118
341
|
for (const [toolName, tool] of Object.entries(mcpTools)) {
|
|
1119
342
|
if (!tool) continue;
|
|
1120
|
-
toolObjects[toolName] = tool;
|
|
1121
|
-
toolsInfo.push({
|
|
1122
|
-
name: toolName,
|
|
1123
|
-
description: tool.description || '',
|
|
1124
|
-
inputSchema: tool.inputSchema || {},
|
|
1125
|
-
});
|
|
343
|
+
toolObjects[toolName] = tool;
|
|
344
|
+
toolsInfo.push({ name: toolName, description: tool.description || '', inputSchema: tool.inputSchema || {} });
|
|
1126
345
|
|
|
1127
|
-
// 注册为 server_toolname 格式,供 ext_call 使用
|
|
1128
346
|
const registeredName = `${name}_${toolName}`;
|
|
1129
347
|
this.tools[registeredName] = {
|
|
1130
348
|
name: registeredName,
|
|
1131
349
|
description: `[${name}] ${tool.description || ''}`,
|
|
1132
350
|
inputSchema: tool.inputSchema || {},
|
|
1133
351
|
execute: async (args) => {
|
|
1134
|
-
// 路由到 mcp_call
|
|
1135
352
|
return await this._framework.executeTool('mcp_call', {
|
|
1136
|
-
server: name,
|
|
1137
|
-
tool: toolName,
|
|
1138
|
-
args_json: JSON.stringify(args || {}),
|
|
353
|
+
server: name, tool: toolName, args_json: JSON.stringify(args || {}),
|
|
1139
354
|
});
|
|
1140
355
|
},
|
|
1141
356
|
};
|
|
1142
357
|
}
|
|
1143
358
|
}
|
|
1144
359
|
|
|
1145
|
-
|
|
1146
|
-
this._clients.set(name, {
|
|
1147
|
-
client,
|
|
1148
|
-
tools: toolsInfo,
|
|
1149
|
-
toolObjects, // 实际工具对象,供 mcp_call 直接调用
|
|
1150
|
-
enabled: config.enabled !== false,
|
|
1151
|
-
});
|
|
1152
|
-
|
|
1153
|
-
log.debug(
|
|
1154
|
-
` Added server '${name}' with ${toolsInfo.length} tools (ext_call names: ${Object.keys(
|
|
1155
|
-
this.tools
|
|
1156
|
-
)
|
|
1157
|
-
.filter((k) => k.startsWith(name + '_'))
|
|
1158
|
-
.join(', ')})`
|
|
1159
|
-
);
|
|
360
|
+
this._clients.set(name, { client, tools: toolsInfo, toolObjects, enabled: config.enabled !== false });
|
|
1160
361
|
|
|
362
|
+
log.debug(` Added server '${name}' with ${toolsInfo.length} tools`);
|
|
1161
363
|
return { success: true, name, toolsCount: toolsInfo.length };
|
|
1162
364
|
}
|
|
1163
365
|
|
|
1164
|
-
/**
|
|
1165
|
-
* 移除 MCP 服务器
|
|
1166
|
-
*/
|
|
1167
366
|
async removeServer(name) {
|
|
1168
367
|
const clientInfo = this._clients.get(name);
|
|
1169
368
|
if (clientInfo) {
|
|
1170
369
|
try {
|
|
1171
|
-
// 添加超时保护,避免 close/destroy 阻塞
|
|
1172
370
|
const closePromise = clientInfo.client.close?.() || clientInfo.client.destroy?.();
|
|
1173
371
|
if (closePromise && typeof closePromise.then === 'function') {
|
|
1174
|
-
await Promise.race([
|
|
1175
|
-
closePromise,
|
|
1176
|
-
new Promise((resolve) => setTimeout(() => resolve(), 3000)),
|
|
1177
|
-
]);
|
|
372
|
+
await Promise.race([closePromise, new Promise((resolve) => setTimeout(() => resolve(), 3000))]);
|
|
1178
373
|
}
|
|
1179
|
-
} catch (e) {
|
|
1180
|
-
// ignore
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
// 清理已注册的工具
|
|
374
|
+
} catch (e) { /* ignore */ }
|
|
1184
375
|
for (const toolName of Object.keys(this.tools)) {
|
|
1185
|
-
if (toolName.startsWith(name + '_'))
|
|
1186
|
-
delete this.tools[toolName];
|
|
1187
|
-
}
|
|
376
|
+
if (toolName.startsWith(name + '_')) delete this.tools[toolName];
|
|
1188
377
|
}
|
|
1189
|
-
|
|
1190
378
|
this._clients.delete(name);
|
|
1191
379
|
}
|
|
1192
380
|
}
|
|
1193
381
|
|
|
1194
|
-
/**
|
|
1195
|
-
* 获取所有服务器
|
|
1196
|
-
*/
|
|
1197
382
|
getServers() {
|
|
1198
|
-
return Array.from(this._clients.entries()).map(([name, info]) => ({
|
|
1199
|
-
name,
|
|
1200
|
-
tools: info.tools.map((t) => t.name),
|
|
1201
|
-
}));
|
|
383
|
+
return Array.from(this._clients.entries()).map(([name, info]) => ({ name, tools: info.tools.map((t) => t.name) }));
|
|
1202
384
|
}
|
|
1203
385
|
|
|
1204
386
|
reload(framework) {
|
|
1205
|
-
// log.info(' Reloading...');
|
|
1206
387
|
this._framework = framework;
|
|
1207
388
|
}
|
|
1208
389
|
|
|
1209
|
-
/**
|
|
1210
|
-
* 重新加载 MCP 配置
|
|
1211
|
-
* @param {Object} config - 新的配置对象,包含 mcpServers
|
|
1212
|
-
*/
|
|
1213
390
|
async reloadConfig(config) {
|
|
1214
|
-
// log.info(' reloadConfig start');
|
|
1215
391
|
const newServers = config?.mcpServers || {};
|
|
1216
392
|
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
for (const [name, clientInfo] of this._clients) {
|
|
1220
|
-
try {
|
|
1221
|
-
// 不等待 close,直接删除
|
|
1222
|
-
clientInfo.client.close?.()?.catch?.(() => {});
|
|
1223
|
-
} catch (e) {}
|
|
393
|
+
for (const [, clientInfo] of this._clients) {
|
|
394
|
+
try { clientInfo.client.close?.()?.catch?.(() => {}); } catch (e) { /* ignore */ }
|
|
1224
395
|
}
|
|
1225
396
|
this._clients.clear();
|
|
1226
|
-
// 清理服务器配置
|
|
1227
397
|
this._serverConfigs.clear();
|
|
1228
398
|
|
|
1229
|
-
// 2. 并行添加新服务器
|
|
1230
|
-
// log.info(' Adding new servers...');
|
|
1231
399
|
const addPromises = Object.entries(newServers).map(async ([name, serverConfig]) => {
|
|
1232
400
|
if (serverConfig.enabled === false) {
|
|
1233
|
-
// log.info(` Skipping disabled server: ${name}`);
|
|
1234
|
-
// 仍保存配置,以便后续动态启用
|
|
1235
401
|
this._serverConfigs.set(name, { name, ...serverConfig });
|
|
1236
402
|
return;
|
|
1237
403
|
}
|
|
1238
|
-
|
|
1239
|
-
name,
|
|
1240
|
-
type: serverConfig.type,
|
|
1241
|
-
url: serverConfig.url,
|
|
1242
|
-
headers: serverConfig.headers,
|
|
1243
|
-
authProvider: serverConfig.authProvider,
|
|
1244
|
-
command: serverConfig.command,
|
|
1245
|
-
args: serverConfig.args,
|
|
1246
|
-
env: serverConfig.env,
|
|
1247
|
-
enabled: true,
|
|
1248
|
-
};
|
|
1249
|
-
await this.addServer(normalizedConfig);
|
|
404
|
+
await this.addServer({ name, ...serverConfig, enabled: true });
|
|
1250
405
|
});
|
|
1251
406
|
|
|
1252
407
|
await Promise.all(addPromises);
|
|
1253
|
-
// log.info(' reloadConfig complete');
|
|
1254
408
|
|
|
1255
|
-
|
|
1256
|
-
if (this._framework) {
|
|
1257
|
-
this._refreshAllAgentsMCPPrompt(this._framework);
|
|
1258
|
-
}
|
|
409
|
+
if (this._framework) this._refreshAllAgentsMCPPrompt(this._framework);
|
|
1259
410
|
}
|
|
1260
411
|
|
|
1261
|
-
/**
|
|
1262
|
-
* 保存 MCP 服务器的 enabled 状态到配置文件
|
|
1263
|
-
*/
|
|
1264
412
|
async _saveMCPServerEnabled(serverName, enabled) {
|
|
1265
|
-
const fs = require('fs');
|
|
1266
|
-
const path = require('path');
|
|
1267
413
|
const configPath = path.resolve('.foliko/mcp_config.json');
|
|
1268
|
-
|
|
1269
414
|
try {
|
|
1270
|
-
if (!fs.existsSync(configPath))
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
1276
|
-
const config = JSON.parse(configContent);
|
|
1277
|
-
|
|
1278
|
-
if (!config.mcpServers || !config.mcpServers[serverName]) {
|
|
1279
|
-
log.warn(` Server '${serverName}' not found in MCP config`);
|
|
1280
|
-
return false;
|
|
1281
|
-
}
|
|
1282
|
-
|
|
415
|
+
if (!fs.existsSync(configPath)) return false;
|
|
416
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
417
|
+
if (!config.mcpServers || !config.mcpServers[serverName]) return false;
|
|
1283
418
|
config.mcpServers[serverName].enabled = enabled;
|
|
1284
419
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
1285
|
-
// log.info(` Saved enabled=${enabled} for server '${serverName}' to config`);
|
|
1286
420
|
return true;
|
|
1287
421
|
} catch (err) {
|
|
1288
422
|
log.error(` Failed to save MCP config:`, err.message);
|
|
@@ -1291,25 +425,26 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
1291
425
|
}
|
|
1292
426
|
|
|
1293
427
|
async uninstall(framework) {
|
|
1294
|
-
|
|
1295
|
-
for (const [name, clientInfo] of this._clients) {
|
|
428
|
+
for (const [, clientInfo] of this._clients) {
|
|
1296
429
|
try {
|
|
1297
430
|
const closePromise = clientInfo.client.close?.() || clientInfo.client.destroy?.();
|
|
1298
431
|
if (closePromise && typeof closePromise.then === 'function') {
|
|
1299
|
-
await Promise.race([
|
|
1300
|
-
closePromise,
|
|
1301
|
-
new Promise((resolve) => setTimeout(() => resolve(), 3000)),
|
|
1302
|
-
]);
|
|
432
|
+
await Promise.race([closePromise, new Promise((resolve) => setTimeout(() => resolve(), 3000))]);
|
|
1303
433
|
}
|
|
1304
|
-
} catch (e) {
|
|
1305
|
-
// ignore
|
|
1306
|
-
}
|
|
434
|
+
} catch (e) { /* ignore */ }
|
|
1307
435
|
}
|
|
1308
436
|
this._clients.clear();
|
|
1309
437
|
this._framework = null;
|
|
1310
438
|
}
|
|
1311
439
|
}
|
|
1312
440
|
|
|
441
|
+
function withTimeout(promise, ms, name) {
|
|
442
|
+
return Promise.race([
|
|
443
|
+
promise,
|
|
444
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`${name} timeout (${ms}ms)`)), ms)),
|
|
445
|
+
]);
|
|
446
|
+
}
|
|
447
|
+
|
|
1313
448
|
module.exports = {
|
|
1314
449
|
MCPExecutorPlugin,
|
|
1315
450
|
MCPClientWrapper,
|