coding-tool-x 3.8.4 → 3.8.6

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 (434) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +86 -15
  3. package/dist/web/assets/AgentsDrawer-Db9wAyaZ.js +1 -0
  4. package/dist/web/assets/AgentsDrawer-Pw2k-ZAa.css +1 -0
  5. package/dist/web/assets/Analytics-C4gMzv7g.js +1 -0
  6. package/dist/web/assets/Analytics-CyHhR126.css +1 -0
  7. package/dist/web/assets/AssetPathField-Bd_nMFu3.js +1 -0
  8. package/dist/web/assets/ChatHistoryDrawer-B2qBQs9L.css +1 -0
  9. package/dist/web/assets/ChatHistoryDrawer-CwzJHyNP.js +1 -0
  10. package/dist/web/assets/CollapseItem-CTCfSiue.js +1 -0
  11. package/dist/web/assets/CommandsDrawer-BZJxlPvV.css +1 -0
  12. package/dist/web/assets/CommandsDrawer-vDghB1U9.js +1 -0
  13. package/dist/web/assets/ConfigExportDrawer-C-XDvlHc.js +1 -0
  14. package/dist/web/assets/ConfigExportDrawer-dwruRKWr.css +1 -0
  15. package/dist/web/assets/ConfigTemplates-CYY8XRED.js +1 -0
  16. package/dist/web/assets/ConfigTemplatesDrawer-BfDi8yO3.css +1 -0
  17. package/dist/web/assets/ConfigTemplatesDrawer-CbsJp76X.js +1 -0
  18. package/dist/web/assets/ConfigTemplatesPanel-CWyPtk67.css +1 -0
  19. package/dist/web/assets/ConfigTemplatesPanel-liMSTh7s.js +1 -0
  20. package/dist/web/assets/Divider-D28g1gwj.js +1 -0
  21. package/dist/web/assets/DrawerContent-ChXgZ1Xm.js +1 -0
  22. package/dist/web/assets/FavoritesDrawer-CG9eI9Ay.css +1 -0
  23. package/dist/web/assets/FavoritesDrawer-D6QSiYaL.js +1 -0
  24. package/dist/web/assets/GatewayConvertDrawer-CuNlf7AC.css +1 -0
  25. package/dist/web/assets/GatewayConvertDrawer-RvET6iNG.js +1 -0
  26. package/dist/web/assets/Home-CSuFuJ9Y.css +1 -0
  27. package/dist/web/assets/Home-P8eseZcf.js +1 -0
  28. package/dist/web/assets/MarkdownEditor-CYmLGYju.js +1 -0
  29. package/dist/web/assets/MarkdownEditor-qfVgSre_.css +1 -0
  30. package/dist/web/assets/McpDrawer-C0dnsZhQ.css +1 -0
  31. package/dist/web/assets/McpDrawer-Ch3bkKIq.js +1 -0
  32. package/dist/web/assets/McpFormDrawer-BRbDcawn.css +1 -0
  33. package/dist/web/assets/McpFormDrawer-BcTlgSpN.js +1 -0
  34. package/dist/web/assets/PluginManager-Dh2o0xkZ.js +1 -0
  35. package/dist/web/assets/PluginsDrawer-DM3QjprE.css +1 -0
  36. package/dist/web/assets/PluginsDrawer-deKQ5u3I.js +1 -0
  37. package/dist/web/assets/PluginsPanel-BofsgQgj.js +1 -0
  38. package/dist/web/assets/PluginsPanel-D6ZqITcR.css +1 -0
  39. package/dist/web/assets/ProjectList-677RLZ8O.css +1 -0
  40. package/dist/web/assets/ProjectList-DhSWJHTA.js +1 -0
  41. package/dist/web/assets/PromptsDrawer-BIgiYCEQ.css +1 -0
  42. package/dist/web/assets/PromptsDrawer-Cfb2FeeU.js +1 -0
  43. package/dist/web/assets/RecentSessionsDrawer-3eXamJhr.js +1 -0
  44. package/dist/web/assets/RecentSessionsDrawer-CKFsOYDM.css +1 -0
  45. package/dist/web/assets/SessionCard-CW0eHURI.js +1 -0
  46. package/dist/web/assets/SessionCard-aM2NHu6i.css +1 -0
  47. package/dist/web/assets/SessionList-BpYRz1cX.css +1 -0
  48. package/dist/web/assets/SessionList-DQfyDGWU.js +1 -0
  49. package/dist/web/assets/SettingsDrawer-Dw5NUgHC.css +1 -0
  50. package/dist/web/assets/SettingsDrawer-u_vwUuPS.js +1 -0
  51. package/dist/web/assets/SkillManager-C0GCT2Kb.css +1 -0
  52. package/dist/web/assets/SkillManager-DHMprJLA.js +1 -0
  53. package/dist/web/assets/SkillsDrawer-BCNme-aJ.css +1 -0
  54. package/dist/web/assets/SkillsDrawer-CkF3BpyC.js +1 -0
  55. package/dist/web/assets/SkillsPanel-B3Nm5j32.css +1 -0
  56. package/dist/web/assets/SkillsPanel-DWE4pFGx.js +1 -0
  57. package/dist/web/assets/SpeedTestDrawer-BgfG1Ked.js +1 -0
  58. package/dist/web/assets/SpeedTestDrawer-Dc0WToJ_.css +1 -0
  59. package/dist/web/assets/Upload-CNwaGOws.js +1 -0
  60. package/dist/web/assets/WorkspaceDrawer-Br5CeCzL.js +1 -0
  61. package/dist/web/assets/WorkspaceDrawer-CGnMu9p1.css +1 -0
  62. package/dist/web/assets/WorkspaceManager-B2O8-qIw.js +1 -0
  63. package/dist/web/assets/WorkspacePanel-5oDKit3c.js +1 -0
  64. package/dist/web/assets/WorkspacePanel-D0-zHTHP.css +1 -0
  65. package/dist/web/assets/_plugin-vue_export-helper-BeLZJZij.js +1 -0
  66. package/dist/web/assets/client-UDwXBP9i.js +1 -0
  67. package/dist/web/assets/clipboard-B46yyoz_.js +1 -0
  68. package/dist/web/assets/headers-cf-QapUq.js +1 -0
  69. package/dist/web/assets/icons-B2Vg-IOC.js +1 -0
  70. package/dist/web/assets/index-BMAUPq5K.js +2 -0
  71. package/dist/web/assets/index-Bi1MQHYX.css +1 -0
  72. package/dist/web/assets/light-Cp0t7v32.js +1 -0
  73. package/dist/web/assets/markdown-u082bbtj.js +1 -0
  74. package/dist/web/assets/platforms-CBkB45JP.js +1 -0
  75. package/dist/web/assets/platforms-FTpjxAT6.js +1 -0
  76. package/dist/web/assets/project-config-B2baPzYL.js +1 -0
  77. package/dist/web/assets/sessions-Cmy31slD.js +1 -0
  78. package/dist/web/assets/sessions-yC_oVVh8.js +1 -0
  79. package/dist/web/assets/skill-repo-input-B_fkGlUK.js +1 -0
  80. package/dist/web/assets/use-lock-html-scroll-DXIqmUW4.js +1 -0
  81. package/dist/web/assets/use-message-DoLM8C1C.js +1 -0
  82. package/dist/web/assets/useResponsiveDrawer-jbmqZuJs.js +1 -0
  83. package/dist/web/assets/vendors-D0c_0KLP.js +1 -0
  84. package/dist/web/assets/vue-vendor-BqZbbXHA.js +1 -0
  85. package/dist/web/assets/workspaces-mjyPVGS8.js +1 -0
  86. package/dist/web/assets/xml-CgY3RMlX.js +1 -0
  87. package/dist/web/index.html +12 -8
  88. package/docs/native-log-memory-2026-09-16.md +43 -0
  89. package/docs/superpowers/plans/2026-08-26-configuration-driven-cli-platform.md +1578 -0
  90. package/docs/superpowers/plans/2026-08-30-project-scoped-config-control.md +1260 -0
  91. package/docs/superpowers/plans/2026-09-01-omp-skill-refresh-boundary.md +213 -0
  92. package/docs/superpowers/plans/2026-09-01-realtime-logs-panel.md +125 -0
  93. package/docs/superpowers/plans/2026-09-02-config-driven-platform-boundary.md +86 -0
  94. package/docs/superpowers/plans/2026-09-02-platform-implementation-drivers.md +508 -0
  95. package/docs/superpowers/plans/2026-09-03-cli-platform-correctness-boundary.md +760 -0
  96. package/docs/superpowers/plans/2026-09-03-dependency-security-remediation.md +133 -0
  97. package/docs/superpowers/plans/2026-09-04-model-catalog-pricing-sync.md +1078 -0
  98. package/docs/superpowers/plans/2026-09-05-codex-project-scan-500.md +128 -0
  99. package/docs/superpowers/plans/2026-09-06-project-skill-path-fix.md +97 -0
  100. package/docs/superpowers/specs/2026-08-26-configuration-driven-cli-platform-design.md +408 -0
  101. package/docs/superpowers/specs/2026-08-30-project-scoped-config-control-design.md +467 -0
  102. package/docs/superpowers/specs/2026-09-01-claude-session-content-repair-design.md +111 -0
  103. package/docs/superpowers/specs/2026-09-01-omp-skill-refresh-boundary-design.md +83 -0
  104. package/docs/superpowers/specs/2026-09-01-realtime-logs-panel-design.md +27 -0
  105. package/docs/superpowers/specs/2026-09-02-config-driven-platform-boundary-design.md +64 -0
  106. package/docs/superpowers/specs/2026-09-02-platform-implementation-driver-boundary-design.md +123 -0
  107. package/docs/superpowers/specs/2026-09-03-cli-platform-correctness-boundary-design.md +249 -0
  108. package/docs/superpowers/specs/2026-09-03-dependency-security-remediation-design.md +87 -0
  109. package/docs/superpowers/specs/2026-09-04-model-catalog-pricing-sync-design.md +301 -0
  110. package/docs/superpowers/specs/2026-09-05-codex-project-scan-500-design.md +24 -0
  111. package/package.json +8 -4
  112. package/src/commands/channels.js +88 -191
  113. package/src/commands/cli-type.js +19 -11
  114. package/src/commands/daemon.js +104 -32
  115. package/src/commands/doctor.js +16 -9
  116. package/src/commands/export-config.js +23 -5
  117. package/src/commands/list.js +130 -52
  118. package/src/commands/logs.js +44 -27
  119. package/src/commands/platform-command-registry.js +78 -0
  120. package/src/commands/plugin.js +11 -5
  121. package/src/commands/port-config.js +34 -52
  122. package/src/commands/proxy-control.js +207 -145
  123. package/src/commands/proxy.js +2 -2
  124. package/src/commands/resume.js +87 -91
  125. package/src/commands/search.js +137 -67
  126. package/src/commands/stats.js +30 -22
  127. package/src/commands/switch.js +1 -1
  128. package/src/commands/toggle-proxy.js +94 -111
  129. package/src/commands/ui.js +67 -6
  130. package/src/commands/workspace.js +90 -8
  131. package/src/config/default.js +6 -53
  132. package/src/config/loader.js +61 -24
  133. package/src/config/model-metadata.js +49 -59
  134. package/src/config/model-metadata.json +4095 -99
  135. package/src/config/paths.js +181 -8
  136. package/src/index.js +134 -71
  137. package/src/platforms/access.js +196 -0
  138. package/src/{server/services → platforms}/agents-service.js +354 -197
  139. package/src/platforms/channel-auth-registry.js +165 -0
  140. package/src/platforms/channel-auth-service.js +107 -0
  141. package/src/{server/services → platforms}/commands-service.js +195 -115
  142. package/src/platforms/driver-registry.js +59 -0
  143. package/src/platforms/drivers/claude/api-operations.js +62 -0
  144. package/src/{server/services/channels.js → platforms/drivers/claude/channels-implementation.js} +131 -33
  145. package/src/platforms/drivers/claude/channels.js +39 -0
  146. package/src/{server/services/config-sync-service.js → platforms/drivers/claude/config-sync.js} +29 -5
  147. package/src/{server/services → platforms/drivers/claude}/health-check.js +4 -4
  148. package/src/platforms/drivers/claude/health.js +22 -0
  149. package/src/platforms/drivers/claude/mcp.js +9 -0
  150. package/src/{server/services/settings-manager.js → platforms/drivers/claude/native-config-implementation.js} +13 -4
  151. package/src/platforms/drivers/claude/native-config.js +46 -0
  152. package/src/platforms/drivers/claude/native-logs.js +72 -0
  153. package/src/{server/services/claude-openai-gateway.js → platforms/drivers/claude/openai-gateway.js} +8 -65
  154. package/src/platforms/drivers/claude/project-config.js +21 -0
  155. package/src/platforms/drivers/claude/projects.js +62 -0
  156. package/src/platforms/drivers/claude/prompts.js +9 -0
  157. package/src/{server/proxy-server.js → platforms/drivers/claude/proxy-implementation.js} +41 -278
  158. package/src/platforms/drivers/claude/proxy.js +16 -0
  159. package/src/platforms/drivers/claude/resource-sync.js +9 -0
  160. package/src/platforms/drivers/claude/session-history-adapter.js +313 -0
  161. package/src/{server/services/sessions.js → platforms/drivers/claude/sessions-implementation.js} +145 -35
  162. package/src/platforms/drivers/claude/sessions.js +76 -0
  163. package/src/{server/services/claude-statistics-service.js → platforms/drivers/claude/statistics-implementation.js} +2 -2
  164. package/src/platforms/drivers/claude/statistics.js +16 -0
  165. package/src/{server/services/claude-wire.js → platforms/drivers/claude/wire.js} +1 -1
  166. package/src/platforms/drivers/codex/api-operations.js +9 -0
  167. package/src/{server/services/codex-channels.js → platforms/drivers/codex/channels-implementation.js} +149 -14
  168. package/src/platforms/drivers/codex/channels.js +45 -0
  169. package/src/{server/services/codex-config.js → platforms/drivers/codex/config.js} +12 -2
  170. package/src/{server/services/codex-env-manager.js → platforms/drivers/codex/env-manager.js} +1 -1
  171. package/src/platforms/drivers/codex/mcp.js +9 -0
  172. package/src/{server/services/codex-settings-manager.js → platforms/drivers/codex/native-config-implementation.js} +15 -6
  173. package/src/platforms/drivers/codex/native-config.js +47 -0
  174. package/src/platforms/drivers/codex/native-logs.js +166 -0
  175. package/src/platforms/drivers/codex/project-config.js +21 -0
  176. package/src/platforms/drivers/codex/projects.js +26 -0
  177. package/src/platforms/drivers/codex/prompts.js +9 -0
  178. package/src/{server/codex-proxy-server.js → platforms/drivers/codex/proxy-implementation.js} +30 -233
  179. package/src/platforms/drivers/codex/proxy.js +16 -0
  180. package/src/platforms/drivers/codex/resource-sync.js +9 -0
  181. package/src/{server/services/session-history-adapters/codex.js → platforms/drivers/codex/session-history-adapter.js} +37 -23
  182. package/src/{server/services/codex-sessions.js → platforms/drivers/codex/sessions-implementation.js} +81 -75
  183. package/src/platforms/drivers/codex/sessions.js +36 -0
  184. package/src/{server/services/codex-statistics-service.js → platforms/drivers/codex/statistics-implementation.js} +2 -2
  185. package/src/platforms/drivers/codex/statistics.js +16 -0
  186. package/src/platforms/drivers/gemini/api-operations.js +9 -0
  187. package/src/{server/services/gemini-channels.js → platforms/drivers/gemini/channels-implementation.js} +202 -37
  188. package/src/platforms/drivers/gemini/channels.js +39 -0
  189. package/src/{server/services/gemini-config.js → platforms/drivers/gemini/config.js} +12 -2
  190. package/src/platforms/drivers/gemini/mcp.js +9 -0
  191. package/src/{server/services/gemini-settings-manager.js → platforms/drivers/gemini/native-config-implementation.js} +14 -5
  192. package/src/platforms/drivers/gemini/native-config.js +39 -0
  193. package/src/platforms/drivers/gemini/native-logs.js +180 -0
  194. package/src/platforms/drivers/gemini/project-config.js +21 -0
  195. package/src/platforms/drivers/gemini/projects.js +26 -0
  196. package/src/platforms/drivers/gemini/prompts.js +9 -0
  197. package/src/{server/gemini-proxy-server.js → platforms/drivers/gemini/proxy-implementation.js} +36 -267
  198. package/src/platforms/drivers/gemini/proxy.js +16 -0
  199. package/src/platforms/drivers/gemini/resource-sync.js +9 -0
  200. package/src/{server/services/session-history-adapters/gemini.js → platforms/drivers/gemini/session-history-adapter.js} +70 -18
  201. package/src/{server/services/gemini-sessions.js → platforms/drivers/gemini/sessions-implementation.js} +53 -37
  202. package/src/platforms/drivers/gemini/sessions.js +47 -0
  203. package/src/{server/services/gemini-statistics-service.js → platforms/drivers/gemini/statistics-implementation.js} +2 -2
  204. package/src/platforms/drivers/gemini/statistics.js +16 -0
  205. package/src/{server/services/gemini-wire.js → platforms/drivers/gemini/wire.js} +1 -1
  206. package/src/platforms/drivers/generic-filesystem.js +166 -0
  207. package/src/platforms/drivers/generic-jsonl.js +141 -0
  208. package/src/platforms/drivers/generic-mcp.js +44 -0
  209. package/src/platforms/drivers/generic-openai-compatible.js +45 -0
  210. package/src/platforms/drivers/generic-prompt.js +41 -0
  211. package/src/platforms/drivers/legacy.js +677 -0
  212. package/src/platforms/drivers/native-config-snapshot.js +125 -0
  213. package/src/platforms/drivers/native-log-utils.js +1292 -0
  214. package/src/platforms/drivers/notification-hooks.js +578 -0
  215. package/src/{server/api/omp-config.js → platforms/drivers/omp/api-operations.js} +52 -64
  216. package/src/{server/services/omp-auth-provider-worker.js → platforms/drivers/omp/auth-provider-worker.js} +1 -1
  217. package/src/{server/services/omp-auth-providers.js → platforms/drivers/omp/auth-providers.js} +1 -1
  218. package/src/{server/services/omp-channels.js → platforms/drivers/omp/channels-implementation.js} +166 -39
  219. package/src/platforms/drivers/omp/channels.js +58 -0
  220. package/src/{server/services/omp-config.js → platforms/drivers/omp/config.js} +53 -13
  221. package/src/{server/services/omp-gateway-routing.js → platforms/drivers/omp/gateway-routing.js} +14 -7
  222. package/src/{server/services/omp-gateway.js → platforms/drivers/omp/gateway.js} +4 -262
  223. package/src/platforms/drivers/omp/mcp.js +9 -0
  224. package/src/{server/services/omp-settings-manager.js → platforms/drivers/omp/native-config-implementation.js} +75 -10
  225. package/src/platforms/drivers/omp/native-config.js +46 -0
  226. package/src/platforms/drivers/omp/native-logs.js +52 -0
  227. package/src/{server/services/omp-native-plugin-adapter.js → platforms/drivers/omp/native-plugin-adapter.js} +91 -4
  228. package/src/platforms/drivers/omp/project-config.js +29 -0
  229. package/src/platforms/drivers/omp/projects.js +20 -0
  230. package/src/platforms/drivers/omp/prompts.js +9 -0
  231. package/src/{server/omp-proxy-server.js → platforms/drivers/omp/proxy-implementation.js} +6 -15
  232. package/src/platforms/drivers/omp/proxy.js +16 -0
  233. package/src/platforms/drivers/omp/resource-sync.js +9 -0
  234. package/src/{server/services/session-history-adapters/omp.js → platforms/drivers/omp/session-history-adapter.js} +42 -18
  235. package/src/{server/services/omp-sessions.js → platforms/drivers/omp/sessions-implementation.js} +130 -94
  236. package/src/platforms/drivers/omp/sessions.js +36 -0
  237. package/src/{server/services/omp-skill-discovery.js → platforms/drivers/omp/skill-discovery.js} +159 -32
  238. package/src/{server/services/omp-skill-settings-service.js → platforms/drivers/omp/skill-settings.js} +1 -1
  239. package/src/{server/services/omp-statistics-service.js → platforms/drivers/omp/statistics-implementation.js} +3 -3
  240. package/src/platforms/drivers/omp/statistics.js +16 -0
  241. package/src/platforms/drivers/opencode/api-operations.js +9 -0
  242. package/src/{server/services/opencode-channels.js → platforms/drivers/opencode/channels-implementation.js} +28 -9
  243. package/src/platforms/drivers/opencode/channels.js +40 -0
  244. package/src/platforms/drivers/opencode/conversion.js +41 -0
  245. package/src/{server/services/opencode-gateway-converter.js → platforms/drivers/opencode/gateway-converter.js} +1 -1
  246. package/src/platforms/drivers/opencode/mcp.js +9 -0
  247. package/src/{server/services/opencode-settings-manager.js → platforms/drivers/opencode/native-config-implementation.js} +17 -6
  248. package/src/platforms/drivers/opencode/native-config.js +26 -0
  249. package/src/platforms/drivers/opencode/native-logs.js +133 -0
  250. package/src/platforms/drivers/opencode/project-config.js +98 -0
  251. package/src/platforms/drivers/opencode/projects.js +20 -0
  252. package/src/platforms/drivers/opencode/prompts.js +9 -0
  253. package/src/{server/opencode-proxy-server.js → platforms/drivers/opencode/proxy-implementation.js} +27 -362
  254. package/src/platforms/drivers/opencode/proxy.js +16 -0
  255. package/src/platforms/drivers/opencode/resource-sync.js +9 -0
  256. package/src/{server/services/opencode-sessions.js → platforms/drivers/opencode/sessions-implementation.js} +211 -64
  257. package/src/platforms/drivers/opencode/sessions.js +48 -0
  258. package/src/{server/services/opencode-statistics-service.js → platforms/drivers/opencode/statistics-implementation.js} +2 -2
  259. package/src/platforms/drivers/opencode/statistics.js +16 -0
  260. package/src/platforms/drivers/secure-file-driver.js +272 -0
  261. package/src/platforms/drivers/unsupported.js +7 -0
  262. package/src/platforms/manifest-schema.js +398 -0
  263. package/src/platforms/manifests/claude.json +489 -0
  264. package/src/platforms/manifests/codex.json +448 -0
  265. package/src/platforms/manifests/gemini.json +447 -0
  266. package/src/platforms/manifests/omp.json +496 -0
  267. package/src/platforms/manifests/opencode.json +422 -0
  268. package/src/{server/services → platforms}/native-oauth-adapters.js +98 -418
  269. package/src/{server/services → platforms}/notification-hooks.js +190 -1023
  270. package/src/platforms/oauth-credentials-service.js +635 -0
  271. package/src/platforms/path-resolver.js +179 -0
  272. package/src/platforms/platform-path-context.js +305 -0
  273. package/src/platforms/registry.js +248 -0
  274. package/src/platforms/remote-notification-providers.js +678 -0
  275. package/src/platforms/runtime.js +259 -0
  276. package/src/{server/services/session-history-adapters/index.js → platforms/session-history-adapters.js} +8 -1
  277. package/src/plugins/plugin-loader.js +2 -2
  278. package/src/plugins/plugin-manager.js +2 -2
  279. package/src/plugins/registry.js +27 -10
  280. package/src/reset-config.js +21 -5
  281. package/src/server/api/agents.js +46 -72
  282. package/src/server/api/commands.js +16 -6
  283. package/src/server/api/config-registry.js +206 -59
  284. package/src/server/api/config-sync.js +3 -2
  285. package/src/server/api/config-templates.js +20 -4
  286. package/src/server/api/config.js +111 -102
  287. package/src/server/api/convert.js +54 -178
  288. package/src/server/api/dashboard.js +168 -102
  289. package/src/server/api/hooks.js +1 -1
  290. package/src/server/api/mcp.js +224 -84
  291. package/src/server/api/platform-api-config.js +102 -0
  292. package/src/server/api/platform-route-factory.js +215 -0
  293. package/src/server/api/platform-routes.js +26 -0
  294. package/src/server/api/platforms.js +110 -0
  295. package/src/server/api/plugins.js +57 -57
  296. package/src/server/api/project-config.js +168 -0
  297. package/src/server/api/prompts.js +58 -45
  298. package/src/server/api/settings.js +55 -8
  299. package/src/server/api/skills.js +262 -133
  300. package/src/server/api/ui-config.js +2 -2
  301. package/src/server/api/validation-errors.js +18 -4
  302. package/src/server/api/workspaces.js +47 -12
  303. package/src/server/index.js +164 -213
  304. package/src/server/platform-context.js +81 -0
  305. package/src/server/services/channel-balance.js +133 -24
  306. package/src/server/services/channel-health.js +21 -6
  307. package/src/server/services/channel-scheduler.js +29 -39
  308. package/src/server/services/channel-sync-utils.js +23 -13
  309. package/src/server/services/config-export-service.js +350 -319
  310. package/src/server/services/config-registry-service.js +280 -54
  311. package/src/server/services/config-sync-manager.js +172 -944
  312. package/src/server/services/config-templates-service.js +419 -353
  313. package/src/server/services/control-manifest-store.js +206 -0
  314. package/src/server/services/dashboard-snapshot-worker.js +656 -100
  315. package/src/server/services/effective-control-service.js +557 -0
  316. package/src/server/services/local-resource-index.js +314 -0
  317. package/src/server/services/mcp-service.js +607 -977
  318. package/src/server/services/model-detector.js +15 -10
  319. package/src/server/services/native-log-channel-resolver.js +214 -0
  320. package/src/server/services/native-log-observer.js +291 -0
  321. package/src/server/services/network-access.js +12 -2
  322. package/src/server/services/platform-catalog.js +82 -0
  323. package/src/server/services/platform-resolution.js +32 -29
  324. package/src/server/services/plugins-service.js +376 -122
  325. package/src/server/services/project-config-service.js +550 -0
  326. package/src/server/services/project-path-validation.js +31 -0
  327. package/src/server/services/prompts-service.js +267 -233
  328. package/src/server/services/proxy-runtime.js +7 -4
  329. package/src/server/services/remote-credential-cache.js +43 -0
  330. package/src/server/services/repo-scanner-base.js +182 -75
  331. package/src/server/services/request-logger.js +7 -4
  332. package/src/server/services/session-history-index.js +876 -214
  333. package/src/server/services/session-history-worker.js +228 -33
  334. package/src/server/services/session-snapshot-worker.js +124 -34
  335. package/src/server/services/session-snapshots.js +4 -2
  336. package/src/server/services/skill-artifact-store.js +355 -0
  337. package/src/server/services/skill-format-adapters.js +65 -0
  338. package/src/server/services/skill-projection-service.js +323 -0
  339. package/src/server/services/skill-refresh-task-service.js +275 -0
  340. package/src/server/services/skill-service.js +2020 -677
  341. package/src/server/services/snapshot-cache.js +30 -8
  342. package/src/server/services/speed-test.js +8 -8
  343. package/src/server/services/sqlite-connection.js +19 -3
  344. package/src/server/services/statistics-service.js +240 -57
  345. package/src/server/services/ui-config.js +107 -76
  346. package/src/server/services/usage-log-utils.js +98 -0
  347. package/src/server/services/web-build.js +1 -1
  348. package/src/server/services/workspace-service.js +59 -29
  349. package/src/server/utils/pricing.js +10 -19
  350. package/src/server/websocket-server.js +153 -28
  351. package/src/{server/services/base → shared}/base-channel-service.js +156 -28
  352. package/src/shared/capability-driver.js +104 -0
  353. package/src/shared/channel-auth-policy.js +27 -0
  354. package/src/shared/channel-driver.js +161 -0
  355. package/src/shared/driver-factories/api.js +180 -0
  356. package/src/shared/driver-factories/mcp.js +244 -0
  357. package/src/shared/driver-factories/projects.js +121 -0
  358. package/src/shared/driver-factories/prompts.js +58 -0
  359. package/src/shared/driver-factories/resource-sync.js +375 -0
  360. package/src/shared/driver-result.js +78 -0
  361. package/src/shared/mcp-format.js +189 -0
  362. package/src/shared/platforms.js +104 -167
  363. package/src/shared/project-config.js +512 -0
  364. package/src/shared/proxy-driver.js +57 -0
  365. package/src/{server/services/base → shared}/response-usage-parser.js +8 -4
  366. package/src/ui/menu.js +91 -67
  367. package/src/utils/port-helper.js +35 -19
  368. package/src/utils/session.js +1 -1
  369. package/dist/web/assets/Analytics-COEUXmd4.js +0 -25
  370. package/dist/web/assets/Analytics-RNn1BUbG.css +0 -1
  371. package/dist/web/assets/ConfigTemplates-DLLkS53H.js +0 -1
  372. package/dist/web/assets/Home-DCEyLzwX.js +0 -1
  373. package/dist/web/assets/Home-DYVy4cGZ.css +0 -1
  374. package/dist/web/assets/PluginManager-Oe1ZHoMO.js +0 -1
  375. package/dist/web/assets/ProjectList-CWjuAFof.js +0 -1
  376. package/dist/web/assets/ProjectList-VeMNXfgO.css +0 -1
  377. package/dist/web/assets/SessionList-C4chCPxK.css +0 -1
  378. package/dist/web/assets/SessionList-v3ehGg9U.js +0 -1
  379. package/dist/web/assets/SkillManager-BsX8Qpj0.css +0 -1
  380. package/dist/web/assets/SkillManager-DUF_qP5S.js +0 -1
  381. package/dist/web/assets/WorkspaceManager-CjAWBosO.js +0 -1
  382. package/dist/web/assets/icons-nGdSKQn2.js +0 -1
  383. package/dist/web/assets/index-D5hyHPj7.js +0 -2
  384. package/dist/web/assets/index-bJRG3RXH.css +0 -1
  385. package/dist/web/assets/markdown-BNJfEgCA.js +0 -1
  386. package/dist/web/assets/markdown-BfC0goYb.css +0 -10
  387. package/dist/web/assets/naive-ui-BALVpMgd.js +0 -1
  388. package/dist/web/assets/vendors-DQA4Hw9v.js +0 -7
  389. package/dist/web/assets/vue-vendor-B5RqjEjQ.js +0 -45
  390. package/src/config/model-pricing.js +0 -35
  391. package/src/server/api/channels.js +0 -439
  392. package/src/server/api/claude-hooks.js +0 -56
  393. package/src/server/api/claude-statistics.js +0 -57
  394. package/src/server/api/codex-channels.js +0 -491
  395. package/src/server/api/codex-projects.js +0 -117
  396. package/src/server/api/codex-proxy.js +0 -293
  397. package/src/server/api/codex-sessions.js +0 -652
  398. package/src/server/api/codex-statistics.js +0 -57
  399. package/src/server/api/gemini-channels.js +0 -392
  400. package/src/server/api/gemini-projects.js +0 -103
  401. package/src/server/api/gemini-proxy.js +0 -231
  402. package/src/server/api/gemini-sessions.js +0 -550
  403. package/src/server/api/gemini-statistics.js +0 -57
  404. package/src/server/api/health-check.js +0 -31
  405. package/src/server/api/oauth-credentials.js +0 -198
  406. package/src/server/api/omp-channels.js +0 -575
  407. package/src/server/api/omp-projects.js +0 -88
  408. package/src/server/api/omp-proxy.js +0 -125
  409. package/src/server/api/omp-sessions.js +0 -386
  410. package/src/server/api/omp-statistics.js +0 -40
  411. package/src/server/api/opencode-channels.js +0 -471
  412. package/src/server/api/opencode-projects.js +0 -110
  413. package/src/server/api/opencode-proxy.js +0 -354
  414. package/src/server/api/opencode-sessions.js +0 -466
  415. package/src/server/api/opencode-statistics.js +0 -57
  416. package/src/server/api/projects.js +0 -120
  417. package/src/server/api/proxy.js +0 -330
  418. package/src/server/api/sessions.js +0 -810
  419. package/src/server/services/oauth-credentials-service.js +0 -945
  420. package/src/server/services/omp-session-log-observer.js +0 -138
  421. package/src/server/services/opencode-gateway-adapters.js +0 -43
  422. package/src/server/services/project-snapshots.js +0 -59
  423. package/src/server/services/proxy-log-helper.js +0 -298
  424. package/src/server/services/session-cache.js +0 -127
  425. package/src/server/services/session-converter.js +0 -582
  426. package/src/server/services/session-history-adapters/claude.js +0 -235
  427. /package/src/{server/services/claude-token-recovery.js → platforms/drivers/claude/token-recovery.js} +0 -0
  428. /package/src/{server/services/codex-parser.js → platforms/drivers/codex/parser.js} +0 -0
  429. /package/src/{server/services/codex-speed-test-template.json → platforms/drivers/codex/speed-test-template.json} +0 -0
  430. /package/src/{server/services/codex-wire.js → platforms/drivers/codex/wire.js} +0 -0
  431. /package/src/{server/services/omp-auth-gateway-client.js → platforms/drivers/omp/auth-gateway-client.js} +0 -0
  432. /package/src/{server/services/opencode-normalization.js → platforms/drivers/opencode/normalization.js} +0 -0
  433. /package/src/{server/services → shared}/config-artifact-paths.js +0 -0
  434. /package/src/{server/services/base → shared}/proxy-utils.js +0 -0
@@ -6,31 +6,18 @@
6
6
 
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
- const os = require('os');
10
- const toml = require('@iarna/toml');
11
9
  const { spawn } = require('child_process');
12
10
  const { McpClient, buildMissingCommandMessage, createMissingCommandHint } = require('./mcp-client');
13
- const { NATIVE_PATHS, PATHS } = require('../../config/paths');
14
- const { resolvePreferredHomeDir } = require('../../utils/home-dir');
15
-
16
- const HOME_DIR = resolvePreferredHomeDir(process.platform, process.env, os.homedir());
17
-
18
- // MCP 配置文件路径
11
+ const mcpFormat = require('../../shared/mcp-format');
12
+ const { convertToOmpMcpFormat, convertFromOmpMcpFormat } = mcpFormat;
13
+ const { PATHS } = require('../../config/paths');
14
+ const { ControlManifestStore } = require('./control-manifest-store');
15
+ const { EffectiveControlService, deriveMcpPolicy } = require('./effective-control-service');
16
+ const { validateMcpId, mergeMcpPatch, redactSecrets } = require('../../shared/project-config');
17
+
18
+ // MCP central storage path. Native paths and serialization belong to Drivers.
19
19
  const MCP_SERVERS_FILE = PATHS.mcpServers;
20
20
 
21
- // 各平台配置文件路径
22
- const CLAUDE_CONFIG_PATH = NATIVE_PATHS.claude.mcp || path.join(HOME_DIR, '.claude.json');
23
- const CODEX_CONFIG_PATH = NATIVE_PATHS.codex.config;
24
- const GEMINI_CONFIG_PATH = path.join(path.dirname(NATIVE_PATHS.gemini.env), '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
- const OMP_MCP_CONFIG_PATH = NATIVE_PATHS.omp?.mcp
32
- || path.join(NATIVE_PATHS.omp?.dir || path.join(HOME_DIR, '.omp', 'agent'), 'mcp.json');
33
-
34
21
  // MCP 客户端连接池
35
22
  // serverId -> { client, timestamp }
36
23
  const mcpClientPool = new Map();
@@ -39,6 +26,15 @@ const STREAMABLE_HTTP_TYPE = 'streamable_http';
39
26
  const MCP_SERVER_TYPES = ['stdio', STREAMABLE_HTTP_TYPE, 'sse'];
40
27
  const REMOTE_MCP_SERVER_TYPES = [STREAMABLE_HTTP_TYPE, 'sse'];
41
28
  const MCP_PLATFORM_KEYS = ['claude', 'codex', 'gemini', 'opencode', 'omp'];
29
+
30
+ const MCP_CONTROL_SERVICE = PATHS.effectiveControlManifest
31
+ ? new EffectiveControlService({
32
+ store: new ControlManifestStore({
33
+ userPath: PATHS.effectiveControlManifest,
34
+ projectPathResolver: ({ projectPath }) => path.join(projectPath, '.ctx-control.json')
35
+ })
36
+ })
37
+ : null;
42
38
  const OMP_MCP_SCHEMA_URL = 'https://raw.githubusercontent.com/can1357/oh-my-omp/main/packages/coding-agent/src/config/mcp-schema.json';
43
39
  const OMP_SERVER_NAME_PATTERN = /^[a-zA-Z0-9_.-]{1,100}$/;
44
40
 
@@ -51,6 +47,64 @@ function normalizeServerSpec(spec = {}) {
51
47
  return normalized;
52
48
  }
53
49
 
50
+ function normalizePlatformKey(platform) {
51
+ return String(platform || '').trim().toLowerCase();
52
+ }
53
+
54
+ function getPlatformServices() {
55
+ const { registry, runtime } = require('../platform-context').getPlatformContext();
56
+ return {
57
+ registry,
58
+ runtime
59
+ };
60
+ }
61
+
62
+ function getMcpPlatformKeys() {
63
+ const { registry } = getPlatformServices();
64
+ const keys = [];
65
+ const seen = new Set();
66
+
67
+ for (const platform of registry.list()) {
68
+ const key = normalizePlatformKey(platform.key);
69
+ if (!key || seen.has(key)) continue;
70
+ const driverId = registry.getCapability(key, 'mcp');
71
+ if (!driverId || driverId === 'unsupported') continue;
72
+ seen.add(key);
73
+ keys.push(key);
74
+ }
75
+
76
+ return keys;
77
+ }
78
+
79
+ function createPlatformCapabilityError(platform, capability, code) {
80
+ const key = normalizePlatformKey(platform);
81
+ const error = new Error(code === 'not_found'
82
+ ? `无效的平台: ${key}`
83
+ : `平台 ${key} 未声明 ${capability} capability`);
84
+ error.status = 404;
85
+ error.code = code;
86
+ error.platform = key;
87
+ error.capability = capability;
88
+ return error;
89
+ }
90
+
91
+ function requireMcpCapability(platform) {
92
+ const key = normalizePlatformKey(platform);
93
+ const { registry, runtime } = getPlatformServices();
94
+ if (!registry.resolve(key)) {
95
+ throw createPlatformCapabilityError(key, 'mcp', 'not_found');
96
+ }
97
+ const driverId = registry.getCapability(key, 'mcp');
98
+ if (!driverId || driverId === 'unsupported') {
99
+ throw createPlatformCapabilityError(key, 'mcp', 'unsupported');
100
+ }
101
+ const driver = runtime.getDriver(key, 'mcp');
102
+ if (!driver) {
103
+ throw createPlatformCapabilityError(key, 'mcp', 'unsupported');
104
+ }
105
+ return { platform: key, driver };
106
+ }
107
+
54
108
  // MCP 预设模板
55
109
  const MCP_PRESETS = [
56
110
  {
@@ -285,14 +339,10 @@ function readJsonFile(filePath, defaultValue = {}) {
285
339
  return JSON.parse(content);
286
340
  }
287
341
  } catch (err) {
288
- console.error(`[MCP] Failed to read ${filePath}:`, err.message);
342
+ console.error(`[MCP] Failed to read ${filePath}:`, sanitizeMcpErrorText(err.message || err));
289
343
  }
290
344
  return defaultValue;
291
345
  }
292
-
293
- /**
294
- * 安全写入 JSON 文件(原子写入)
295
- */
296
346
  function writeJsonFile(filePath, data) {
297
347
  ensureDir(path.dirname(filePath));
298
348
  const tempPath = filePath + '.tmp';
@@ -300,138 +350,6 @@ function writeJsonFile(filePath, data) {
300
350
  fs.renameSync(tempPath, filePath);
301
351
  }
302
352
 
303
- /**
304
- * 安全读取 TOML 文件
305
- */
306
- function readTomlFile(filePath, defaultValue = {}) {
307
- if (!fs.existsSync(filePath)) {
308
- return defaultValue;
309
- }
310
-
311
- try {
312
- const content = fs.readFileSync(filePath, 'utf-8');
313
- return toml.parse(content);
314
- } catch (err) {
315
- throw new Error(`Failed to parse ${filePath}: ${err.message}`);
316
- }
317
- }
318
-
319
- /**
320
- * 安全写入 TOML 文件(原子写入)
321
- */
322
- function writeTomlFile(filePath, data) {
323
- ensureDir(path.dirname(filePath));
324
- const tempPath = filePath + '.tmp';
325
- fs.writeFileSync(tempPath, toml.stringify(data), 'utf-8');
326
- fs.renameSync(tempPath, filePath);
327
- }
328
-
329
- /**
330
- * 去除 JSONC 注释
331
- */
332
- function stripJsonComments(input) {
333
- let result = '';
334
- let inString = false;
335
- let quote = '';
336
- let index = 0;
337
-
338
- while (index < input.length) {
339
- const ch = input[index];
340
- const next = input[index + 1];
341
-
342
- if (inString) {
343
- result += ch;
344
- if (ch === '\\') {
345
- if (next) {
346
- result += next;
347
- index += 2;
348
- continue;
349
- }
350
- } else if (ch === quote) {
351
- inString = false;
352
- }
353
- index += 1;
354
- continue;
355
- }
356
-
357
- if (ch === '"' || ch === '\'') {
358
- inString = true;
359
- quote = ch;
360
- result += ch;
361
- index += 1;
362
- continue;
363
- }
364
-
365
- if (ch === '/' && next === '/') {
366
- index += 2;
367
- while (index < input.length && input[index] !== '\n') {
368
- index += 1;
369
- }
370
- continue;
371
- }
372
-
373
- if (ch === '/' && next === '*') {
374
- index += 2;
375
- while (index < input.length - 1 && !(input[index] === '*' && input[index + 1] === '/')) {
376
- index += 1;
377
- }
378
- index += 2;
379
- continue;
380
- }
381
-
382
- result += ch;
383
- index += 1;
384
- }
385
-
386
- return result;
387
- }
388
-
389
- /**
390
- * 选择 OpenCode 配置文件路径
391
- */
392
- function selectOpenCodeConfigPath() {
393
- if (fs.existsSync(OPENCODE_CONFIG_PATHS.jsonc)) return OPENCODE_CONFIG_PATHS.jsonc;
394
- if (fs.existsSync(OPENCODE_CONFIG_PATHS.json)) return OPENCODE_CONFIG_PATHS.json;
395
- if (fs.existsSync(OPENCODE_CONFIG_PATHS.legacy)) return OPENCODE_CONFIG_PATHS.legacy;
396
- return OPENCODE_CONFIG_PATHS.json;
397
- }
398
-
399
- /**
400
- * 读取 OpenCode 配置
401
- */
402
- function readOpenCodeConfig() {
403
- const filePath = selectOpenCodeConfigPath();
404
-
405
- if (!fs.existsSync(filePath)) {
406
- return { path: filePath, config: {} };
407
- }
408
-
409
- try {
410
- const raw = fs.readFileSync(filePath, 'utf-8');
411
- if (!raw.trim()) {
412
- return { path: filePath, config: {} };
413
- }
414
-
415
- const content = filePath.endsWith('.jsonc') ? stripJsonComments(raw) : raw;
416
- return {
417
- path: filePath,
418
- config: JSON.parse(content)
419
- };
420
- } catch (err) {
421
- console.error(`[MCP] Failed to read OpenCode config:`, err.message);
422
- return { path: filePath, config: {} };
423
- }
424
- }
425
-
426
- /**
427
- * 写入 OpenCode 配置(保持 JSON 格式)
428
- */
429
- function writeOpenCodeConfig(filePath, data) {
430
- ensureDir(path.dirname(filePath));
431
- const tempPath = filePath + '.tmp';
432
- fs.writeFileSync(tempPath, JSON.stringify(data, null, 2), 'utf-8');
433
- fs.renameSync(tempPath, filePath);
434
- }
435
353
 
436
354
  function getPathEnvKey(envObj = {}) {
437
355
  return Object.keys(envObj).find(key => key.toLowerCase() === 'path') || 'PATH';
@@ -520,10 +438,79 @@ function extractMcpHint(error) {
520
438
  return error?.data?.hint || error?.hint || null;
521
439
  }
522
440
 
441
+ function sanitizeMcpErrorText(value) {
442
+ return String(value || '')
443
+ .replace(/(https?:\/\/[^\s?]+)\?[^\s]+/gi, '$1?[REDACTED]')
444
+ .replace(/(Bearer\s+)[^\s]+/gi, '$1[REDACTED]')
445
+ .replace(/([?&](?:token|secret|password|key)=)[^&\s]+/gi, '$1[REDACTED]')
446
+ .replace(/((?:token|secret|password|api[-_]?key|authorization)\s*[:=]\s*)[^,\s}]+/gi, '$1[REDACTED]');
447
+ }
448
+
449
+ const SENSITIVE_MCP_HINT_KEYS = /(?:token|secret|password|api[-_]?key|access[_-]?token|authorization|headers?|env(?:ironment)?|envvars|experimentalenvironment|auth|oauth|credential|private[_-]?key)/i;
450
+
451
+ function sanitizeMcpHint(value) {
452
+ if (Array.isArray(value)) {
453
+ const sanitized = value.map(sanitizeMcpHint);
454
+ return sanitized.every((item, index) => item === value[index]) ? value : sanitized;
455
+ }
456
+ if (!value || typeof value !== 'object') {
457
+ return typeof value === 'string' ? sanitizeCommandLineValue(value) : value;
458
+ }
459
+ let changed = false;
460
+ const sanitized = Object.fromEntries(Object.entries(value).map(([key, child]) => {
461
+ const safe = SENSITIVE_MCP_HINT_KEYS.test(key)
462
+ ? '[REDACTED]'
463
+ : sanitizeMcpHint(child);
464
+ if (safe !== child) changed = true;
465
+ return [key, safe];
466
+ }));
467
+ return changed ? sanitized : value;
468
+ }
469
+
470
+ const SENSITIVE_MCP_RESULT_KEYS = /(?:token|secret|password|authorization|api[-_]?key|access[_-]?token|credential|private[_-]?key|env(?:ironment)?|envvars|experimentalenvironment|headers?|auth|oauth)/i;
471
+
472
+ const SENSITIVE_COMMAND_FLAG = /^(?:--?|\/)(?:token|secret|password|api[-_]?key|access[_-]?token|authorization|key|auth|credential|client[-_]?secret|bearer)$/i;
473
+
474
+ function sanitizeCommandLineValue(value) {
475
+ if (Array.isArray(value)) {
476
+ let redactNext = false;
477
+ return value.map(item => {
478
+ if (redactNext) {
479
+ redactNext = false;
480
+ return '[REDACTED]';
481
+ }
482
+ if (typeof item !== 'string') return '[REDACTED]';
483
+ if (SENSITIVE_COMMAND_FLAG.test(item)) {
484
+ redactNext = true;
485
+ return item;
486
+ }
487
+ const inline = item.match(/^((?:--?|\/)(?:token|secret|password|api[-_]?key|access[_-]?token|authorization|key|auth|credential|client[-_]?secret|bearer)=).*/i);
488
+ if (inline) return `${inline[1]}[REDACTED]`;
489
+ return sanitizeMcpErrorText(item);
490
+ });
491
+ }
492
+ if (typeof value !== 'string') return value;
493
+ return sanitizeMcpErrorText(value)
494
+ .replace(/((?:^|\s)(?:--?|\/)(?:token|secret|password|api[-_]?key|access[_-]?token|authorization|key|auth|credential|client[-_]?secret|bearer)(?:=|\s+))\S+/gi, '$1[REDACTED]');
495
+ }
496
+
497
+
498
+ function sanitizeMcpToolValue(value, key = '') {
499
+ if (SENSITIVE_MCP_RESULT_KEYS.test(String(key))) return '[REDACTED]';
500
+ if (/^(?:command|args)$/i.test(key)) return sanitizeCommandLineValue(value);
501
+ if (typeof value === 'string') return sanitizeMcpErrorText(value);
502
+ if (Array.isArray(value)) return value.map(item => sanitizeMcpToolValue(item));
503
+ if (!value || typeof value !== 'object') return value;
504
+ return Object.fromEntries(Object.entries(value).map(([childKey, childValue]) => [
505
+ childKey,
506
+ sanitizeMcpToolValue(childValue, childKey)
507
+ ]));
508
+ }
509
+
523
510
  function buildMcpFailureResult(error, fallbackMessage, duration) {
524
- const hint = extractMcpHint(error);
511
+ const hint = sanitizeMcpHint(extractMcpHint(error));
525
512
  return {
526
- message: hint?.title || fallbackMessage || error?.message || '操作失败',
513
+ message: sanitizeMcpErrorText(hint?.title || fallbackMessage || error?.message || '操作失败'),
527
514
  hint,
528
515
  duration
529
516
  };
@@ -533,38 +520,223 @@ function buildMcpFailureResult(error, fallbackMessage, duration) {
533
520
  // MCP 数据管理
534
521
  // ============================================================================
535
522
 
536
- const DEFAULT_SERVER_APPS = {
537
- claude: true,
538
- codex: false,
539
- gemini: false,
540
- opencode: false,
541
- omp: false
542
- };
523
+ function normalizeServerApps(apps = {}, fallbackApps = { claude: true }) {
524
+ const source = apps && typeof apps === 'object' && !Array.isArray(apps) ? apps : {};
525
+ const fallback = fallbackApps && typeof fallbackApps === 'object' && !Array.isArray(fallbackApps)
526
+ ? fallbackApps
527
+ : {};
528
+ const platformKeys = new Set(getMcpPlatformKeys());
529
+ const normalized = {};
543
530
 
544
- function normalizeServerApps(apps = {}, fallbackApps = DEFAULT_SERVER_APPS) {
545
- return {
546
- claude: apps.claude !== undefined ? !!apps.claude : !!fallbackApps.claude,
547
- codex: apps.codex !== undefined ? !!apps.codex : !!fallbackApps.codex,
548
- gemini: apps.gemini !== undefined ? !!apps.gemini : !!fallbackApps.gemini,
549
- opencode: apps.opencode !== undefined ? !!apps.opencode : !!fallbackApps.opencode,
550
- omp: apps.omp !== undefined ? !!apps.omp: !!fallbackApps.omp
551
- };
531
+ for (const platform of platformKeys) {
532
+ normalized[platform] = source[platform] !== undefined
533
+ ? !!source[platform]
534
+ : !!fallback[platform];
535
+ }
536
+
537
+ // Preserve historical flags for platforms that are temporarily absent from the
538
+ // current registry so disabling/re-enabling a Manifest does not erase data.
539
+ for (const [platform, enabled] of Object.entries(source)) {
540
+ if (!platformKeys.has(platform)) normalized[platform] = !!enabled;
541
+ }
542
+
543
+ for (const [platform, enabled] of Object.entries(fallback)) {
544
+ if (!platformKeys.has(platform) && source[platform] === undefined) {
545
+ normalized[platform] = !!enabled;
546
+ }
547
+ }
548
+
549
+ return normalized;
550
+ }
551
+
552
+ function getControlMcpPlatformKeys() {
553
+ const dynamic = getMcpPlatformKeys();
554
+ return dynamic.length > 0 ? dynamic : MCP_PLATFORM_KEYS;
555
+ }
556
+ function mcpControlKey(platform, serverId, scope = 'user') {
557
+ return `mcp:${platform}:${scope}:${serverId}`;
558
+ }
559
+
560
+ function secretRefsForServer(server = {}) {
561
+ const refs = [];
562
+ for (const key of Object.keys(server.server?.env || {})) refs.push(`env:${key}`);
563
+ for (const key of Object.keys(server.server?.headers || {})) refs.push(`header:${key}`);
564
+ if (server.server?.url) refs.push('url:configured');
565
+ return refs;
566
+ }
567
+
568
+ function ensureMcpControlEntry(server, platform) {
569
+ if (!MCP_CONTROL_SERVICE || !server?.id) return null;
570
+ const controlKey = mcpControlKey(platform, server.id);
571
+ const existing = MCP_CONTROL_SERVICE.getMcp(controlKey, { scope: 'user' });
572
+ const policy = deriveMcpPolicy({ transport: server.server?.type, scope: 'user' });
573
+ if (existing) {
574
+ const secretRefs = secretRefsForServer(server);
575
+ if (
576
+ existing.transport !== policy.transport
577
+ || existing.riskTier !== policy.riskTier
578
+ || existing.egressProfile !== policy.egressProfile
579
+ || !existing.source
580
+ || JSON.stringify(existing.secretRefs || []) !== JSON.stringify(secretRefs)
581
+ ) {
582
+ return MCP_CONTROL_SERVICE.registerMcp({
583
+ ...existing,
584
+ ...policy,
585
+ source: existing.source || 'catalog',
586
+ secretRefs
587
+ });
588
+ }
589
+ return existing;
590
+ }
591
+ return MCP_CONTROL_SERVICE.registerMcp({
592
+ kind: 'mcp',
593
+ controlKey,
594
+ platform,
595
+ scope: 'user',
596
+ name: server.id,
597
+ source: 'catalog',
598
+ enabled: server.apps?.[platform] === true,
599
+ trust: 'approved',
600
+ ...policy,
601
+ secretRefs: secretRefsForServer(server),
602
+ managed: true
603
+ });
604
+ }
605
+
606
+ function promoteMcpControlEntry(server, platform, enabled) {
607
+ if (!MCP_CONTROL_SERVICE || !server?.id) return null;
608
+ const controlKey = mcpControlKey(platform, server.id);
609
+ const existing = MCP_CONTROL_SERVICE.getMcp(controlKey, { scope: 'user' });
610
+ if (!existing) {
611
+ return ensureMcpControlEntry(server, platform);
612
+ }
613
+ if (existing.managed !== true) {
614
+ return MCP_CONTROL_SERVICE.registerMcp({
615
+ ...existing,
616
+ ...deriveMcpPolicy({ transport: server.server?.type, scope: 'user' }),
617
+ source: 'catalog',
618
+ enabled: enabled === true,
619
+ trust: 'approved',
620
+ secretRefs: secretRefsForServer(server),
621
+ managed: true
622
+ });
623
+ }
624
+ ensureMcpControlEntry(server, platform);
625
+ return MCP_CONTROL_SERVICE.setMcpPolicy({
626
+ platform,
627
+ controlKey,
628
+ scope: 'user',
629
+ enabled: enabled === true
630
+ });
631
+ }
632
+
633
+ function getMcpControlEnabled(server, platform) {
634
+ const entry = ensureMcpControlEntry(server, platform);
635
+ return entry
636
+ ? entry.managed === true && entry.trust === 'approved' && entry.enabled === true
637
+ : server.apps?.[platform] === true;
638
+ }
639
+
640
+ function applyMcpControlApps(server) {
641
+ if (!MCP_CONTROL_SERVICE || !server?.id) return;
642
+ for (const platform of getControlMcpPlatformKeys()) {
643
+ const controlKey = mcpControlKey(platform, server.id);
644
+ const existing = MCP_CONTROL_SERVICE.getMcp(controlKey, { scope: 'user' });
645
+ if (!existing) {
646
+ ensureMcpControlEntry(server, platform);
647
+ continue;
648
+ }
649
+ if (existing.managed !== true) {
650
+ promoteMcpControlEntry(server, platform, server.apps?.[platform] === true);
651
+ continue;
652
+ }
653
+ MCP_CONTROL_SERVICE.setMcpPolicy({
654
+ platform,
655
+ controlKey,
656
+ scope: 'user',
657
+ enabled: server.apps?.[platform] === true
658
+ });
659
+ }
660
+ }
661
+
662
+ function readNativeMcpEntries() {
663
+ const entries = [];
664
+ for (const platform of getMcpPlatformKeys()) {
665
+ try {
666
+ const { driver } = requireMcpCapability(platform);
667
+ const result = driver.entries ? driver.entries() : driver.read?.();
668
+ if (result && typeof result.then === 'function') continue;
669
+ const config = result?.status === 'ok' ? result.data : result;
670
+ const nativeEntries = driver.entries
671
+ ? config
672
+ : getGenericMcpServerMap(config || {});
673
+ for (const [rawId, spec] of Object.entries(nativeEntries || {})) {
674
+ let id;
675
+ try {
676
+ id = validateMcpId(rawId);
677
+ } catch {
678
+ console.warn(`[MCP] Ignoring invalid native server id for ${platform}`);
679
+ continue;
680
+ }
681
+ const normalized = driver.normalize ? driver.normalize(spec) : normalizeServerSpec(spec);
682
+ const internalSpec = normalized?.status === 'ok' ? normalized.data : normalized;
683
+ entries.push({ platform, id, spec: internalSpec });
684
+ }
685
+ } catch {
686
+ // A missing or malformed native config must not block central inventory.
687
+ }
688
+ }
689
+ return entries;
690
+ }
691
+
692
+ function ensureExternalNativeMcpControl(platform, id, spec) {
693
+ if (!MCP_CONTROL_SERVICE) return null;
694
+ const controlKey = mcpControlKey(platform, id);
695
+ const existing = MCP_CONTROL_SERVICE.getMcp(controlKey, { scope: 'user' });
696
+ if (existing) return existing;
697
+ return MCP_CONTROL_SERVICE.registerMcp({
698
+ kind: 'mcp',
699
+ controlKey,
700
+ platform,
701
+ scope: 'user',
702
+ name: id,
703
+ source: 'native',
704
+ enabled: false,
705
+ trust: 'approved',
706
+ ...deriveMcpPolicy({ transport: spec?.type, scope: 'user' }),
707
+ secretRefs: secretRefsForServer({ server: spec }),
708
+ managed: false
709
+ });
552
710
  }
553
711
 
554
712
  /**
555
713
  * 获取所有 MCP 服务器
556
714
  */
557
715
  function getAllServers() {
558
- const servers = readJsonFile(MCP_SERVERS_FILE, {});
716
+ const persisted = readJsonFile(MCP_SERVERS_FILE, {});
717
+ const servers = {};
559
718
 
560
- for (const server of Object.values(servers)) {
561
- if (!server || typeof server !== 'object') {
719
+ for (const [rawId, server] of Object.entries(persisted)) {
720
+ const normalizedId = validateMcpId(server?.id || rawId);
721
+ if (!server || typeof server !== 'object' || Array.isArray(server)) {
562
722
  continue;
563
723
  }
564
- server.apps = normalizeServerApps(server.apps);
724
+ server.id = normalizedId;
725
+ server.apps = server.apps == null
726
+ ? normalizeServerApps(server.apps)
727
+ : normalizeServerApps(server.apps, {});
565
728
  server.server = normalizeServerSpec(server.server);
729
+ for (const platform of getControlMcpPlatformKeys()) {
730
+ server.apps[platform] = getMcpControlEnabled(server, platform);
731
+ }
732
+ servers[normalizedId] = server;
566
733
  }
567
734
 
735
+ for (const entry of readNativeMcpEntries()) {
736
+ if (!servers[entry.id]) {
737
+ ensureExternalNativeMcpControl(entry.platform, entry.id, entry.spec);
738
+ }
739
+ }
568
740
  return servers;
569
741
  }
570
742
 
@@ -572,8 +744,9 @@ function getAllServers() {
572
744
  * 获取单个 MCP 服务器
573
745
  */
574
746
  function getServer(id) {
747
+ const normalizedId = validateMcpId(id);
575
748
  const servers = getAllServers();
576
- return servers[id] || null;
749
+ return servers[normalizedId] || null;
577
750
  }
578
751
 
579
752
  /**
@@ -581,18 +754,19 @@ function getServer(id) {
581
754
  */
582
755
  async function saveServer(server, options = {}) {
583
756
  const { syncPlatforms = true } = options;
584
-
585
- if (!server.id || !server.id.trim()) {
586
- throw new Error('MCP 服务器 ID 不能为空');
757
+ if (!server || typeof server !== 'object') {
758
+ throw new Error('MCP server must be an object');
587
759
  }
588
-
589
- server.server = normalizeServerSpec(server.server);
760
+ server.id = validateMcpId(server.id);
761
+ const incomingSpec = normalizeServerSpec(server.server);
762
+ const servers = getAllServers();
763
+ const existingServer = servers[server.id];
764
+ server.server = existingServer
765
+ ? mergeMcpPatch(existingServer.server, incomingSpec)
766
+ : incomingSpec;
590
767
 
591
768
  // 验证服务器配置
592
769
  validateServerSpec(server.server);
593
-
594
- const servers = getAllServers();
595
- const existingServer = servers[server.id];
596
770
  const previousApps = existingServer ? normalizeServerApps(existingServer.apps) : null;
597
771
 
598
772
  // 如果是新服务器,设置默认值
@@ -603,23 +777,33 @@ async function saveServer(server, options = {}) {
603
777
  }
604
778
  server.updatedAt = Date.now();
605
779
 
606
- // 确保 apps 字段存在
780
+ // Ensure apps is present while preserving existing flags.
607
781
  if (!server.apps) {
608
- // Updating a server without explicit app flags should preserve existing platform toggles.
609
782
  server.apps = previousApps
610
783
  ? normalizeServerApps(previousApps)
611
- : normalizeServerApps(DEFAULT_SERVER_APPS);
784
+ : normalizeServerApps({ claude: true });
612
785
  } else {
613
- server.apps = normalizeServerApps(server.apps, previousApps || DEFAULT_SERVER_APPS);
786
+ server.apps = normalizeServerApps(server.apps, previousApps || {});
614
787
  }
615
788
 
616
- // 同步到各平台配置
789
+ if (MCP_CONTROL_SERVICE) {
790
+ const requestedApps = normalizeServerApps(server.apps);
791
+ const effectiveApps = { ...requestedApps };
792
+ for (const platform of getControlMcpPlatformKeys()) {
793
+ const control = promoteMcpControlEntry({ ...server, apps: requestedApps }, platform, requestedApps[platform]);
794
+ effectiveApps[platform] = control
795
+ ? control.managed === true && control.trust === 'approved' && control.enabled === true
796
+ : requestedApps[platform];
797
+ }
798
+ server.apps = effectiveApps;
799
+ }
617
800
  if (syncPlatforms) {
618
801
  await syncServerToAllPlatforms(server, previousApps);
619
802
  }
620
803
 
621
804
  servers[server.id] = server;
622
805
  writeJsonFile(MCP_SERVERS_FILE, servers);
806
+ applyMcpControlApps(server);
623
807
 
624
808
  return server;
625
809
  }
@@ -628,18 +812,23 @@ async function saveServer(server, options = {}) {
628
812
  * 删除 MCP 服务器
629
813
  */
630
814
  async function deleteServer(id) {
815
+ const normalizedId = validateMcpId(id);
631
816
  const servers = getAllServers();
632
- const server = servers[id];
817
+ const server = servers[normalizedId];
633
818
 
634
819
  if (!server) {
635
820
  return false;
636
821
  }
637
822
 
638
- // 从所有平台配置中移除
639
- await removeServerFromAllPlatforms(id);
640
-
641
- delete servers[id];
823
+ await removeServerFromAllPlatforms(normalizedId);
824
+ delete servers[normalizedId];
642
825
  writeJsonFile(MCP_SERVERS_FILE, servers);
826
+ for (const platform of getControlMcpPlatformKeys()) {
827
+ MCP_CONTROL_SERVICE?.removeMcp?.({
828
+ controlKey: mcpControlKey(platform, normalizedId),
829
+ scope: 'user'
830
+ });
831
+ }
643
832
 
644
833
  return true;
645
834
  }
@@ -648,25 +837,37 @@ async function deleteServer(id) {
648
837
  * 切换 MCP 服务器在某平台的启用状态
649
838
  */
650
839
  async function toggleServerApp(serverId, app, enabled) {
840
+ const normalizedId = validateMcpId(serverId);
651
841
  const servers = getAllServers();
652
- const server = servers[serverId];
842
+ const server = servers[normalizedId];
653
843
 
654
844
  if (!server) {
655
845
  throw new Error(`MCP 服务器 "${serverId}" 不存在`);
656
846
  }
657
847
 
658
- if (!MCP_PLATFORM_KEYS.includes(app)) {
659
- throw new Error(`无效的平台: ${app}`);
848
+ const { platform: normalizedPlatform } = requireMcpCapability(app);
849
+
850
+ const policy = MCP_CONTROL_SERVICE
851
+ ? MCP_CONTROL_SERVICE.setMcpPolicy({
852
+ platform: normalizedPlatform,
853
+ controlKey: mcpControlKey(normalizedPlatform, normalizedId),
854
+ scope: 'user',
855
+ enabled: enabled === true
856
+ })
857
+ : { enabled: enabled === true };
858
+ if (policy.status === 'needs_approval' || policy.status === 'blocked') {
859
+ return { ...server, apps: { ...server.apps, [normalizedPlatform]: policy.enabled === true }, status: policy.status };
660
860
  }
661
861
 
662
- server.apps[app] = enabled;
862
+ server.apps = normalizeServerApps(server.apps);
863
+ server.apps[normalizedPlatform] = policy.enabled === true;
663
864
  server.updatedAt = Date.now();
664
865
 
665
- // 同步到对应平台
666
- if (enabled) {
667
- await syncServerToPlatform(server, app);
866
+ // Sync to the selected platform.
867
+ if (server.apps[normalizedPlatform]) {
868
+ await syncServerToPlatform(server, normalizedPlatform);
668
869
  } else {
669
- await removeServerFromPlatform(serverId, app);
870
+ await removeServerFromPlatform(normalizedId, normalizedPlatform);
670
871
  }
671
872
 
672
873
  writeJsonFile(MCP_SERVERS_FILE, servers);
@@ -698,14 +899,17 @@ function validateServerSpec(spec) {
698
899
  if (!MCP_SERVER_TYPES.includes(type)) {
699
900
  throw new Error(`无效的服务器类型: ${spec.type || type},必须是 stdio、streamable_http 或 sse`);
700
901
  }
902
+ if (spec.args !== undefined && (!Array.isArray(spec.args) || spec.args.some(arg => typeof arg !== 'string'))) {
903
+ throw new Error('MCP server args must be an array of strings');
904
+ }
701
905
 
702
906
  if (type === 'stdio') {
703
- if (!spec.command || !spec.command.trim()) {
704
- throw new Error('stdio 类型必须指定 command');
907
+ if (typeof spec.command !== 'string' || !spec.command.trim()) {
908
+ throw new Error('stdio 类型必须指定 string command');
705
909
  }
706
910
  } else if (REMOTE_MCP_SERVER_TYPES.includes(type)) {
707
- if (!spec.url || !spec.url.trim()) {
708
- throw new Error(`${type} 类型必须指定 url`);
911
+ if (typeof spec.url !== 'string' || !spec.url.trim()) {
912
+ throw new Error(`${type} 类型必须指定 string url`);
709
913
  }
710
914
  }
711
915
  }
@@ -727,7 +931,7 @@ async function syncServerToAllPlatforms(server, previousApps = null) {
727
931
  return previous[platform] && !apps[platform];
728
932
  };
729
933
 
730
- for (const platform of MCP_PLATFORM_KEYS) {
934
+ for (const platform of getMcpPlatformKeys()) {
731
935
  if (apps[platform]) {
732
936
  await syncServerToPlatform(server, platform);
733
937
  } else if (shouldRemoveFromPlatform(platform)) {
@@ -740,7 +944,12 @@ async function syncServerToAllPlatforms(server, previousApps = null) {
740
944
  * 从所有平台移除服务器
741
945
  */
742
946
  async function removeServerFromAllPlatforms(serverId) {
743
- for (const platform of MCP_PLATFORM_KEYS) {
947
+ for (const platform of getMcpPlatformKeys()) {
948
+ const control = MCP_CONTROL_SERVICE?.getMcp?.(
949
+ mcpControlKey(platform, serverId),
950
+ { scope: 'user' }
951
+ );
952
+ if (control && control.managed !== true) continue;
744
953
  await removeServerFromPlatform(serverId, platform);
745
954
  }
746
955
  }
@@ -750,462 +959,143 @@ async function removeServerFromAllPlatforms(serverId) {
750
959
  */
751
960
  async function syncServerToPlatform(server, platform) {
752
961
  try {
753
- switch (platform) {
754
- case 'claude':
755
- syncToClaudeConfig(server);
756
- break;
757
- case 'codex':
758
- syncToCodexConfig(server);
759
- break;
760
- case 'gemini':
761
- syncToGeminiConfig(server);
762
- break;
763
- case 'opencode':
764
- syncToOpenCodeConfig(server);
765
- break;
766
- case 'omp':
767
- syncToOmpMcpConfig(server);
768
- break;
962
+ const { platform: normalizedPlatform, driver } = requireMcpCapability(platform);
963
+ let result;
964
+ if (typeof driver.sync === 'function') {
965
+ result = await driver.sync(server);
966
+ } else {
967
+ result = await syncGenericMcpServer(driver, normalizedPlatform, server);
769
968
  }
770
- console.log(`[MCP] Synced "${server.id}" to ${platform}`);
969
+ const failure = getDriverFailure(result, normalizedPlatform, 'sync');
970
+ if (failure) throw failure;
971
+ console.log(`[MCP] Synced "${server.id}" to ${normalizedPlatform}`);
972
+ return result;
771
973
  } catch (err) {
772
- console.error(`[MCP] Failed to sync "${server.id}" to ${platform}:`, err.message);
974
+ console.error(`[MCP] Failed to sync "${server.id}" to ${platform}:`, sanitizeMcpErrorText(err.message || err));
773
975
  throw err;
774
976
  }
775
977
  }
776
978
 
777
- /**
778
- * 从指定平台移除服务器
779
- */
780
979
  async function removeServerFromPlatform(serverId, platform) {
781
980
  try {
782
- switch (platform) {
783
- case 'claude':
784
- removeFromClaudeConfig(serverId);
785
- break;
786
- case 'codex':
787
- removeFromCodexConfig(serverId);
788
- break;
789
- case 'gemini':
790
- removeFromGeminiConfig(serverId);
791
- break;
792
- case 'opencode':
793
- removeFromOpenCodeConfig(serverId);
794
- break;
795
- case 'omp':
796
- removeFromOmpMcpConfig(serverId);
797
- break;
981
+ const { platform: normalizedPlatform, driver } = requireMcpCapability(platform);
982
+ let result;
983
+ if (typeof driver.remove === 'function') {
984
+ result = await driver.remove(serverId);
985
+ } else {
986
+ result = await removeGenericMcpServer(driver, normalizedPlatform, serverId);
798
987
  }
799
- console.log(`[MCP] Removed "${serverId}" from ${platform}`);
988
+ const failure = getDriverFailure(result, normalizedPlatform, 'remove');
989
+ if (failure) throw failure;
990
+ console.log(`[MCP] Removed "${serverId}" from ${normalizedPlatform}`);
991
+ return result;
800
992
  } catch (err) {
801
- console.error(`[MCP] Failed to remove "${serverId}" from ${platform}:`, err.message);
993
+ console.error(`[MCP] Failed to remove "${serverId}" from ${platform}:`, sanitizeMcpErrorText(err.message || err));
802
994
  throw err;
803
995
  }
804
996
  }
805
997
 
806
- // ============================================================================
807
- // Claude 配置同步
808
- // ============================================================================
809
-
810
- /**
811
- * 同步到 Claude 配置
812
- */
813
- function syncToClaudeConfig(server) {
814
- const config = readJsonFile(CLAUDE_CONFIG_PATH, {});
815
-
816
- if (!config.mcpServers) {
817
- config.mcpServers = {};
818
- }
819
-
820
- // 只写入 server spec,不写入元数据
821
- config.mcpServers[server.id] = extractServerSpec(server.server);
822
-
823
- writeJsonFile(CLAUDE_CONFIG_PATH, config);
998
+ function getDriverFailure(result, platform, operation) {
999
+ if (!result || result.status !== 'failed' || result.capability !== 'mcp') return null;
1000
+ if (result.cause instanceof Error) return result.cause;
1001
+ return new Error(result.error || `MCP ${operation} failed for ${platform}`);
824
1002
  }
825
1003
 
826
- /**
827
- * 从 Claude 配置移除
828
- */
829
- function removeFromClaudeConfig(serverId) {
830
- const config = readJsonFile(CLAUDE_CONFIG_PATH, {});
831
-
832
- if (config.mcpServers && config.mcpServers[serverId]) {
833
- delete config.mcpServers[serverId];
834
- writeJsonFile(CLAUDE_CONFIG_PATH, config);
1004
+ async function readGenericMcpConfig(driver, platform, allowMissing = false) {
1005
+ const result = await driver.read();
1006
+ const failure = getDriverFailure(result, platform, 'read');
1007
+ if (failure) {
1008
+ if (allowMissing && failure.code === 'ENOENT') return {};
1009
+ throw failure;
835
1010
  }
836
- }
837
-
838
- // ============================================================================
839
- // Codex 配置同步 (TOML 格式)
840
- // ============================================================================
841
-
842
- /**
843
- * 同步到 Codex 配置
844
- */
845
- function syncToCodexConfig(server) {
846
- if (!fs.existsSync(CODEX_CONFIG_PATH)) {
847
- throw new Error('Codex config.toml not found. Please run Codex CLI at least once before syncing MCP servers.');
848
- }
849
-
850
- const config = readTomlFile(CODEX_CONFIG_PATH, {});
851
- const nextSpec = convertToCodexFormat(server.server);
852
-
853
- if (!config.mcp_servers) {
854
- config.mcp_servers = {};
855
- }
856
-
857
- if (JSON.stringify(config.mcp_servers[server.id] || null) === JSON.stringify(nextSpec)) {
858
- return;
859
- }
860
-
861
- config.mcp_servers[server.id] = nextSpec;
862
-
863
- writeTomlFile(CODEX_CONFIG_PATH, config);
864
- }
865
-
866
- /**
867
- * 从 Codex 配置移除
868
- */
869
- function removeFromCodexConfig(serverId) {
870
- if (!fs.existsSync(CODEX_CONFIG_PATH)) {
871
- return;
872
- }
873
-
874
- const config = readTomlFile(CODEX_CONFIG_PATH, {});
875
-
876
- if (config.mcp_servers && config.mcp_servers[serverId]) {
877
- delete config.mcp_servers[serverId];
878
- if (Object.keys(config.mcp_servers).length === 0) {
879
- delete config.mcp_servers;
880
- }
881
- writeTomlFile(CODEX_CONFIG_PATH, config);
882
- }
883
- }
884
-
885
- /**
886
- * 转换为 Codex TOML 格式
887
- */
888
- function convertToCodexFormat(spec) {
889
- const result = {
890
- type: spec.type || 'stdio'
891
- };
892
-
893
- if (result.type === 'stdio') {
894
- result.command = spec.command || '';
895
- if (spec.args && spec.args.length > 0) {
896
- result.args = spec.args;
897
- }
898
- if (spec.env && Object.keys(spec.env).length > 0) {
899
- result.env = spec.env;
900
- }
901
- if (spec.cwd) {
902
- result.cwd = spec.cwd;
903
- }
904
- } else if (REMOTE_MCP_SERVER_TYPES.includes(result.type)) {
905
- result.url = spec.url || '';
906
- if (spec.headers && Object.keys(spec.headers).length > 0) {
907
- result.http_headers = spec.headers;
908
- }
909
- }
910
-
911
- return result;
912
- }
913
-
914
- // ============================================================================
915
- // Gemini 配置同步
916
- // ============================================================================
917
-
918
- /**
919
- * 同步到 Gemini 配置
920
- */
921
- function syncToGeminiConfig(server) {
922
- const config = readJsonFile(GEMINI_CONFIG_PATH, {});
923
-
924
- if (!config.mcpServers) {
925
- config.mcpServers = {};
926
- }
927
-
928
- // 只写入 server spec,不写入元数据
929
- config.mcpServers[server.id] = extractServerSpec(server.server);
930
-
931
- writeJsonFile(GEMINI_CONFIG_PATH, config);
932
- }
933
-
934
- /**
935
- * 从 Gemini 配置移除
936
- */
937
- function removeFromGeminiConfig(serverId) {
938
- const config = readJsonFile(GEMINI_CONFIG_PATH, {});
939
-
940
- if (config.mcpServers && config.mcpServers[serverId]) {
941
- delete config.mcpServers[serverId];
942
- writeJsonFile(GEMINI_CONFIG_PATH, config);
943
- }
944
- }
945
-
946
- // ============================================================================
947
- // OMP 配置同步
948
- // ============================================================================
949
-
950
- function validateOmpServerName(serverId) {
951
- if (!OMP_SERVER_NAME_PATTERN.test(serverId)) {
952
- throw new Error(`OMP MCP 服务器 ID "${serverId}" 无效。OMP 仅支持 1-100 个字母、数字、下划线、点或连字符。`);
953
- }
954
- }
955
-
956
- function readOmpMcpConfig() {
957
- const config = readJsonFile(OMP_MCP_CONFIG_PATH, { mcpServers: {} });
1011
+ const config = result?.status === 'ok' ? result.data : result;
958
1012
  if (!config || typeof config !== 'object' || Array.isArray(config)) {
959
- return { mcpServers: {} };
960
- }
961
- if (!config.mcpServers || typeof config.mcpServers !== 'object' || Array.isArray(config.mcpServers)) {
962
- config.mcpServers = {};
1013
+ throw new Error(`MCP 配置必须是 JSON 对象: ${platform}`);
963
1014
  }
964
1015
  return config;
965
1016
  }
966
1017
 
967
- function copyCommonMcpFields(source, target) {
968
- for (const key of ['timeout', 'auth', 'oauth']) {
969
- if (source[key] !== undefined) {
970
- target[key] = source[key];
971
- }
972
- }
973
- return target;
1018
+ async function writeGenericMcpConfig(driver, platform, config) {
1019
+ const result = await driver.write(config);
1020
+ const failure = getDriverFailure(result, platform, 'write');
1021
+ if (failure) throw failure;
1022
+ return result?.status === 'ok' ? result.data : result;
974
1023
  }
975
1024
 
976
- function convertToOmpMcpFormat(spec = {}) {
977
- const sourceType = spec.type || 'stdio';
978
- let result;
979
-
980
- if (sourceType === 'stdio') {
981
- result = {
982
- type: 'stdio',
983
- command: spec.command || ''
984
- };
985
- if (Array.isArray(spec.args) && spec.args.length > 0) {
986
- result.args = spec.args;
987
- }
988
- if (spec.env && Object.keys(spec.env).length > 0) {
989
- result.env = spec.env;
990
- }
991
- if (spec.cwd) {
992
- result.cwd = spec.cwd;
993
- }
994
- } else if (sourceType === STREAMABLE_HTTP_TYPE) {
995
- result = {
996
- type: 'http',
997
- url: spec.url || ''
998
- };
999
- if (spec.headers && Object.keys(spec.headers).length > 0) {
1000
- result.headers = spec.headers;
1001
- }
1002
- } else if (sourceType === 'sse') {
1003
- result = {
1004
- type: 'sse',
1005
- url: spec.url || ''
1006
- };
1007
- if (spec.headers && Object.keys(spec.headers).length > 0) {
1008
- result.headers = spec.headers;
1009
- }
1010
- } else {
1011
- result = extractServerSpec(spec);
1025
+ function invokeMcpDriver(platform, operation, args = []) {
1026
+ const { driver } = requireMcpCapability(platform);
1027
+ if (typeof driver[operation] !== 'function') {
1028
+ const error = createPlatformCapabilityError(platform, 'mcp', 'unsupported');
1029
+ error.operation = operation;
1030
+ throw error;
1012
1031
  }
1013
-
1014
- return copyCommonMcpFields(spec, result);
1032
+ const normalize = result => {
1033
+ const failure = getDriverFailure(result, platform, operation);
1034
+ if (failure) throw failure;
1035
+ return result?.status === 'ok' ? result.data : result;
1036
+ };
1037
+ const result = driver[operation](...args);
1038
+ return result && typeof result.then === 'function' ? result.then(normalize) : normalize(result);
1015
1039
  }
1016
1040
 
1017
- function convertFromOmpMcpFormat(spec = {}) {
1018
- const sourceType = spec.type || 'stdio';
1019
- let result;
1020
1041
 
1021
- if (sourceType === 'stdio') {
1022
- result = {
1023
- type: 'stdio',
1024
- command: spec.command || ''
1025
- };
1026
- if (spec.args) {
1027
- result.args = spec.args;
1028
- }
1029
- if (spec.env) {
1030
- result.env = spec.env;
1042
+ function getGenericMcpServerMap(config) {
1043
+ for (const key of ['mcpServers', 'mcp_servers', 'mcp']) {
1044
+ if (!Object.prototype.hasOwnProperty.call(config, key)) continue;
1045
+ if (!config[key] || typeof config[key] !== 'object' || Array.isArray(config[key])) {
1046
+ throw new Error(`MCP 配置 ${key} 必须是对象`);
1031
1047
  }
1032
- if (spec.cwd) {
1033
- result.cwd = spec.cwd;
1034
- }
1035
- } else if (sourceType === 'http') {
1036
- result = {
1037
- type: STREAMABLE_HTTP_TYPE,
1038
- url: spec.url || ''
1039
- };
1040
- if (spec.headers) {
1041
- result.headers = spec.headers;
1042
- }
1043
- } else if (sourceType === 'sse') {
1044
- result = {
1045
- type: 'sse',
1046
- url: spec.url || ''
1047
- };
1048
- if (spec.headers) {
1049
- result.headers = spec.headers;
1050
- }
1051
- } else {
1052
- result = convertFromCodexFormat(spec);
1048
+ return config[key];
1053
1049
  }
1054
-
1055
- return copyCommonMcpFields(spec, result);
1050
+ return config;
1056
1051
  }
1057
1052
 
1058
- function syncToOmpMcpConfig(server) {
1059
- validateOmpServerName(server.id);
1060
- const config = readOmpMcpConfig();
1061
- config.mcpServers[server.id] = convertToOmpMcpFormat(server.server);
1062
- writeJsonFile(OMP_MCP_CONFIG_PATH, config);
1053
+ async function syncGenericMcpServer(driver, platform, server) {
1054
+ const config = await readGenericMcpConfig(driver, platform, true);
1055
+ const mcpServers = getGenericMcpServerMap(config);
1056
+ mcpServers[server.id] = extractServerSpec(server.server);
1057
+ return writeGenericMcpConfig(driver, platform, config);
1063
1058
  }
1064
1059
 
1065
- function removeFromOmpMcpConfig(serverId) {
1066
- const config = readOmpMcpConfig();
1067
- if (config.mcpServers && config.mcpServers[serverId]) {
1068
- delete config.mcpServers[serverId];
1069
- writeJsonFile(OMP_MCP_CONFIG_PATH, config);
1070
- }
1060
+ async function removeGenericMcpServer(driver, platform, serverId) {
1061
+ const config = await readGenericMcpConfig(driver, platform, true);
1062
+ const mcpServers = getGenericMcpServerMap(config);
1063
+ if (!Object.prototype.hasOwnProperty.call(mcpServers, serverId)) return config;
1064
+ delete mcpServers[serverId];
1065
+ return writeGenericMcpConfig(driver, platform, config);
1071
1066
  }
1072
1067
 
1073
- // ============================================================================
1074
- // OpenCode 配置同步
1075
- // ============================================================================
1076
-
1077
- /**
1078
- * 转换为 OpenCode 配置格式
1079
- */
1080
- function convertToOpenCodeFormat(spec) {
1081
- const sourceType = spec.type || 'stdio';
1082
-
1083
- if (sourceType === 'local' || sourceType === 'remote') {
1084
- const result = { ...spec };
1085
- result.enabled = spec.enabled !== false;
1086
- if (sourceType === 'local' && typeof result.command === 'string') {
1087
- result.command = result.command ? [result.command] : [];
1088
- }
1089
- return result;
1090
- }
1091
-
1092
- if (sourceType === 'stdio') {
1093
- const command = [];
1094
- if (spec.command) {
1095
- command.push(spec.command);
1096
- }
1097
- if (Array.isArray(spec.args) && spec.args.length > 0) {
1098
- command.push(...spec.args);
1099
- }
1100
-
1101
- const result = {
1102
- type: 'local',
1103
- command,
1104
- enabled: true
1105
- };
1106
-
1107
- if (spec.env && Object.keys(spec.env).length > 0) {
1108
- result.environment = spec.env;
1109
- }
1110
- if (spec.cwd) {
1111
- result.cwd = spec.cwd;
1112
- }
1113
-
1114
- return result;
1115
- }
1116
-
1117
- const result = {
1118
- type: 'remote',
1119
- url: spec.url || '',
1120
- enabled: true
1121
- };
1122
-
1123
- if (spec.headers && Object.keys(spec.headers).length > 0) {
1124
- result.headers = spec.headers;
1125
- }
1126
-
1127
- return result;
1128
- }
1068
+ async function importGenericMcpServers(driver, platform, servers) {
1069
+ const config = await readGenericMcpConfig(driver, platform, true);
1070
+ const mcpServers = getGenericMcpServerMap(config);
1071
+ let count = 0;
1129
1072
 
1130
- /**
1131
- * 从 OpenCode 格式转换到通用格式
1132
- */
1133
- function convertFromOpenCodeFormat(spec) {
1134
- const sourceType = spec.type || (Array.isArray(spec.command) ? 'local' : 'remote');
1135
-
1136
- if (sourceType === 'local') {
1137
- const result = { type: 'stdio' };
1138
- if (Array.isArray(spec.command) && spec.command.length > 0) {
1139
- result.command = spec.command[0];
1140
- if (spec.command.length > 1) {
1141
- result.args = spec.command.slice(1);
1073
+ for (const [id, spec] of Object.entries(mcpServers)) {
1074
+ if (!spec || typeof spec !== 'object' || Array.isArray(spec)) continue;
1075
+ if (servers[id]) {
1076
+ servers[id].apps = normalizeServerApps(servers[id].apps, {});
1077
+ if (!servers[id].apps[platform]) {
1078
+ servers[id].apps[platform] = true;
1079
+ count++;
1142
1080
  }
1143
- } else if (typeof spec.command === 'string') {
1144
- result.command = spec.command;
1145
- } else {
1146
- result.command = '';
1081
+ continue;
1147
1082
  }
1148
1083
 
1149
- if (spec.environment && typeof spec.environment === 'object') {
1150
- result.env = spec.environment;
1151
- } else if (spec.env && typeof spec.env === 'object') {
1152
- result.env = spec.env;
1153
- }
1154
- if (spec.cwd) {
1155
- result.cwd = spec.cwd;
1156
- }
1157
- return result;
1158
- }
1159
-
1160
- if (sourceType === 'remote') {
1161
- const result = {
1162
- type: STREAMABLE_HTTP_TYPE,
1163
- url: spec.url || ''
1084
+ const now = Date.now();
1085
+ servers[id] = {
1086
+ id,
1087
+ name: id,
1088
+ server: normalizeServerSpec(spec),
1089
+ apps: { [platform]: true },
1090
+ createdAt: now,
1091
+ updatedAt: now
1164
1092
  };
1165
- if (spec.headers && typeof spec.headers === 'object') {
1166
- result.headers = spec.headers;
1167
- }
1168
- return result;
1169
- }
1170
-
1171
- // 已经是通用格式时直接兼容处理
1172
- if (sourceType === 'stdio' || REMOTE_MCP_SERVER_TYPES.includes(sourceType)) {
1173
- return convertFromCodexFormat(spec);
1174
- }
1175
-
1176
- return {
1177
- type: 'stdio',
1178
- command: ''
1179
- };
1180
- }
1181
-
1182
- /**
1183
- * 同步到 OpenCode 配置
1184
- */
1185
- function syncToOpenCodeConfig(server) {
1186
- const { path: configPath, config } = readOpenCodeConfig();
1187
- const nextConfig = config && typeof config === 'object' ? config : {};
1188
-
1189
- if (!nextConfig.mcp || typeof nextConfig.mcp !== 'object') {
1190
- nextConfig.mcp = {};
1093
+ count++;
1191
1094
  }
1192
1095
 
1193
- nextConfig.mcp[server.id] = convertToOpenCodeFormat(server.server);
1194
- writeOpenCodeConfig(configPath, nextConfig);
1096
+ return count;
1195
1097
  }
1196
1098
 
1197
- /**
1198
- * 从 OpenCode 配置移除
1199
- */
1200
- function removeFromOpenCodeConfig(serverId) {
1201
- const { path: configPath, config } = readOpenCodeConfig();
1202
- const nextConfig = config && typeof config === 'object' ? config : {};
1203
-
1204
- if (nextConfig.mcp && nextConfig.mcp[serverId]) {
1205
- delete nextConfig.mcp[serverId];
1206
- writeOpenCodeConfig(configPath, nextConfig);
1207
- }
1208
- }
1209
1099
 
1210
1100
  // ============================================================================
1211
1101
  // 导入功能
@@ -1215,247 +1105,76 @@ function removeFromOpenCodeConfig(serverId) {
1215
1105
  * 从指定平台导入 MCP 配置
1216
1106
  */
1217
1107
  async function importFromPlatform(platform) {
1218
- let importedCount = 0;
1108
+ const { platform: normalizedPlatform, driver } = requireMcpCapability(platform);
1219
1109
  const servers = getAllServers();
1220
-
1221
- switch (platform) {
1222
- case 'claude':
1223
- importedCount = importFromClaude(servers);
1224
- break;
1225
- case 'codex':
1226
- importedCount = importFromCodex(servers);
1227
- break;
1228
- case 'gemini':
1229
- importedCount = importFromGemini(servers);
1230
- break;
1231
- case 'opencode':
1232
- importedCount = importFromOpenCode(servers);
1233
- break;
1234
- case 'omp':
1235
- importedCount = importFromOmp(servers);
1236
- break;
1237
- default:
1238
- throw new Error(`无效的平台: ${platform}`);
1239
- }
1110
+ const result = typeof driver.import === 'function'
1111
+ ? await driver.import(servers)
1112
+ : await importGenericMcpServers(driver, normalizedPlatform, servers);
1113
+ const failure = getDriverFailure(result, normalizedPlatform, 'import');
1114
+ if (failure) throw failure;
1115
+ const importedCount = result?.status === 'ok' ? result.data : result;
1240
1116
 
1241
1117
  if (importedCount > 0) {
1242
1118
  writeJsonFile(MCP_SERVERS_FILE, servers);
1119
+ for (const server of Object.values(servers)) {
1120
+ applyMcpControlApps(server);
1121
+ }
1243
1122
  }
1244
-
1245
1123
  return importedCount;
1246
1124
  }
1247
1125
 
1248
- /**
1249
- * 从 Claude 导入
1250
- */
1251
- function importFromClaude(servers) {
1252
- const config = readJsonFile(CLAUDE_CONFIG_PATH, {});
1253
- const mcpServers = config.mcpServers || {};
1254
- let count = 0;
1255
-
1256
- for (const [id, spec] of Object.entries(mcpServers)) {
1257
- if (servers[id]) {
1258
- // 已存在,只启用 Claude
1259
- if (!servers[id].apps.claude) {
1260
- servers[id].apps.claude = true;
1261
- count++;
1262
- }
1263
- } else {
1264
- // 新服务器
1265
- servers[id] = {
1266
- id,
1267
- name: id,
1268
- server: spec,
1269
- apps: { claude: true, codex: false, gemini: false, opencode: false },
1270
- createdAt: Date.now(),
1271
- updatedAt: Date.now()
1272
- };
1273
- count++;
1274
- }
1275
- }
1276
1126
 
1277
- return count;
1278
- }
1279
1127
 
1280
1128
  /**
1281
- * 从 Codex 导入
1129
+ * 提取纯净的服务器规范(移除元数据)
1282
1130
  */
1283
- function importFromCodex(servers) {
1284
- const config = readTomlFile(CODEX_CONFIG_PATH, {});
1285
- const mcpServers = config.mcp_servers || {};
1286
- let count = 0;
1287
-
1288
- for (const [id, spec] of Object.entries(mcpServers)) {
1289
- // 转换 Codex 格式到通用格式
1290
- const convertedSpec = convertFromCodexFormat(spec);
1291
-
1292
- if (servers[id]) {
1293
- // 已存在,只启用 Codex
1294
- if (!servers[id].apps.codex) {
1295
- servers[id].apps.codex = true;
1296
- count++;
1297
- }
1298
- } else {
1299
- // 新服务器
1300
- servers[id] = {
1301
- id,
1302
- name: id,
1303
- server: convertedSpec,
1304
- apps: { claude: false, codex: true, gemini: false, opencode: false },
1305
- createdAt: Date.now(),
1306
- updatedAt: Date.now()
1307
- };
1308
- count++;
1309
- }
1310
- }
1311
-
1312
- return count;
1131
+ function extractServerSpec(spec) {
1132
+ return mcpFormat.extractServerSpec(spec);
1313
1133
  }
1314
1134
 
1315
- /**
1316
- * 从 Gemini 导入
1317
- */
1318
- function importFromGemini(servers) {
1319
- const config = readJsonFile(GEMINI_CONFIG_PATH, {});
1320
- const mcpServers = config.mcpServers || {};
1321
- let count = 0;
1135
+ const EXPORT_SECRET_KEY_PATTERN = /(?:token|secret|password|authorization|api[-_]?key|access[_-]?token|credential|private[_-]?key|secretrefs?|auth|oauth|envvars|experimentalenvironment)/i;
1322
1136
 
1323
- for (const [id, spec] of Object.entries(mcpServers)) {
1324
- if (servers[id]) {
1325
- // 已存在,只启用 Gemini
1326
- if (!servers[id].apps.gemini) {
1327
- servers[id].apps.gemini = true;
1328
- count++;
1329
- }
1330
- } else {
1331
- // 新服务器
1332
- servers[id] = {
1333
- id,
1334
- name: id,
1335
- server: spec,
1336
- apps: { claude: false, codex: false, gemini: true, opencode: false },
1337
- createdAt: Date.now(),
1338
- updatedAt: Date.now()
1339
- };
1340
- count++;
1341
- }
1137
+ function sanitizeExportValue(value, key = '') {
1138
+ if (/^url$/i.test(key)) return typeof value === 'string' ? redactSecrets({ url: value }).url : value;
1139
+ if (/^(?:auth|oauth|bearer_token_env_var|secretRefs?)$/i.test(key) || EXPORT_SECRET_KEY_PATTERN.test(key)) {
1140
+ return '[REDACTED]';
1342
1141
  }
1343
-
1344
- return count;
1345
- }
1346
-
1347
- /**
1348
- * 从 OpenCode 导入
1349
- */
1350
- function importFromOpenCode(servers) {
1351
- const { config } = readOpenCodeConfig();
1352
- const mcpServers = config.mcp || {};
1353
- let count = 0;
1354
-
1355
- for (const [id, spec] of Object.entries(mcpServers)) {
1356
- const convertedSpec = convertFromOpenCodeFormat(spec || {});
1357
-
1358
- if (servers[id]) {
1359
- if (!servers[id].apps.opencode) {
1360
- servers[id].apps.opencode = true;
1361
- count++;
1362
- }
1363
- } else {
1364
- servers[id] = {
1365
- id,
1366
- name: id,
1367
- server: convertedSpec,
1368
- apps: { claude: false, codex: false, gemini: false, opencode: true },
1369
- createdAt: Date.now(),
1370
- updatedAt: Date.now()
1371
- };
1372
- count++;
1142
+ if (/^(?:command|args)$/i.test(key)) return sanitizeCommandLineValue(value);
1143
+ if (typeof value === 'string') return sanitizeMcpErrorText(value);
1144
+ if (/^(?:env|environment|env_vars|experimental_environment)$/i.test(key)) {
1145
+ if (Array.isArray(value)) {
1146
+ return value.map(item => (
1147
+ typeof item === 'string' && /^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(item)
1148
+ ? item
1149
+ : '[REDACTED]'
1150
+ ));
1373
1151
  }
1374
- }
1375
-
1376
- return count;
1377
- }
1378
-
1379
- /**
1380
- * 从 OMP 导入
1381
- */
1382
- function importFromOmp(servers) {
1383
- const config = readOmpMcpConfig();
1384
- const mcpServers = config.mcpServers || {};
1385
- let count = 0;
1386
-
1387
- for (const [id, spec] of Object.entries(mcpServers)) {
1388
- const convertedSpec = convertFromOmpMcpFormat(spec || {});
1389
-
1390
- if (servers[id]) {
1391
- servers[id].apps = normalizeServerApps(servers[id].apps);
1392
- if (!servers[id].apps.omp) {
1393
- servers[id].apps.omp = true;
1394
- count++;
1395
- }
1396
- } else {
1397
- servers[id] = {
1398
- id,
1399
- name: id,
1400
- server: convertedSpec,
1401
- apps: { claude: false, codex: false, gemini: false, opencode: false, omp: true },
1402
- createdAt: Date.now(),
1403
- updatedAt: Date.now()
1404
- };
1405
- count++;
1152
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
1153
+ return Object.fromEntries(Object.entries(value).map(([childKey, childValue]) => [
1154
+ childKey,
1155
+ typeof childValue === 'string' && /^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(childValue)
1156
+ ? childValue
1157
+ : '[REDACTED]'
1158
+ ]));
1406
1159
  }
1160
+ return '[REDACTED]';
1407
1161
  }
1408
-
1409
- return count;
1410
- }
1411
-
1412
- /**
1413
- * 从 Codex 格式转换
1414
- */
1415
- function convertFromCodexFormat(spec) {
1416
- const result = {
1417
- type: spec.type || 'stdio'
1418
- };
1419
-
1420
- if (result.type === 'stdio') {
1421
- result.command = spec.command || '';
1422
- if (spec.args) {
1423
- result.args = spec.args;
1424
- }
1425
- if (spec.env) {
1426
- result.env = spec.env;
1427
- }
1428
- if (spec.cwd) {
1429
- result.cwd = spec.cwd;
1430
- }
1431
- } else if (REMOTE_MCP_SERVER_TYPES.includes(result.type)) {
1432
- result.url = spec.url || '';
1433
- if (spec.http_headers) {
1434
- result.headers = spec.http_headers;
1435
- } else if (spec.headers) {
1436
- result.headers = spec.headers;
1437
- }
1162
+ if (/^(?:headers|http_headers)$/i.test(key) && value && typeof value === 'object' && !Array.isArray(value)) {
1163
+ return Object.fromEntries(Object.entries(value).map(([header, headerValue]) => [
1164
+ header,
1165
+ /^(?:accept|content-type|user-agent)$/i.test(header) ? headerValue : '[REDACTED]'
1166
+ ]));
1438
1167
  }
1439
-
1440
- return result;
1168
+ if (Array.isArray(value)) return value.map(item => sanitizeExportValue(item));
1169
+ if (!value || typeof value !== 'object') return value;
1170
+ return Object.fromEntries(Object.entries(value).map(([childKey, childValue]) => [
1171
+ childKey,
1172
+ sanitizeExportValue(childValue, childKey)
1173
+ ]));
1441
1174
  }
1442
1175
 
1443
- /**
1444
- * 提取纯净的服务器规范(移除元数据)
1445
- */
1446
- function extractServerSpec(spec) {
1447
- const result = { ...spec };
1448
- // 移除可能存在的非规范字段
1449
- delete result.id;
1450
- delete result.name;
1451
- delete result.description;
1452
- delete result.tags;
1453
- delete result.homepage;
1454
- delete result.docs;
1455
- delete result.apps;
1456
- delete result.createdAt;
1457
- delete result.updatedAt;
1458
- return result;
1176
+ function sanitizeExportSpec(spec = {}) {
1177
+ return sanitizeExportValue(extractServerSpec(spec));
1459
1178
  }
1460
1179
 
1461
1180
  /**
@@ -1464,15 +1183,13 @@ function extractServerSpec(spec) {
1464
1183
  function getStats() {
1465
1184
  const servers = getAllServers();
1466
1185
  const serverList = Object.values(servers);
1186
+ const stats = { total: serverList.length };
1467
1187
 
1468
- return {
1469
- total: serverList.length,
1470
- claude: serverList.filter(s => s.apps?.claude).length,
1471
- codex: serverList.filter(s => s.apps?.codex).length,
1472
- gemini: serverList.filter(s => s.apps?.gemini).length,
1473
- opencode: serverList.filter(s => s.apps?.opencode).length,
1474
- omp: serverList.filter(s => s.apps?.omp).length
1475
- };
1188
+ for (const platform of getMcpPlatformKeys()) {
1189
+ stats[platform] = serverList.filter(server => server.apps?.[platform]).length;
1190
+ }
1191
+
1192
+ return stats;
1476
1193
  }
1477
1194
 
1478
1195
  // ============================================================================
@@ -1489,6 +1206,7 @@ async function testServer(serverId) {
1489
1206
  if (!server) {
1490
1207
  throw new Error(`MCP 服务器 "${serverId}" 不存在`);
1491
1208
  }
1209
+ assertMcpServerActivated(server);
1492
1210
 
1493
1211
  const spec = server.server;
1494
1212
  const type = spec.type || 'stdio';
@@ -1585,7 +1303,7 @@ async function testStdioServer(spec) {
1585
1303
  } else {
1586
1304
  done({
1587
1305
  success: false,
1588
- message: `启动失败: ${err.message}`,
1306
+ message: sanitizeMcpErrorText(`启动失败: ${err.message}`),
1589
1307
  duration: Date.now() - startTime
1590
1308
  });
1591
1309
  }
@@ -1601,7 +1319,7 @@ async function testStdioServer(spec) {
1601
1319
  } else {
1602
1320
  done({
1603
1321
  success: false,
1604
- message: stderr || `进程退出码: ${code}`,
1322
+ message: sanitizeMcpErrorText(stderr || `进程退出码: ${code}`),
1605
1323
  duration: Date.now() - startTime
1606
1324
  });
1607
1325
  }
@@ -1676,6 +1394,13 @@ async function testHttpServer(spec) {
1676
1394
  }
1677
1395
  }
1678
1396
 
1397
+ function assertMcpServerActivated(server) {
1398
+ if (getControlMcpPlatformKeys().some(platform => server.apps?.[platform] === true)) return;
1399
+ const error = new Error('MCP server is disabled by the effective control policy');
1400
+ error.code = 'MCP_DISABLED';
1401
+ throw error;
1402
+ }
1403
+
1679
1404
  /**
1680
1405
  * Get tools list from MCP server
1681
1406
  * @param {string} serverId - Server ID from config
@@ -1686,6 +1411,7 @@ async function getServerTools(serverId) {
1686
1411
  if (!server) {
1687
1412
  throw new Error(`MCP 服务器 "${serverId}" 不存在`);
1688
1413
  }
1414
+ assertMcpServerActivated(server);
1689
1415
 
1690
1416
  const startTime = Date.now();
1691
1417
  const spec = server.server;
@@ -1709,7 +1435,7 @@ async function getServerTools(serverId) {
1709
1435
  try {
1710
1436
  await cached.client.disconnect();
1711
1437
  } catch (err) {
1712
- console.error(`[MCP] Error disconnecting expired client: ${err.message}`);
1438
+ console.error(`[MCP] Error disconnecting expired client: ${sanitizeMcpErrorText(err.message || err)}`);
1713
1439
  }
1714
1440
  mcpClientPool.delete(serverId);
1715
1441
  }
@@ -1733,7 +1459,7 @@ async function getServerTools(serverId) {
1733
1459
  }
1734
1460
 
1735
1461
  // Get tools list
1736
- const tools = await client.listTools();
1462
+ const tools = sanitizeMcpToolValue(await client.listTools());
1737
1463
 
1738
1464
  return {
1739
1465
  tools,
@@ -1777,6 +1503,7 @@ async function callServerTool(serverId, toolName, arguments = {}) {
1777
1503
  if (!server) {
1778
1504
  throw new Error(`MCP 服务器 "${serverId}" 不存在`);
1779
1505
  }
1506
+ assertMcpServerActivated(server);
1780
1507
 
1781
1508
  const startTime = Date.now();
1782
1509
  const spec = server.server;
@@ -1802,7 +1529,7 @@ async function callServerTool(serverId, toolName, arguments = {}) {
1802
1529
  try {
1803
1530
  await cached.client.disconnect();
1804
1531
  } catch (err) {
1805
- console.error(`[MCP] Error disconnecting expired client: ${err.message}`);
1532
+ console.error(`[MCP] Error disconnecting expired client: ${sanitizeMcpErrorText(err.message || err)}`);
1806
1533
  }
1807
1534
  mcpClientPool.delete(serverId);
1808
1535
  }
@@ -1827,27 +1554,30 @@ async function callServerTool(serverId, toolName, arguments = {}) {
1827
1554
 
1828
1555
  // Call the tool
1829
1556
  const result = await client.callTool(toolName, arguments);
1830
-
1557
+ const safeResult = sanitizeMcpToolValue(result);
1558
+ const resultObject = safeResult && typeof safeResult === 'object' && !Array.isArray(safeResult)
1559
+ ? safeResult
1560
+ : { value: safeResult };
1831
1561
  const duration = Date.now() - startTime;
1832
1562
 
1833
1563
  // Check result size, truncate if > 10KB
1834
- const resultStr = JSON.stringify(result);
1564
+ const resultStr = JSON.stringify(resultObject);
1835
1565
  if (resultStr.length > 10 * 1024) {
1836
1566
  return {
1837
1567
  result: {
1838
- ...result,
1568
+ ...resultObject,
1839
1569
  truncated: true
1840
1570
  },
1841
1571
  truncatedSize: resultStr.length,
1842
1572
  duration,
1843
- isError: result.isError || false
1573
+ isError: resultObject.isError || false
1844
1574
  };
1845
1575
  }
1846
1576
 
1847
1577
  return {
1848
- result,
1578
+ result: resultObject,
1849
1579
  duration,
1850
- isError: result.isError || false
1580
+ isError: resultObject.isError || false
1851
1581
  };
1852
1582
 
1853
1583
  } catch (err) {
@@ -1866,8 +1596,7 @@ async function callServerTool(serverId, toolName, arguments = {}) {
1866
1596
  return {
1867
1597
  result: {
1868
1598
  error: failure.message,
1869
- code: err.code,
1870
- data: err.data
1599
+ ...(err.code ? { code: sanitizeMcpErrorText(err.code) } : {})
1871
1600
  },
1872
1601
  duration: failure.duration,
1873
1602
  isError: true,
@@ -1908,8 +1637,9 @@ function updateServerOrder(serverIds) {
1908
1637
 
1909
1638
  // 更新每个服务器的排序索引
1910
1639
  serverIds.forEach((id, index) => {
1911
- if (servers[id]) {
1912
- servers[id].order = index;
1640
+ const normalizedId = validateMcpId(id);
1641
+ if (Object.prototype.hasOwnProperty.call(servers, normalizedId)) {
1642
+ servers[normalizedId].order = index;
1913
1643
  }
1914
1644
  });
1915
1645
 
@@ -1923,26 +1653,19 @@ function updateServerOrder(serverIds) {
1923
1653
 
1924
1654
  /**
1925
1655
  * 导出所有 MCP 配置
1926
- * @param {string} format - 导出格式: 'json' | 'claude' | 'codex' | 'opencode' | 'gemini' | 'omp'
1656
+ * @param {string} format - 导出格式: json 或声明了导出操作的 MCP 平台 key
1927
1657
  */
1928
1658
  function exportServers(format = 'json') {
1929
1659
  const servers = getAllServers();
1660
+ if (format === 'json') return exportAsJson(servers);
1930
1661
 
1931
- switch (format) {
1932
- case 'claude':
1933
- return exportForClaude(servers);
1934
- case 'codex':
1935
- return exportForCodex(servers);
1936
- case 'opencode':
1937
- return exportForOpenCode(servers);
1938
- case 'gemini':
1939
- return exportForGemini(servers);
1940
- case 'omp':
1941
- return exportForOmp(servers);
1942
- case 'json':
1943
- default:
1944
- return exportAsJson(servers);
1662
+ const { platform, driver } = requireMcpCapability(format);
1663
+ if (typeof driver.export !== 'function') {
1664
+ const error = createPlatformCapabilityError(platform, 'mcp', 'unsupported');
1665
+ error.operation = 'export';
1666
+ throw error;
1945
1667
  }
1668
+ return driver.export(servers);
1946
1669
  }
1947
1670
 
1948
1671
  /**
@@ -1952,111 +1675,17 @@ function exportAsJson(servers) {
1952
1675
  const mcpServers = {};
1953
1676
 
1954
1677
  for (const [id, server] of Object.entries(servers)) {
1955
- mcpServers[id] = extractServerSpec(server.server);
1678
+ mcpServers[id] = sanitizeExportSpec(server.server);
1956
1679
  }
1957
1680
 
1958
1681
  return {
1959
1682
  format: 'json',
1960
1683
  content: JSON.stringify({ mcpServers }, null, 2),
1961
- filename: 'mcp-servers.json'
1962
- };
1963
- }
1964
-
1965
- /**
1966
- * 导出为 Claude 格式
1967
- */
1968
- function exportForClaude(servers) {
1969
- const mcpServers = {};
1970
-
1971
- for (const [id, server] of Object.entries(servers)) {
1972
- if (server.apps?.claude) {
1973
- mcpServers[id] = extractServerSpec(server.server);
1974
- }
1975
- }
1976
-
1977
- return {
1978
- format: 'claude',
1979
- content: JSON.stringify({ mcpServers }, null, 2),
1980
- filename: 'claude-mcp-config.json'
1981
- };
1982
- }
1983
-
1984
- /**
1985
- * 导出为 Codex 格式
1986
- */
1987
- function exportForCodex(servers) {
1988
- const mcp_servers = {};
1989
-
1990
- for (const [id, server] of Object.entries(servers)) {
1991
- if (server.apps?.codex) {
1992
- mcp_servers[id] = convertToCodexFormat(server.server);
1993
- }
1994
- }
1995
-
1996
- return {
1997
- format: 'codex',
1998
- content: toml.stringify({ mcp_servers }),
1999
- filename: 'codex-mcp-config.toml'
2000
- };
2001
- }
2002
-
2003
- /**
2004
- * 导出为 OpenCode 格式
2005
- */
2006
- function exportForOpenCode(servers) {
2007
- const mcp = {};
2008
-
2009
- for (const [id, server] of Object.entries(servers)) {
2010
- if (server.apps?.opencode) {
2011
- mcp[id] = convertToOpenCodeFormat(server.server);
2012
- }
2013
- }
2014
-
2015
- return {
2016
- format: 'opencode',
2017
- content: JSON.stringify({ mcp }, null, 2),
2018
- filename: 'opencode-mcp-config.json'
2019
- };
2020
- }
2021
-
2022
- /**
2023
- * 导出为 Gemini 格式
2024
- */
2025
- function exportForGemini(servers) {
2026
- const mcpServers = {};
2027
-
2028
- for (const [id, server] of Object.entries(servers)) {
2029
- if (server.apps?.gemini) {
2030
- mcpServers[id] = extractServerSpec(server.server);
2031
- }
2032
- }
2033
-
2034
- return {
2035
- format: 'gemini',
2036
- content: JSON.stringify({ mcpServers }, null, 2),
2037
- filename: 'gemini-mcp-config.json'
1684
+ filename: 'mcp-servers.json',
1685
+ contentType: 'application/json'
2038
1686
  };
2039
1687
  }
2040
1688
 
2041
- /**
2042
- * 导出为 OMP 格式
2043
- */
2044
- function exportForOmp(servers) {
2045
- const mcpServers = {};
2046
-
2047
- for (const [id, server] of Object.entries(servers)) {
2048
- if (server.apps?.omp) {
2049
- validateOmpServerName(id);
2050
- mcpServers[id] = convertToOmpMcpFormat(server.server);
2051
- }
2052
- }
2053
-
2054
- return {
2055
- format: 'omp',
2056
- content: JSON.stringify({ $schema: OMP_MCP_SCHEMA_URL, mcpServers }, null, 2),
2057
- filename: 'omp-mcp-config.json'
2058
- };
2059
- }
2060
1689
 
2061
1690
  module.exports = {
2062
1691
  getAllServers,
@@ -2068,6 +1697,7 @@ module.exports = {
2068
1697
  importFromPlatform,
2069
1698
  getStats,
2070
1699
  validateServerSpec,
1700
+ getMcpPlatformKeys,
2071
1701
  // 新增功能
2072
1702
  testServer,
2073
1703
  getServerTools,