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.
Files changed (185) hide show
  1. package/CHANGELOG.md +599 -0
  2. package/LICENSE +21 -0
  3. package/README.md +439 -0
  4. package/bin/ctx.js +8 -0
  5. package/dist/web/assets/Analytics-DN_YsnkW.js +39 -0
  6. package/dist/web/assets/Analytics-DuYvId7u.css +1 -0
  7. package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
  8. package/dist/web/assets/ConfigTemplates-DpXIMy0p.js +1 -0
  9. package/dist/web/assets/Home-38JTUlYt.js +1 -0
  10. package/dist/web/assets/Home-CjupSEWE.css +1 -0
  11. package/dist/web/assets/PluginManager-CX2tgq2H.js +1 -0
  12. package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
  13. package/dist/web/assets/ProjectList-C1lDcsn6.js +1 -0
  14. package/dist/web/assets/ProjectList-oJIyIRkP.css +1 -0
  15. package/dist/web/assets/SessionList-C55tjV7i.css +1 -0
  16. package/dist/web/assets/SessionList-CZ7T6rVx.js +1 -0
  17. package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
  18. package/dist/web/assets/SkillManager-DLN9f79y.js +1 -0
  19. package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
  20. package/dist/web/assets/WorkspaceManager-DxlHZkpZ.js +1 -0
  21. package/dist/web/assets/icons-DRrXwWZi.js +1 -0
  22. package/dist/web/assets/index-CetESrXw.css +1 -0
  23. package/dist/web/assets/index-Cfvn-2Gb.js +2 -0
  24. package/dist/web/assets/markdown-BfC0goYb.css +10 -0
  25. package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
  26. package/dist/web/assets/naive-ui-DlpKk-8M.js +1 -0
  27. package/dist/web/assets/vendors-DMjSfzlv.js +7 -0
  28. package/dist/web/assets/vue-vendor-DET08QYg.js +45 -0
  29. package/dist/web/favicon.ico +0 -0
  30. package/dist/web/index.html +20 -0
  31. package/dist/web/logo.png +0 -0
  32. package/docs/bannel.png +0 -0
  33. package/docs/home.png +0 -0
  34. package/docs/logo.png +0 -0
  35. package/docs/model-redirection.md +251 -0
  36. package/docs/multi-channel-load-balancing.md +249 -0
  37. package/package.json +80 -0
  38. package/src/commands/channels.js +551 -0
  39. package/src/commands/cli-type.js +101 -0
  40. package/src/commands/daemon.js +365 -0
  41. package/src/commands/doctor.js +333 -0
  42. package/src/commands/export-config.js +205 -0
  43. package/src/commands/list.js +222 -0
  44. package/src/commands/logs.js +261 -0
  45. package/src/commands/plugin.js +585 -0
  46. package/src/commands/port-config.js +135 -0
  47. package/src/commands/proxy-control.js +264 -0
  48. package/src/commands/proxy.js +152 -0
  49. package/src/commands/resume.js +137 -0
  50. package/src/commands/search.js +190 -0
  51. package/src/commands/security.js +37 -0
  52. package/src/commands/stats.js +398 -0
  53. package/src/commands/switch.js +48 -0
  54. package/src/commands/toggle-proxy.js +247 -0
  55. package/src/commands/ui.js +99 -0
  56. package/src/commands/update.js +97 -0
  57. package/src/commands/workspace.js +454 -0
  58. package/src/config/default.js +69 -0
  59. package/src/config/loader.js +149 -0
  60. package/src/config/model-metadata.js +167 -0
  61. package/src/config/model-metadata.json +125 -0
  62. package/src/config/model-pricing.js +35 -0
  63. package/src/config/paths.js +190 -0
  64. package/src/index.js +680 -0
  65. package/src/plugins/constants.js +15 -0
  66. package/src/plugins/event-bus.js +54 -0
  67. package/src/plugins/manifest-validator.js +129 -0
  68. package/src/plugins/plugin-api.js +128 -0
  69. package/src/plugins/plugin-installer.js +601 -0
  70. package/src/plugins/plugin-loader.js +229 -0
  71. package/src/plugins/plugin-manager.js +170 -0
  72. package/src/plugins/registry.js +152 -0
  73. package/src/plugins/schema/plugin-manifest.json +115 -0
  74. package/src/reset-config.js +94 -0
  75. package/src/server/api/agents.js +826 -0
  76. package/src/server/api/aliases.js +36 -0
  77. package/src/server/api/channels.js +368 -0
  78. package/src/server/api/claude-hooks.js +480 -0
  79. package/src/server/api/codex-channels.js +417 -0
  80. package/src/server/api/codex-projects.js +104 -0
  81. package/src/server/api/codex-proxy.js +195 -0
  82. package/src/server/api/codex-sessions.js +483 -0
  83. package/src/server/api/codex-statistics.js +57 -0
  84. package/src/server/api/commands.js +482 -0
  85. package/src/server/api/config-export.js +212 -0
  86. package/src/server/api/config-registry.js +357 -0
  87. package/src/server/api/config-sync.js +155 -0
  88. package/src/server/api/config-templates.js +248 -0
  89. package/src/server/api/config.js +521 -0
  90. package/src/server/api/convert.js +260 -0
  91. package/src/server/api/dashboard.js +142 -0
  92. package/src/server/api/env.js +144 -0
  93. package/src/server/api/favorites.js +77 -0
  94. package/src/server/api/gemini-channels.js +366 -0
  95. package/src/server/api/gemini-projects.js +91 -0
  96. package/src/server/api/gemini-proxy.js +173 -0
  97. package/src/server/api/gemini-sessions.js +376 -0
  98. package/src/server/api/gemini-statistics.js +57 -0
  99. package/src/server/api/health-check.js +31 -0
  100. package/src/server/api/mcp.js +399 -0
  101. package/src/server/api/opencode-channels.js +419 -0
  102. package/src/server/api/opencode-projects.js +99 -0
  103. package/src/server/api/opencode-proxy.js +207 -0
  104. package/src/server/api/opencode-sessions.js +327 -0
  105. package/src/server/api/opencode-statistics.js +57 -0
  106. package/src/server/api/plugins.js +463 -0
  107. package/src/server/api/pm2-autostart.js +269 -0
  108. package/src/server/api/projects.js +124 -0
  109. package/src/server/api/prompts.js +279 -0
  110. package/src/server/api/proxy.js +306 -0
  111. package/src/server/api/security.js +53 -0
  112. package/src/server/api/sessions.js +514 -0
  113. package/src/server/api/settings.js +142 -0
  114. package/src/server/api/skills.js +570 -0
  115. package/src/server/api/statistics.js +238 -0
  116. package/src/server/api/ui-config.js +64 -0
  117. package/src/server/api/workspaces.js +456 -0
  118. package/src/server/codex-proxy-server.js +681 -0
  119. package/src/server/dev-server.js +26 -0
  120. package/src/server/gemini-proxy-server.js +610 -0
  121. package/src/server/index.js +422 -0
  122. package/src/server/opencode-proxy-server.js +4771 -0
  123. package/src/server/proxy-server.js +669 -0
  124. package/src/server/services/agents-service.js +1137 -0
  125. package/src/server/services/alias.js +71 -0
  126. package/src/server/services/channel-health.js +234 -0
  127. package/src/server/services/channel-scheduler.js +240 -0
  128. package/src/server/services/channels.js +447 -0
  129. package/src/server/services/codex-channels.js +705 -0
  130. package/src/server/services/codex-config.js +90 -0
  131. package/src/server/services/codex-parser.js +322 -0
  132. package/src/server/services/codex-sessions.js +936 -0
  133. package/src/server/services/codex-settings-manager.js +619 -0
  134. package/src/server/services/codex-speed-test-template.json +24 -0
  135. package/src/server/services/codex-statistics-service.js +161 -0
  136. package/src/server/services/commands-service.js +574 -0
  137. package/src/server/services/config-export-service.js +1165 -0
  138. package/src/server/services/config-registry-service.js +828 -0
  139. package/src/server/services/config-sync-manager.js +941 -0
  140. package/src/server/services/config-sync-service.js +504 -0
  141. package/src/server/services/config-templates-service.js +913 -0
  142. package/src/server/services/enhanced-cache.js +196 -0
  143. package/src/server/services/env-checker.js +409 -0
  144. package/src/server/services/env-manager.js +436 -0
  145. package/src/server/services/favorites.js +165 -0
  146. package/src/server/services/format-converter.js +620 -0
  147. package/src/server/services/gemini-channels.js +459 -0
  148. package/src/server/services/gemini-config.js +73 -0
  149. package/src/server/services/gemini-sessions.js +689 -0
  150. package/src/server/services/gemini-settings-manager.js +263 -0
  151. package/src/server/services/gemini-statistics-service.js +157 -0
  152. package/src/server/services/health-check.js +85 -0
  153. package/src/server/services/mcp-client.js +790 -0
  154. package/src/server/services/mcp-service.js +1732 -0
  155. package/src/server/services/model-detector.js +1245 -0
  156. package/src/server/services/network-access.js +80 -0
  157. package/src/server/services/opencode-channels.js +366 -0
  158. package/src/server/services/opencode-gateway-adapters.js +1168 -0
  159. package/src/server/services/opencode-gateway-converter.js +639 -0
  160. package/src/server/services/opencode-sessions.js +931 -0
  161. package/src/server/services/opencode-settings-manager.js +478 -0
  162. package/src/server/services/opencode-statistics-service.js +161 -0
  163. package/src/server/services/plugins-service.js +1268 -0
  164. package/src/server/services/prompts-service.js +534 -0
  165. package/src/server/services/proxy-runtime.js +79 -0
  166. package/src/server/services/repo-scanner-base.js +708 -0
  167. package/src/server/services/request-logger.js +130 -0
  168. package/src/server/services/response-decoder.js +21 -0
  169. package/src/server/services/security-config.js +131 -0
  170. package/src/server/services/session-cache.js +127 -0
  171. package/src/server/services/session-converter.js +577 -0
  172. package/src/server/services/sessions.js +900 -0
  173. package/src/server/services/settings-manager.js +163 -0
  174. package/src/server/services/skill-service.js +1482 -0
  175. package/src/server/services/speed-test.js +1146 -0
  176. package/src/server/services/statistics-service.js +1043 -0
  177. package/src/server/services/ui-config.js +132 -0
  178. package/src/server/services/workspace-service.js +830 -0
  179. package/src/server/utils/pricing.js +73 -0
  180. package/src/server/websocket-server.js +513 -0
  181. package/src/ui/menu.js +139 -0
  182. package/src/ui/prompts.js +100 -0
  183. package/src/utils/format.js +43 -0
  184. package/src/utils/port-helper.js +108 -0
  185. 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
+ };