coding-tool-x 3.2.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/CHANGELOG.md +599 -0
- package/LICENSE +21 -0
- package/README.md +439 -0
- package/bin/ctx.js +8 -0
- package/dist/web/assets/Analytics-DN_YsnkW.js +39 -0
- package/dist/web/assets/Analytics-DuYvId7u.css +1 -0
- package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
- package/dist/web/assets/ConfigTemplates-DpXIMy0p.js +1 -0
- package/dist/web/assets/Home-38JTUlYt.js +1 -0
- package/dist/web/assets/Home-CjupSEWE.css +1 -0
- package/dist/web/assets/PluginManager-CX2tgq2H.js +1 -0
- package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
- package/dist/web/assets/ProjectList-C1lDcsn6.js +1 -0
- package/dist/web/assets/ProjectList-oJIyIRkP.css +1 -0
- package/dist/web/assets/SessionList-C55tjV7i.css +1 -0
- package/dist/web/assets/SessionList-CZ7T6rVx.js +1 -0
- package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
- package/dist/web/assets/SkillManager-DLN9f79y.js +1 -0
- package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
- package/dist/web/assets/WorkspaceManager-DxlHZkpZ.js +1 -0
- package/dist/web/assets/icons-DRrXwWZi.js +1 -0
- package/dist/web/assets/index-CetESrXw.css +1 -0
- package/dist/web/assets/index-Cfvn-2Gb.js +2 -0
- package/dist/web/assets/markdown-BfC0goYb.css +10 -0
- package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
- package/dist/web/assets/naive-ui-DlpKk-8M.js +1 -0
- package/dist/web/assets/vendors-DMjSfzlv.js +7 -0
- package/dist/web/assets/vue-vendor-DET08QYg.js +45 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/index.html +20 -0
- package/dist/web/logo.png +0 -0
- package/docs/bannel.png +0 -0
- package/docs/home.png +0 -0
- package/docs/logo.png +0 -0
- package/docs/model-redirection.md +251 -0
- package/docs/multi-channel-load-balancing.md +249 -0
- package/package.json +80 -0
- package/src/commands/channels.js +551 -0
- package/src/commands/cli-type.js +101 -0
- package/src/commands/daemon.js +365 -0
- package/src/commands/doctor.js +333 -0
- package/src/commands/export-config.js +205 -0
- package/src/commands/list.js +222 -0
- package/src/commands/logs.js +261 -0
- package/src/commands/plugin.js +585 -0
- package/src/commands/port-config.js +135 -0
- package/src/commands/proxy-control.js +264 -0
- package/src/commands/proxy.js +152 -0
- package/src/commands/resume.js +137 -0
- package/src/commands/search.js +190 -0
- package/src/commands/security.js +37 -0
- package/src/commands/stats.js +398 -0
- package/src/commands/switch.js +48 -0
- package/src/commands/toggle-proxy.js +247 -0
- package/src/commands/ui.js +99 -0
- package/src/commands/update.js +97 -0
- package/src/commands/workspace.js +454 -0
- package/src/config/default.js +69 -0
- package/src/config/loader.js +149 -0
- package/src/config/model-metadata.js +167 -0
- package/src/config/model-metadata.json +125 -0
- package/src/config/model-pricing.js +35 -0
- package/src/config/paths.js +190 -0
- package/src/index.js +680 -0
- package/src/plugins/constants.js +15 -0
- package/src/plugins/event-bus.js +54 -0
- package/src/plugins/manifest-validator.js +129 -0
- package/src/plugins/plugin-api.js +128 -0
- package/src/plugins/plugin-installer.js +601 -0
- package/src/plugins/plugin-loader.js +229 -0
- package/src/plugins/plugin-manager.js +170 -0
- package/src/plugins/registry.js +152 -0
- package/src/plugins/schema/plugin-manifest.json +115 -0
- package/src/reset-config.js +94 -0
- package/src/server/api/agents.js +826 -0
- package/src/server/api/aliases.js +36 -0
- package/src/server/api/channels.js +368 -0
- package/src/server/api/claude-hooks.js +480 -0
- package/src/server/api/codex-channels.js +417 -0
- package/src/server/api/codex-projects.js +104 -0
- package/src/server/api/codex-proxy.js +195 -0
- package/src/server/api/codex-sessions.js +483 -0
- package/src/server/api/codex-statistics.js +57 -0
- package/src/server/api/commands.js +482 -0
- package/src/server/api/config-export.js +212 -0
- package/src/server/api/config-registry.js +357 -0
- package/src/server/api/config-sync.js +155 -0
- package/src/server/api/config-templates.js +248 -0
- package/src/server/api/config.js +521 -0
- package/src/server/api/convert.js +260 -0
- package/src/server/api/dashboard.js +142 -0
- package/src/server/api/env.js +144 -0
- package/src/server/api/favorites.js +77 -0
- package/src/server/api/gemini-channels.js +366 -0
- package/src/server/api/gemini-projects.js +91 -0
- package/src/server/api/gemini-proxy.js +173 -0
- package/src/server/api/gemini-sessions.js +376 -0
- package/src/server/api/gemini-statistics.js +57 -0
- package/src/server/api/health-check.js +31 -0
- package/src/server/api/mcp.js +399 -0
- package/src/server/api/opencode-channels.js +419 -0
- package/src/server/api/opencode-projects.js +99 -0
- package/src/server/api/opencode-proxy.js +207 -0
- package/src/server/api/opencode-sessions.js +327 -0
- package/src/server/api/opencode-statistics.js +57 -0
- package/src/server/api/plugins.js +463 -0
- package/src/server/api/pm2-autostart.js +269 -0
- package/src/server/api/projects.js +124 -0
- package/src/server/api/prompts.js +279 -0
- package/src/server/api/proxy.js +306 -0
- package/src/server/api/security.js +53 -0
- package/src/server/api/sessions.js +514 -0
- package/src/server/api/settings.js +142 -0
- package/src/server/api/skills.js +570 -0
- package/src/server/api/statistics.js +238 -0
- package/src/server/api/ui-config.js +64 -0
- package/src/server/api/workspaces.js +456 -0
- package/src/server/codex-proxy-server.js +681 -0
- package/src/server/dev-server.js +26 -0
- package/src/server/gemini-proxy-server.js +610 -0
- package/src/server/index.js +422 -0
- package/src/server/opencode-proxy-server.js +4771 -0
- package/src/server/proxy-server.js +669 -0
- package/src/server/services/agents-service.js +1137 -0
- package/src/server/services/alias.js +71 -0
- package/src/server/services/channel-health.js +234 -0
- package/src/server/services/channel-scheduler.js +240 -0
- package/src/server/services/channels.js +447 -0
- package/src/server/services/codex-channels.js +705 -0
- package/src/server/services/codex-config.js +90 -0
- package/src/server/services/codex-parser.js +322 -0
- package/src/server/services/codex-sessions.js +936 -0
- package/src/server/services/codex-settings-manager.js +619 -0
- package/src/server/services/codex-speed-test-template.json +24 -0
- package/src/server/services/codex-statistics-service.js +161 -0
- package/src/server/services/commands-service.js +574 -0
- package/src/server/services/config-export-service.js +1165 -0
- package/src/server/services/config-registry-service.js +828 -0
- package/src/server/services/config-sync-manager.js +941 -0
- package/src/server/services/config-sync-service.js +504 -0
- package/src/server/services/config-templates-service.js +913 -0
- package/src/server/services/enhanced-cache.js +196 -0
- package/src/server/services/env-checker.js +409 -0
- package/src/server/services/env-manager.js +436 -0
- package/src/server/services/favorites.js +165 -0
- package/src/server/services/format-converter.js +620 -0
- package/src/server/services/gemini-channels.js +459 -0
- package/src/server/services/gemini-config.js +73 -0
- package/src/server/services/gemini-sessions.js +689 -0
- package/src/server/services/gemini-settings-manager.js +263 -0
- package/src/server/services/gemini-statistics-service.js +157 -0
- package/src/server/services/health-check.js +85 -0
- package/src/server/services/mcp-client.js +790 -0
- package/src/server/services/mcp-service.js +1732 -0
- package/src/server/services/model-detector.js +1245 -0
- package/src/server/services/network-access.js +80 -0
- package/src/server/services/opencode-channels.js +366 -0
- package/src/server/services/opencode-gateway-adapters.js +1168 -0
- package/src/server/services/opencode-gateway-converter.js +639 -0
- package/src/server/services/opencode-sessions.js +931 -0
- package/src/server/services/opencode-settings-manager.js +478 -0
- package/src/server/services/opencode-statistics-service.js +161 -0
- package/src/server/services/plugins-service.js +1268 -0
- package/src/server/services/prompts-service.js +534 -0
- package/src/server/services/proxy-runtime.js +79 -0
- package/src/server/services/repo-scanner-base.js +708 -0
- package/src/server/services/request-logger.js +130 -0
- package/src/server/services/response-decoder.js +21 -0
- package/src/server/services/security-config.js +131 -0
- package/src/server/services/session-cache.js +127 -0
- package/src/server/services/session-converter.js +577 -0
- package/src/server/services/sessions.js +900 -0
- package/src/server/services/settings-manager.js +163 -0
- package/src/server/services/skill-service.js +1482 -0
- package/src/server/services/speed-test.js +1146 -0
- package/src/server/services/statistics-service.js +1043 -0
- package/src/server/services/ui-config.js +132 -0
- package/src/server/services/workspace-service.js +830 -0
- package/src/server/utils/pricing.js +73 -0
- package/src/server/websocket-server.js +513 -0
- package/src/ui/menu.js +139 -0
- package/src/ui/prompts.js +100 -0
- package/src/utils/format.js +43 -0
- package/src/utils/port-helper.js +108 -0
- package/src/utils/session.js +240 -0
|
@@ -0,0 +1,1732 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP 服务器管理服务
|
|
3
|
+
*
|
|
4
|
+
* 负责 MCP 服务器的 CRUD 操作和多平台配置同步
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const toml = require('@iarna/toml');
|
|
11
|
+
const { spawn } = require('child_process');
|
|
12
|
+
const http = require('http');
|
|
13
|
+
const https = require('https');
|
|
14
|
+
const { McpClient } = require('./mcp-client');
|
|
15
|
+
const { NATIVE_PATHS } = require('../../config/paths');
|
|
16
|
+
|
|
17
|
+
// MCP 配置文件路径
|
|
18
|
+
const CC_TOOL_DIR = path.join(os.homedir(), '.cc-tool');
|
|
19
|
+
const MCP_SERVERS_FILE = path.join(CC_TOOL_DIR, 'mcp-servers.json');
|
|
20
|
+
|
|
21
|
+
// 各平台配置文件路径
|
|
22
|
+
const CLAUDE_CONFIG_PATH = path.join(os.homedir(), '.claude.json');
|
|
23
|
+
const CODEX_CONFIG_PATH = path.join(os.homedir(), '.codex', 'config.toml');
|
|
24
|
+
const GEMINI_CONFIG_PATH = path.join(os.homedir(), '.gemini', 'settings.json');
|
|
25
|
+
const OPENCODE_CONFIG_DIR = NATIVE_PATHS.opencode.config;
|
|
26
|
+
const OPENCODE_CONFIG_PATHS = {
|
|
27
|
+
jsonc: path.join(OPENCODE_CONFIG_DIR, 'opencode.jsonc'),
|
|
28
|
+
json: path.join(OPENCODE_CONFIG_DIR, 'opencode.json'),
|
|
29
|
+
legacy: path.join(OPENCODE_CONFIG_DIR, 'config.json')
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// MCP 客户端连接池
|
|
33
|
+
// serverId -> { client, timestamp }
|
|
34
|
+
const mcpClientPool = new Map();
|
|
35
|
+
const POOL_TTL = 5 * 60 * 1000; // 5 minutes
|
|
36
|
+
|
|
37
|
+
// MCP 预设模板
|
|
38
|
+
const MCP_PRESETS = [
|
|
39
|
+
{
|
|
40
|
+
id: 'fetch',
|
|
41
|
+
name: 'mcp-server-fetch',
|
|
42
|
+
description: '获取网页内容',
|
|
43
|
+
tags: ['http', 'web', 'fetch'],
|
|
44
|
+
server: {
|
|
45
|
+
type: 'stdio',
|
|
46
|
+
command: 'uvx',
|
|
47
|
+
args: ['mcp-server-fetch']
|
|
48
|
+
},
|
|
49
|
+
homepage: 'https://github.com/modelcontextprotocol/servers',
|
|
50
|
+
docs: 'https://github.com/modelcontextprotocol/servers/tree/main/src/fetch'
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'time',
|
|
54
|
+
name: '@modelcontextprotocol/server-time',
|
|
55
|
+
description: '获取当前时间和时区信息',
|
|
56
|
+
tags: ['time', 'utility'],
|
|
57
|
+
server: {
|
|
58
|
+
type: 'stdio',
|
|
59
|
+
command: 'npx',
|
|
60
|
+
args: ['-y', '@modelcontextprotocol/server-time']
|
|
61
|
+
},
|
|
62
|
+
homepage: 'https://github.com/modelcontextprotocol/servers',
|
|
63
|
+
docs: 'https://github.com/modelcontextprotocol/servers/tree/main/src/time'
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'memory',
|
|
67
|
+
name: '@modelcontextprotocol/server-memory',
|
|
68
|
+
description: '知识图谱记忆存储',
|
|
69
|
+
tags: ['memory', 'graph', 'knowledge'],
|
|
70
|
+
server: {
|
|
71
|
+
type: 'stdio',
|
|
72
|
+
command: 'npx',
|
|
73
|
+
args: ['-y', '@modelcontextprotocol/server-memory']
|
|
74
|
+
},
|
|
75
|
+
homepage: 'https://github.com/modelcontextprotocol/servers',
|
|
76
|
+
docs: 'https://github.com/modelcontextprotocol/servers/tree/main/src/memory'
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: 'sequential-thinking',
|
|
80
|
+
name: '@modelcontextprotocol/server-sequential-thinking',
|
|
81
|
+
description: '顺序思维推理',
|
|
82
|
+
tags: ['thinking', 'reasoning'],
|
|
83
|
+
server: {
|
|
84
|
+
type: 'stdio',
|
|
85
|
+
command: 'npx',
|
|
86
|
+
args: ['-y', '@modelcontextprotocol/server-sequential-thinking']
|
|
87
|
+
},
|
|
88
|
+
homepage: 'https://github.com/modelcontextprotocol/servers',
|
|
89
|
+
docs: 'https://github.com/modelcontextprotocol/servers/tree/main/src/sequentialthinking'
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: 'filesystem',
|
|
93
|
+
name: '@anthropic/mcp-server-filesystem',
|
|
94
|
+
description: '文件系统读写访问',
|
|
95
|
+
tags: ['filesystem', 'files'],
|
|
96
|
+
server: {
|
|
97
|
+
type: 'stdio',
|
|
98
|
+
command: 'npx',
|
|
99
|
+
args: ['-y', '@anthropic/mcp-server-filesystem', '/tmp']
|
|
100
|
+
},
|
|
101
|
+
homepage: 'https://github.com/anthropics/anthropic-quickstarts',
|
|
102
|
+
docs: 'https://github.com/anthropics/anthropic-quickstarts/tree/main/mcp-server-filesystem'
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: 'context7',
|
|
106
|
+
name: '@upstash/context7-mcp',
|
|
107
|
+
description: '文档搜索和上下文增强',
|
|
108
|
+
tags: ['docs', 'search', 'context'],
|
|
109
|
+
server: {
|
|
110
|
+
type: 'stdio',
|
|
111
|
+
command: 'npx',
|
|
112
|
+
args: ['-y', '@upstash/context7-mcp']
|
|
113
|
+
},
|
|
114
|
+
homepage: 'https://context7.com',
|
|
115
|
+
docs: 'https://github.com/upstash/context7/blob/master/README.md'
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: 'brave-search',
|
|
119
|
+
name: '@anthropic/mcp-server-brave-search',
|
|
120
|
+
description: 'Brave 搜索引擎',
|
|
121
|
+
tags: ['search', 'web'],
|
|
122
|
+
server: {
|
|
123
|
+
type: 'stdio',
|
|
124
|
+
command: 'npx',
|
|
125
|
+
args: ['-y', '@anthropic/mcp-server-brave-search'],
|
|
126
|
+
env: {
|
|
127
|
+
BRAVE_API_KEY: '<your-api-key>'
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
homepage: 'https://github.com/anthropics/anthropic-quickstarts',
|
|
131
|
+
docs: 'https://brave.com/search/api/'
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: 'github',
|
|
135
|
+
name: '@modelcontextprotocol/server-github',
|
|
136
|
+
description: 'GitHub API 集成',
|
|
137
|
+
tags: ['github', 'git', 'api'],
|
|
138
|
+
server: {
|
|
139
|
+
type: 'stdio',
|
|
140
|
+
command: 'npx',
|
|
141
|
+
args: ['-y', '@modelcontextprotocol/server-github'],
|
|
142
|
+
env: {
|
|
143
|
+
GITHUB_PERSONAL_ACCESS_TOKEN: '<your-token>'
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
homepage: 'https://github.com/modelcontextprotocol/servers',
|
|
147
|
+
docs: 'https://github.com/modelcontextprotocol/servers/tree/main/src/github'
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: 'puppeteer',
|
|
151
|
+
name: '@anthropic/mcp-server-puppeteer',
|
|
152
|
+
description: '浏览器自动化',
|
|
153
|
+
tags: ['browser', 'automation', 'web'],
|
|
154
|
+
server: {
|
|
155
|
+
type: 'stdio',
|
|
156
|
+
command: 'npx',
|
|
157
|
+
args: ['-y', '@anthropic/mcp-server-puppeteer']
|
|
158
|
+
},
|
|
159
|
+
homepage: 'https://github.com/anthropics/anthropic-quickstarts',
|
|
160
|
+
docs: 'https://pptr.dev/'
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: 'playwright',
|
|
164
|
+
name: '@anthropic/mcp-server-playwright',
|
|
165
|
+
description: 'Playwright 浏览器自动化',
|
|
166
|
+
tags: ['browser', 'automation', 'testing'],
|
|
167
|
+
server: {
|
|
168
|
+
type: 'stdio',
|
|
169
|
+
command: 'npx',
|
|
170
|
+
args: ['-y', '@anthropic/mcp-server-playwright']
|
|
171
|
+
},
|
|
172
|
+
homepage: 'https://github.com/anthropics/anthropic-quickstarts',
|
|
173
|
+
docs: 'https://playwright.dev/'
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: 'sqlite',
|
|
177
|
+
name: '@anthropic/mcp-server-sqlite',
|
|
178
|
+
description: 'SQLite 数据库访问',
|
|
179
|
+
tags: ['database', 'sql', 'sqlite'],
|
|
180
|
+
server: {
|
|
181
|
+
type: 'stdio',
|
|
182
|
+
command: 'npx',
|
|
183
|
+
args: ['-y', '@anthropic/mcp-server-sqlite', '--db-path', '/path/to/database.db']
|
|
184
|
+
},
|
|
185
|
+
homepage: 'https://github.com/anthropics/anthropic-quickstarts',
|
|
186
|
+
docs: 'https://www.sqlite.org/docs.html'
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: 'postgres',
|
|
190
|
+
name: '@anthropic/mcp-server-postgres',
|
|
191
|
+
description: 'PostgreSQL 数据库访问',
|
|
192
|
+
tags: ['database', 'sql', 'postgres'],
|
|
193
|
+
server: {
|
|
194
|
+
type: 'stdio',
|
|
195
|
+
command: 'npx',
|
|
196
|
+
args: ['-y', '@anthropic/mcp-server-postgres'],
|
|
197
|
+
env: {
|
|
198
|
+
POSTGRES_CONNECTION_STRING: 'postgresql://user:pass@localhost:5432/db'
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
homepage: 'https://github.com/anthropics/anthropic-quickstarts',
|
|
202
|
+
docs: 'https://www.postgresql.org/docs/'
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: 'slack',
|
|
206
|
+
name: '@modelcontextprotocol/server-slack',
|
|
207
|
+
description: 'Slack 消息和频道访问',
|
|
208
|
+
tags: ['slack', 'chat', 'messaging'],
|
|
209
|
+
server: {
|
|
210
|
+
type: 'stdio',
|
|
211
|
+
command: 'npx',
|
|
212
|
+
args: ['-y', '@modelcontextprotocol/server-slack'],
|
|
213
|
+
env: {
|
|
214
|
+
SLACK_BOT_TOKEN: '<your-bot-token>',
|
|
215
|
+
SLACK_TEAM_ID: '<your-team-id>'
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
homepage: 'https://github.com/modelcontextprotocol/servers',
|
|
219
|
+
docs: 'https://api.slack.com/docs'
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: 'google-drive',
|
|
223
|
+
name: '@modelcontextprotocol/server-gdrive',
|
|
224
|
+
description: 'Google Drive 文件访问',
|
|
225
|
+
tags: ['google', 'drive', 'files'],
|
|
226
|
+
server: {
|
|
227
|
+
type: 'stdio',
|
|
228
|
+
command: 'npx',
|
|
229
|
+
args: ['-y', '@modelcontextprotocol/server-gdrive']
|
|
230
|
+
},
|
|
231
|
+
homepage: 'https://github.com/modelcontextprotocol/servers',
|
|
232
|
+
docs: 'https://developers.google.com/drive'
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
id: 'everart',
|
|
236
|
+
name: '@modelcontextprotocol/server-everart',
|
|
237
|
+
description: 'AI 图片生成',
|
|
238
|
+
tags: ['image', 'art', 'generation'],
|
|
239
|
+
server: {
|
|
240
|
+
type: 'stdio',
|
|
241
|
+
command: 'npx',
|
|
242
|
+
args: ['-y', '@modelcontextprotocol/server-everart'],
|
|
243
|
+
env: {
|
|
244
|
+
EVERART_API_KEY: '<your-api-key>'
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
homepage: 'https://github.com/modelcontextprotocol/servers',
|
|
248
|
+
docs: 'https://everart.ai/docs'
|
|
249
|
+
}
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* 确保目录存在
|
|
254
|
+
*/
|
|
255
|
+
function ensureDir(dirPath) {
|
|
256
|
+
if (!fs.existsSync(dirPath)) {
|
|
257
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* 安全读取 JSON 文件
|
|
263
|
+
*/
|
|
264
|
+
function readJsonFile(filePath, defaultValue = {}) {
|
|
265
|
+
try {
|
|
266
|
+
if (fs.existsSync(filePath)) {
|
|
267
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
268
|
+
return JSON.parse(content);
|
|
269
|
+
}
|
|
270
|
+
} catch (err) {
|
|
271
|
+
console.error(`[MCP] Failed to read ${filePath}:`, err.message);
|
|
272
|
+
}
|
|
273
|
+
return defaultValue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* 安全写入 JSON 文件(原子写入)
|
|
278
|
+
*/
|
|
279
|
+
function writeJsonFile(filePath, data) {
|
|
280
|
+
ensureDir(path.dirname(filePath));
|
|
281
|
+
const tempPath = filePath + '.tmp';
|
|
282
|
+
fs.writeFileSync(tempPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
283
|
+
fs.renameSync(tempPath, filePath);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 安全读取 TOML 文件
|
|
288
|
+
*/
|
|
289
|
+
function readTomlFile(filePath, defaultValue = {}) {
|
|
290
|
+
try {
|
|
291
|
+
if (fs.existsSync(filePath)) {
|
|
292
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
293
|
+
return toml.parse(content);
|
|
294
|
+
}
|
|
295
|
+
} catch (err) {
|
|
296
|
+
console.error(`[MCP] Failed to read ${filePath}:`, err.message);
|
|
297
|
+
}
|
|
298
|
+
return defaultValue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 安全写入 TOML 文件(原子写入)
|
|
303
|
+
*/
|
|
304
|
+
function writeTomlFile(filePath, data) {
|
|
305
|
+
ensureDir(path.dirname(filePath));
|
|
306
|
+
const tempPath = filePath + '.tmp';
|
|
307
|
+
fs.writeFileSync(tempPath, toml.stringify(data), 'utf-8');
|
|
308
|
+
fs.renameSync(tempPath, filePath);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* 去除 JSONC 注释
|
|
313
|
+
*/
|
|
314
|
+
function stripJsonComments(input) {
|
|
315
|
+
let result = '';
|
|
316
|
+
let inString = false;
|
|
317
|
+
let quote = '';
|
|
318
|
+
let index = 0;
|
|
319
|
+
|
|
320
|
+
while (index < input.length) {
|
|
321
|
+
const ch = input[index];
|
|
322
|
+
const next = input[index + 1];
|
|
323
|
+
|
|
324
|
+
if (inString) {
|
|
325
|
+
result += ch;
|
|
326
|
+
if (ch === '\\') {
|
|
327
|
+
if (next) {
|
|
328
|
+
result += next;
|
|
329
|
+
index += 2;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
} else if (ch === quote) {
|
|
333
|
+
inString = false;
|
|
334
|
+
}
|
|
335
|
+
index += 1;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (ch === '"' || ch === '\'') {
|
|
340
|
+
inString = true;
|
|
341
|
+
quote = ch;
|
|
342
|
+
result += ch;
|
|
343
|
+
index += 1;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (ch === '/' && next === '/') {
|
|
348
|
+
index += 2;
|
|
349
|
+
while (index < input.length && input[index] !== '\n') {
|
|
350
|
+
index += 1;
|
|
351
|
+
}
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (ch === '/' && next === '*') {
|
|
356
|
+
index += 2;
|
|
357
|
+
while (index < input.length - 1 && !(input[index] === '*' && input[index + 1] === '/')) {
|
|
358
|
+
index += 1;
|
|
359
|
+
}
|
|
360
|
+
index += 2;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
result += ch;
|
|
365
|
+
index += 1;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return result;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* 选择 OpenCode 配置文件路径
|
|
373
|
+
*/
|
|
374
|
+
function selectOpenCodeConfigPath() {
|
|
375
|
+
if (fs.existsSync(OPENCODE_CONFIG_PATHS.jsonc)) return OPENCODE_CONFIG_PATHS.jsonc;
|
|
376
|
+
if (fs.existsSync(OPENCODE_CONFIG_PATHS.json)) return OPENCODE_CONFIG_PATHS.json;
|
|
377
|
+
if (fs.existsSync(OPENCODE_CONFIG_PATHS.legacy)) return OPENCODE_CONFIG_PATHS.legacy;
|
|
378
|
+
return OPENCODE_CONFIG_PATHS.json;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* 读取 OpenCode 配置
|
|
383
|
+
*/
|
|
384
|
+
function readOpenCodeConfig() {
|
|
385
|
+
const filePath = selectOpenCodeConfigPath();
|
|
386
|
+
|
|
387
|
+
if (!fs.existsSync(filePath)) {
|
|
388
|
+
return { path: filePath, config: {} };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
393
|
+
if (!raw.trim()) {
|
|
394
|
+
return { path: filePath, config: {} };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const content = filePath.endsWith('.jsonc') ? stripJsonComments(raw) : raw;
|
|
398
|
+
return {
|
|
399
|
+
path: filePath,
|
|
400
|
+
config: JSON.parse(content)
|
|
401
|
+
};
|
|
402
|
+
} catch (err) {
|
|
403
|
+
console.error(`[MCP] Failed to read OpenCode config:`, err.message);
|
|
404
|
+
return { path: filePath, config: {} };
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* 写入 OpenCode 配置(保持 JSON 格式)
|
|
410
|
+
*/
|
|
411
|
+
function writeOpenCodeConfig(filePath, data) {
|
|
412
|
+
ensureDir(path.dirname(filePath));
|
|
413
|
+
const tempPath = filePath + '.tmp';
|
|
414
|
+
fs.writeFileSync(tempPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
415
|
+
fs.renameSync(tempPath, filePath);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ============================================================================
|
|
419
|
+
// MCP 数据管理
|
|
420
|
+
// ============================================================================
|
|
421
|
+
|
|
422
|
+
function normalizeServerApps(apps = {}) {
|
|
423
|
+
return {
|
|
424
|
+
claude: apps.claude !== undefined ? !!apps.claude : true,
|
|
425
|
+
codex: !!apps.codex,
|
|
426
|
+
gemini: !!apps.gemini,
|
|
427
|
+
opencode: !!apps.opencode
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* 获取所有 MCP 服务器
|
|
433
|
+
*/
|
|
434
|
+
function getAllServers() {
|
|
435
|
+
const servers = readJsonFile(MCP_SERVERS_FILE, {});
|
|
436
|
+
|
|
437
|
+
for (const server of Object.values(servers)) {
|
|
438
|
+
if (!server || typeof server !== 'object') {
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
server.apps = normalizeServerApps(server.apps);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return servers;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* 获取单个 MCP 服务器
|
|
449
|
+
*/
|
|
450
|
+
function getServer(id) {
|
|
451
|
+
const servers = getAllServers();
|
|
452
|
+
return servers[id] || null;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* 保存 MCP 服务器(添加或更新)
|
|
457
|
+
*/
|
|
458
|
+
async function saveServer(server) {
|
|
459
|
+
if (!server.id || !server.id.trim()) {
|
|
460
|
+
throw new Error('MCP 服务器 ID 不能为空');
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// 验证服务器配置
|
|
464
|
+
validateServerSpec(server.server);
|
|
465
|
+
|
|
466
|
+
const servers = getAllServers();
|
|
467
|
+
|
|
468
|
+
// 如果是新服务器,设置默认值
|
|
469
|
+
if (!servers[server.id]) {
|
|
470
|
+
server.createdAt = Date.now();
|
|
471
|
+
}
|
|
472
|
+
server.updatedAt = Date.now();
|
|
473
|
+
|
|
474
|
+
// 确保 apps 字段存在
|
|
475
|
+
if (!server.apps) {
|
|
476
|
+
server.apps = { claude: true, codex: false, gemini: false, opencode: false };
|
|
477
|
+
} else {
|
|
478
|
+
server.apps = normalizeServerApps(server.apps);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
servers[server.id] = server;
|
|
482
|
+
writeJsonFile(MCP_SERVERS_FILE, servers);
|
|
483
|
+
|
|
484
|
+
// 同步到各平台配置
|
|
485
|
+
await syncServerToAllPlatforms(server);
|
|
486
|
+
|
|
487
|
+
return server;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* 删除 MCP 服务器
|
|
492
|
+
*/
|
|
493
|
+
async function deleteServer(id) {
|
|
494
|
+
const servers = getAllServers();
|
|
495
|
+
const server = servers[id];
|
|
496
|
+
|
|
497
|
+
if (!server) {
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
delete servers[id];
|
|
502
|
+
writeJsonFile(MCP_SERVERS_FILE, servers);
|
|
503
|
+
|
|
504
|
+
// 从所有平台配置中移除
|
|
505
|
+
await removeServerFromAllPlatforms(id);
|
|
506
|
+
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* 切换 MCP 服务器在某平台的启用状态
|
|
512
|
+
*/
|
|
513
|
+
async function toggleServerApp(serverId, app, enabled) {
|
|
514
|
+
const servers = getAllServers();
|
|
515
|
+
const server = servers[serverId];
|
|
516
|
+
|
|
517
|
+
if (!server) {
|
|
518
|
+
throw new Error(`MCP 服务器 "${serverId}" 不存在`);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (!['claude', 'codex', 'gemini', 'opencode'].includes(app)) {
|
|
522
|
+
throw new Error(`无效的平台: ${app}`);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
server.apps[app] = enabled;
|
|
526
|
+
server.updatedAt = Date.now();
|
|
527
|
+
|
|
528
|
+
writeJsonFile(MCP_SERVERS_FILE, servers);
|
|
529
|
+
|
|
530
|
+
// 同步到对应平台
|
|
531
|
+
if (enabled) {
|
|
532
|
+
await syncServerToPlatform(server, app);
|
|
533
|
+
} else {
|
|
534
|
+
await removeServerFromPlatform(serverId, app);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return server;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* 获取 MCP 预设模板列表
|
|
542
|
+
*/
|
|
543
|
+
function getPresets() {
|
|
544
|
+
return MCP_PRESETS;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ============================================================================
|
|
548
|
+
// 服务器配置验证
|
|
549
|
+
// ============================================================================
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* 验证 MCP 服务器配置
|
|
553
|
+
*/
|
|
554
|
+
function validateServerSpec(spec) {
|
|
555
|
+
if (!spec || typeof spec !== 'object') {
|
|
556
|
+
throw new Error('服务器配置必须是对象');
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const type = spec.type || 'stdio';
|
|
560
|
+
|
|
561
|
+
if (!['stdio', 'http', 'sse'].includes(type)) {
|
|
562
|
+
throw new Error(`无效的服务器类型: ${type},必须是 stdio、http 或 sse`);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (type === 'stdio') {
|
|
566
|
+
if (!spec.command || !spec.command.trim()) {
|
|
567
|
+
throw new Error('stdio 类型必须指定 command');
|
|
568
|
+
}
|
|
569
|
+
} else if (type === 'http' || type === 'sse') {
|
|
570
|
+
if (!spec.url || !spec.url.trim()) {
|
|
571
|
+
throw new Error(`${type} 类型必须指定 url`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// ============================================================================
|
|
577
|
+
// 平台配置同步
|
|
578
|
+
// ============================================================================
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* 同步服务器到所有已启用的平台
|
|
582
|
+
*/
|
|
583
|
+
async function syncServerToAllPlatforms(server) {
|
|
584
|
+
const { apps } = server;
|
|
585
|
+
|
|
586
|
+
if (apps.claude) {
|
|
587
|
+
await syncServerToPlatform(server, 'claude');
|
|
588
|
+
} else {
|
|
589
|
+
await removeServerFromPlatform(server.id, 'claude');
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (apps.codex) {
|
|
593
|
+
await syncServerToPlatform(server, 'codex');
|
|
594
|
+
} else {
|
|
595
|
+
await removeServerFromPlatform(server.id, 'codex');
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (apps.gemini) {
|
|
599
|
+
await syncServerToPlatform(server, 'gemini');
|
|
600
|
+
} else {
|
|
601
|
+
await removeServerFromPlatform(server.id, 'gemini');
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (apps.opencode) {
|
|
605
|
+
await syncServerToPlatform(server, 'opencode');
|
|
606
|
+
} else {
|
|
607
|
+
await removeServerFromPlatform(server.id, 'opencode');
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* 从所有平台移除服务器
|
|
613
|
+
*/
|
|
614
|
+
async function removeServerFromAllPlatforms(serverId) {
|
|
615
|
+
await removeServerFromPlatform(serverId, 'claude');
|
|
616
|
+
await removeServerFromPlatform(serverId, 'codex');
|
|
617
|
+
await removeServerFromPlatform(serverId, 'gemini');
|
|
618
|
+
await removeServerFromPlatform(serverId, 'opencode');
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* 同步服务器到指定平台
|
|
623
|
+
*/
|
|
624
|
+
async function syncServerToPlatform(server, platform) {
|
|
625
|
+
try {
|
|
626
|
+
switch (platform) {
|
|
627
|
+
case 'claude':
|
|
628
|
+
syncToClaudeConfig(server);
|
|
629
|
+
break;
|
|
630
|
+
case 'codex':
|
|
631
|
+
syncToCodexConfig(server);
|
|
632
|
+
break;
|
|
633
|
+
case 'gemini':
|
|
634
|
+
syncToGeminiConfig(server);
|
|
635
|
+
break;
|
|
636
|
+
case 'opencode':
|
|
637
|
+
syncToOpenCodeConfig(server);
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
console.log(`[MCP] Synced "${server.id}" to ${platform}`);
|
|
641
|
+
} catch (err) {
|
|
642
|
+
console.error(`[MCP] Failed to sync "${server.id}" to ${platform}:`, err.message);
|
|
643
|
+
throw err;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* 从指定平台移除服务器
|
|
649
|
+
*/
|
|
650
|
+
async function removeServerFromPlatform(serverId, platform) {
|
|
651
|
+
try {
|
|
652
|
+
switch (platform) {
|
|
653
|
+
case 'claude':
|
|
654
|
+
removeFromClaudeConfig(serverId);
|
|
655
|
+
break;
|
|
656
|
+
case 'codex':
|
|
657
|
+
removeFromCodexConfig(serverId);
|
|
658
|
+
break;
|
|
659
|
+
case 'gemini':
|
|
660
|
+
removeFromGeminiConfig(serverId);
|
|
661
|
+
break;
|
|
662
|
+
case 'opencode':
|
|
663
|
+
removeFromOpenCodeConfig(serverId);
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
console.log(`[MCP] Removed "${serverId}" from ${platform}`);
|
|
667
|
+
} catch (err) {
|
|
668
|
+
console.error(`[MCP] Failed to remove "${serverId}" from ${platform}:`, err.message);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// ============================================================================
|
|
673
|
+
// Claude 配置同步
|
|
674
|
+
// ============================================================================
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* 同步到 Claude 配置
|
|
678
|
+
*/
|
|
679
|
+
function syncToClaudeConfig(server) {
|
|
680
|
+
const config = readJsonFile(CLAUDE_CONFIG_PATH, {});
|
|
681
|
+
|
|
682
|
+
if (!config.mcpServers) {
|
|
683
|
+
config.mcpServers = {};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// 只写入 server spec,不写入元数据
|
|
687
|
+
config.mcpServers[server.id] = extractServerSpec(server.server);
|
|
688
|
+
|
|
689
|
+
writeJsonFile(CLAUDE_CONFIG_PATH, config);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* 从 Claude 配置移除
|
|
694
|
+
*/
|
|
695
|
+
function removeFromClaudeConfig(serverId) {
|
|
696
|
+
const config = readJsonFile(CLAUDE_CONFIG_PATH, {});
|
|
697
|
+
|
|
698
|
+
if (config.mcpServers && config.mcpServers[serverId]) {
|
|
699
|
+
delete config.mcpServers[serverId];
|
|
700
|
+
writeJsonFile(CLAUDE_CONFIG_PATH, config);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// ============================================================================
|
|
705
|
+
// Codex 配置同步 (TOML 格式)
|
|
706
|
+
// ============================================================================
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* 同步到 Codex 配置
|
|
710
|
+
*/
|
|
711
|
+
function syncToCodexConfig(server) {
|
|
712
|
+
const config = readTomlFile(CODEX_CONFIG_PATH, {});
|
|
713
|
+
|
|
714
|
+
if (!config.mcp_servers) {
|
|
715
|
+
config.mcp_servers = {};
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// 转换为 Codex TOML 格式
|
|
719
|
+
config.mcp_servers[server.id] = convertToCodexFormat(server.server);
|
|
720
|
+
|
|
721
|
+
writeTomlFile(CODEX_CONFIG_PATH, config);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* 从 Codex 配置移除
|
|
726
|
+
*/
|
|
727
|
+
function removeFromCodexConfig(serverId) {
|
|
728
|
+
const config = readTomlFile(CODEX_CONFIG_PATH, {});
|
|
729
|
+
|
|
730
|
+
if (config.mcp_servers && config.mcp_servers[serverId]) {
|
|
731
|
+
delete config.mcp_servers[serverId];
|
|
732
|
+
writeTomlFile(CODEX_CONFIG_PATH, config);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* 转换为 Codex TOML 格式
|
|
738
|
+
*/
|
|
739
|
+
function convertToCodexFormat(spec) {
|
|
740
|
+
const result = {
|
|
741
|
+
type: spec.type || 'stdio'
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
if (result.type === 'stdio') {
|
|
745
|
+
result.command = spec.command || '';
|
|
746
|
+
if (spec.args && spec.args.length > 0) {
|
|
747
|
+
result.args = spec.args;
|
|
748
|
+
}
|
|
749
|
+
if (spec.env && Object.keys(spec.env).length > 0) {
|
|
750
|
+
result.env = spec.env;
|
|
751
|
+
}
|
|
752
|
+
if (spec.cwd) {
|
|
753
|
+
result.cwd = spec.cwd;
|
|
754
|
+
}
|
|
755
|
+
} else if (result.type === 'http' || result.type === 'sse') {
|
|
756
|
+
result.url = spec.url || '';
|
|
757
|
+
if (spec.headers && Object.keys(spec.headers).length > 0) {
|
|
758
|
+
result.http_headers = spec.headers;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return result;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// ============================================================================
|
|
766
|
+
// Gemini 配置同步
|
|
767
|
+
// ============================================================================
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* 同步到 Gemini 配置
|
|
771
|
+
*/
|
|
772
|
+
function syncToGeminiConfig(server) {
|
|
773
|
+
const config = readJsonFile(GEMINI_CONFIG_PATH, {});
|
|
774
|
+
|
|
775
|
+
if (!config.mcpServers) {
|
|
776
|
+
config.mcpServers = {};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// 只写入 server spec,不写入元数据
|
|
780
|
+
config.mcpServers[server.id] = extractServerSpec(server.server);
|
|
781
|
+
|
|
782
|
+
writeJsonFile(GEMINI_CONFIG_PATH, config);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* 从 Gemini 配置移除
|
|
787
|
+
*/
|
|
788
|
+
function removeFromGeminiConfig(serverId) {
|
|
789
|
+
const config = readJsonFile(GEMINI_CONFIG_PATH, {});
|
|
790
|
+
|
|
791
|
+
if (config.mcpServers && config.mcpServers[serverId]) {
|
|
792
|
+
delete config.mcpServers[serverId];
|
|
793
|
+
writeJsonFile(GEMINI_CONFIG_PATH, config);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// ============================================================================
|
|
798
|
+
// OpenCode 配置同步
|
|
799
|
+
// ============================================================================
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* 转换为 OpenCode 配置格式
|
|
803
|
+
*/
|
|
804
|
+
function convertToOpenCodeFormat(spec) {
|
|
805
|
+
const sourceType = spec.type || 'stdio';
|
|
806
|
+
|
|
807
|
+
if (sourceType === 'local' || sourceType === 'remote') {
|
|
808
|
+
const result = { ...spec };
|
|
809
|
+
result.enabled = spec.enabled !== false;
|
|
810
|
+
if (sourceType === 'local' && typeof result.command === 'string') {
|
|
811
|
+
result.command = result.command ? [result.command] : [];
|
|
812
|
+
}
|
|
813
|
+
return result;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (sourceType === 'stdio') {
|
|
817
|
+
const command = [];
|
|
818
|
+
if (spec.command) {
|
|
819
|
+
command.push(spec.command);
|
|
820
|
+
}
|
|
821
|
+
if (Array.isArray(spec.args) && spec.args.length > 0) {
|
|
822
|
+
command.push(...spec.args);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const result = {
|
|
826
|
+
type: 'local',
|
|
827
|
+
command,
|
|
828
|
+
enabled: true
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
if (spec.env && Object.keys(spec.env).length > 0) {
|
|
832
|
+
result.environment = spec.env;
|
|
833
|
+
}
|
|
834
|
+
if (spec.cwd) {
|
|
835
|
+
result.cwd = spec.cwd;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return result;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const result = {
|
|
842
|
+
type: 'remote',
|
|
843
|
+
url: spec.url || '',
|
|
844
|
+
enabled: true
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
if (spec.headers && Object.keys(spec.headers).length > 0) {
|
|
848
|
+
result.headers = spec.headers;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
return result;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* 从 OpenCode 格式转换到通用格式
|
|
856
|
+
*/
|
|
857
|
+
function convertFromOpenCodeFormat(spec) {
|
|
858
|
+
const sourceType = spec.type || (Array.isArray(spec.command) ? 'local' : 'remote');
|
|
859
|
+
|
|
860
|
+
if (sourceType === 'local') {
|
|
861
|
+
const result = { type: 'stdio' };
|
|
862
|
+
if (Array.isArray(spec.command) && spec.command.length > 0) {
|
|
863
|
+
result.command = spec.command[0];
|
|
864
|
+
if (spec.command.length > 1) {
|
|
865
|
+
result.args = spec.command.slice(1);
|
|
866
|
+
}
|
|
867
|
+
} else if (typeof spec.command === 'string') {
|
|
868
|
+
result.command = spec.command;
|
|
869
|
+
} else {
|
|
870
|
+
result.command = '';
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (spec.environment && typeof spec.environment === 'object') {
|
|
874
|
+
result.env = spec.environment;
|
|
875
|
+
} else if (spec.env && typeof spec.env === 'object') {
|
|
876
|
+
result.env = spec.env;
|
|
877
|
+
}
|
|
878
|
+
if (spec.cwd) {
|
|
879
|
+
result.cwd = spec.cwd;
|
|
880
|
+
}
|
|
881
|
+
return result;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (sourceType === 'remote') {
|
|
885
|
+
const result = {
|
|
886
|
+
type: 'http',
|
|
887
|
+
url: spec.url || ''
|
|
888
|
+
};
|
|
889
|
+
if (spec.headers && typeof spec.headers === 'object') {
|
|
890
|
+
result.headers = spec.headers;
|
|
891
|
+
}
|
|
892
|
+
return result;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// 已经是通用格式时直接兼容处理
|
|
896
|
+
if (sourceType === 'stdio' || sourceType === 'http' || sourceType === 'sse') {
|
|
897
|
+
return convertFromCodexFormat(spec);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
return {
|
|
901
|
+
type: 'stdio',
|
|
902
|
+
command: ''
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* 同步到 OpenCode 配置
|
|
908
|
+
*/
|
|
909
|
+
function syncToOpenCodeConfig(server) {
|
|
910
|
+
const { path: configPath, config } = readOpenCodeConfig();
|
|
911
|
+
const nextConfig = config && typeof config === 'object' ? config : {};
|
|
912
|
+
|
|
913
|
+
if (!nextConfig.mcp || typeof nextConfig.mcp !== 'object') {
|
|
914
|
+
nextConfig.mcp = {};
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
nextConfig.mcp[server.id] = convertToOpenCodeFormat(server.server);
|
|
918
|
+
writeOpenCodeConfig(configPath, nextConfig);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* 从 OpenCode 配置移除
|
|
923
|
+
*/
|
|
924
|
+
function removeFromOpenCodeConfig(serverId) {
|
|
925
|
+
const { path: configPath, config } = readOpenCodeConfig();
|
|
926
|
+
const nextConfig = config && typeof config === 'object' ? config : {};
|
|
927
|
+
|
|
928
|
+
if (nextConfig.mcp && nextConfig.mcp[serverId]) {
|
|
929
|
+
delete nextConfig.mcp[serverId];
|
|
930
|
+
writeOpenCodeConfig(configPath, nextConfig);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// ============================================================================
|
|
935
|
+
// 导入功能
|
|
936
|
+
// ============================================================================
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* 从指定平台导入 MCP 配置
|
|
940
|
+
*/
|
|
941
|
+
async function importFromPlatform(platform) {
|
|
942
|
+
let importedCount = 0;
|
|
943
|
+
const servers = getAllServers();
|
|
944
|
+
|
|
945
|
+
switch (platform) {
|
|
946
|
+
case 'claude':
|
|
947
|
+
importedCount = importFromClaude(servers);
|
|
948
|
+
break;
|
|
949
|
+
case 'codex':
|
|
950
|
+
importedCount = importFromCodex(servers);
|
|
951
|
+
break;
|
|
952
|
+
case 'gemini':
|
|
953
|
+
importedCount = importFromGemini(servers);
|
|
954
|
+
break;
|
|
955
|
+
case 'opencode':
|
|
956
|
+
importedCount = importFromOpenCode(servers);
|
|
957
|
+
break;
|
|
958
|
+
default:
|
|
959
|
+
throw new Error(`无效的平台: ${platform}`);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (importedCount > 0) {
|
|
963
|
+
writeJsonFile(MCP_SERVERS_FILE, servers);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
return importedCount;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* 从 Claude 导入
|
|
971
|
+
*/
|
|
972
|
+
function importFromClaude(servers) {
|
|
973
|
+
const config = readJsonFile(CLAUDE_CONFIG_PATH, {});
|
|
974
|
+
const mcpServers = config.mcpServers || {};
|
|
975
|
+
let count = 0;
|
|
976
|
+
|
|
977
|
+
for (const [id, spec] of Object.entries(mcpServers)) {
|
|
978
|
+
if (servers[id]) {
|
|
979
|
+
// 已存在,只启用 Claude
|
|
980
|
+
if (!servers[id].apps.claude) {
|
|
981
|
+
servers[id].apps.claude = true;
|
|
982
|
+
count++;
|
|
983
|
+
}
|
|
984
|
+
} else {
|
|
985
|
+
// 新服务器
|
|
986
|
+
servers[id] = {
|
|
987
|
+
id,
|
|
988
|
+
name: id,
|
|
989
|
+
server: spec,
|
|
990
|
+
apps: { claude: true, codex: false, gemini: false, opencode: false },
|
|
991
|
+
createdAt: Date.now(),
|
|
992
|
+
updatedAt: Date.now()
|
|
993
|
+
};
|
|
994
|
+
count++;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
return count;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* 从 Codex 导入
|
|
1003
|
+
*/
|
|
1004
|
+
function importFromCodex(servers) {
|
|
1005
|
+
const config = readTomlFile(CODEX_CONFIG_PATH, {});
|
|
1006
|
+
const mcpServers = config.mcp_servers || {};
|
|
1007
|
+
let count = 0;
|
|
1008
|
+
|
|
1009
|
+
for (const [id, spec] of Object.entries(mcpServers)) {
|
|
1010
|
+
// 转换 Codex 格式到通用格式
|
|
1011
|
+
const convertedSpec = convertFromCodexFormat(spec);
|
|
1012
|
+
|
|
1013
|
+
if (servers[id]) {
|
|
1014
|
+
// 已存在,只启用 Codex
|
|
1015
|
+
if (!servers[id].apps.codex) {
|
|
1016
|
+
servers[id].apps.codex = true;
|
|
1017
|
+
count++;
|
|
1018
|
+
}
|
|
1019
|
+
} else {
|
|
1020
|
+
// 新服务器
|
|
1021
|
+
servers[id] = {
|
|
1022
|
+
id,
|
|
1023
|
+
name: id,
|
|
1024
|
+
server: convertedSpec,
|
|
1025
|
+
apps: { claude: false, codex: true, gemini: false, opencode: false },
|
|
1026
|
+
createdAt: Date.now(),
|
|
1027
|
+
updatedAt: Date.now()
|
|
1028
|
+
};
|
|
1029
|
+
count++;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
return count;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* 从 Gemini 导入
|
|
1038
|
+
*/
|
|
1039
|
+
function importFromGemini(servers) {
|
|
1040
|
+
const config = readJsonFile(GEMINI_CONFIG_PATH, {});
|
|
1041
|
+
const mcpServers = config.mcpServers || {};
|
|
1042
|
+
let count = 0;
|
|
1043
|
+
|
|
1044
|
+
for (const [id, spec] of Object.entries(mcpServers)) {
|
|
1045
|
+
if (servers[id]) {
|
|
1046
|
+
// 已存在,只启用 Gemini
|
|
1047
|
+
if (!servers[id].apps.gemini) {
|
|
1048
|
+
servers[id].apps.gemini = true;
|
|
1049
|
+
count++;
|
|
1050
|
+
}
|
|
1051
|
+
} else {
|
|
1052
|
+
// 新服务器
|
|
1053
|
+
servers[id] = {
|
|
1054
|
+
id,
|
|
1055
|
+
name: id,
|
|
1056
|
+
server: spec,
|
|
1057
|
+
apps: { claude: false, codex: false, gemini: true, opencode: false },
|
|
1058
|
+
createdAt: Date.now(),
|
|
1059
|
+
updatedAt: Date.now()
|
|
1060
|
+
};
|
|
1061
|
+
count++;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
return count;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* 从 OpenCode 导入
|
|
1070
|
+
*/
|
|
1071
|
+
function importFromOpenCode(servers) {
|
|
1072
|
+
const { config } = readOpenCodeConfig();
|
|
1073
|
+
const mcpServers = config.mcp || {};
|
|
1074
|
+
let count = 0;
|
|
1075
|
+
|
|
1076
|
+
for (const [id, spec] of Object.entries(mcpServers)) {
|
|
1077
|
+
const convertedSpec = convertFromOpenCodeFormat(spec || {});
|
|
1078
|
+
|
|
1079
|
+
if (servers[id]) {
|
|
1080
|
+
if (!servers[id].apps.opencode) {
|
|
1081
|
+
servers[id].apps.opencode = true;
|
|
1082
|
+
count++;
|
|
1083
|
+
}
|
|
1084
|
+
} else {
|
|
1085
|
+
servers[id] = {
|
|
1086
|
+
id,
|
|
1087
|
+
name: id,
|
|
1088
|
+
server: convertedSpec,
|
|
1089
|
+
apps: { claude: false, codex: false, gemini: false, opencode: true },
|
|
1090
|
+
createdAt: Date.now(),
|
|
1091
|
+
updatedAt: Date.now()
|
|
1092
|
+
};
|
|
1093
|
+
count++;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
return count;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* 从 Codex 格式转换
|
|
1102
|
+
*/
|
|
1103
|
+
function convertFromCodexFormat(spec) {
|
|
1104
|
+
const result = {
|
|
1105
|
+
type: spec.type || 'stdio'
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
if (result.type === 'stdio') {
|
|
1109
|
+
result.command = spec.command || '';
|
|
1110
|
+
if (spec.args) {
|
|
1111
|
+
result.args = spec.args;
|
|
1112
|
+
}
|
|
1113
|
+
if (spec.env) {
|
|
1114
|
+
result.env = spec.env;
|
|
1115
|
+
}
|
|
1116
|
+
if (spec.cwd) {
|
|
1117
|
+
result.cwd = spec.cwd;
|
|
1118
|
+
}
|
|
1119
|
+
} else if (result.type === 'http' || result.type === 'sse') {
|
|
1120
|
+
result.url = spec.url || '';
|
|
1121
|
+
if (spec.http_headers) {
|
|
1122
|
+
result.headers = spec.http_headers;
|
|
1123
|
+
} else if (spec.headers) {
|
|
1124
|
+
result.headers = spec.headers;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
return result;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* 提取纯净的服务器规范(移除元数据)
|
|
1133
|
+
*/
|
|
1134
|
+
function extractServerSpec(spec) {
|
|
1135
|
+
const result = { ...spec };
|
|
1136
|
+
// 移除可能存在的非规范字段
|
|
1137
|
+
delete result.id;
|
|
1138
|
+
delete result.name;
|
|
1139
|
+
delete result.description;
|
|
1140
|
+
delete result.tags;
|
|
1141
|
+
delete result.homepage;
|
|
1142
|
+
delete result.docs;
|
|
1143
|
+
delete result.apps;
|
|
1144
|
+
delete result.createdAt;
|
|
1145
|
+
delete result.updatedAt;
|
|
1146
|
+
return result;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* 获取统计信息
|
|
1151
|
+
*/
|
|
1152
|
+
function getStats() {
|
|
1153
|
+
const servers = getAllServers();
|
|
1154
|
+
const serverList = Object.values(servers);
|
|
1155
|
+
|
|
1156
|
+
return {
|
|
1157
|
+
total: serverList.length,
|
|
1158
|
+
claude: serverList.filter(s => s.apps?.claude).length,
|
|
1159
|
+
codex: serverList.filter(s => s.apps?.codex).length,
|
|
1160
|
+
gemini: serverList.filter(s => s.apps?.gemini).length,
|
|
1161
|
+
opencode: serverList.filter(s => s.apps?.opencode).length
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// ============================================================================
|
|
1166
|
+
// 服务器测试功能
|
|
1167
|
+
// ============================================================================
|
|
1168
|
+
|
|
1169
|
+
/**
|
|
1170
|
+
* 测试 MCP 服务器连接
|
|
1171
|
+
* @param {string} serverId - 服务器 ID
|
|
1172
|
+
* @returns {Promise<{success: boolean, message: string, duration?: number}>}
|
|
1173
|
+
*/
|
|
1174
|
+
async function testServer(serverId) {
|
|
1175
|
+
const server = getServer(serverId);
|
|
1176
|
+
if (!server) {
|
|
1177
|
+
throw new Error(`MCP 服务器 "${serverId}" 不存在`);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
const spec = server.server;
|
|
1181
|
+
const type = spec.type || 'stdio';
|
|
1182
|
+
const startTime = Date.now();
|
|
1183
|
+
|
|
1184
|
+
try {
|
|
1185
|
+
if (type === 'stdio') {
|
|
1186
|
+
return await testStdioServer(spec);
|
|
1187
|
+
} else if (type === 'http' || type === 'sse') {
|
|
1188
|
+
return await testHttpServer(spec);
|
|
1189
|
+
} else {
|
|
1190
|
+
return { success: false, message: `不支持的服务器类型: ${type}` };
|
|
1191
|
+
}
|
|
1192
|
+
} catch (err) {
|
|
1193
|
+
return {
|
|
1194
|
+
success: false,
|
|
1195
|
+
message: err.message,
|
|
1196
|
+
duration: Date.now() - startTime
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/**
|
|
1202
|
+
* 测试 stdio 类型服务器
|
|
1203
|
+
*/
|
|
1204
|
+
async function testStdioServer(spec) {
|
|
1205
|
+
return new Promise((resolve) => {
|
|
1206
|
+
const startTime = Date.now();
|
|
1207
|
+
const timeout = 10000; // 10 秒超时
|
|
1208
|
+
|
|
1209
|
+
// 检查命令是否存在
|
|
1210
|
+
const command = spec.command;
|
|
1211
|
+
const args = spec.args || [];
|
|
1212
|
+
|
|
1213
|
+
let child;
|
|
1214
|
+
let resolved = false;
|
|
1215
|
+
let stdout = '';
|
|
1216
|
+
let stderr = '';
|
|
1217
|
+
|
|
1218
|
+
const cleanup = () => {
|
|
1219
|
+
if (child && !child.killed) {
|
|
1220
|
+
child.kill('SIGTERM');
|
|
1221
|
+
setTimeout(() => {
|
|
1222
|
+
if (!child.killed) child.kill('SIGKILL');
|
|
1223
|
+
}, 1000);
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
|
|
1227
|
+
const done = (result) => {
|
|
1228
|
+
if (resolved) return;
|
|
1229
|
+
resolved = true;
|
|
1230
|
+
cleanup();
|
|
1231
|
+
resolve(result);
|
|
1232
|
+
};
|
|
1233
|
+
|
|
1234
|
+
try {
|
|
1235
|
+
child = spawn(command, args, {
|
|
1236
|
+
env: { ...process.env, ...spec.env },
|
|
1237
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1238
|
+
cwd: spec.cwd || process.cwd()
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
child.stdout.on('data', (data) => {
|
|
1242
|
+
stdout += data.toString();
|
|
1243
|
+
// MCP 服务器启动成功通常会输出 JSON-RPC 相关内容
|
|
1244
|
+
if (stdout.includes('{') || stdout.length > 0) {
|
|
1245
|
+
done({
|
|
1246
|
+
success: true,
|
|
1247
|
+
message: '服务器启动成功',
|
|
1248
|
+
duration: Date.now() - startTime
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
child.stderr.on('data', (data) => {
|
|
1254
|
+
stderr += data.toString();
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
child.on('error', (err) => {
|
|
1258
|
+
if (err.code === 'ENOENT') {
|
|
1259
|
+
done({
|
|
1260
|
+
success: false,
|
|
1261
|
+
message: `命令 "${command}" 未找到,请确保已安装`,
|
|
1262
|
+
duration: Date.now() - startTime
|
|
1263
|
+
});
|
|
1264
|
+
} else {
|
|
1265
|
+
done({
|
|
1266
|
+
success: false,
|
|
1267
|
+
message: `启动失败: ${err.message}`,
|
|
1268
|
+
duration: Date.now() - startTime
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
child.on('close', (code) => {
|
|
1274
|
+
if (code === 0 || stdout.length > 0) {
|
|
1275
|
+
done({
|
|
1276
|
+
success: true,
|
|
1277
|
+
message: '服务器测试通过',
|
|
1278
|
+
duration: Date.now() - startTime
|
|
1279
|
+
});
|
|
1280
|
+
} else {
|
|
1281
|
+
done({
|
|
1282
|
+
success: false,
|
|
1283
|
+
message: stderr || `进程退出码: ${code}`,
|
|
1284
|
+
duration: Date.now() - startTime
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
// 超时处理
|
|
1290
|
+
setTimeout(() => {
|
|
1291
|
+
// 如果进程还在运行,说明服务器正常启动了
|
|
1292
|
+
if (!resolved && child && !child.killed) {
|
|
1293
|
+
done({
|
|
1294
|
+
success: true,
|
|
1295
|
+
message: '服务器正常运行中',
|
|
1296
|
+
duration: Date.now() - startTime
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
}, 3000); // 3 秒后如果还在运行就认为成功
|
|
1300
|
+
|
|
1301
|
+
// 最终超时
|
|
1302
|
+
setTimeout(() => {
|
|
1303
|
+
done({
|
|
1304
|
+
success: false,
|
|
1305
|
+
message: '测试超时',
|
|
1306
|
+
duration: timeout
|
|
1307
|
+
});
|
|
1308
|
+
}, timeout);
|
|
1309
|
+
|
|
1310
|
+
} catch (err) {
|
|
1311
|
+
done({
|
|
1312
|
+
success: false,
|
|
1313
|
+
message: `测试失败: ${err.message}`,
|
|
1314
|
+
duration: Date.now() - startTime
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* 测试 http/sse 类型服务器
|
|
1322
|
+
*/
|
|
1323
|
+
async function testHttpServer(spec) {
|
|
1324
|
+
return new Promise((resolve) => {
|
|
1325
|
+
const startTime = Date.now();
|
|
1326
|
+
const timeout = 10000;
|
|
1327
|
+
|
|
1328
|
+
try {
|
|
1329
|
+
const url = new URL(spec.url);
|
|
1330
|
+
const isHttps = url.protocol === 'https:';
|
|
1331
|
+
const client = isHttps ? https : http;
|
|
1332
|
+
|
|
1333
|
+
const options = {
|
|
1334
|
+
hostname: url.hostname,
|
|
1335
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
1336
|
+
path: url.pathname + url.search,
|
|
1337
|
+
method: 'GET',
|
|
1338
|
+
timeout: timeout,
|
|
1339
|
+
headers: {
|
|
1340
|
+
...spec.headers
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
|
|
1344
|
+
const req = client.request(options, (res) => {
|
|
1345
|
+
resolve({
|
|
1346
|
+
success: res.statusCode >= 200 && res.statusCode < 500,
|
|
1347
|
+
message: res.statusCode >= 200 && res.statusCode < 400
|
|
1348
|
+
? `服务器响应正常 (HTTP ${res.statusCode})`
|
|
1349
|
+
: `服务器响应异常 (HTTP ${res.statusCode})`,
|
|
1350
|
+
duration: Date.now() - startTime
|
|
1351
|
+
});
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
req.on('error', (err) => {
|
|
1355
|
+
resolve({
|
|
1356
|
+
success: false,
|
|
1357
|
+
message: `连接失败: ${err.message}`,
|
|
1358
|
+
duration: Date.now() - startTime
|
|
1359
|
+
});
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
req.on('timeout', () => {
|
|
1363
|
+
req.destroy();
|
|
1364
|
+
resolve({
|
|
1365
|
+
success: false,
|
|
1366
|
+
message: '连接超时',
|
|
1367
|
+
duration: timeout
|
|
1368
|
+
});
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
req.end();
|
|
1372
|
+
} catch (err) {
|
|
1373
|
+
resolve({
|
|
1374
|
+
success: false,
|
|
1375
|
+
message: `URL 无效: ${err.message}`,
|
|
1376
|
+
duration: Date.now() - startTime
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
/**
|
|
1383
|
+
* Get tools list from MCP server
|
|
1384
|
+
* @param {string} serverId - Server ID from config
|
|
1385
|
+
* @returns {Promise<{tools: Array, duration: number, status: string}>}
|
|
1386
|
+
*/
|
|
1387
|
+
async function getServerTools(serverId) {
|
|
1388
|
+
const server = getServer(serverId);
|
|
1389
|
+
if (!server) {
|
|
1390
|
+
throw new Error(`MCP 服务器 "${serverId}" 不存在`);
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
const startTime = Date.now();
|
|
1394
|
+
const spec = server.server;
|
|
1395
|
+
|
|
1396
|
+
try {
|
|
1397
|
+
// Check if we have a cached connection
|
|
1398
|
+
const cached = mcpClientPool.get(serverId);
|
|
1399
|
+
const now = Date.now();
|
|
1400
|
+
|
|
1401
|
+
let client;
|
|
1402
|
+
let needsInitialization = false;
|
|
1403
|
+
|
|
1404
|
+
if (cached && now - cached.timestamp < POOL_TTL && cached.client.connected) {
|
|
1405
|
+
// Reuse existing connection
|
|
1406
|
+
client = cached.client;
|
|
1407
|
+
console.log(`[MCP] Reusing pooled connection for "${serverId}"`);
|
|
1408
|
+
} else {
|
|
1409
|
+
// Create new connection
|
|
1410
|
+
if (cached) {
|
|
1411
|
+
// Clean up expired connection
|
|
1412
|
+
try {
|
|
1413
|
+
await cached.client.disconnect();
|
|
1414
|
+
} catch (err) {
|
|
1415
|
+
console.error(`[MCP] Error disconnecting expired client: ${err.message}`);
|
|
1416
|
+
}
|
|
1417
|
+
mcpClientPool.delete(serverId);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// Create new client with 10s timeout
|
|
1421
|
+
client = new McpClient(spec, { timeout: 10000 });
|
|
1422
|
+
needsInitialization = true;
|
|
1423
|
+
console.log(`[MCP] Creating new connection for "${serverId}"`);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// Connect and initialize if needed
|
|
1427
|
+
if (needsInitialization) {
|
|
1428
|
+
await client.connect();
|
|
1429
|
+
await client.initialize();
|
|
1430
|
+
|
|
1431
|
+
// Cache the connection
|
|
1432
|
+
mcpClientPool.set(serverId, {
|
|
1433
|
+
client,
|
|
1434
|
+
timestamp: Date.now()
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// Get tools list
|
|
1439
|
+
const tools = await client.listTools();
|
|
1440
|
+
|
|
1441
|
+
return {
|
|
1442
|
+
tools,
|
|
1443
|
+
duration: Date.now() - startTime,
|
|
1444
|
+
status: 'online'
|
|
1445
|
+
};
|
|
1446
|
+
|
|
1447
|
+
} catch (err) {
|
|
1448
|
+
// Clean up failed connection from pool
|
|
1449
|
+
const cached = mcpClientPool.get(serverId);
|
|
1450
|
+
if (cached) {
|
|
1451
|
+
try {
|
|
1452
|
+
await cached.client.disconnect();
|
|
1453
|
+
} catch (e) {
|
|
1454
|
+
// ignore
|
|
1455
|
+
}
|
|
1456
|
+
mcpClientPool.delete(serverId);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
return {
|
|
1460
|
+
tools: [],
|
|
1461
|
+
duration: Date.now() - startTime,
|
|
1462
|
+
status: 'error',
|
|
1463
|
+
error: err.message
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
/**
|
|
1469
|
+
* Execute a tool on MCP server
|
|
1470
|
+
* @param {string} serverId - Server ID
|
|
1471
|
+
* @param {string} toolName - Tool name
|
|
1472
|
+
* @param {Object} arguments - Tool arguments
|
|
1473
|
+
* @returns {Promise<{result: Object, duration: number, isError: boolean, truncated?: boolean, truncatedSize?: number}>}
|
|
1474
|
+
*/
|
|
1475
|
+
async function callServerTool(serverId, toolName, arguments = {}) {
|
|
1476
|
+
const server = getServer(serverId);
|
|
1477
|
+
if (!server) {
|
|
1478
|
+
throw new Error(`MCP 服务器 "${serverId}" 不存在`);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
const startTime = Date.now();
|
|
1482
|
+
const spec = server.server;
|
|
1483
|
+
|
|
1484
|
+
try {
|
|
1485
|
+
// Check if we have a cached connection
|
|
1486
|
+
const cached = mcpClientPool.get(serverId);
|
|
1487
|
+
const now = Date.now();
|
|
1488
|
+
|
|
1489
|
+
let client;
|
|
1490
|
+
let needsInitialization = false;
|
|
1491
|
+
|
|
1492
|
+
if (cached && now - cached.timestamp < POOL_TTL && cached.client.connected) {
|
|
1493
|
+
// Reuse existing connection
|
|
1494
|
+
client = cached.client;
|
|
1495
|
+
// Update timestamp
|
|
1496
|
+
cached.timestamp = now;
|
|
1497
|
+
console.log(`[MCP] Reusing pooled connection for "${serverId}"`);
|
|
1498
|
+
} else {
|
|
1499
|
+
// Create new connection
|
|
1500
|
+
if (cached) {
|
|
1501
|
+
// Clean up expired connection
|
|
1502
|
+
try {
|
|
1503
|
+
await cached.client.disconnect();
|
|
1504
|
+
} catch (err) {
|
|
1505
|
+
console.error(`[MCP] Error disconnecting expired client: ${err.message}`);
|
|
1506
|
+
}
|
|
1507
|
+
mcpClientPool.delete(serverId);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// Create new client with 30s timeout
|
|
1511
|
+
client = new McpClient(spec, { timeout: 30000 });
|
|
1512
|
+
needsInitialization = true;
|
|
1513
|
+
console.log(`[MCP] Creating new connection for "${serverId}"`);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// Connect and initialize if needed
|
|
1517
|
+
if (needsInitialization) {
|
|
1518
|
+
await client.connect();
|
|
1519
|
+
await client.initialize();
|
|
1520
|
+
|
|
1521
|
+
// Cache the connection
|
|
1522
|
+
mcpClientPool.set(serverId, {
|
|
1523
|
+
client,
|
|
1524
|
+
timestamp: Date.now()
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
// Call the tool
|
|
1529
|
+
const result = await client.callTool(toolName, arguments);
|
|
1530
|
+
|
|
1531
|
+
const duration = Date.now() - startTime;
|
|
1532
|
+
|
|
1533
|
+
// Check result size, truncate if > 10KB
|
|
1534
|
+
const resultStr = JSON.stringify(result);
|
|
1535
|
+
if (resultStr.length > 10 * 1024) {
|
|
1536
|
+
return {
|
|
1537
|
+
result: {
|
|
1538
|
+
...result,
|
|
1539
|
+
truncated: true
|
|
1540
|
+
},
|
|
1541
|
+
truncatedSize: resultStr.length,
|
|
1542
|
+
duration,
|
|
1543
|
+
isError: result.isError || false
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
return {
|
|
1548
|
+
result,
|
|
1549
|
+
duration,
|
|
1550
|
+
isError: result.isError || false
|
|
1551
|
+
};
|
|
1552
|
+
|
|
1553
|
+
} catch (err) {
|
|
1554
|
+
// Clean up failed connection from pool
|
|
1555
|
+
const cached = mcpClientPool.get(serverId);
|
|
1556
|
+
if (cached) {
|
|
1557
|
+
try {
|
|
1558
|
+
await cached.client.disconnect();
|
|
1559
|
+
} catch (e) {
|
|
1560
|
+
// ignore
|
|
1561
|
+
}
|
|
1562
|
+
mcpClientPool.delete(serverId);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
return {
|
|
1566
|
+
result: {
|
|
1567
|
+
error: err.message,
|
|
1568
|
+
code: err.code,
|
|
1569
|
+
data: err.data
|
|
1570
|
+
},
|
|
1571
|
+
duration: Date.now() - startTime,
|
|
1572
|
+
isError: true
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
/**
|
|
1578
|
+
* 更新服务器状态
|
|
1579
|
+
*/
|
|
1580
|
+
async function updateServerStatus(serverId, status) {
|
|
1581
|
+
const servers = getAllServers();
|
|
1582
|
+
const server = servers[serverId];
|
|
1583
|
+
|
|
1584
|
+
if (!server) {
|
|
1585
|
+
throw new Error(`MCP 服务器 "${serverId}" 不存在`);
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
server.status = status;
|
|
1589
|
+
server.lastChecked = Date.now();
|
|
1590
|
+
|
|
1591
|
+
writeJsonFile(MCP_SERVERS_FILE, servers);
|
|
1592
|
+
return server;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// ============================================================================
|
|
1596
|
+
// 排序功能
|
|
1597
|
+
// ============================================================================
|
|
1598
|
+
|
|
1599
|
+
/**
|
|
1600
|
+
* 更新服务器排序
|
|
1601
|
+
* @param {string[]} serverIds - 按顺序排列的服务器 ID 数组
|
|
1602
|
+
*/
|
|
1603
|
+
function updateServerOrder(serverIds) {
|
|
1604
|
+
const servers = getAllServers();
|
|
1605
|
+
|
|
1606
|
+
// 更新每个服务器的排序索引
|
|
1607
|
+
serverIds.forEach((id, index) => {
|
|
1608
|
+
if (servers[id]) {
|
|
1609
|
+
servers[id].order = index;
|
|
1610
|
+
}
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
writeJsonFile(MCP_SERVERS_FILE, servers);
|
|
1614
|
+
return servers;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// ============================================================================
|
|
1618
|
+
// 导出功能
|
|
1619
|
+
// ============================================================================
|
|
1620
|
+
|
|
1621
|
+
/**
|
|
1622
|
+
* 导出所有 MCP 配置
|
|
1623
|
+
* @param {string} format - 导出格式: 'json' | 'claude' | 'codex' | 'opencode'
|
|
1624
|
+
*/
|
|
1625
|
+
function exportServers(format = 'json') {
|
|
1626
|
+
const servers = getAllServers();
|
|
1627
|
+
|
|
1628
|
+
switch (format) {
|
|
1629
|
+
case 'claude':
|
|
1630
|
+
return exportForClaude(servers);
|
|
1631
|
+
case 'codex':
|
|
1632
|
+
return exportForCodex(servers);
|
|
1633
|
+
case 'opencode':
|
|
1634
|
+
return exportForOpenCode(servers);
|
|
1635
|
+
case 'json':
|
|
1636
|
+
default:
|
|
1637
|
+
return exportAsJson(servers);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
/**
|
|
1642
|
+
* 导出为通用 JSON 格式
|
|
1643
|
+
*/
|
|
1644
|
+
function exportAsJson(servers) {
|
|
1645
|
+
const mcpServers = {};
|
|
1646
|
+
|
|
1647
|
+
for (const [id, server] of Object.entries(servers)) {
|
|
1648
|
+
mcpServers[id] = extractServerSpec(server.server);
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
return {
|
|
1652
|
+
format: 'json',
|
|
1653
|
+
content: JSON.stringify({ mcpServers }, null, 2),
|
|
1654
|
+
filename: 'mcp-servers.json'
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
/**
|
|
1659
|
+
* 导出为 Claude 格式
|
|
1660
|
+
*/
|
|
1661
|
+
function exportForClaude(servers) {
|
|
1662
|
+
const mcpServers = {};
|
|
1663
|
+
|
|
1664
|
+
for (const [id, server] of Object.entries(servers)) {
|
|
1665
|
+
if (server.apps?.claude) {
|
|
1666
|
+
mcpServers[id] = extractServerSpec(server.server);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
return {
|
|
1671
|
+
format: 'claude',
|
|
1672
|
+
content: JSON.stringify({ mcpServers }, null, 2),
|
|
1673
|
+
filename: 'claude-mcp-config.json'
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
/**
|
|
1678
|
+
* 导出为 Codex 格式
|
|
1679
|
+
*/
|
|
1680
|
+
function exportForCodex(servers) {
|
|
1681
|
+
const mcp_servers = {};
|
|
1682
|
+
|
|
1683
|
+
for (const [id, server] of Object.entries(servers)) {
|
|
1684
|
+
if (server.apps?.codex) {
|
|
1685
|
+
mcp_servers[id] = convertToCodexFormat(server.server);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
return {
|
|
1690
|
+
format: 'codex',
|
|
1691
|
+
content: toml.stringify({ mcp_servers }),
|
|
1692
|
+
filename: 'codex-mcp-config.toml'
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
/**
|
|
1697
|
+
* 导出为 OpenCode 格式
|
|
1698
|
+
*/
|
|
1699
|
+
function exportForOpenCode(servers) {
|
|
1700
|
+
const mcp = {};
|
|
1701
|
+
|
|
1702
|
+
for (const [id, server] of Object.entries(servers)) {
|
|
1703
|
+
if (server.apps?.opencode) {
|
|
1704
|
+
mcp[id] = convertToOpenCodeFormat(server.server);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
return {
|
|
1709
|
+
format: 'opencode',
|
|
1710
|
+
content: JSON.stringify({ mcp }, null, 2),
|
|
1711
|
+
filename: 'opencode-mcp-config.json'
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
module.exports = {
|
|
1716
|
+
getAllServers,
|
|
1717
|
+
getServer,
|
|
1718
|
+
saveServer,
|
|
1719
|
+
deleteServer,
|
|
1720
|
+
toggleServerApp,
|
|
1721
|
+
getPresets,
|
|
1722
|
+
importFromPlatform,
|
|
1723
|
+
getStats,
|
|
1724
|
+
validateServerSpec,
|
|
1725
|
+
// 新增功能
|
|
1726
|
+
testServer,
|
|
1727
|
+
getServerTools,
|
|
1728
|
+
callServerTool,
|
|
1729
|
+
updateServerStatus,
|
|
1730
|
+
updateServerOrder,
|
|
1731
|
+
exportServers
|
|
1732
|
+
};
|