coding-tool-x 3.8.3 → 3.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +86 -15
- package/dist/web/assets/AgentsDrawer-BG6P3DX-.js +1 -0
- package/dist/web/assets/AgentsDrawer-Pw2k-ZAa.css +1 -0
- package/dist/web/assets/Analytics--z_FmL5P.js +1 -0
- package/dist/web/assets/Analytics-CyHhR126.css +1 -0
- package/dist/web/assets/AssetPathField-CrJtRKHy.js +1 -0
- package/dist/web/assets/ChatHistoryDrawer-B2qBQs9L.css +1 -0
- package/dist/web/assets/ChatHistoryDrawer-BiuEryJj.js +1 -0
- package/dist/web/assets/CollapseItem-BAYEsL_Q.js +1 -0
- package/dist/web/assets/CommandsDrawer-BZJxlPvV.css +1 -0
- package/dist/web/assets/CommandsDrawer-YJ-U7BXN.js +1 -0
- package/dist/web/assets/ConfigExportDrawer-Ce_hAknW.js +1 -0
- package/dist/web/assets/ConfigExportDrawer-dwruRKWr.css +1 -0
- package/dist/web/assets/ConfigTemplates-Bvr1NN6K.js +1 -0
- package/dist/web/assets/ConfigTemplatesDrawer-BfDi8yO3.css +1 -0
- package/dist/web/assets/ConfigTemplatesDrawer-BjIeHfF-.js +1 -0
- package/dist/web/assets/ConfigTemplatesPanel-AV00tm_D.js +1 -0
- package/dist/web/assets/ConfigTemplatesPanel-CWyPtk67.css +1 -0
- package/dist/web/assets/Divider-D28g1gwj.js +1 -0
- package/dist/web/assets/DrawerContent-ChXgZ1Xm.js +1 -0
- package/dist/web/assets/FavoritesDrawer-CG9eI9Ay.css +1 -0
- package/dist/web/assets/FavoritesDrawer-Cz95o4CD.js +1 -0
- package/dist/web/assets/GatewayConvertDrawer-CuNlf7AC.css +1 -0
- package/dist/web/assets/GatewayConvertDrawer-DLlbMz27.js +1 -0
- package/dist/web/assets/Home-CSuFuJ9Y.css +1 -0
- package/dist/web/assets/Home-D7CCOFQC.js +1 -0
- package/dist/web/assets/MarkdownEditor-qfVgSre_.css +1 -0
- package/dist/web/assets/MarkdownEditor-rEZ-c72L.js +1 -0
- package/dist/web/assets/McpDrawer-C0dnsZhQ.css +1 -0
- package/dist/web/assets/McpDrawer-DfoCyxU2.js +1 -0
- package/dist/web/assets/McpFormDrawer-BRbDcawn.css +1 -0
- package/dist/web/assets/McpFormDrawer-SKu8HNUR.js +1 -0
- package/dist/web/assets/PluginManager-BbeK-3rV.js +1 -0
- package/dist/web/assets/PluginsDrawer-CWVJ7yjS.js +1 -0
- package/dist/web/assets/PluginsDrawer-DM3QjprE.css +1 -0
- package/dist/web/assets/PluginsPanel-Cm2N3StG.js +1 -0
- package/dist/web/assets/PluginsPanel-D6ZqITcR.css +1 -0
- package/dist/web/assets/ProjectList-677RLZ8O.css +1 -0
- package/dist/web/assets/ProjectList-CsjnsNPv.js +1 -0
- package/dist/web/assets/PromptsDrawer-BIgiYCEQ.css +1 -0
- package/dist/web/assets/PromptsDrawer-EGd3djFa.js +1 -0
- package/dist/web/assets/RecentSessionsDrawer-CKFsOYDM.css +1 -0
- package/dist/web/assets/RecentSessionsDrawer-D1vtoLAs.js +1 -0
- package/dist/web/assets/SessionCard-BgLg49U3.js +1 -0
- package/dist/web/assets/SessionCard-aM2NHu6i.css +1 -0
- package/dist/web/assets/SessionList-BpYRz1cX.css +1 -0
- package/dist/web/assets/SessionList-TRiTklWk.js +1 -0
- package/dist/web/assets/SettingsDrawer-d5liXjHq.css +1 -0
- package/dist/web/assets/SettingsDrawer-rz2WejGY.js +1 -0
- package/dist/web/assets/SkillManager-BTjTUbHq.js +1 -0
- package/dist/web/assets/SkillManager-C0GCT2Kb.css +1 -0
- package/dist/web/assets/SkillsDrawer-BCNme-aJ.css +1 -0
- package/dist/web/assets/SkillsDrawer-pN0hbkK3.js +1 -0
- package/dist/web/assets/SkillsPanel-B3Nm5j32.css +1 -0
- package/dist/web/assets/SkillsPanel-gmBWEoiF.js +1 -0
- package/dist/web/assets/SpeedTestDrawer-Dc0WToJ_.css +1 -0
- package/dist/web/assets/SpeedTestDrawer-DcNzuVJk.js +1 -0
- package/dist/web/assets/Upload-v5rV0qTT.js +1 -0
- package/dist/web/assets/WorkspaceDrawer-CGnMu9p1.css +1 -0
- package/dist/web/assets/WorkspaceDrawer-DxmicM4U.js +1 -0
- package/dist/web/assets/WorkspaceManager-CJHCjARN.js +1 -0
- package/dist/web/assets/WorkspacePanel-D0-zHTHP.css +1 -0
- package/dist/web/assets/WorkspacePanel-D8_Zq-du.js +1 -0
- package/dist/web/assets/_plugin-vue_export-helper-BeLZJZij.js +1 -0
- package/dist/web/assets/client-UDwXBP9i.js +1 -0
- package/dist/web/assets/clipboard-B46yyoz_.js +1 -0
- package/dist/web/assets/headers-cf-QapUq.js +1 -0
- package/dist/web/assets/icons-B2Vg-IOC.js +1 -0
- package/dist/web/assets/index-C0EHRym0.css +1 -0
- package/dist/web/assets/index-DnEocV0M.js +2 -0
- package/dist/web/assets/light-Cp0t7v32.js +1 -0
- package/dist/web/assets/markdown-u082bbtj.js +1 -0
- package/dist/web/assets/platforms-CBkB45JP.js +1 -0
- package/dist/web/assets/platforms-FTpjxAT6.js +1 -0
- package/dist/web/assets/project-config-B2baPzYL.js +1 -0
- package/dist/web/assets/sessions-Cmy31slD.js +1 -0
- package/dist/web/assets/sessions-yC_oVVh8.js +1 -0
- package/dist/web/assets/skill-repo-input-B_fkGlUK.js +1 -0
- package/dist/web/assets/use-lock-html-scroll-DXIqmUW4.js +1 -0
- package/dist/web/assets/use-message-DoLM8C1C.js +1 -0
- package/dist/web/assets/useResponsiveDrawer-B3uojyvV.js +1 -0
- package/dist/web/assets/vendors-D0c_0KLP.js +1 -0
- package/dist/web/assets/vue-vendor-BqZbbXHA.js +1 -0
- package/dist/web/assets/workspaces-mjyPVGS8.js +1 -0
- package/dist/web/assets/xml-CgY3RMlX.js +1 -0
- package/dist/web/index.html +12 -8
- package/docs/native-log-memory-2026-09-16.md +43 -0
- package/docs/superpowers/plans/2026-08-26-configuration-driven-cli-platform.md +1578 -0
- package/docs/superpowers/plans/2026-08-30-project-scoped-config-control.md +1260 -0
- package/docs/superpowers/plans/2026-09-01-omp-skill-refresh-boundary.md +213 -0
- package/docs/superpowers/plans/2026-09-01-realtime-logs-panel.md +125 -0
- package/docs/superpowers/plans/2026-09-02-config-driven-platform-boundary.md +86 -0
- package/docs/superpowers/plans/2026-09-02-platform-implementation-drivers.md +508 -0
- package/docs/superpowers/plans/2026-09-03-cli-platform-correctness-boundary.md +760 -0
- package/docs/superpowers/plans/2026-09-03-dependency-security-remediation.md +133 -0
- package/docs/superpowers/plans/2026-09-04-model-catalog-pricing-sync.md +1078 -0
- package/docs/superpowers/plans/2026-09-05-codex-project-scan-500.md +128 -0
- package/docs/superpowers/plans/2026-09-06-project-skill-path-fix.md +97 -0
- package/docs/superpowers/specs/2026-08-26-configuration-driven-cli-platform-design.md +408 -0
- package/docs/superpowers/specs/2026-08-30-project-scoped-config-control-design.md +467 -0
- package/docs/superpowers/specs/2026-09-01-claude-session-content-repair-design.md +111 -0
- package/docs/superpowers/specs/2026-09-01-omp-skill-refresh-boundary-design.md +83 -0
- package/docs/superpowers/specs/2026-09-01-realtime-logs-panel-design.md +27 -0
- package/docs/superpowers/specs/2026-09-02-config-driven-platform-boundary-design.md +64 -0
- package/docs/superpowers/specs/2026-09-02-platform-implementation-driver-boundary-design.md +123 -0
- package/docs/superpowers/specs/2026-09-03-cli-platform-correctness-boundary-design.md +249 -0
- package/docs/superpowers/specs/2026-09-03-dependency-security-remediation-design.md +87 -0
- package/docs/superpowers/specs/2026-09-04-model-catalog-pricing-sync-design.md +301 -0
- package/docs/superpowers/specs/2026-09-05-codex-project-scan-500-design.md +24 -0
- package/package.json +13 -8
- package/src/commands/channels.js +88 -191
- package/src/commands/cli-type.js +19 -11
- package/src/commands/daemon.js +94 -32
- package/src/commands/doctor.js +16 -9
- package/src/commands/export-config.js +23 -5
- package/src/commands/list.js +130 -52
- package/src/commands/logs.js +44 -27
- package/src/commands/platform-command-registry.js +78 -0
- package/src/commands/plugin.js +11 -5
- package/src/commands/port-config.js +34 -52
- package/src/commands/proxy-control.js +207 -145
- package/src/commands/proxy.js +2 -2
- package/src/commands/resume.js +87 -91
- package/src/commands/search.js +137 -67
- package/src/commands/stats.js +30 -22
- package/src/commands/switch.js +1 -1
- package/src/commands/toggle-proxy.js +94 -111
- package/src/commands/ui.js +67 -6
- package/src/commands/workspace.js +90 -8
- package/src/config/default.js +6 -53
- package/src/config/loader.js +61 -24
- package/src/config/model-metadata.js +49 -59
- package/src/config/model-metadata.json +4095 -99
- package/src/config/paths.js +181 -8
- package/src/index.js +134 -71
- package/src/platforms/access.js +196 -0
- package/src/{server/services → platforms}/agents-service.js +354 -197
- package/src/platforms/channel-auth-registry.js +162 -0
- package/src/platforms/channel-auth-service.js +82 -0
- package/src/{server/services → platforms}/commands-service.js +195 -115
- package/src/platforms/driver-registry.js +59 -0
- package/src/platforms/drivers/claude/api-operations.js +62 -0
- package/src/{server/services/channels.js → platforms/drivers/claude/channels-implementation.js} +131 -33
- package/src/platforms/drivers/claude/channels.js +39 -0
- package/src/{server/services/config-sync-service.js → platforms/drivers/claude/config-sync.js} +29 -5
- package/src/{server/services → platforms/drivers/claude}/health-check.js +4 -4
- package/src/platforms/drivers/claude/health.js +22 -0
- package/src/platforms/drivers/claude/mcp.js +9 -0
- package/src/{server/services/settings-manager.js → platforms/drivers/claude/native-config-implementation.js} +13 -4
- package/src/platforms/drivers/claude/native-config.js +46 -0
- package/src/platforms/drivers/claude/native-logs.js +72 -0
- package/src/{server/services/claude-openai-gateway.js → platforms/drivers/claude/openai-gateway.js} +8 -65
- package/src/platforms/drivers/claude/project-config.js +21 -0
- package/src/platforms/drivers/claude/projects.js +62 -0
- package/src/platforms/drivers/claude/prompts.js +9 -0
- package/src/{server/proxy-server.js → platforms/drivers/claude/proxy-implementation.js} +41 -278
- package/src/platforms/drivers/claude/proxy.js +16 -0
- package/src/platforms/drivers/claude/resource-sync.js +9 -0
- package/src/platforms/drivers/claude/session-history-adapter.js +313 -0
- package/src/{server/services/sessions.js → platforms/drivers/claude/sessions-implementation.js} +145 -35
- package/src/platforms/drivers/claude/sessions.js +76 -0
- package/src/{server/services/claude-statistics-service.js → platforms/drivers/claude/statistics-implementation.js} +2 -2
- package/src/platforms/drivers/claude/statistics.js +16 -0
- package/src/{server/services/claude-wire.js → platforms/drivers/claude/wire.js} +1 -1
- package/src/platforms/drivers/codex/api-operations.js +9 -0
- package/src/{server/services/codex-channels.js → platforms/drivers/codex/channels-implementation.js} +149 -14
- package/src/platforms/drivers/codex/channels.js +45 -0
- package/src/{server/services/codex-config.js → platforms/drivers/codex/config.js} +12 -2
- package/src/{server/services/codex-env-manager.js → platforms/drivers/codex/env-manager.js} +1 -1
- package/src/platforms/drivers/codex/mcp.js +9 -0
- package/src/{server/services/codex-settings-manager.js → platforms/drivers/codex/native-config-implementation.js} +15 -6
- package/src/platforms/drivers/codex/native-config.js +47 -0
- package/src/platforms/drivers/codex/native-logs.js +166 -0
- package/src/platforms/drivers/codex/project-config.js +21 -0
- package/src/platforms/drivers/codex/projects.js +26 -0
- package/src/platforms/drivers/codex/prompts.js +9 -0
- package/src/{server/codex-proxy-server.js → platforms/drivers/codex/proxy-implementation.js} +30 -233
- package/src/platforms/drivers/codex/proxy.js +16 -0
- package/src/platforms/drivers/codex/resource-sync.js +9 -0
- package/src/{server/services/session-history-adapters/codex.js → platforms/drivers/codex/session-history-adapter.js} +37 -23
- package/src/{server/services/codex-sessions.js → platforms/drivers/codex/sessions-implementation.js} +81 -75
- package/src/platforms/drivers/codex/sessions.js +36 -0
- package/src/{server/services/codex-statistics-service.js → platforms/drivers/codex/statistics-implementation.js} +2 -2
- package/src/platforms/drivers/codex/statistics.js +16 -0
- package/src/platforms/drivers/gemini/api-operations.js +9 -0
- package/src/{server/services/gemini-channels.js → platforms/drivers/gemini/channels-implementation.js} +202 -37
- package/src/platforms/drivers/gemini/channels.js +39 -0
- package/src/{server/services/gemini-config.js → platforms/drivers/gemini/config.js} +12 -2
- package/src/platforms/drivers/gemini/mcp.js +9 -0
- package/src/{server/services/gemini-settings-manager.js → platforms/drivers/gemini/native-config-implementation.js} +14 -5
- package/src/platforms/drivers/gemini/native-config.js +39 -0
- package/src/platforms/drivers/gemini/native-logs.js +180 -0
- package/src/platforms/drivers/gemini/project-config.js +21 -0
- package/src/platforms/drivers/gemini/projects.js +26 -0
- package/src/platforms/drivers/gemini/prompts.js +9 -0
- package/src/{server/gemini-proxy-server.js → platforms/drivers/gemini/proxy-implementation.js} +36 -267
- package/src/platforms/drivers/gemini/proxy.js +16 -0
- package/src/platforms/drivers/gemini/resource-sync.js +9 -0
- package/src/{server/services/session-history-adapters/gemini.js → platforms/drivers/gemini/session-history-adapter.js} +70 -18
- package/src/{server/services/gemini-sessions.js → platforms/drivers/gemini/sessions-implementation.js} +53 -37
- package/src/platforms/drivers/gemini/sessions.js +47 -0
- package/src/{server/services/gemini-statistics-service.js → platforms/drivers/gemini/statistics-implementation.js} +2 -2
- package/src/platforms/drivers/gemini/statistics.js +16 -0
- package/src/{server/services/gemini-wire.js → platforms/drivers/gemini/wire.js} +1 -1
- package/src/platforms/drivers/generic-filesystem.js +166 -0
- package/src/platforms/drivers/generic-jsonl.js +141 -0
- package/src/platforms/drivers/generic-mcp.js +44 -0
- package/src/platforms/drivers/generic-openai-compatible.js +45 -0
- package/src/platforms/drivers/generic-prompt.js +41 -0
- package/src/platforms/drivers/legacy.js +677 -0
- package/src/platforms/drivers/native-config-snapshot.js +125 -0
- package/src/platforms/drivers/native-log-utils.js +1292 -0
- package/src/platforms/drivers/notification-hooks.js +578 -0
- package/src/{server/api/omp-config.js → platforms/drivers/omp/api-operations.js} +52 -64
- package/src/{server/services/omp-auth-provider-worker.js → platforms/drivers/omp/auth-provider-worker.js} +1 -1
- package/src/{server/services/omp-auth-providers.js → platforms/drivers/omp/auth-providers.js} +1 -1
- package/src/{server/services/omp-channels.js → platforms/drivers/omp/channels-implementation.js} +166 -39
- package/src/platforms/drivers/omp/channels.js +58 -0
- package/src/{server/services/omp-config.js → platforms/drivers/omp/config.js} +53 -13
- package/src/{server/services/omp-gateway-routing.js → platforms/drivers/omp/gateway-routing.js} +14 -7
- package/src/{server/services/omp-gateway.js → platforms/drivers/omp/gateway.js} +4 -262
- package/src/platforms/drivers/omp/mcp.js +9 -0
- package/src/{server/services/omp-settings-manager.js → platforms/drivers/omp/native-config-implementation.js} +75 -10
- package/src/platforms/drivers/omp/native-config.js +46 -0
- package/src/platforms/drivers/omp/native-logs.js +52 -0
- package/src/{server/services/omp-native-plugin-adapter.js → platforms/drivers/omp/native-plugin-adapter.js} +91 -4
- package/src/platforms/drivers/omp/project-config.js +29 -0
- package/src/platforms/drivers/omp/projects.js +20 -0
- package/src/platforms/drivers/omp/prompts.js +9 -0
- package/src/{server/omp-proxy-server.js → platforms/drivers/omp/proxy-implementation.js} +6 -15
- package/src/platforms/drivers/omp/proxy.js +16 -0
- package/src/platforms/drivers/omp/resource-sync.js +9 -0
- package/src/{server/services/session-history-adapters/omp.js → platforms/drivers/omp/session-history-adapter.js} +42 -18
- package/src/{server/services/omp-sessions.js → platforms/drivers/omp/sessions-implementation.js} +130 -94
- package/src/platforms/drivers/omp/sessions.js +36 -0
- package/src/{server/services/omp-skill-discovery.js → platforms/drivers/omp/skill-discovery.js} +159 -32
- package/src/{server/services/omp-skill-settings-service.js → platforms/drivers/omp/skill-settings.js} +1 -1
- package/src/{server/services/omp-statistics-service.js → platforms/drivers/omp/statistics-implementation.js} +3 -3
- package/src/platforms/drivers/omp/statistics.js +16 -0
- package/src/platforms/drivers/opencode/api-operations.js +9 -0
- package/src/{server/services/opencode-channels.js → platforms/drivers/opencode/channels-implementation.js} +28 -9
- package/src/platforms/drivers/opencode/channels.js +40 -0
- package/src/platforms/drivers/opencode/conversion.js +41 -0
- package/src/{server/services/opencode-gateway-converter.js → platforms/drivers/opencode/gateway-converter.js} +1 -1
- package/src/platforms/drivers/opencode/mcp.js +9 -0
- package/src/{server/services/opencode-settings-manager.js → platforms/drivers/opencode/native-config-implementation.js} +17 -6
- package/src/platforms/drivers/opencode/native-config.js +26 -0
- package/src/platforms/drivers/opencode/native-logs.js +133 -0
- package/src/platforms/drivers/opencode/project-config.js +98 -0
- package/src/platforms/drivers/opencode/projects.js +20 -0
- package/src/platforms/drivers/opencode/prompts.js +9 -0
- package/src/{server/opencode-proxy-server.js → platforms/drivers/opencode/proxy-implementation.js} +27 -362
- package/src/platforms/drivers/opencode/proxy.js +16 -0
- package/src/platforms/drivers/opencode/resource-sync.js +9 -0
- package/src/{server/services/opencode-sessions.js → platforms/drivers/opencode/sessions-implementation.js} +211 -64
- package/src/platforms/drivers/opencode/sessions.js +48 -0
- package/src/{server/services/opencode-statistics-service.js → platforms/drivers/opencode/statistics-implementation.js} +2 -2
- package/src/platforms/drivers/opencode/statistics.js +16 -0
- package/src/platforms/drivers/secure-file-driver.js +272 -0
- package/src/platforms/drivers/unsupported.js +7 -0
- package/src/platforms/manifest-schema.js +398 -0
- package/src/platforms/manifests/claude.json +489 -0
- package/src/platforms/manifests/codex.json +448 -0
- package/src/platforms/manifests/gemini.json +447 -0
- package/src/platforms/manifests/omp.json +496 -0
- package/src/platforms/manifests/opencode.json +422 -0
- package/src/{server/services → platforms}/native-oauth-adapters.js +87 -417
- package/src/{server/services → platforms}/notification-hooks.js +140 -1023
- package/src/platforms/oauth-credentials-service.js +506 -0
- package/src/platforms/path-resolver.js +179 -0
- package/src/platforms/platform-path-context.js +305 -0
- package/src/platforms/registry.js +248 -0
- package/src/platforms/remote-notification-providers.js +678 -0
- package/src/platforms/runtime.js +259 -0
- package/src/{server/services/session-history-adapters/index.js → platforms/session-history-adapters.js} +8 -1
- package/src/plugins/plugin-loader.js +2 -2
- package/src/plugins/plugin-manager.js +2 -2
- package/src/plugins/registry.js +27 -10
- package/src/reset-config.js +21 -5
- package/src/server/api/agents.js +46 -72
- package/src/server/api/commands.js +16 -6
- package/src/server/api/config-registry.js +206 -59
- package/src/server/api/config-sync.js +3 -2
- package/src/server/api/config-templates.js +20 -4
- package/src/server/api/config.js +111 -102
- package/src/server/api/convert.js +54 -178
- package/src/server/api/dashboard.js +168 -102
- package/src/server/api/hooks.js +1 -1
- package/src/server/api/mcp.js +224 -84
- package/src/server/api/platform-api-config.js +102 -0
- package/src/server/api/platform-route-factory.js +215 -0
- package/src/server/api/platform-routes.js +26 -0
- package/src/server/api/platforms.js +110 -0
- package/src/server/api/plugins.js +57 -57
- package/src/server/api/project-config.js +168 -0
- package/src/server/api/prompts.js +58 -45
- package/src/server/api/settings.js +55 -8
- package/src/server/api/skills.js +262 -133
- package/src/server/api/ui-config.js +2 -2
- package/src/server/api/validation-errors.js +18 -4
- package/src/server/api/workspaces.js +47 -12
- package/src/server/index.js +164 -213
- package/src/server/platform-context.js +81 -0
- package/src/server/services/channel-balance.js +11 -20
- package/src/server/services/channel-health.js +21 -6
- package/src/server/services/channel-scheduler.js +29 -39
- package/src/server/services/channel-sync-utils.js +23 -13
- package/src/server/services/config-export-service.js +350 -319
- package/src/server/services/config-registry-service.js +280 -54
- package/src/server/services/config-sync-manager.js +172 -944
- package/src/server/services/config-templates-service.js +419 -353
- package/src/server/services/control-manifest-store.js +206 -0
- package/src/server/services/dashboard-snapshot-worker.js +656 -100
- package/src/server/services/effective-control-service.js +557 -0
- package/src/server/services/local-resource-index.js +314 -0
- package/src/server/services/mcp-service.js +607 -977
- package/src/server/services/model-detector.js +15 -10
- package/src/server/services/native-log-channel-resolver.js +214 -0
- package/src/server/services/native-log-observer.js +291 -0
- package/src/server/services/network-access.js +1 -1
- package/src/server/services/platform-catalog.js +82 -0
- package/src/server/services/platform-resolution.js +32 -29
- package/src/server/services/plugins-service.js +376 -122
- package/src/server/services/project-config-service.js +550 -0
- package/src/server/services/project-path-validation.js +31 -0
- package/src/server/services/prompts-service.js +267 -233
- package/src/server/services/proxy-runtime.js +7 -4
- package/src/server/services/remote-credential-cache.js +43 -0
- package/src/server/services/repo-scanner-base.js +182 -75
- package/src/server/services/request-logger.js +7 -4
- package/src/server/services/session-history-index.js +780 -213
- package/src/server/services/session-history-worker.js +228 -33
- package/src/server/services/session-snapshot-worker.js +124 -34
- package/src/server/services/session-snapshots.js +4 -2
- package/src/server/services/skill-artifact-store.js +355 -0
- package/src/server/services/skill-format-adapters.js +65 -0
- package/src/server/services/skill-projection-service.js +323 -0
- package/src/server/services/skill-refresh-task-service.js +275 -0
- package/src/server/services/skill-service.js +2020 -677
- package/src/server/services/snapshot-cache.js +30 -8
- package/src/server/services/speed-test.js +8 -8
- package/src/server/services/sqlite-connection.js +19 -3
- package/src/server/services/statistics-service.js +240 -57
- package/src/server/services/ui-config.js +107 -76
- package/src/server/services/usage-log-utils.js +98 -0
- package/src/server/services/web-build.js +1 -1
- package/src/server/services/workspace-service.js +59 -29
- package/src/server/utils/pricing.js +10 -19
- package/src/server/websocket-server.js +153 -28
- package/src/{server/services/base → shared}/base-channel-service.js +156 -28
- package/src/shared/capability-driver.js +104 -0
- package/src/shared/channel-auth-policy.js +27 -0
- package/src/shared/channel-driver.js +161 -0
- package/src/shared/driver-factories/api.js +180 -0
- package/src/shared/driver-factories/mcp.js +244 -0
- package/src/shared/driver-factories/projects.js +121 -0
- package/src/shared/driver-factories/prompts.js +58 -0
- package/src/shared/driver-factories/resource-sync.js +375 -0
- package/src/shared/driver-result.js +78 -0
- package/src/shared/mcp-format.js +189 -0
- package/src/shared/platforms.js +104 -167
- package/src/shared/project-config.js +512 -0
- package/src/shared/proxy-driver.js +57 -0
- package/src/{server/services/base → shared}/response-usage-parser.js +8 -4
- package/src/ui/menu.js +91 -67
- package/src/utils/port-helper.js +35 -19
- package/src/utils/session.js +1 -1
- package/dist/web/assets/Analytics-COEUXmd4.js +0 -25
- package/dist/web/assets/Analytics-RNn1BUbG.css +0 -1
- package/dist/web/assets/ConfigTemplates-DLLkS53H.js +0 -1
- package/dist/web/assets/Home-DCEyLzwX.js +0 -1
- package/dist/web/assets/Home-DYVy4cGZ.css +0 -1
- package/dist/web/assets/PluginManager-Oe1ZHoMO.js +0 -1
- package/dist/web/assets/ProjectList-CWjuAFof.js +0 -1
- package/dist/web/assets/ProjectList-VeMNXfgO.css +0 -1
- package/dist/web/assets/SessionList-C4chCPxK.css +0 -1
- package/dist/web/assets/SessionList-v3ehGg9U.js +0 -1
- package/dist/web/assets/SkillManager-BsX8Qpj0.css +0 -1
- package/dist/web/assets/SkillManager-DUF_qP5S.js +0 -1
- package/dist/web/assets/WorkspaceManager-CjAWBosO.js +0 -1
- package/dist/web/assets/icons-nGdSKQn2.js +0 -1
- package/dist/web/assets/index-D5hyHPj7.js +0 -2
- package/dist/web/assets/index-bJRG3RXH.css +0 -1
- package/dist/web/assets/markdown-BNJfEgCA.js +0 -1
- package/dist/web/assets/markdown-BfC0goYb.css +0 -10
- package/dist/web/assets/naive-ui-BALVpMgd.js +0 -1
- package/dist/web/assets/vendors-DQA4Hw9v.js +0 -7
- package/dist/web/assets/vue-vendor-B5RqjEjQ.js +0 -45
- package/src/config/model-pricing.js +0 -35
- package/src/server/api/channels.js +0 -439
- package/src/server/api/claude-hooks.js +0 -56
- package/src/server/api/claude-statistics.js +0 -57
- package/src/server/api/codex-channels.js +0 -491
- package/src/server/api/codex-projects.js +0 -117
- package/src/server/api/codex-proxy.js +0 -293
- package/src/server/api/codex-sessions.js +0 -652
- package/src/server/api/codex-statistics.js +0 -57
- package/src/server/api/gemini-channels.js +0 -392
- package/src/server/api/gemini-projects.js +0 -103
- package/src/server/api/gemini-proxy.js +0 -231
- package/src/server/api/gemini-sessions.js +0 -550
- package/src/server/api/gemini-statistics.js +0 -57
- package/src/server/api/health-check.js +0 -31
- package/src/server/api/oauth-credentials.js +0 -198
- package/src/server/api/omp-channels.js +0 -575
- package/src/server/api/omp-projects.js +0 -88
- package/src/server/api/omp-proxy.js +0 -125
- package/src/server/api/omp-sessions.js +0 -386
- package/src/server/api/omp-statistics.js +0 -40
- package/src/server/api/opencode-channels.js +0 -471
- package/src/server/api/opencode-projects.js +0 -110
- package/src/server/api/opencode-proxy.js +0 -354
- package/src/server/api/opencode-sessions.js +0 -466
- package/src/server/api/opencode-statistics.js +0 -57
- package/src/server/api/projects.js +0 -120
- package/src/server/api/proxy.js +0 -330
- package/src/server/api/sessions.js +0 -810
- package/src/server/services/oauth-credentials-service.js +0 -945
- package/src/server/services/omp-session-log-observer.js +0 -138
- package/src/server/services/opencode-gateway-adapters.js +0 -43
- package/src/server/services/project-snapshots.js +0 -59
- package/src/server/services/proxy-log-helper.js +0 -298
- package/src/server/services/session-cache.js +0 -127
- package/src/server/services/session-converter.js +0 -582
- package/src/server/services/session-history-adapters/claude.js +0 -235
- /package/src/{server/services/claude-token-recovery.js → platforms/drivers/claude/token-recovery.js} +0 -0
- /package/src/{server/services/codex-parser.js → platforms/drivers/codex/parser.js} +0 -0
- /package/src/{server/services/codex-speed-test-template.json → platforms/drivers/codex/speed-test-template.json} +0 -0
- /package/src/{server/services/codex-wire.js → platforms/drivers/codex/wire.js} +0 -0
- /package/src/{server/services/omp-auth-gateway-client.js → platforms/drivers/omp/auth-gateway-client.js} +0 -0
- /package/src/{server/services/opencode-normalization.js → platforms/drivers/opencode/normalization.js} +0 -0
- /package/src/{server/services → shared}/config-artifact-paths.js +0 -0
- /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
|
|
14
|
-
const {
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
|
716
|
+
const persisted = readJsonFile(MCP_SERVERS_FILE, {});
|
|
717
|
+
const servers = {};
|
|
559
718
|
|
|
560
|
-
for (const server of Object.
|
|
561
|
-
|
|
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.
|
|
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[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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(
|
|
784
|
+
: normalizeServerApps({ claude: true });
|
|
612
785
|
} else {
|
|
613
|
-
server.apps = normalizeServerApps(server.apps, previousApps ||
|
|
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[
|
|
817
|
+
const server = servers[normalizedId];
|
|
633
818
|
|
|
634
819
|
if (!server) {
|
|
635
820
|
return false;
|
|
636
821
|
}
|
|
637
822
|
|
|
638
|
-
|
|
639
|
-
|
|
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[
|
|
842
|
+
const server = servers[normalizedId];
|
|
653
843
|
|
|
654
844
|
if (!server) {
|
|
655
845
|
throw new Error(`MCP 服务器 "${serverId}" 不存在`);
|
|
656
846
|
}
|
|
657
847
|
|
|
658
|
-
|
|
659
|
-
|
|
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
|
|
862
|
+
server.apps = normalizeServerApps(server.apps);
|
|
863
|
+
server.apps[normalizedPlatform] = policy.enabled === true;
|
|
663
864
|
server.updatedAt = Date.now();
|
|
664
865
|
|
|
665
|
-
//
|
|
666
|
-
if (
|
|
667
|
-
await syncServerToPlatform(server,
|
|
866
|
+
// Sync to the selected platform.
|
|
867
|
+
if (server.apps[normalizedPlatform]) {
|
|
868
|
+
await syncServerToPlatform(server, normalizedPlatform);
|
|
668
869
|
} else {
|
|
669
|
-
await removeServerFromPlatform(
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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
|
-
|
|
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
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
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
|
-
|
|
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
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
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
|
|
977
|
-
const
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
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
|
-
|
|
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
|
|
1059
|
-
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
|
|
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
|
|
1066
|
-
const config =
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
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
|
-
|
|
1144
|
-
result.command = spec.command;
|
|
1145
|
-
} else {
|
|
1146
|
-
result.command = '';
|
|
1081
|
+
continue;
|
|
1147
1082
|
}
|
|
1148
1083
|
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1108
|
+
const { platform: normalizedPlatform, driver } = requireMcpCapability(platform);
|
|
1219
1109
|
const servers = getAllServers();
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
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
|
-
*
|
|
1129
|
+
* 提取纯净的服务器规范(移除元数据)
|
|
1282
1130
|
*/
|
|
1283
|
-
function
|
|
1284
|
-
|
|
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
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
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
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
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
|
-
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
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(
|
|
1564
|
+
const resultStr = JSON.stringify(resultObject);
|
|
1835
1565
|
if (resultStr.length > 10 * 1024) {
|
|
1836
1566
|
return {
|
|
1837
1567
|
result: {
|
|
1838
|
-
...
|
|
1568
|
+
...resultObject,
|
|
1839
1569
|
truncated: true
|
|
1840
1570
|
},
|
|
1841
1571
|
truncatedSize: resultStr.length,
|
|
1842
1572
|
duration,
|
|
1843
|
-
isError:
|
|
1573
|
+
isError: resultObject.isError || false
|
|
1844
1574
|
};
|
|
1845
1575
|
}
|
|
1846
1576
|
|
|
1847
1577
|
return {
|
|
1848
|
-
result,
|
|
1578
|
+
result: resultObject,
|
|
1849
1579
|
duration,
|
|
1850
|
-
isError:
|
|
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
|
-
|
|
1912
|
-
|
|
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 - 导出格式:
|
|
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
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
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] =
|
|
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,
|