@adversity/coding-tool-x 2.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 +333 -0
- package/LICENSE +21 -0
- package/README.md +404 -0
- package/bin/ctx.js +8 -0
- package/dist/web/assets/index-D1AYlFLZ.js +3220 -0
- package/dist/web/assets/index-aL3cKxSK.css +41 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/index.html +14 -0
- package/dist/web/logo.png +0 -0
- package/docs/CHANGELOG.md +582 -0
- package/docs/DIRECTORY_MIGRATION.md +112 -0
- package/docs/PROJECT_STRUCTURE.md +396 -0
- package/docs/bannel.png +0 -0
- package/docs/home.png +0 -0
- package/docs/logo.png +0 -0
- package/docs/multi-channel-load-balancing.md +249 -0
- package/package.json +73 -0
- package/src/commands/channels.js +504 -0
- package/src/commands/cli-type.js +99 -0
- package/src/commands/daemon.js +286 -0
- package/src/commands/doctor.js +332 -0
- package/src/commands/list.js +222 -0
- package/src/commands/logs.js +259 -0
- package/src/commands/port-config.js +115 -0
- package/src/commands/proxy-control.js +258 -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/stats.js +224 -0
- package/src/commands/switch.js +48 -0
- package/src/commands/toggle-proxy.js +222 -0
- package/src/commands/ui.js +92 -0
- package/src/commands/workspace.js +454 -0
- package/src/config/default.js +40 -0
- package/src/config/loader.js +75 -0
- package/src/config/paths.js +121 -0
- package/src/index.js +373 -0
- package/src/reset-config.js +92 -0
- package/src/server/api/agents.js +248 -0
- package/src/server/api/aliases.js +36 -0
- package/src/server/api/channels.js +258 -0
- package/src/server/api/claude-hooks.js +480 -0
- package/src/server/api/codex-channels.js +312 -0
- package/src/server/api/codex-projects.js +91 -0
- package/src/server/api/codex-proxy.js +182 -0
- package/src/server/api/codex-sessions.js +491 -0
- package/src/server/api/codex-statistics.js +57 -0
- package/src/server/api/commands.js +245 -0
- package/src/server/api/config-templates.js +182 -0
- package/src/server/api/config.js +147 -0
- package/src/server/api/convert.js +127 -0
- package/src/server/api/dashboard.js +125 -0
- package/src/server/api/env.js +144 -0
- package/src/server/api/favorites.js +77 -0
- package/src/server/api/gemini-channels.js +261 -0
- package/src/server/api/gemini-projects.js +91 -0
- package/src/server/api/gemini-proxy.js +160 -0
- package/src/server/api/gemini-sessions.js +397 -0
- package/src/server/api/gemini-statistics.js +57 -0
- package/src/server/api/health-check.js +118 -0
- package/src/server/api/mcp.js +336 -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 +235 -0
- package/src/server/api/rules.js +271 -0
- package/src/server/api/sessions.js +595 -0
- package/src/server/api/settings.js +61 -0
- package/src/server/api/skills.js +305 -0
- package/src/server/api/statistics.js +91 -0
- package/src/server/api/terminal.js +202 -0
- package/src/server/api/ui-config.js +64 -0
- package/src/server/api/workspaces.js +407 -0
- package/src/server/codex-proxy-server.js +538 -0
- package/src/server/dev-server.js +26 -0
- package/src/server/gemini-proxy-server.js +518 -0
- package/src/server/index.js +305 -0
- package/src/server/proxy-server.js +469 -0
- package/src/server/services/agents-service.js +354 -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 +234 -0
- package/src/server/services/channels.js +347 -0
- package/src/server/services/codex-channels.js +625 -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 +665 -0
- package/src/server/services/codex-settings-manager.js +397 -0
- package/src/server/services/codex-speed-test-template.json +24 -0
- package/src/server/services/codex-statistics-service.js +255 -0
- package/src/server/services/commands-service.js +360 -0
- package/src/server/services/config-templates-service.js +732 -0
- package/src/server/services/env-checker.js +307 -0
- package/src/server/services/env-manager.js +300 -0
- package/src/server/services/favorites.js +163 -0
- package/src/server/services/gemini-channels.js +333 -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 +253 -0
- package/src/server/services/health-check.js +399 -0
- package/src/server/services/mcp-service.js +1188 -0
- package/src/server/services/prompts-service.js +492 -0
- package/src/server/services/proxy-runtime.js +79 -0
- package/src/server/services/pty-manager.js +435 -0
- package/src/server/services/rules-service.js +401 -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 +757 -0
- package/src/server/services/settings-manager.js +163 -0
- package/src/server/services/skill-service.js +965 -0
- package/src/server/services/speed-test.js +545 -0
- package/src/server/services/statistics-service.js +386 -0
- package/src/server/services/terminal-commands.js +155 -0
- package/src/server/services/terminal-config.js +140 -0
- package/src/server/services/terminal-detector.js +306 -0
- package/src/server/services/ui-config.js +130 -0
- package/src/server/services/workspace-service.js +662 -0
- package/src/server/utils/pricing.js +41 -0
- package/src/server/websocket-server.js +557 -0
- package/src/ui/menu.js +129 -0
- package/src/ui/prompts.js +100 -0
- package/src/utils/format.js +43 -0
- package/src/utils/port-helper.js +94 -0
- package/src/utils/session.js +239 -0
|
@@ -0,0 +1,1188 @@
|
|
|
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
|
+
|
|
15
|
+
// MCP 配置文件路径
|
|
16
|
+
const CC_TOOL_DIR = path.join(os.homedir(), '.claude', 'cc-tool');
|
|
17
|
+
const MCP_SERVERS_FILE = path.join(CC_TOOL_DIR, 'mcp-servers.json');
|
|
18
|
+
|
|
19
|
+
// 各平台配置文件路径
|
|
20
|
+
const CLAUDE_CONFIG_PATH = path.join(os.homedir(), '.claude.json');
|
|
21
|
+
const CODEX_CONFIG_PATH = path.join(os.homedir(), '.codex', 'config.toml');
|
|
22
|
+
const GEMINI_CONFIG_PATH = path.join(os.homedir(), '.gemini', 'settings.json');
|
|
23
|
+
|
|
24
|
+
// MCP 预设模板
|
|
25
|
+
const MCP_PRESETS = [
|
|
26
|
+
{
|
|
27
|
+
id: 'fetch',
|
|
28
|
+
name: 'mcp-server-fetch',
|
|
29
|
+
description: '获取网页内容',
|
|
30
|
+
tags: ['http', 'web', 'fetch'],
|
|
31
|
+
server: {
|
|
32
|
+
type: 'stdio',
|
|
33
|
+
command: 'uvx',
|
|
34
|
+
args: ['mcp-server-fetch']
|
|
35
|
+
},
|
|
36
|
+
homepage: 'https://github.com/modelcontextprotocol/servers',
|
|
37
|
+
docs: 'https://github.com/modelcontextprotocol/servers/tree/main/src/fetch'
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'time',
|
|
41
|
+
name: '@modelcontextprotocol/server-time',
|
|
42
|
+
description: '获取当前时间和时区信息',
|
|
43
|
+
tags: ['time', 'utility'],
|
|
44
|
+
server: {
|
|
45
|
+
type: 'stdio',
|
|
46
|
+
command: 'npx',
|
|
47
|
+
args: ['-y', '@modelcontextprotocol/server-time']
|
|
48
|
+
},
|
|
49
|
+
homepage: 'https://github.com/modelcontextprotocol/servers',
|
|
50
|
+
docs: 'https://github.com/modelcontextprotocol/servers/tree/main/src/time'
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'memory',
|
|
54
|
+
name: '@modelcontextprotocol/server-memory',
|
|
55
|
+
description: '知识图谱记忆存储',
|
|
56
|
+
tags: ['memory', 'graph', 'knowledge'],
|
|
57
|
+
server: {
|
|
58
|
+
type: 'stdio',
|
|
59
|
+
command: 'npx',
|
|
60
|
+
args: ['-y', '@modelcontextprotocol/server-memory']
|
|
61
|
+
},
|
|
62
|
+
homepage: 'https://github.com/modelcontextprotocol/servers',
|
|
63
|
+
docs: 'https://github.com/modelcontextprotocol/servers/tree/main/src/memory'
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'sequential-thinking',
|
|
67
|
+
name: '@modelcontextprotocol/server-sequential-thinking',
|
|
68
|
+
description: '顺序思维推理',
|
|
69
|
+
tags: ['thinking', 'reasoning'],
|
|
70
|
+
server: {
|
|
71
|
+
type: 'stdio',
|
|
72
|
+
command: 'npx',
|
|
73
|
+
args: ['-y', '@modelcontextprotocol/server-sequential-thinking']
|
|
74
|
+
},
|
|
75
|
+
homepage: 'https://github.com/modelcontextprotocol/servers',
|
|
76
|
+
docs: 'https://github.com/modelcontextprotocol/servers/tree/main/src/sequentialthinking'
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: 'filesystem',
|
|
80
|
+
name: '@anthropic/mcp-server-filesystem',
|
|
81
|
+
description: '文件系统读写访问',
|
|
82
|
+
tags: ['filesystem', 'files'],
|
|
83
|
+
server: {
|
|
84
|
+
type: 'stdio',
|
|
85
|
+
command: 'npx',
|
|
86
|
+
args: ['-y', '@anthropic/mcp-server-filesystem', '/tmp']
|
|
87
|
+
},
|
|
88
|
+
homepage: 'https://github.com/anthropics/anthropic-quickstarts',
|
|
89
|
+
docs: 'https://github.com/anthropics/anthropic-quickstarts/tree/main/mcp-server-filesystem'
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: 'context7',
|
|
93
|
+
name: '@upstash/context7-mcp',
|
|
94
|
+
description: '文档搜索和上下文增强',
|
|
95
|
+
tags: ['docs', 'search', 'context'],
|
|
96
|
+
server: {
|
|
97
|
+
type: 'stdio',
|
|
98
|
+
command: 'npx',
|
|
99
|
+
args: ['-y', '@upstash/context7-mcp']
|
|
100
|
+
},
|
|
101
|
+
homepage: 'https://context7.com',
|
|
102
|
+
docs: 'https://github.com/upstash/context7/blob/master/README.md'
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: 'brave-search',
|
|
106
|
+
name: '@anthropic/mcp-server-brave-search',
|
|
107
|
+
description: 'Brave 搜索引擎',
|
|
108
|
+
tags: ['search', 'web'],
|
|
109
|
+
server: {
|
|
110
|
+
type: 'stdio',
|
|
111
|
+
command: 'npx',
|
|
112
|
+
args: ['-y', '@anthropic/mcp-server-brave-search'],
|
|
113
|
+
env: {
|
|
114
|
+
BRAVE_API_KEY: '<your-api-key>'
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
homepage: 'https://github.com/anthropics/anthropic-quickstarts',
|
|
118
|
+
docs: 'https://brave.com/search/api/'
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 'github',
|
|
122
|
+
name: '@modelcontextprotocol/server-github',
|
|
123
|
+
description: 'GitHub API 集成',
|
|
124
|
+
tags: ['github', 'git', 'api'],
|
|
125
|
+
server: {
|
|
126
|
+
type: 'stdio',
|
|
127
|
+
command: 'npx',
|
|
128
|
+
args: ['-y', '@modelcontextprotocol/server-github'],
|
|
129
|
+
env: {
|
|
130
|
+
GITHUB_PERSONAL_ACCESS_TOKEN: '<your-token>'
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
homepage: 'https://github.com/modelcontextprotocol/servers',
|
|
134
|
+
docs: 'https://github.com/modelcontextprotocol/servers/tree/main/src/github'
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: 'puppeteer',
|
|
138
|
+
name: '@anthropic/mcp-server-puppeteer',
|
|
139
|
+
description: '浏览器自动化',
|
|
140
|
+
tags: ['browser', 'automation', 'web'],
|
|
141
|
+
server: {
|
|
142
|
+
type: 'stdio',
|
|
143
|
+
command: 'npx',
|
|
144
|
+
args: ['-y', '@anthropic/mcp-server-puppeteer']
|
|
145
|
+
},
|
|
146
|
+
homepage: 'https://github.com/anthropics/anthropic-quickstarts',
|
|
147
|
+
docs: 'https://pptr.dev/'
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: 'playwright',
|
|
151
|
+
name: '@anthropic/mcp-server-playwright',
|
|
152
|
+
description: 'Playwright 浏览器自动化',
|
|
153
|
+
tags: ['browser', 'automation', 'testing'],
|
|
154
|
+
server: {
|
|
155
|
+
type: 'stdio',
|
|
156
|
+
command: 'npx',
|
|
157
|
+
args: ['-y', '@anthropic/mcp-server-playwright']
|
|
158
|
+
},
|
|
159
|
+
homepage: 'https://github.com/anthropics/anthropic-quickstarts',
|
|
160
|
+
docs: 'https://playwright.dev/'
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: 'sqlite',
|
|
164
|
+
name: '@anthropic/mcp-server-sqlite',
|
|
165
|
+
description: 'SQLite 数据库访问',
|
|
166
|
+
tags: ['database', 'sql', 'sqlite'],
|
|
167
|
+
server: {
|
|
168
|
+
type: 'stdio',
|
|
169
|
+
command: 'npx',
|
|
170
|
+
args: ['-y', '@anthropic/mcp-server-sqlite', '--db-path', '/path/to/database.db']
|
|
171
|
+
},
|
|
172
|
+
homepage: 'https://github.com/anthropics/anthropic-quickstarts',
|
|
173
|
+
docs: 'https://www.sqlite.org/docs.html'
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: 'postgres',
|
|
177
|
+
name: '@anthropic/mcp-server-postgres',
|
|
178
|
+
description: 'PostgreSQL 数据库访问',
|
|
179
|
+
tags: ['database', 'sql', 'postgres'],
|
|
180
|
+
server: {
|
|
181
|
+
type: 'stdio',
|
|
182
|
+
command: 'npx',
|
|
183
|
+
args: ['-y', '@anthropic/mcp-server-postgres'],
|
|
184
|
+
env: {
|
|
185
|
+
POSTGRES_CONNECTION_STRING: 'postgresql://user:pass@localhost:5432/db'
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
homepage: 'https://github.com/anthropics/anthropic-quickstarts',
|
|
189
|
+
docs: 'https://www.postgresql.org/docs/'
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
id: 'slack',
|
|
193
|
+
name: '@modelcontextprotocol/server-slack',
|
|
194
|
+
description: 'Slack 消息和频道访问',
|
|
195
|
+
tags: ['slack', 'chat', 'messaging'],
|
|
196
|
+
server: {
|
|
197
|
+
type: 'stdio',
|
|
198
|
+
command: 'npx',
|
|
199
|
+
args: ['-y', '@modelcontextprotocol/server-slack'],
|
|
200
|
+
env: {
|
|
201
|
+
SLACK_BOT_TOKEN: '<your-bot-token>',
|
|
202
|
+
SLACK_TEAM_ID: '<your-team-id>'
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
homepage: 'https://github.com/modelcontextprotocol/servers',
|
|
206
|
+
docs: 'https://api.slack.com/docs'
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: 'google-drive',
|
|
210
|
+
name: '@modelcontextprotocol/server-gdrive',
|
|
211
|
+
description: 'Google Drive 文件访问',
|
|
212
|
+
tags: ['google', 'drive', 'files'],
|
|
213
|
+
server: {
|
|
214
|
+
type: 'stdio',
|
|
215
|
+
command: 'npx',
|
|
216
|
+
args: ['-y', '@modelcontextprotocol/server-gdrive']
|
|
217
|
+
},
|
|
218
|
+
homepage: 'https://github.com/modelcontextprotocol/servers',
|
|
219
|
+
docs: 'https://developers.google.com/drive'
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: 'everart',
|
|
223
|
+
name: '@modelcontextprotocol/server-everart',
|
|
224
|
+
description: 'AI 图片生成',
|
|
225
|
+
tags: ['image', 'art', 'generation'],
|
|
226
|
+
server: {
|
|
227
|
+
type: 'stdio',
|
|
228
|
+
command: 'npx',
|
|
229
|
+
args: ['-y', '@modelcontextprotocol/server-everart'],
|
|
230
|
+
env: {
|
|
231
|
+
EVERART_API_KEY: '<your-api-key>'
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
homepage: 'https://github.com/modelcontextprotocol/servers',
|
|
235
|
+
docs: 'https://everart.ai/docs'
|
|
236
|
+
}
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 确保目录存在
|
|
241
|
+
*/
|
|
242
|
+
function ensureDir(dirPath) {
|
|
243
|
+
if (!fs.existsSync(dirPath)) {
|
|
244
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* 安全读取 JSON 文件
|
|
250
|
+
*/
|
|
251
|
+
function readJsonFile(filePath, defaultValue = {}) {
|
|
252
|
+
try {
|
|
253
|
+
if (fs.existsSync(filePath)) {
|
|
254
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
255
|
+
return JSON.parse(content);
|
|
256
|
+
}
|
|
257
|
+
} catch (err) {
|
|
258
|
+
console.error(`[MCP] Failed to read ${filePath}:`, err.message);
|
|
259
|
+
}
|
|
260
|
+
return defaultValue;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* 安全写入 JSON 文件(原子写入)
|
|
265
|
+
*/
|
|
266
|
+
function writeJsonFile(filePath, data) {
|
|
267
|
+
ensureDir(path.dirname(filePath));
|
|
268
|
+
const tempPath = filePath + '.tmp';
|
|
269
|
+
fs.writeFileSync(tempPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
270
|
+
fs.renameSync(tempPath, filePath);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 安全读取 TOML 文件
|
|
275
|
+
*/
|
|
276
|
+
function readTomlFile(filePath, defaultValue = {}) {
|
|
277
|
+
try {
|
|
278
|
+
if (fs.existsSync(filePath)) {
|
|
279
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
280
|
+
return toml.parse(content);
|
|
281
|
+
}
|
|
282
|
+
} catch (err) {
|
|
283
|
+
console.error(`[MCP] Failed to read ${filePath}:`, err.message);
|
|
284
|
+
}
|
|
285
|
+
return defaultValue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* 安全写入 TOML 文件(原子写入)
|
|
290
|
+
*/
|
|
291
|
+
function writeTomlFile(filePath, data) {
|
|
292
|
+
ensureDir(path.dirname(filePath));
|
|
293
|
+
const tempPath = filePath + '.tmp';
|
|
294
|
+
fs.writeFileSync(tempPath, toml.stringify(data), 'utf-8');
|
|
295
|
+
fs.renameSync(tempPath, filePath);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ============================================================================
|
|
299
|
+
// MCP 数据管理
|
|
300
|
+
// ============================================================================
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* 获取所有 MCP 服务器
|
|
304
|
+
*/
|
|
305
|
+
function getAllServers() {
|
|
306
|
+
return readJsonFile(MCP_SERVERS_FILE, {});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* 获取单个 MCP 服务器
|
|
311
|
+
*/
|
|
312
|
+
function getServer(id) {
|
|
313
|
+
const servers = getAllServers();
|
|
314
|
+
return servers[id] || null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* 保存 MCP 服务器(添加或更新)
|
|
319
|
+
*/
|
|
320
|
+
async function saveServer(server) {
|
|
321
|
+
if (!server.id || !server.id.trim()) {
|
|
322
|
+
throw new Error('MCP 服务器 ID 不能为空');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// 验证服务器配置
|
|
326
|
+
validateServerSpec(server.server);
|
|
327
|
+
|
|
328
|
+
const servers = getAllServers();
|
|
329
|
+
|
|
330
|
+
// 如果是新服务器,设置默认值
|
|
331
|
+
if (!servers[server.id]) {
|
|
332
|
+
server.createdAt = Date.now();
|
|
333
|
+
}
|
|
334
|
+
server.updatedAt = Date.now();
|
|
335
|
+
|
|
336
|
+
// 确保 apps 字段存在
|
|
337
|
+
if (!server.apps) {
|
|
338
|
+
server.apps = { claude: true, codex: false, gemini: false };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
servers[server.id] = server;
|
|
342
|
+
writeJsonFile(MCP_SERVERS_FILE, servers);
|
|
343
|
+
|
|
344
|
+
// 同步到各平台配置
|
|
345
|
+
await syncServerToAllPlatforms(server);
|
|
346
|
+
|
|
347
|
+
return server;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* 删除 MCP 服务器
|
|
352
|
+
*/
|
|
353
|
+
async function deleteServer(id) {
|
|
354
|
+
const servers = getAllServers();
|
|
355
|
+
const server = servers[id];
|
|
356
|
+
|
|
357
|
+
if (!server) {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
delete servers[id];
|
|
362
|
+
writeJsonFile(MCP_SERVERS_FILE, servers);
|
|
363
|
+
|
|
364
|
+
// 从所有平台配置中移除
|
|
365
|
+
await removeServerFromAllPlatforms(id);
|
|
366
|
+
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* 切换 MCP 服务器在某平台的启用状态
|
|
372
|
+
*/
|
|
373
|
+
async function toggleServerApp(serverId, app, enabled) {
|
|
374
|
+
const servers = getAllServers();
|
|
375
|
+
const server = servers[serverId];
|
|
376
|
+
|
|
377
|
+
if (!server) {
|
|
378
|
+
throw new Error(`MCP 服务器 "${serverId}" 不存在`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (!['claude', 'codex', 'gemini'].includes(app)) {
|
|
382
|
+
throw new Error(`无效的平台: ${app}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
server.apps[app] = enabled;
|
|
386
|
+
server.updatedAt = Date.now();
|
|
387
|
+
|
|
388
|
+
writeJsonFile(MCP_SERVERS_FILE, servers);
|
|
389
|
+
|
|
390
|
+
// 同步到对应平台
|
|
391
|
+
if (enabled) {
|
|
392
|
+
await syncServerToPlatform(server, app);
|
|
393
|
+
} else {
|
|
394
|
+
await removeServerFromPlatform(serverId, app);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return server;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* 获取 MCP 预设模板列表
|
|
402
|
+
*/
|
|
403
|
+
function getPresets() {
|
|
404
|
+
return MCP_PRESETS;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ============================================================================
|
|
408
|
+
// 服务器配置验证
|
|
409
|
+
// ============================================================================
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* 验证 MCP 服务器配置
|
|
413
|
+
*/
|
|
414
|
+
function validateServerSpec(spec) {
|
|
415
|
+
if (!spec || typeof spec !== 'object') {
|
|
416
|
+
throw new Error('服务器配置必须是对象');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const type = spec.type || 'stdio';
|
|
420
|
+
|
|
421
|
+
if (!['stdio', 'http', 'sse'].includes(type)) {
|
|
422
|
+
throw new Error(`无效的服务器类型: ${type},必须是 stdio、http 或 sse`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (type === 'stdio') {
|
|
426
|
+
if (!spec.command || !spec.command.trim()) {
|
|
427
|
+
throw new Error('stdio 类型必须指定 command');
|
|
428
|
+
}
|
|
429
|
+
} else if (type === 'http' || type === 'sse') {
|
|
430
|
+
if (!spec.url || !spec.url.trim()) {
|
|
431
|
+
throw new Error(`${type} 类型必须指定 url`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ============================================================================
|
|
437
|
+
// 平台配置同步
|
|
438
|
+
// ============================================================================
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* 同步服务器到所有已启用的平台
|
|
442
|
+
*/
|
|
443
|
+
async function syncServerToAllPlatforms(server) {
|
|
444
|
+
const { apps } = server;
|
|
445
|
+
|
|
446
|
+
if (apps.claude) {
|
|
447
|
+
await syncServerToPlatform(server, 'claude');
|
|
448
|
+
} else {
|
|
449
|
+
await removeServerFromPlatform(server.id, 'claude');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (apps.codex) {
|
|
453
|
+
await syncServerToPlatform(server, 'codex');
|
|
454
|
+
} else {
|
|
455
|
+
await removeServerFromPlatform(server.id, 'codex');
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (apps.gemini) {
|
|
459
|
+
await syncServerToPlatform(server, 'gemini');
|
|
460
|
+
} else {
|
|
461
|
+
await removeServerFromPlatform(server.id, 'gemini');
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* 从所有平台移除服务器
|
|
467
|
+
*/
|
|
468
|
+
async function removeServerFromAllPlatforms(serverId) {
|
|
469
|
+
await removeServerFromPlatform(serverId, 'claude');
|
|
470
|
+
await removeServerFromPlatform(serverId, 'codex');
|
|
471
|
+
await removeServerFromPlatform(serverId, 'gemini');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* 同步服务器到指定平台
|
|
476
|
+
*/
|
|
477
|
+
async function syncServerToPlatform(server, platform) {
|
|
478
|
+
try {
|
|
479
|
+
switch (platform) {
|
|
480
|
+
case 'claude':
|
|
481
|
+
syncToClaudeConfig(server);
|
|
482
|
+
break;
|
|
483
|
+
case 'codex':
|
|
484
|
+
syncToCodexConfig(server);
|
|
485
|
+
break;
|
|
486
|
+
case 'gemini':
|
|
487
|
+
syncToGeminiConfig(server);
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
console.log(`[MCP] Synced "${server.id}" to ${platform}`);
|
|
491
|
+
} catch (err) {
|
|
492
|
+
console.error(`[MCP] Failed to sync "${server.id}" to ${platform}:`, err.message);
|
|
493
|
+
throw err;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* 从指定平台移除服务器
|
|
499
|
+
*/
|
|
500
|
+
async function removeServerFromPlatform(serverId, platform) {
|
|
501
|
+
try {
|
|
502
|
+
switch (platform) {
|
|
503
|
+
case 'claude':
|
|
504
|
+
removeFromClaudeConfig(serverId);
|
|
505
|
+
break;
|
|
506
|
+
case 'codex':
|
|
507
|
+
removeFromCodexConfig(serverId);
|
|
508
|
+
break;
|
|
509
|
+
case 'gemini':
|
|
510
|
+
removeFromGeminiConfig(serverId);
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
console.log(`[MCP] Removed "${serverId}" from ${platform}`);
|
|
514
|
+
} catch (err) {
|
|
515
|
+
console.error(`[MCP] Failed to remove "${serverId}" from ${platform}:`, err.message);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ============================================================================
|
|
520
|
+
// Claude 配置同步
|
|
521
|
+
// ============================================================================
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* 同步到 Claude 配置
|
|
525
|
+
*/
|
|
526
|
+
function syncToClaudeConfig(server) {
|
|
527
|
+
const config = readJsonFile(CLAUDE_CONFIG_PATH, {});
|
|
528
|
+
|
|
529
|
+
if (!config.mcpServers) {
|
|
530
|
+
config.mcpServers = {};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// 只写入 server spec,不写入元数据
|
|
534
|
+
config.mcpServers[server.id] = extractServerSpec(server.server);
|
|
535
|
+
|
|
536
|
+
writeJsonFile(CLAUDE_CONFIG_PATH, config);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* 从 Claude 配置移除
|
|
541
|
+
*/
|
|
542
|
+
function removeFromClaudeConfig(serverId) {
|
|
543
|
+
const config = readJsonFile(CLAUDE_CONFIG_PATH, {});
|
|
544
|
+
|
|
545
|
+
if (config.mcpServers && config.mcpServers[serverId]) {
|
|
546
|
+
delete config.mcpServers[serverId];
|
|
547
|
+
writeJsonFile(CLAUDE_CONFIG_PATH, config);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// ============================================================================
|
|
552
|
+
// Codex 配置同步 (TOML 格式)
|
|
553
|
+
// ============================================================================
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* 同步到 Codex 配置
|
|
557
|
+
*/
|
|
558
|
+
function syncToCodexConfig(server) {
|
|
559
|
+
const config = readTomlFile(CODEX_CONFIG_PATH, {});
|
|
560
|
+
|
|
561
|
+
if (!config.mcp_servers) {
|
|
562
|
+
config.mcp_servers = {};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// 转换为 Codex TOML 格式
|
|
566
|
+
config.mcp_servers[server.id] = convertToCodexFormat(server.server);
|
|
567
|
+
|
|
568
|
+
writeTomlFile(CODEX_CONFIG_PATH, config);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* 从 Codex 配置移除
|
|
573
|
+
*/
|
|
574
|
+
function removeFromCodexConfig(serverId) {
|
|
575
|
+
const config = readTomlFile(CODEX_CONFIG_PATH, {});
|
|
576
|
+
|
|
577
|
+
if (config.mcp_servers && config.mcp_servers[serverId]) {
|
|
578
|
+
delete config.mcp_servers[serverId];
|
|
579
|
+
writeTomlFile(CODEX_CONFIG_PATH, config);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* 转换为 Codex TOML 格式
|
|
585
|
+
*/
|
|
586
|
+
function convertToCodexFormat(spec) {
|
|
587
|
+
const result = {
|
|
588
|
+
type: spec.type || 'stdio'
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
if (result.type === 'stdio') {
|
|
592
|
+
result.command = spec.command || '';
|
|
593
|
+
if (spec.args && spec.args.length > 0) {
|
|
594
|
+
result.args = spec.args;
|
|
595
|
+
}
|
|
596
|
+
if (spec.env && Object.keys(spec.env).length > 0) {
|
|
597
|
+
result.env = spec.env;
|
|
598
|
+
}
|
|
599
|
+
if (spec.cwd) {
|
|
600
|
+
result.cwd = spec.cwd;
|
|
601
|
+
}
|
|
602
|
+
} else if (result.type === 'http' || result.type === 'sse') {
|
|
603
|
+
result.url = spec.url || '';
|
|
604
|
+
if (spec.headers && Object.keys(spec.headers).length > 0) {
|
|
605
|
+
result.http_headers = spec.headers;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return result;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// ============================================================================
|
|
613
|
+
// Gemini 配置同步
|
|
614
|
+
// ============================================================================
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* 同步到 Gemini 配置
|
|
618
|
+
*/
|
|
619
|
+
function syncToGeminiConfig(server) {
|
|
620
|
+
const config = readJsonFile(GEMINI_CONFIG_PATH, {});
|
|
621
|
+
|
|
622
|
+
if (!config.mcpServers) {
|
|
623
|
+
config.mcpServers = {};
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// 只写入 server spec,不写入元数据
|
|
627
|
+
config.mcpServers[server.id] = extractServerSpec(server.server);
|
|
628
|
+
|
|
629
|
+
writeJsonFile(GEMINI_CONFIG_PATH, config);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* 从 Gemini 配置移除
|
|
634
|
+
*/
|
|
635
|
+
function removeFromGeminiConfig(serverId) {
|
|
636
|
+
const config = readJsonFile(GEMINI_CONFIG_PATH, {});
|
|
637
|
+
|
|
638
|
+
if (config.mcpServers && config.mcpServers[serverId]) {
|
|
639
|
+
delete config.mcpServers[serverId];
|
|
640
|
+
writeJsonFile(GEMINI_CONFIG_PATH, config);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// ============================================================================
|
|
645
|
+
// 导入功能
|
|
646
|
+
// ============================================================================
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* 从指定平台导入 MCP 配置
|
|
650
|
+
*/
|
|
651
|
+
async function importFromPlatform(platform) {
|
|
652
|
+
let importedCount = 0;
|
|
653
|
+
const servers = getAllServers();
|
|
654
|
+
|
|
655
|
+
switch (platform) {
|
|
656
|
+
case 'claude':
|
|
657
|
+
importedCount = importFromClaude(servers);
|
|
658
|
+
break;
|
|
659
|
+
case 'codex':
|
|
660
|
+
importedCount = importFromCodex(servers);
|
|
661
|
+
break;
|
|
662
|
+
case 'gemini':
|
|
663
|
+
importedCount = importFromGemini(servers);
|
|
664
|
+
break;
|
|
665
|
+
default:
|
|
666
|
+
throw new Error(`无效的平台: ${platform}`);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (importedCount > 0) {
|
|
670
|
+
writeJsonFile(MCP_SERVERS_FILE, servers);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return importedCount;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* 从 Claude 导入
|
|
678
|
+
*/
|
|
679
|
+
function importFromClaude(servers) {
|
|
680
|
+
const config = readJsonFile(CLAUDE_CONFIG_PATH, {});
|
|
681
|
+
const mcpServers = config.mcpServers || {};
|
|
682
|
+
let count = 0;
|
|
683
|
+
|
|
684
|
+
for (const [id, spec] of Object.entries(mcpServers)) {
|
|
685
|
+
if (servers[id]) {
|
|
686
|
+
// 已存在,只启用 Claude
|
|
687
|
+
if (!servers[id].apps.claude) {
|
|
688
|
+
servers[id].apps.claude = true;
|
|
689
|
+
count++;
|
|
690
|
+
}
|
|
691
|
+
} else {
|
|
692
|
+
// 新服务器
|
|
693
|
+
servers[id] = {
|
|
694
|
+
id,
|
|
695
|
+
name: id,
|
|
696
|
+
server: spec,
|
|
697
|
+
apps: { claude: true, codex: false, gemini: false },
|
|
698
|
+
createdAt: Date.now(),
|
|
699
|
+
updatedAt: Date.now()
|
|
700
|
+
};
|
|
701
|
+
count++;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
return count;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* 从 Codex 导入
|
|
710
|
+
*/
|
|
711
|
+
function importFromCodex(servers) {
|
|
712
|
+
const config = readTomlFile(CODEX_CONFIG_PATH, {});
|
|
713
|
+
const mcpServers = config.mcp_servers || {};
|
|
714
|
+
let count = 0;
|
|
715
|
+
|
|
716
|
+
for (const [id, spec] of Object.entries(mcpServers)) {
|
|
717
|
+
// 转换 Codex 格式到通用格式
|
|
718
|
+
const convertedSpec = convertFromCodexFormat(spec);
|
|
719
|
+
|
|
720
|
+
if (servers[id]) {
|
|
721
|
+
// 已存在,只启用 Codex
|
|
722
|
+
if (!servers[id].apps.codex) {
|
|
723
|
+
servers[id].apps.codex = true;
|
|
724
|
+
count++;
|
|
725
|
+
}
|
|
726
|
+
} else {
|
|
727
|
+
// 新服务器
|
|
728
|
+
servers[id] = {
|
|
729
|
+
id,
|
|
730
|
+
name: id,
|
|
731
|
+
server: convertedSpec,
|
|
732
|
+
apps: { claude: false, codex: true, gemini: false },
|
|
733
|
+
createdAt: Date.now(),
|
|
734
|
+
updatedAt: Date.now()
|
|
735
|
+
};
|
|
736
|
+
count++;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return count;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* 从 Gemini 导入
|
|
745
|
+
*/
|
|
746
|
+
function importFromGemini(servers) {
|
|
747
|
+
const config = readJsonFile(GEMINI_CONFIG_PATH, {});
|
|
748
|
+
const mcpServers = config.mcpServers || {};
|
|
749
|
+
let count = 0;
|
|
750
|
+
|
|
751
|
+
for (const [id, spec] of Object.entries(mcpServers)) {
|
|
752
|
+
if (servers[id]) {
|
|
753
|
+
// 已存在,只启用 Gemini
|
|
754
|
+
if (!servers[id].apps.gemini) {
|
|
755
|
+
servers[id].apps.gemini = true;
|
|
756
|
+
count++;
|
|
757
|
+
}
|
|
758
|
+
} else {
|
|
759
|
+
// 新服务器
|
|
760
|
+
servers[id] = {
|
|
761
|
+
id,
|
|
762
|
+
name: id,
|
|
763
|
+
server: spec,
|
|
764
|
+
apps: { claude: false, codex: false, gemini: true },
|
|
765
|
+
createdAt: Date.now(),
|
|
766
|
+
updatedAt: Date.now()
|
|
767
|
+
};
|
|
768
|
+
count++;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return count;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* 从 Codex 格式转换
|
|
777
|
+
*/
|
|
778
|
+
function convertFromCodexFormat(spec) {
|
|
779
|
+
const result = {
|
|
780
|
+
type: spec.type || 'stdio'
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
if (result.type === 'stdio') {
|
|
784
|
+
result.command = spec.command || '';
|
|
785
|
+
if (spec.args) {
|
|
786
|
+
result.args = spec.args;
|
|
787
|
+
}
|
|
788
|
+
if (spec.env) {
|
|
789
|
+
result.env = spec.env;
|
|
790
|
+
}
|
|
791
|
+
if (spec.cwd) {
|
|
792
|
+
result.cwd = spec.cwd;
|
|
793
|
+
}
|
|
794
|
+
} else if (result.type === 'http' || result.type === 'sse') {
|
|
795
|
+
result.url = spec.url || '';
|
|
796
|
+
if (spec.http_headers) {
|
|
797
|
+
result.headers = spec.http_headers;
|
|
798
|
+
} else if (spec.headers) {
|
|
799
|
+
result.headers = spec.headers;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
return result;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* 提取纯净的服务器规范(移除元数据)
|
|
808
|
+
*/
|
|
809
|
+
function extractServerSpec(spec) {
|
|
810
|
+
const result = { ...spec };
|
|
811
|
+
// 移除可能存在的非规范字段
|
|
812
|
+
delete result.id;
|
|
813
|
+
delete result.name;
|
|
814
|
+
delete result.description;
|
|
815
|
+
delete result.tags;
|
|
816
|
+
delete result.homepage;
|
|
817
|
+
delete result.docs;
|
|
818
|
+
delete result.apps;
|
|
819
|
+
delete result.createdAt;
|
|
820
|
+
delete result.updatedAt;
|
|
821
|
+
return result;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* 获取统计信息
|
|
826
|
+
*/
|
|
827
|
+
function getStats() {
|
|
828
|
+
const servers = getAllServers();
|
|
829
|
+
const serverList = Object.values(servers);
|
|
830
|
+
|
|
831
|
+
return {
|
|
832
|
+
total: serverList.length,
|
|
833
|
+
claude: serverList.filter(s => s.apps?.claude).length,
|
|
834
|
+
codex: serverList.filter(s => s.apps?.codex).length,
|
|
835
|
+
gemini: serverList.filter(s => s.apps?.gemini).length
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// ============================================================================
|
|
840
|
+
// 服务器测试功能
|
|
841
|
+
// ============================================================================
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* 测试 MCP 服务器连接
|
|
845
|
+
* @param {string} serverId - 服务器 ID
|
|
846
|
+
* @returns {Promise<{success: boolean, message: string, duration?: number}>}
|
|
847
|
+
*/
|
|
848
|
+
async function testServer(serverId) {
|
|
849
|
+
const server = getServer(serverId);
|
|
850
|
+
if (!server) {
|
|
851
|
+
throw new Error(`MCP 服务器 "${serverId}" 不存在`);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const spec = server.server;
|
|
855
|
+
const type = spec.type || 'stdio';
|
|
856
|
+
const startTime = Date.now();
|
|
857
|
+
|
|
858
|
+
try {
|
|
859
|
+
if (type === 'stdio') {
|
|
860
|
+
return await testStdioServer(spec);
|
|
861
|
+
} else if (type === 'http' || type === 'sse') {
|
|
862
|
+
return await testHttpServer(spec);
|
|
863
|
+
} else {
|
|
864
|
+
return { success: false, message: `不支持的服务器类型: ${type}` };
|
|
865
|
+
}
|
|
866
|
+
} catch (err) {
|
|
867
|
+
return {
|
|
868
|
+
success: false,
|
|
869
|
+
message: err.message,
|
|
870
|
+
duration: Date.now() - startTime
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* 测试 stdio 类型服务器
|
|
877
|
+
*/
|
|
878
|
+
async function testStdioServer(spec) {
|
|
879
|
+
return new Promise((resolve) => {
|
|
880
|
+
const startTime = Date.now();
|
|
881
|
+
const timeout = 10000; // 10 秒超时
|
|
882
|
+
|
|
883
|
+
// 检查命令是否存在
|
|
884
|
+
const command = spec.command;
|
|
885
|
+
const args = spec.args || [];
|
|
886
|
+
|
|
887
|
+
let child;
|
|
888
|
+
let resolved = false;
|
|
889
|
+
let stdout = '';
|
|
890
|
+
let stderr = '';
|
|
891
|
+
|
|
892
|
+
const cleanup = () => {
|
|
893
|
+
if (child && !child.killed) {
|
|
894
|
+
child.kill('SIGTERM');
|
|
895
|
+
setTimeout(() => {
|
|
896
|
+
if (!child.killed) child.kill('SIGKILL');
|
|
897
|
+
}, 1000);
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
const done = (result) => {
|
|
902
|
+
if (resolved) return;
|
|
903
|
+
resolved = true;
|
|
904
|
+
cleanup();
|
|
905
|
+
resolve(result);
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
try {
|
|
909
|
+
child = spawn(command, args, {
|
|
910
|
+
env: { ...process.env, ...spec.env },
|
|
911
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
912
|
+
cwd: spec.cwd || process.cwd()
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
child.stdout.on('data', (data) => {
|
|
916
|
+
stdout += data.toString();
|
|
917
|
+
// MCP 服务器启动成功通常会输出 JSON-RPC 相关内容
|
|
918
|
+
if (stdout.includes('{') || stdout.length > 0) {
|
|
919
|
+
done({
|
|
920
|
+
success: true,
|
|
921
|
+
message: '服务器启动成功',
|
|
922
|
+
duration: Date.now() - startTime
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
child.stderr.on('data', (data) => {
|
|
928
|
+
stderr += data.toString();
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
child.on('error', (err) => {
|
|
932
|
+
if (err.code === 'ENOENT') {
|
|
933
|
+
done({
|
|
934
|
+
success: false,
|
|
935
|
+
message: `命令 "${command}" 未找到,请确保已安装`,
|
|
936
|
+
duration: Date.now() - startTime
|
|
937
|
+
});
|
|
938
|
+
} else {
|
|
939
|
+
done({
|
|
940
|
+
success: false,
|
|
941
|
+
message: `启动失败: ${err.message}`,
|
|
942
|
+
duration: Date.now() - startTime
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
child.on('close', (code) => {
|
|
948
|
+
if (code === 0 || stdout.length > 0) {
|
|
949
|
+
done({
|
|
950
|
+
success: true,
|
|
951
|
+
message: '服务器测试通过',
|
|
952
|
+
duration: Date.now() - startTime
|
|
953
|
+
});
|
|
954
|
+
} else {
|
|
955
|
+
done({
|
|
956
|
+
success: false,
|
|
957
|
+
message: stderr || `进程退出码: ${code}`,
|
|
958
|
+
duration: Date.now() - startTime
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
// 超时处理
|
|
964
|
+
setTimeout(() => {
|
|
965
|
+
// 如果进程还在运行,说明服务器正常启动了
|
|
966
|
+
if (!resolved && child && !child.killed) {
|
|
967
|
+
done({
|
|
968
|
+
success: true,
|
|
969
|
+
message: '服务器正常运行中',
|
|
970
|
+
duration: Date.now() - startTime
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
}, 3000); // 3 秒后如果还在运行就认为成功
|
|
974
|
+
|
|
975
|
+
// 最终超时
|
|
976
|
+
setTimeout(() => {
|
|
977
|
+
done({
|
|
978
|
+
success: false,
|
|
979
|
+
message: '测试超时',
|
|
980
|
+
duration: timeout
|
|
981
|
+
});
|
|
982
|
+
}, timeout);
|
|
983
|
+
|
|
984
|
+
} catch (err) {
|
|
985
|
+
done({
|
|
986
|
+
success: false,
|
|
987
|
+
message: `测试失败: ${err.message}`,
|
|
988
|
+
duration: Date.now() - startTime
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* 测试 http/sse 类型服务器
|
|
996
|
+
*/
|
|
997
|
+
async function testHttpServer(spec) {
|
|
998
|
+
return new Promise((resolve) => {
|
|
999
|
+
const startTime = Date.now();
|
|
1000
|
+
const timeout = 10000;
|
|
1001
|
+
|
|
1002
|
+
try {
|
|
1003
|
+
const url = new URL(spec.url);
|
|
1004
|
+
const isHttps = url.protocol === 'https:';
|
|
1005
|
+
const client = isHttps ? https : http;
|
|
1006
|
+
|
|
1007
|
+
const options = {
|
|
1008
|
+
hostname: url.hostname,
|
|
1009
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
1010
|
+
path: url.pathname + url.search,
|
|
1011
|
+
method: 'GET',
|
|
1012
|
+
timeout: timeout,
|
|
1013
|
+
headers: {
|
|
1014
|
+
...spec.headers
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
|
|
1018
|
+
const req = client.request(options, (res) => {
|
|
1019
|
+
resolve({
|
|
1020
|
+
success: res.statusCode >= 200 && res.statusCode < 500,
|
|
1021
|
+
message: res.statusCode >= 200 && res.statusCode < 400
|
|
1022
|
+
? `服务器响应正常 (HTTP ${res.statusCode})`
|
|
1023
|
+
: `服务器响应异常 (HTTP ${res.statusCode})`,
|
|
1024
|
+
duration: Date.now() - startTime
|
|
1025
|
+
});
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
req.on('error', (err) => {
|
|
1029
|
+
resolve({
|
|
1030
|
+
success: false,
|
|
1031
|
+
message: `连接失败: ${err.message}`,
|
|
1032
|
+
duration: Date.now() - startTime
|
|
1033
|
+
});
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
req.on('timeout', () => {
|
|
1037
|
+
req.destroy();
|
|
1038
|
+
resolve({
|
|
1039
|
+
success: false,
|
|
1040
|
+
message: '连接超时',
|
|
1041
|
+
duration: timeout
|
|
1042
|
+
});
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
req.end();
|
|
1046
|
+
} catch (err) {
|
|
1047
|
+
resolve({
|
|
1048
|
+
success: false,
|
|
1049
|
+
message: `URL 无效: ${err.message}`,
|
|
1050
|
+
duration: Date.now() - startTime
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* 更新服务器状态
|
|
1058
|
+
*/
|
|
1059
|
+
async function updateServerStatus(serverId, status) {
|
|
1060
|
+
const servers = getAllServers();
|
|
1061
|
+
const server = servers[serverId];
|
|
1062
|
+
|
|
1063
|
+
if (!server) {
|
|
1064
|
+
throw new Error(`MCP 服务器 "${serverId}" 不存在`);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
server.status = status;
|
|
1068
|
+
server.lastChecked = Date.now();
|
|
1069
|
+
|
|
1070
|
+
writeJsonFile(MCP_SERVERS_FILE, servers);
|
|
1071
|
+
return server;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// ============================================================================
|
|
1075
|
+
// 排序功能
|
|
1076
|
+
// ============================================================================
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* 更新服务器排序
|
|
1080
|
+
* @param {string[]} serverIds - 按顺序排列的服务器 ID 数组
|
|
1081
|
+
*/
|
|
1082
|
+
function updateServerOrder(serverIds) {
|
|
1083
|
+
const servers = getAllServers();
|
|
1084
|
+
|
|
1085
|
+
// 更新每个服务器的排序索引
|
|
1086
|
+
serverIds.forEach((id, index) => {
|
|
1087
|
+
if (servers[id]) {
|
|
1088
|
+
servers[id].order = index;
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
writeJsonFile(MCP_SERVERS_FILE, servers);
|
|
1093
|
+
return servers;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// ============================================================================
|
|
1097
|
+
// 导出功能
|
|
1098
|
+
// ============================================================================
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* 导出所有 MCP 配置
|
|
1102
|
+
* @param {string} format - 导出格式: 'json' | 'claude' | 'codex'
|
|
1103
|
+
*/
|
|
1104
|
+
function exportServers(format = 'json') {
|
|
1105
|
+
const servers = getAllServers();
|
|
1106
|
+
|
|
1107
|
+
switch (format) {
|
|
1108
|
+
case 'claude':
|
|
1109
|
+
return exportForClaude(servers);
|
|
1110
|
+
case 'codex':
|
|
1111
|
+
return exportForCodex(servers);
|
|
1112
|
+
case 'json':
|
|
1113
|
+
default:
|
|
1114
|
+
return exportAsJson(servers);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* 导出为通用 JSON 格式
|
|
1120
|
+
*/
|
|
1121
|
+
function exportAsJson(servers) {
|
|
1122
|
+
const mcpServers = {};
|
|
1123
|
+
|
|
1124
|
+
for (const [id, server] of Object.entries(servers)) {
|
|
1125
|
+
mcpServers[id] = extractServerSpec(server.server);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
return {
|
|
1129
|
+
format: 'json',
|
|
1130
|
+
content: JSON.stringify({ mcpServers }, null, 2),
|
|
1131
|
+
filename: 'mcp-servers.json'
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* 导出为 Claude 格式
|
|
1137
|
+
*/
|
|
1138
|
+
function exportForClaude(servers) {
|
|
1139
|
+
const mcpServers = {};
|
|
1140
|
+
|
|
1141
|
+
for (const [id, server] of Object.entries(servers)) {
|
|
1142
|
+
if (server.apps?.claude) {
|
|
1143
|
+
mcpServers[id] = extractServerSpec(server.server);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
return {
|
|
1148
|
+
format: 'claude',
|
|
1149
|
+
content: JSON.stringify({ mcpServers }, null, 2),
|
|
1150
|
+
filename: 'claude-mcp-config.json'
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* 导出为 Codex 格式
|
|
1156
|
+
*/
|
|
1157
|
+
function exportForCodex(servers) {
|
|
1158
|
+
const mcp_servers = {};
|
|
1159
|
+
|
|
1160
|
+
for (const [id, server] of Object.entries(servers)) {
|
|
1161
|
+
if (server.apps?.codex) {
|
|
1162
|
+
mcp_servers[id] = convertToCodexFormat(server.server);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
return {
|
|
1167
|
+
format: 'codex',
|
|
1168
|
+
content: toml.stringify({ mcp_servers }),
|
|
1169
|
+
filename: 'codex-mcp-config.toml'
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
module.exports = {
|
|
1174
|
+
getAllServers,
|
|
1175
|
+
getServer,
|
|
1176
|
+
saveServer,
|
|
1177
|
+
deleteServer,
|
|
1178
|
+
toggleServerApp,
|
|
1179
|
+
getPresets,
|
|
1180
|
+
importFromPlatform,
|
|
1181
|
+
getStats,
|
|
1182
|
+
validateServerSpec,
|
|
1183
|
+
// 新增功能
|
|
1184
|
+
testServer,
|
|
1185
|
+
updateServerStatus,
|
|
1186
|
+
updateServerOrder,
|
|
1187
|
+
exportServers
|
|
1188
|
+
};
|