@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.
Files changed (125) hide show
  1. package/CHANGELOG.md +333 -0
  2. package/LICENSE +21 -0
  3. package/README.md +404 -0
  4. package/bin/ctx.js +8 -0
  5. package/dist/web/assets/index-D1AYlFLZ.js +3220 -0
  6. package/dist/web/assets/index-aL3cKxSK.css +41 -0
  7. package/dist/web/favicon.ico +0 -0
  8. package/dist/web/index.html +14 -0
  9. package/dist/web/logo.png +0 -0
  10. package/docs/CHANGELOG.md +582 -0
  11. package/docs/DIRECTORY_MIGRATION.md +112 -0
  12. package/docs/PROJECT_STRUCTURE.md +396 -0
  13. package/docs/bannel.png +0 -0
  14. package/docs/home.png +0 -0
  15. package/docs/logo.png +0 -0
  16. package/docs/multi-channel-load-balancing.md +249 -0
  17. package/package.json +73 -0
  18. package/src/commands/channels.js +504 -0
  19. package/src/commands/cli-type.js +99 -0
  20. package/src/commands/daemon.js +286 -0
  21. package/src/commands/doctor.js +332 -0
  22. package/src/commands/list.js +222 -0
  23. package/src/commands/logs.js +259 -0
  24. package/src/commands/port-config.js +115 -0
  25. package/src/commands/proxy-control.js +258 -0
  26. package/src/commands/proxy.js +152 -0
  27. package/src/commands/resume.js +137 -0
  28. package/src/commands/search.js +190 -0
  29. package/src/commands/stats.js +224 -0
  30. package/src/commands/switch.js +48 -0
  31. package/src/commands/toggle-proxy.js +222 -0
  32. package/src/commands/ui.js +92 -0
  33. package/src/commands/workspace.js +454 -0
  34. package/src/config/default.js +40 -0
  35. package/src/config/loader.js +75 -0
  36. package/src/config/paths.js +121 -0
  37. package/src/index.js +373 -0
  38. package/src/reset-config.js +92 -0
  39. package/src/server/api/agents.js +248 -0
  40. package/src/server/api/aliases.js +36 -0
  41. package/src/server/api/channels.js +258 -0
  42. package/src/server/api/claude-hooks.js +480 -0
  43. package/src/server/api/codex-channels.js +312 -0
  44. package/src/server/api/codex-projects.js +91 -0
  45. package/src/server/api/codex-proxy.js +182 -0
  46. package/src/server/api/codex-sessions.js +491 -0
  47. package/src/server/api/codex-statistics.js +57 -0
  48. package/src/server/api/commands.js +245 -0
  49. package/src/server/api/config-templates.js +182 -0
  50. package/src/server/api/config.js +147 -0
  51. package/src/server/api/convert.js +127 -0
  52. package/src/server/api/dashboard.js +125 -0
  53. package/src/server/api/env.js +144 -0
  54. package/src/server/api/favorites.js +77 -0
  55. package/src/server/api/gemini-channels.js +261 -0
  56. package/src/server/api/gemini-projects.js +91 -0
  57. package/src/server/api/gemini-proxy.js +160 -0
  58. package/src/server/api/gemini-sessions.js +397 -0
  59. package/src/server/api/gemini-statistics.js +57 -0
  60. package/src/server/api/health-check.js +118 -0
  61. package/src/server/api/mcp.js +336 -0
  62. package/src/server/api/pm2-autostart.js +269 -0
  63. package/src/server/api/projects.js +124 -0
  64. package/src/server/api/prompts.js +279 -0
  65. package/src/server/api/proxy.js +235 -0
  66. package/src/server/api/rules.js +271 -0
  67. package/src/server/api/sessions.js +595 -0
  68. package/src/server/api/settings.js +61 -0
  69. package/src/server/api/skills.js +305 -0
  70. package/src/server/api/statistics.js +91 -0
  71. package/src/server/api/terminal.js +202 -0
  72. package/src/server/api/ui-config.js +64 -0
  73. package/src/server/api/workspaces.js +407 -0
  74. package/src/server/codex-proxy-server.js +538 -0
  75. package/src/server/dev-server.js +26 -0
  76. package/src/server/gemini-proxy-server.js +518 -0
  77. package/src/server/index.js +305 -0
  78. package/src/server/proxy-server.js +469 -0
  79. package/src/server/services/agents-service.js +354 -0
  80. package/src/server/services/alias.js +71 -0
  81. package/src/server/services/channel-health.js +234 -0
  82. package/src/server/services/channel-scheduler.js +234 -0
  83. package/src/server/services/channels.js +347 -0
  84. package/src/server/services/codex-channels.js +625 -0
  85. package/src/server/services/codex-config.js +90 -0
  86. package/src/server/services/codex-parser.js +322 -0
  87. package/src/server/services/codex-sessions.js +665 -0
  88. package/src/server/services/codex-settings-manager.js +397 -0
  89. package/src/server/services/codex-speed-test-template.json +24 -0
  90. package/src/server/services/codex-statistics-service.js +255 -0
  91. package/src/server/services/commands-service.js +360 -0
  92. package/src/server/services/config-templates-service.js +732 -0
  93. package/src/server/services/env-checker.js +307 -0
  94. package/src/server/services/env-manager.js +300 -0
  95. package/src/server/services/favorites.js +163 -0
  96. package/src/server/services/gemini-channels.js +333 -0
  97. package/src/server/services/gemini-config.js +73 -0
  98. package/src/server/services/gemini-sessions.js +689 -0
  99. package/src/server/services/gemini-settings-manager.js +263 -0
  100. package/src/server/services/gemini-statistics-service.js +253 -0
  101. package/src/server/services/health-check.js +399 -0
  102. package/src/server/services/mcp-service.js +1188 -0
  103. package/src/server/services/prompts-service.js +492 -0
  104. package/src/server/services/proxy-runtime.js +79 -0
  105. package/src/server/services/pty-manager.js +435 -0
  106. package/src/server/services/rules-service.js +401 -0
  107. package/src/server/services/session-cache.js +127 -0
  108. package/src/server/services/session-converter.js +577 -0
  109. package/src/server/services/sessions.js +757 -0
  110. package/src/server/services/settings-manager.js +163 -0
  111. package/src/server/services/skill-service.js +965 -0
  112. package/src/server/services/speed-test.js +545 -0
  113. package/src/server/services/statistics-service.js +386 -0
  114. package/src/server/services/terminal-commands.js +155 -0
  115. package/src/server/services/terminal-config.js +140 -0
  116. package/src/server/services/terminal-detector.js +306 -0
  117. package/src/server/services/ui-config.js +130 -0
  118. package/src/server/services/workspace-service.js +662 -0
  119. package/src/server/utils/pricing.js +41 -0
  120. package/src/server/websocket-server.js +557 -0
  121. package/src/ui/menu.js +129 -0
  122. package/src/ui/prompts.js +100 -0
  123. package/src/utils/format.js +43 -0
  124. package/src/utils/port-helper.js +94 -0
  125. 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
+ };