hammoc 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +422 -403
- package/bin/hammoc.js +0 -6
- package/package.json +100 -93
- package/packages/client/dist/assets/agentExampleHighlight-BgwTm15v.js +1 -0
- package/packages/client/dist/assets/commandTokenHighlight-BljHwnrK.js +1 -0
- package/packages/client/dist/assets/index-CjyjnXB8.css +32 -0
- package/packages/client/dist/assets/index-D3LxqW3f.js +2 -0
- package/packages/client/dist/assets/index-NqJdhlek.js +1498 -0
- package/packages/client/dist/assets/snippetTokenHighlight-DWsaQXX0.js +1 -0
- package/packages/client/dist/index.html +2 -2
- package/packages/client/dist/sw.js +1 -1
- package/packages/server/dist/app.d.ts.map +1 -1
- package/packages/server/dist/app.js +24 -24
- package/packages/server/dist/app.js.map +1 -1
- package/packages/server/dist/controllers/boardController.d.ts.map +1 -1
- package/packages/server/dist/controllers/boardController.js +0 -5
- package/packages/server/dist/controllers/boardController.js.map +1 -1
- package/packages/server/dist/controllers/claudeMdController.d.ts +26 -0
- package/packages/server/dist/controllers/claudeMdController.d.ts.map +1 -0
- package/packages/server/dist/controllers/claudeMdController.js +158 -0
- package/packages/server/dist/controllers/claudeMdController.js.map +1 -0
- package/packages/server/dist/controllers/fileSystemController.d.ts +4 -0
- package/packages/server/dist/controllers/fileSystemController.d.ts.map +1 -1
- package/packages/server/dist/controllers/fileSystemController.js +20 -2
- package/packages/server/dist/controllers/fileSystemController.js.map +1 -1
- package/packages/server/dist/controllers/harnessAgentController.d.ts +28 -0
- package/packages/server/dist/controllers/harnessAgentController.d.ts.map +1 -0
- package/packages/server/dist/controllers/harnessAgentController.js +339 -0
- package/packages/server/dist/controllers/harnessAgentController.js.map +1 -0
- package/packages/server/dist/controllers/harnessCommandController.d.ts +28 -0
- package/packages/server/dist/controllers/harnessCommandController.d.ts.map +1 -0
- package/packages/server/dist/controllers/harnessCommandController.js +382 -0
- package/packages/server/dist/controllers/harnessCommandController.js.map +1 -0
- package/packages/server/dist/controllers/harnessController.d.ts +21 -0
- package/packages/server/dist/controllers/harnessController.d.ts.map +1 -0
- package/packages/server/dist/controllers/harnessController.js +176 -0
- package/packages/server/dist/controllers/harnessController.js.map +1 -0
- package/packages/server/dist/controllers/harnessHookController.d.ts +32 -0
- package/packages/server/dist/controllers/harnessHookController.d.ts.map +1 -0
- package/packages/server/dist/controllers/harnessHookController.js +363 -0
- package/packages/server/dist/controllers/harnessHookController.js.map +1 -0
- package/packages/server/dist/controllers/harnessLintController.d.ts +18 -0
- package/packages/server/dist/controllers/harnessLintController.d.ts.map +1 -0
- package/packages/server/dist/controllers/harnessLintController.js +72 -0
- package/packages/server/dist/controllers/harnessLintController.js.map +1 -0
- package/packages/server/dist/controllers/harnessMcpController.d.ts +28 -0
- package/packages/server/dist/controllers/harnessMcpController.d.ts.map +1 -0
- package/packages/server/dist/controllers/harnessMcpController.js +310 -0
- package/packages/server/dist/controllers/harnessMcpController.js.map +1 -0
- package/packages/server/dist/controllers/harnessPluginController.d.ts +17 -0
- package/packages/server/dist/controllers/harnessPluginController.d.ts.map +1 -0
- package/packages/server/dist/controllers/harnessPluginController.js +115 -0
- package/packages/server/dist/controllers/harnessPluginController.js.map +1 -0
- package/packages/server/dist/controllers/harnessShareScopeController.d.ts +15 -0
- package/packages/server/dist/controllers/harnessShareScopeController.d.ts.map +1 -0
- package/packages/server/dist/controllers/harnessShareScopeController.js +73 -0
- package/packages/server/dist/controllers/harnessShareScopeController.js.map +1 -0
- package/packages/server/dist/controllers/harnessSkillController.d.ts +32 -0
- package/packages/server/dist/controllers/harnessSkillController.d.ts.map +1 -0
- package/packages/server/dist/controllers/harnessSkillController.js +453 -0
- package/packages/server/dist/controllers/harnessSkillController.js.map +1 -0
- package/packages/server/dist/controllers/projectController.d.ts.map +1 -1
- package/packages/server/dist/controllers/projectController.js +11 -0
- package/packages/server/dist/controllers/projectController.js.map +1 -1
- package/packages/server/dist/controllers/serverController.d.ts.map +1 -1
- package/packages/server/dist/controllers/serverController.js +84 -49
- package/packages/server/dist/controllers/serverController.js.map +1 -1
- package/packages/server/dist/controllers/snippetController.d.ts +35 -0
- package/packages/server/dist/controllers/snippetController.d.ts.map +1 -0
- package/packages/server/dist/controllers/snippetController.js +294 -0
- package/packages/server/dist/controllers/snippetController.js.map +1 -0
- package/packages/server/dist/handlers/websocket.d.ts +16 -0
- package/packages/server/dist/handlers/websocket.d.ts.map +1 -1
- package/packages/server/dist/handlers/websocket.js +221 -8
- package/packages/server/dist/handlers/websocket.js.map +1 -1
- package/packages/server/dist/index.js +66 -0
- package/packages/server/dist/index.js.map +1 -1
- package/packages/server/dist/locales/en/server.json +41 -6
- package/packages/server/dist/locales/es/server.json +3 -5
- package/packages/server/dist/locales/ja/server.json +3 -5
- package/packages/server/dist/locales/ko/server.json +4 -6
- package/packages/server/dist/locales/pt/server.json +3 -5
- package/packages/server/dist/locales/zh-CN/server.json +3 -5
- package/packages/server/dist/routes/account.d.ts +7 -0
- package/packages/server/dist/routes/account.d.ts.map +1 -0
- package/packages/server/dist/routes/account.js +35 -0
- package/packages/server/dist/routes/account.js.map +1 -0
- package/packages/server/dist/routes/debug.d.ts +1 -1
- package/packages/server/dist/routes/debug.d.ts.map +1 -1
- package/packages/server/dist/routes/debug.js +60 -1
- package/packages/server/dist/routes/debug.js.map +1 -1
- package/packages/server/dist/routes/harness.d.ts +8 -0
- package/packages/server/dist/routes/harness.d.ts.map +1 -0
- package/packages/server/dist/routes/harness.js +92 -0
- package/packages/server/dist/routes/harness.js.map +1 -0
- package/packages/server/dist/routes/preferences.d.ts.map +1 -1
- package/packages/server/dist/routes/preferences.js +11 -2
- package/packages/server/dist/routes/preferences.js.map +1 -1
- package/packages/server/dist/routes/projects.d.ts.map +1 -1
- package/packages/server/dist/routes/projects.js +5 -60
- package/packages/server/dist/routes/projects.js.map +1 -1
- package/packages/server/dist/routes/snippets.d.ts +14 -0
- package/packages/server/dist/routes/snippets.d.ts.map +1 -0
- package/packages/server/dist/routes/snippets.js +27 -0
- package/packages/server/dist/routes/snippets.js.map +1 -0
- package/packages/server/dist/services/accountInfoService.d.ts +38 -0
- package/packages/server/dist/services/accountInfoService.d.ts.map +1 -0
- package/packages/server/dist/services/accountInfoService.js +118 -0
- package/packages/server/dist/services/accountInfoService.js.map +1 -0
- package/packages/server/dist/services/bmadStatusService.d.ts +6 -2
- package/packages/server/dist/services/bmadStatusService.d.ts.map +1 -1
- package/packages/server/dist/services/bmadStatusService.js +88 -32
- package/packages/server/dist/services/bmadStatusService.js.map +1 -1
- package/packages/server/dist/services/chatService.d.ts +3 -0
- package/packages/server/dist/services/chatService.d.ts.map +1 -1
- package/packages/server/dist/services/chatService.js +36 -8
- package/packages/server/dist/services/chatService.js.map +1 -1
- package/packages/server/dist/services/claudeMdService.d.ts +48 -0
- package/packages/server/dist/services/claudeMdService.d.ts.map +1 -0
- package/packages/server/dist/services/claudeMdService.js +240 -0
- package/packages/server/dist/services/claudeMdService.js.map +1 -0
- package/packages/server/dist/services/commandService.d.ts +10 -0
- package/packages/server/dist/services/commandService.d.ts.map +1 -1
- package/packages/server/dist/services/commandService.js +129 -4
- package/packages/server/dist/services/commandService.js.map +1 -1
- package/packages/server/dist/services/fileSystemService.d.ts +7 -1
- package/packages/server/dist/services/fileSystemService.d.ts.map +1 -1
- package/packages/server/dist/services/fileSystemService.js +67 -8
- package/packages/server/dist/services/fileSystemService.js.map +1 -1
- package/packages/server/dist/services/fileWatcherService.d.ts +59 -0
- package/packages/server/dist/services/fileWatcherService.d.ts.map +1 -0
- package/packages/server/dist/services/fileWatcherService.js +329 -0
- package/packages/server/dist/services/fileWatcherService.js.map +1 -0
- package/packages/server/dist/services/gitService.d.ts.map +1 -1
- package/packages/server/dist/services/gitService.js +67 -7
- package/packages/server/dist/services/gitService.js.map +1 -1
- package/packages/server/dist/services/harnessAgentService.d.ts +79 -0
- package/packages/server/dist/services/harnessAgentService.d.ts.map +1 -0
- package/packages/server/dist/services/harnessAgentService.js +933 -0
- package/packages/server/dist/services/harnessAgentService.js.map +1 -0
- package/packages/server/dist/services/harnessCommandService.d.ts +60 -0
- package/packages/server/dist/services/harnessCommandService.d.ts.map +1 -0
- package/packages/server/dist/services/harnessCommandService.js +853 -0
- package/packages/server/dist/services/harnessCommandService.js.map +1 -0
- package/packages/server/dist/services/harnessHookService.d.ts +55 -0
- package/packages/server/dist/services/harnessHookService.d.ts.map +1 -0
- package/packages/server/dist/services/harnessHookService.js +1060 -0
- package/packages/server/dist/services/harnessHookService.js.map +1 -0
- package/packages/server/dist/services/harnessLintService.d.ts +49 -0
- package/packages/server/dist/services/harnessLintService.d.ts.map +1 -0
- package/packages/server/dist/services/harnessLintService.js +628 -0
- package/packages/server/dist/services/harnessLintService.js.map +1 -0
- package/packages/server/dist/services/harnessMcpService.d.ts +77 -0
- package/packages/server/dist/services/harnessMcpService.d.ts.map +1 -0
- package/packages/server/dist/services/harnessMcpService.js +814 -0
- package/packages/server/dist/services/harnessMcpService.js.map +1 -0
- package/packages/server/dist/services/harnessPluginService.d.ts +66 -0
- package/packages/server/dist/services/harnessPluginService.d.ts.map +1 -0
- package/packages/server/dist/services/harnessPluginService.js +559 -0
- package/packages/server/dist/services/harnessPluginService.js.map +1 -0
- package/packages/server/dist/services/harnessService.d.ts +40 -0
- package/packages/server/dist/services/harnessService.d.ts.map +1 -0
- package/packages/server/dist/services/harnessService.js +222 -0
- package/packages/server/dist/services/harnessService.js.map +1 -0
- package/packages/server/dist/services/harnessShareScopeService.d.ts +31 -0
- package/packages/server/dist/services/harnessShareScopeService.d.ts.map +1 -0
- package/packages/server/dist/services/harnessShareScopeService.js +93 -0
- package/packages/server/dist/services/harnessShareScopeService.js.map +1 -0
- package/packages/server/dist/services/harnessSkillService.d.ts +70 -0
- package/packages/server/dist/services/harnessSkillService.d.ts.map +1 -0
- package/packages/server/dist/services/harnessSkillService.js +636 -0
- package/packages/server/dist/services/harnessSkillService.js.map +1 -0
- package/packages/server/dist/services/historyParser.d.ts +4 -14
- package/packages/server/dist/services/historyParser.d.ts.map +1 -1
- package/packages/server/dist/services/historyParser.js +60 -5
- package/packages/server/dist/services/historyParser.js.map +1 -1
- package/packages/server/dist/services/issueService.d.ts.map +1 -1
- package/packages/server/dist/services/issueService.js +10 -2
- package/packages/server/dist/services/issueService.js.map +1 -1
- package/packages/server/dist/services/manualSyncService.d.ts +19 -0
- package/packages/server/dist/services/manualSyncService.d.ts.map +1 -0
- package/packages/server/dist/services/manualSyncService.js +110 -0
- package/packages/server/dist/services/manualSyncService.js.map +1 -0
- package/packages/server/dist/services/notificationService.d.ts.map +1 -1
- package/packages/server/dist/services/notificationService.js +34 -9
- package/packages/server/dist/services/notificationService.js.map +1 -1
- package/packages/server/dist/services/preferencesService.d.ts.map +1 -1
- package/packages/server/dist/services/preferencesService.js +8 -1
- package/packages/server/dist/services/preferencesService.js.map +1 -1
- package/packages/server/dist/services/projectService.d.ts +5 -0
- package/packages/server/dist/services/projectService.d.ts.map +1 -1
- package/packages/server/dist/services/projectService.js +42 -2
- package/packages/server/dist/services/projectService.js.map +1 -1
- package/packages/server/dist/services/ptyService.d.ts +1 -0
- package/packages/server/dist/services/ptyService.d.ts.map +1 -1
- package/packages/server/dist/services/ptyService.js +36 -5
- package/packages/server/dist/services/ptyService.js.map +1 -1
- package/packages/server/dist/services/queueService.d.ts.map +1 -1
- package/packages/server/dist/services/queueService.js +83 -14
- package/packages/server/dist/services/queueService.js.map +1 -1
- package/packages/server/dist/services/sessionBufferManager.d.ts.map +1 -1
- package/packages/server/dist/services/sessionBufferManager.js +26 -0
- package/packages/server/dist/services/sessionBufferManager.js.map +1 -1
- package/packages/server/dist/services/sessionService.d.ts +4 -3
- package/packages/server/dist/services/sessionService.d.ts.map +1 -1
- package/packages/server/dist/services/sessionService.js +5 -4
- package/packages/server/dist/services/sessionService.js.map +1 -1
- package/packages/server/dist/services/snippetService.d.ts +54 -0
- package/packages/server/dist/services/snippetService.d.ts.map +1 -0
- package/packages/server/dist/services/snippetService.js +371 -0
- package/packages/server/dist/services/snippetService.js.map +1 -0
- package/packages/server/dist/services/utils/applyYamlFrontmatterPatch.d.ts +46 -0
- package/packages/server/dist/services/utils/applyYamlFrontmatterPatch.d.ts.map +1 -0
- package/packages/server/dist/services/utils/applyYamlFrontmatterPatch.js +125 -0
- package/packages/server/dist/services/utils/applyYamlFrontmatterPatch.js.map +1 -0
- package/packages/server/dist/services/webPushService.d.ts.map +1 -1
- package/packages/server/dist/services/webPushService.js +8 -1
- package/packages/server/dist/services/webPushService.js.map +1 -1
- package/packages/server/dist/snippets/split-commit +9 -0
- package/packages/server/dist/utils/applySecretsPolicy.d.ts +53 -0
- package/packages/server/dist/utils/applySecretsPolicy.d.ts.map +1 -0
- package/packages/server/dist/utils/applySecretsPolicy.js +204 -0
- package/packages/server/dist/utils/applySecretsPolicy.js.map +1 -0
- package/packages/server/dist/utils/assertNoSecretOnShared.d.ts +40 -0
- package/packages/server/dist/utils/assertNoSecretOnShared.d.ts.map +1 -0
- package/packages/server/dist/utils/assertNoSecretOnShared.js +47 -0
- package/packages/server/dist/utils/assertNoSecretOnShared.js.map +1 -0
- package/packages/server/dist/utils/effortUtils.d.ts +21 -0
- package/packages/server/dist/utils/effortUtils.d.ts.map +1 -0
- package/packages/server/dist/utils/effortUtils.js +36 -0
- package/packages/server/dist/utils/effortUtils.js.map +1 -0
- package/packages/server/dist/utils/gitignoreFilter.d.ts +23 -0
- package/packages/server/dist/utils/gitignoreFilter.d.ts.map +1 -0
- package/packages/server/dist/utils/gitignoreFilter.js +42 -0
- package/packages/server/dist/utils/gitignoreFilter.js.map +1 -0
- package/packages/server/dist/utils/harnessBundleSchema.d.ts +105 -0
- package/packages/server/dist/utils/harnessBundleSchema.d.ts.map +1 -0
- package/packages/server/dist/utils/harnessBundleSchema.js +79 -0
- package/packages/server/dist/utils/harnessBundleSchema.js.map +1 -0
- package/packages/server/dist/utils/harnessPaths.d.ts +34 -0
- package/packages/server/dist/utils/harnessPaths.d.ts.map +1 -0
- package/packages/server/dist/utils/harnessPaths.js +124 -0
- package/packages/server/dist/utils/harnessPaths.js.map +1 -0
- package/packages/server/dist/utils/pathUtils.d.ts +3 -2
- package/packages/server/dist/utils/pathUtils.d.ts.map +1 -1
- package/packages/server/dist/utils/pathUtils.js +26 -2
- package/packages/server/dist/utils/pathUtils.js.map +1 -1
- package/packages/server/dist/utils/secretHeuristic.d.ts +72 -0
- package/packages/server/dist/utils/secretHeuristic.d.ts.map +1 -0
- package/packages/server/dist/utils/secretHeuristic.js +163 -0
- package/packages/server/dist/utils/secretHeuristic.js.map +1 -0
- package/packages/server/dist/utils/secretPlaceholderNamer.d.ts +41 -0
- package/packages/server/dist/utils/secretPlaceholderNamer.d.ts.map +1 -0
- package/packages/server/dist/utils/secretPlaceholderNamer.js +81 -0
- package/packages/server/dist/utils/secretPlaceholderNamer.js.map +1 -0
- package/packages/server/dist/utils/serverPathResolver.d.ts +29 -0
- package/packages/server/dist/utils/serverPathResolver.d.ts.map +1 -0
- package/packages/server/dist/utils/serverPathResolver.js +59 -0
- package/packages/server/dist/utils/serverPathResolver.js.map +1 -0
- package/packages/server/dist/utils/snippetPaths.d.ts +61 -0
- package/packages/server/dist/utils/snippetPaths.d.ts.map +1 -0
- package/packages/server/dist/utils/snippetPaths.js +123 -0
- package/packages/server/dist/utils/snippetPaths.js.map +1 -0
- package/packages/server/dist/utils/structuredEditor.d.ts +34 -0
- package/packages/server/dist/utils/structuredEditor.d.ts.map +1 -0
- package/packages/server/dist/utils/structuredEditor.js +111 -0
- package/packages/server/dist/utils/structuredEditor.js.map +1 -0
- package/packages/server/package.json +6 -2
- package/packages/server/resources/internals/INDEX.md +23 -0
- package/packages/server/resources/internals/harness-files.md +63 -0
- package/packages/server/resources/internals/image-storage.md +43 -0
- package/packages/server/resources/manual/01-getting-started.md +104 -0
- package/packages/server/resources/manual/02-chat.md +285 -0
- package/packages/server/resources/manual/03-sessions.md +48 -0
- package/packages/server/resources/manual/04-slash-commands-favorites.md +152 -0
- package/packages/server/resources/manual/05-projects.md +74 -0
- package/packages/server/resources/manual/06-file-explorer-editor.md +90 -0
- package/packages/server/resources/manual/07-git.md +94 -0
- package/packages/server/resources/manual/08-terminal.md +59 -0
- package/packages/server/resources/manual/09-queue-runner.md +262 -0
- package/packages/server/resources/manual/10-project-board.md +193 -0
- package/packages/server/resources/manual/11-bmad-method-integration.md +128 -0
- package/packages/server/resources/manual/12-harness-workbench.md +175 -0
- package/packages/server/resources/manual/13-settings.md +241 -0
- package/packages/server/resources/manual/14-keyboard-shortcuts.md +68 -0
- package/packages/server/resources/manual/15-environment-variables.md +28 -0
- package/packages/server/resources/manual/16-troubleshooting.md +110 -0
- package/packages/server/resources/manual/INDEX.md +60 -0
- package/packages/shared/dist/index.d.ts +3 -0
- package/packages/shared/dist/index.d.ts.map +1 -1
- package/packages/shared/dist/index.js +6 -0
- package/packages/shared/dist/index.js.map +1 -1
- package/packages/shared/dist/types/command.d.ts +3 -3
- package/packages/shared/dist/types/command.d.ts.map +1 -1
- package/packages/shared/dist/types/fileSystem.d.ts +19 -0
- package/packages/shared/dist/types/fileSystem.d.ts.map +1 -1
- package/packages/shared/dist/types/fileSystem.js +5 -0
- package/packages/shared/dist/types/fileSystem.js.map +1 -1
- package/packages/shared/dist/types/git.d.ts +6 -1
- package/packages/shared/dist/types/git.d.ts.map +1 -1
- package/packages/shared/dist/types/git.js.map +1 -1
- package/packages/shared/dist/types/harness.d.ts +1211 -0
- package/packages/shared/dist/types/harness.d.ts.map +1 -0
- package/packages/shared/dist/types/harness.js +107 -0
- package/packages/shared/dist/types/harness.js.map +1 -0
- package/packages/shared/dist/types/harnessBundle.d.ts +170 -0
- package/packages/shared/dist/types/harnessBundle.d.ts.map +1 -0
- package/packages/shared/dist/types/harnessBundle.js +18 -0
- package/packages/shared/dist/types/harnessBundle.js.map +1 -0
- package/packages/shared/dist/types/history.d.ts +7 -0
- package/packages/shared/dist/types/history.d.ts.map +1 -1
- package/packages/shared/dist/types/preferences.d.ts +4 -1
- package/packages/shared/dist/types/preferences.d.ts.map +1 -1
- package/packages/shared/dist/types/preferences.js +1 -0
- package/packages/shared/dist/types/preferences.js.map +1 -1
- package/packages/shared/dist/types/queue.d.ts +9 -0
- package/packages/shared/dist/types/queue.d.ts.map +1 -1
- package/packages/shared/dist/types/sdk.d.ts +42 -1
- package/packages/shared/dist/types/sdk.d.ts.map +1 -1
- package/packages/shared/dist/types/sdk.js +26 -2
- package/packages/shared/dist/types/sdk.js.map +1 -1
- package/packages/shared/dist/types/websocket.d.ts +24 -0
- package/packages/shared/dist/types/websocket.d.ts.map +1 -1
- package/packages/shared/dist/utils/markdownSections.d.ts +50 -0
- package/packages/shared/dist/utils/markdownSections.d.ts.map +1 -0
- package/packages/shared/dist/utils/markdownSections.js +111 -0
- package/packages/shared/dist/utils/markdownSections.js.map +1 -0
- package/packages/shared/dist/utils/queueParser.d.ts.map +1 -1
- package/packages/shared/dist/utils/queueParser.js +104 -0
- package/packages/shared/dist/utils/queueParser.js.map +1 -1
- package/scripts/build-manual-shards.mjs +100 -0
- package/scripts/mock-telegram.mjs +172 -0
- package/scripts/run-integration-test.mjs +362 -0
- package/packages/client/dist/assets/index-Bf0D9oVJ.css +0 -32
- package/packages/client/dist/assets/index-CRmzoqHy.js +0 -2
- package/packages/client/dist/assets/index-CszGQ29O.js +0 -1432
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Story 28.3: Harness MCP service.
|
|
3
|
+
*
|
|
4
|
+
* Combines three sources of MCP server definitions — the active project's
|
|
5
|
+
* `<projectRoot>/.mcp.json`, the global `~/.claude/.mcp.json` (or
|
|
6
|
+
* `~/.claude/settings.json.mcpServers` if Spike B confirms the alternative),
|
|
7
|
+
* and every installed plugin bundle (`<installPath>/.mcp.json` + the
|
|
8
|
+
* `mcpServers` field of `<installPath>/.claude-plugin/plugin.json`) — into a
|
|
9
|
+
* single card list used by the "Harness Workbench → MCP" panel.
|
|
10
|
+
*
|
|
11
|
+
* Spike A / B routing:
|
|
12
|
+
* - `DISABLE_STRATEGY` is wired into every read/list/update path; the
|
|
13
|
+
* post-impl default is `'backup'` (entry moves into mcp.disabled.json).
|
|
14
|
+
* If Spike A later confirms that Claude Code honors a per-server `enabled`
|
|
15
|
+
* flag on disk, switching the constant to `'flag'` and updating the
|
|
16
|
+
* `disable()` branch of `toggleEnabled()` is the only required change.
|
|
17
|
+
* - `getUserMcpFilePath()` returns `~/.claude/.mcp.json` by default. If
|
|
18
|
+
* Spike B confirms candidate 2 (settings.json.mcpServers), update this
|
|
19
|
+
* helper to return the settings.json path and bump the response
|
|
20
|
+
* `userFileKind` to `'settings.json'` — every other call site already
|
|
21
|
+
* consumes that field.
|
|
22
|
+
*
|
|
23
|
+
* Containment guards differ by scope:
|
|
24
|
+
* - `<projectRoot>/.mcp.json` is the SIBLING of `.claude/`, so the standard
|
|
25
|
+
* `harnessService` helpers (which clamp to `.claude/`) cannot reach it.
|
|
26
|
+
* Project-scope reads / writes go through dedicated `MainFileAccessor`
|
|
27
|
+
* helpers that fs-stat the file directly with a strict containment guard.
|
|
28
|
+
* - plugin reads use `path.resolve()` containment under each plugin's
|
|
29
|
+
* `installPath`, mirroring `harnessSkillService.enumeratePluginSkills`.
|
|
30
|
+
* - global `~/.claude/.mcp.json` (or `~/.claude/settings.json`) and the two
|
|
31
|
+
* `mcp.disabled.json` backup files all live inside one of the two
|
|
32
|
+
* `harnessService` scopes, so the round-trip there reuses the existing
|
|
33
|
+
* clamp.
|
|
34
|
+
*/
|
|
35
|
+
import path from 'path';
|
|
36
|
+
import fs from 'fs/promises';
|
|
37
|
+
import { HARNESS_ERRORS, } from '@hammoc/shared';
|
|
38
|
+
import { harnessService } from './harnessService.js';
|
|
39
|
+
import { projectService } from './projectService.js';
|
|
40
|
+
import { fileWatcherService } from './fileWatcherService.js';
|
|
41
|
+
import { getUserHarnessRoot } from '../utils/harnessPaths.js';
|
|
42
|
+
import { applyJsoncPatch } from '../utils/structuredEditor.js';
|
|
43
|
+
import { createLogger } from '../utils/logger.js';
|
|
44
|
+
import { detectSecretsInValue } from '../utils/secretHeuristic.js';
|
|
45
|
+
import { assertNoSecretOnShared } from '../utils/assertNoSecretOnShared.js';
|
|
46
|
+
const log = createLogger('harnessMcpService');
|
|
47
|
+
/** Spike A outcome — adjust to `'flag'` once Claude Code CLI honors disk `enabled` keys. */
|
|
48
|
+
const DISABLE_STRATEGY = 'backup';
|
|
49
|
+
/** Spike B outcome — adjust to `'settings.json'` when the global file lives there instead. */
|
|
50
|
+
const USER_FILE_KIND = 'mcp.json';
|
|
51
|
+
/** Priority used to pick a card's `activeScope` when multiple sources exist. */
|
|
52
|
+
const SCOPE_PRIORITY = {
|
|
53
|
+
project: 0,
|
|
54
|
+
user: 1,
|
|
55
|
+
plugin: 2,
|
|
56
|
+
};
|
|
57
|
+
function throwMapped(code, message, extras) {
|
|
58
|
+
const err = new Error(message);
|
|
59
|
+
err.code = code;
|
|
60
|
+
if (extras)
|
|
61
|
+
Object.assign(err, extras);
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
function isFileNotFound(err) {
|
|
65
|
+
const code = err?.code;
|
|
66
|
+
return code === 'ENOENT' || code === HARNESS_ERRORS.HARNESS_FILE_NOT_FOUND.code;
|
|
67
|
+
}
|
|
68
|
+
export function detectSecretsInConfig(value, basePath = []) {
|
|
69
|
+
const { matched, paths } = detectSecretsInValue(value, basePath);
|
|
70
|
+
return { matched, paths };
|
|
71
|
+
}
|
|
72
|
+
const PLUGIN_ROOT_TOKEN = '${CLAUDE_PLUGIN_ROOT}';
|
|
73
|
+
function containsPluginRootToken(value) {
|
|
74
|
+
if (typeof value === 'string')
|
|
75
|
+
return value.includes(PLUGIN_ROOT_TOKEN);
|
|
76
|
+
if (Array.isArray(value))
|
|
77
|
+
return value.some((v) => containsPluginRootToken(v));
|
|
78
|
+
if (value && typeof value === 'object') {
|
|
79
|
+
return Object.values(value).some((v) => containsPluginRootToken(v));
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
/** Default-resolve `type` for a config that omitted the key (per official MCP schema). */
|
|
84
|
+
function resolveType(config) {
|
|
85
|
+
return config.type ?? 'stdio';
|
|
86
|
+
}
|
|
87
|
+
async function getProjectMcpFilePath(projectSlug) {
|
|
88
|
+
const projectRoot = await projectService.resolveOriginalPath(projectSlug);
|
|
89
|
+
return path.join(projectRoot, '.mcp.json');
|
|
90
|
+
}
|
|
91
|
+
function getUserMcpFilePath() {
|
|
92
|
+
if (USER_FILE_KIND === null)
|
|
93
|
+
return null;
|
|
94
|
+
return path.join(getUserHarnessRoot(), USER_FILE_KIND === 'mcp.json' ? '.mcp.json' : 'settings.json');
|
|
95
|
+
}
|
|
96
|
+
function buildHarnessRefAccessor(ref, absolutePath) {
|
|
97
|
+
const read = async () => {
|
|
98
|
+
try {
|
|
99
|
+
const res = await harnessService.read(ref);
|
|
100
|
+
const text = res.content ?? '';
|
|
101
|
+
const trimmed = text.trim();
|
|
102
|
+
if (!trimmed)
|
|
103
|
+
return { servers: {}, mtime: res.mtime, rawText: text };
|
|
104
|
+
const parsed = safeParseJsonc(trimmed);
|
|
105
|
+
if (parsed === null) {
|
|
106
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, `failed to parse ${ref.relativePath}`);
|
|
107
|
+
}
|
|
108
|
+
const servers = extractServers(parsed, /*wrapped*/ true);
|
|
109
|
+
return { servers: servers ?? {}, mtime: res.mtime, rawText: text };
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
if (err?.code === HARNESS_ERRORS.HARNESS_FILE_NOT_FOUND.code) {
|
|
113
|
+
return { servers: {}, mtime: '', rawText: '' };
|
|
114
|
+
}
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
return {
|
|
119
|
+
absolutePath,
|
|
120
|
+
read,
|
|
121
|
+
async patch(name, value, expectedMtime) {
|
|
122
|
+
const current = await read();
|
|
123
|
+
// Refuse stale writes upfront so we get the conventional 409 envelope
|
|
124
|
+
// even when the file is being created (current.mtime === '').
|
|
125
|
+
if (expectedMtime !== undefined && expectedMtime !== current.mtime) {
|
|
126
|
+
throwMapped(HARNESS_ERRORS.HARNESS_STALE_WRITE.code, 'file changed on disk', {
|
|
127
|
+
currentMtime: current.mtime,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
// Story 30.1 (AC4.b): block writes that would land plaintext secrets
|
|
131
|
+
// in any git-tracked harness-ref MCP file (including the project-scope
|
|
132
|
+
// `mcp.disabled.json` backup). User-scope refs are no-op (guard
|
|
133
|
+
// short-circuits when scope !== 'project').
|
|
134
|
+
if (ref.scope === 'project' && ref.projectSlug) {
|
|
135
|
+
const secrets = detectSecretsInValue(value);
|
|
136
|
+
await assertNoSecretOnShared({
|
|
137
|
+
scope: 'project',
|
|
138
|
+
projectSlug: ref.projectSlug,
|
|
139
|
+
relativePath: `.claude/${ref.relativePath ?? ''}`,
|
|
140
|
+
secretDetected: secrets.matched,
|
|
141
|
+
detectedAt: { paths: secrets.paths },
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
const sourceText = current.rawText.length === 0 ? '{}' : current.rawText;
|
|
145
|
+
const patched = applyJsoncPatch(sourceText, [{ path: ['mcpServers', name], value }]);
|
|
146
|
+
const written = await harnessService.write(ref, {
|
|
147
|
+
content: patched,
|
|
148
|
+
// Skip the mtime guard inside `harnessService.write` — we already
|
|
149
|
+
// performed it above against the parsed read, and re-passing
|
|
150
|
+
// expectedMtime would force the missing-file path to throw.
|
|
151
|
+
});
|
|
152
|
+
return { mtime: written.mtime };
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
async function buildProjectMcpAccessor(projectSlug) {
|
|
157
|
+
const filePath = await getProjectMcpFilePath(projectSlug);
|
|
158
|
+
// Containment guard — strict equality on the resolved sibling so future
|
|
159
|
+
// refactors cannot accidentally widen the surface.
|
|
160
|
+
const projectRoot = path.dirname(filePath);
|
|
161
|
+
if (path.basename(filePath) !== '.mcp.json' || path.dirname(filePath) !== projectRoot) {
|
|
162
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PATH_DENIED.code, 'project mcp path escapes containment');
|
|
163
|
+
}
|
|
164
|
+
const read = async () => {
|
|
165
|
+
let stat;
|
|
166
|
+
try {
|
|
167
|
+
stat = await fs.stat(filePath);
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
if (err.code === 'ENOENT') {
|
|
171
|
+
return { servers: {}, mtime: '', rawText: '' };
|
|
172
|
+
}
|
|
173
|
+
throw err;
|
|
174
|
+
}
|
|
175
|
+
if (!stat.isFile()) {
|
|
176
|
+
throwMapped(HARNESS_ERRORS.HARNESS_NOT_A_FILE.code, '.mcp.json is not a regular file');
|
|
177
|
+
}
|
|
178
|
+
const text = await fs.readFile(filePath, 'utf-8');
|
|
179
|
+
const trimmed = text.trim();
|
|
180
|
+
if (!trimmed)
|
|
181
|
+
return { servers: {}, mtime: stat.mtime.toISOString(), rawText: text };
|
|
182
|
+
const parsed = safeParseJsonc(trimmed);
|
|
183
|
+
if (parsed === null) {
|
|
184
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'failed to parse .mcp.json');
|
|
185
|
+
}
|
|
186
|
+
const servers = extractServers(parsed, /*wrapped*/ true);
|
|
187
|
+
return { servers: servers ?? {}, mtime: stat.mtime.toISOString(), rawText: text };
|
|
188
|
+
};
|
|
189
|
+
return {
|
|
190
|
+
absolutePath: filePath,
|
|
191
|
+
read,
|
|
192
|
+
async patch(name, value, expectedMtime) {
|
|
193
|
+
const current = await read();
|
|
194
|
+
if (expectedMtime !== undefined && expectedMtime !== current.mtime) {
|
|
195
|
+
throwMapped(HARNESS_ERRORS.HARNESS_STALE_WRITE.code, 'file changed on disk', {
|
|
196
|
+
currentMtime: current.mtime,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
// Story 30.1 (AC4.b): block writes that would land plaintext secrets
|
|
200
|
+
// in a git-tracked `.mcp.json`. The MCP project file lives at
|
|
201
|
+
// `<projectRoot>/.mcp.json` (sibling of `.claude/`) which is the
|
|
202
|
+
// canonical relative path the share-scope service expects.
|
|
203
|
+
const secrets = detectSecretsInValue(value);
|
|
204
|
+
await assertNoSecretOnShared({
|
|
205
|
+
scope: 'project',
|
|
206
|
+
projectSlug,
|
|
207
|
+
relativePath: '.mcp.json',
|
|
208
|
+
secretDetected: secrets.matched,
|
|
209
|
+
detectedAt: { paths: secrets.paths },
|
|
210
|
+
});
|
|
211
|
+
const sourceText = current.rawText.length === 0 ? '{}' : current.rawText;
|
|
212
|
+
const patched = applyJsoncPatch(sourceText, [{ path: ['mcpServers', name], value }]);
|
|
213
|
+
try {
|
|
214
|
+
await fs.writeFile(filePath, patched, 'utf-8');
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
if (err.code === 'EACCES') {
|
|
218
|
+
throwMapped(HARNESS_ERRORS.HARNESS_FORBIDDEN.code, 'permission denied');
|
|
219
|
+
}
|
|
220
|
+
throwMapped(HARNESS_ERRORS.HARNESS_WRITE_ERROR.code, 'failed to write .mcp.json');
|
|
221
|
+
}
|
|
222
|
+
fileWatcherService.noteLocalWrite(filePath);
|
|
223
|
+
const stat = await fs.stat(filePath);
|
|
224
|
+
return { mtime: stat.mtime.toISOString() };
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
async function getMainAccessor(scope, projectSlug) {
|
|
229
|
+
if (scope === 'user') {
|
|
230
|
+
const filePath = getUserMcpFilePath();
|
|
231
|
+
if (!filePath) {
|
|
232
|
+
throwMapped(HARNESS_ERRORS.HARNESS_FORBIDDEN.code, 'global MCP file is not configured');
|
|
233
|
+
}
|
|
234
|
+
const ref = {
|
|
235
|
+
scope: 'user',
|
|
236
|
+
relativePath: USER_FILE_KIND === 'mcp.json' ? '.mcp.json' : 'settings.json',
|
|
237
|
+
};
|
|
238
|
+
return buildHarnessRefAccessor(ref, filePath);
|
|
239
|
+
}
|
|
240
|
+
if (!projectSlug) {
|
|
241
|
+
throwMapped(HARNESS_ERRORS.HARNESS_ROOT_MISSING.code, 'projectSlug required for project scope');
|
|
242
|
+
}
|
|
243
|
+
return buildProjectMcpAccessor(projectSlug);
|
|
244
|
+
}
|
|
245
|
+
async function getBackupAccessor(scope, projectSlug) {
|
|
246
|
+
if (scope === 'user') {
|
|
247
|
+
const filePath = path.join(getUserHarnessRoot(), 'mcp.disabled.json');
|
|
248
|
+
return buildHarnessRefAccessor({ scope: 'user', relativePath: 'mcp.disabled.json' }, filePath);
|
|
249
|
+
}
|
|
250
|
+
if (!projectSlug) {
|
|
251
|
+
throwMapped(HARNESS_ERRORS.HARNESS_ROOT_MISSING.code, 'projectSlug required for project scope');
|
|
252
|
+
}
|
|
253
|
+
const projectRoot = await projectService.resolveOriginalPath(projectSlug);
|
|
254
|
+
const filePath = path.join(projectRoot, '.claude', 'mcp.disabled.json');
|
|
255
|
+
return buildHarnessRefAccessor({ scope: 'project', projectSlug, relativePath: 'mcp.disabled.json' }, filePath);
|
|
256
|
+
}
|
|
257
|
+
class HarnessMcpService {
|
|
258
|
+
// ---- public surface ----------------------------------------------------
|
|
259
|
+
async listCards(currentProjectSlug) {
|
|
260
|
+
const sources = new Map();
|
|
261
|
+
const malformed = [];
|
|
262
|
+
if (currentProjectSlug) {
|
|
263
|
+
try {
|
|
264
|
+
await this.enumerateProjectMcps(currentProjectSlug, sources, malformed);
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
if (err?.code !== HARNESS_ERRORS.HARNESS_ROOT_MISSING.code) {
|
|
268
|
+
throw err;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
await this.enumerateUserMcps(sources, malformed);
|
|
273
|
+
await this.enumeratePluginMcps(sources, malformed);
|
|
274
|
+
const cards = [];
|
|
275
|
+
for (const [name, entries] of sources) {
|
|
276
|
+
entries.sort((a, b) => SCOPE_PRIORITY[a.scope] - SCOPE_PRIORITY[b.scope]);
|
|
277
|
+
// Active source = first non-backup; if every source is backup-only, fall
|
|
278
|
+
// back to the first entry so the card still renders (disabled).
|
|
279
|
+
const active = entries.find((e) => !e.disabledByBackup) ?? entries[0];
|
|
280
|
+
const enabled = computeEnabled(active);
|
|
281
|
+
cards.push({
|
|
282
|
+
name,
|
|
283
|
+
activeType: resolveType(active.config),
|
|
284
|
+
enabled,
|
|
285
|
+
sources: entries,
|
|
286
|
+
activeScope: active.scope,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
cards.sort((a, b) => a.name.localeCompare(b.name));
|
|
290
|
+
return {
|
|
291
|
+
cards,
|
|
292
|
+
malformed,
|
|
293
|
+
userFileKind: USER_FILE_KIND,
|
|
294
|
+
disableStrategy: DISABLE_STRATEGY,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
async readServer(source, name) {
|
|
298
|
+
const { config, raw, mtime } = await readServerEntry(source, name);
|
|
299
|
+
return {
|
|
300
|
+
source,
|
|
301
|
+
config,
|
|
302
|
+
raw,
|
|
303
|
+
mtime,
|
|
304
|
+
disabledByBackup: source.absoluteFile.endsWith('mcp.disabled.json'),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
async updateServer(source, name, body) {
|
|
308
|
+
if (source.scope === 'plugin') {
|
|
309
|
+
throwMapped(HARNESS_ERRORS.HARNESS_FORBIDDEN.code, 'plugin-scope MCP servers are read-only');
|
|
310
|
+
}
|
|
311
|
+
if (source.scope !== 'project' && source.scope !== 'user') {
|
|
312
|
+
throwMapped(HARNESS_ERRORS.HARNESS_FORBIDDEN.code, 'unsupported source scope for update');
|
|
313
|
+
}
|
|
314
|
+
if (body.enabled !== undefined) {
|
|
315
|
+
return this.toggleEnabled(source, name, body.enabled, body.expectedMtime);
|
|
316
|
+
}
|
|
317
|
+
let nextValue;
|
|
318
|
+
if (body.raw !== undefined) {
|
|
319
|
+
let parsed;
|
|
320
|
+
try {
|
|
321
|
+
parsed = JSON.parse(body.raw);
|
|
322
|
+
}
|
|
323
|
+
catch (cause) {
|
|
324
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, `raw payload is not valid JSON: ${cause.message}`);
|
|
325
|
+
}
|
|
326
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
327
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'raw payload must be an object');
|
|
328
|
+
}
|
|
329
|
+
nextValue = parsed;
|
|
330
|
+
}
|
|
331
|
+
else if (body.config !== undefined) {
|
|
332
|
+
validateConfigShape(body.config);
|
|
333
|
+
nextValue = body.config;
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'one of config / raw / enabled is required');
|
|
337
|
+
}
|
|
338
|
+
const accessor = await getMainAccessor(source.scope, source.projectSlug);
|
|
339
|
+
const result = await accessor.patch(name, nextValue, body.expectedMtime);
|
|
340
|
+
return { success: true, mtime: result.mtime };
|
|
341
|
+
}
|
|
342
|
+
async copyServer(req) {
|
|
343
|
+
if (req.sourceScope === req.targetScope
|
|
344
|
+
&& req.sourceName === req.targetName
|
|
345
|
+
&& req.sourceProjectSlug === req.targetProjectSlug) {
|
|
346
|
+
throwMapped(HARNESS_ERRORS.HARNESS_MCP_NAME_CONFLICT.code, 'same-scope copy must use a different targetName');
|
|
347
|
+
}
|
|
348
|
+
const sourceLocation = await resolveSourceLocation({
|
|
349
|
+
scope: req.sourceScope,
|
|
350
|
+
name: req.sourceName,
|
|
351
|
+
projectSlug: req.sourceProjectSlug,
|
|
352
|
+
pluginKey: req.sourcePluginKey,
|
|
353
|
+
fileKind: req.sourceFileKind,
|
|
354
|
+
});
|
|
355
|
+
let sourceConfig;
|
|
356
|
+
{
|
|
357
|
+
const entry = await readServerEntry(sourceLocation, req.sourceName);
|
|
358
|
+
sourceConfig = entry.config;
|
|
359
|
+
}
|
|
360
|
+
const secrets = detectSecretsInConfig(sourceConfig);
|
|
361
|
+
if (secrets.matched && req.acknowledgedSecret !== true) {
|
|
362
|
+
throwMapped(HARNESS_ERRORS.HARNESS_FORBIDDEN.code, 'secrets detected — client must show the secret-confirmation modal', { cause: 'secret-not-acknowledged', details: { paths: secrets.paths } });
|
|
363
|
+
}
|
|
364
|
+
const accessor = await getMainAccessor(req.targetScope, req.targetProjectSlug);
|
|
365
|
+
const existing = await accessor.read();
|
|
366
|
+
const conflicts = Object.prototype.hasOwnProperty.call(existing.servers, req.targetName);
|
|
367
|
+
if (conflicts) {
|
|
368
|
+
switch (req.onConflict) {
|
|
369
|
+
case 'skip':
|
|
370
|
+
return { success: true, finalName: req.targetName, skipped: true };
|
|
371
|
+
case 'overwrite':
|
|
372
|
+
break;
|
|
373
|
+
case 'rename':
|
|
374
|
+
throwMapped(HARNESS_ERRORS.HARNESS_MCP_NAME_CONFLICT.code, `target name already in use: ${req.targetName}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
await accessor.patch(req.targetName, sourceConfig, existing.mtime || undefined);
|
|
378
|
+
const warnings = [];
|
|
379
|
+
if (req.sourceScope === 'plugin' && containsPluginRootToken(sourceConfig)) {
|
|
380
|
+
warnings.push('plugin-root-reference');
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
success: true,
|
|
384
|
+
finalName: req.targetName,
|
|
385
|
+
skipped: false,
|
|
386
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
async deleteServer(source, name, body) {
|
|
390
|
+
if (source.scope === 'plugin') {
|
|
391
|
+
throwMapped(HARNESS_ERRORS.HARNESS_FORBIDDEN.code, 'plugin-scope MCP servers are read-only');
|
|
392
|
+
}
|
|
393
|
+
const accessor = await getMainAccessor(source.scope, source.projectSlug);
|
|
394
|
+
await accessor.patch(name, undefined, body.expectedMtime);
|
|
395
|
+
return { success: true };
|
|
396
|
+
}
|
|
397
|
+
// ---- enumeration -------------------------------------------------------
|
|
398
|
+
async enumerateProjectMcps(projectSlug, sources, malformed) {
|
|
399
|
+
const filePath = await getProjectMcpFilePath(projectSlug);
|
|
400
|
+
await readMcpFile({
|
|
401
|
+
absoluteFile: filePath,
|
|
402
|
+
scope: 'project',
|
|
403
|
+
projectSlug,
|
|
404
|
+
sourceFileKind: 'mcp.json',
|
|
405
|
+
wrapped: true,
|
|
406
|
+
sources,
|
|
407
|
+
malformed,
|
|
408
|
+
});
|
|
409
|
+
if (DISABLE_STRATEGY === 'backup') {
|
|
410
|
+
const projectRoot = await projectService.resolveOriginalPath(projectSlug);
|
|
411
|
+
await readMcpFile({
|
|
412
|
+
absoluteFile: path.join(projectRoot, '.claude', 'mcp.disabled.json'),
|
|
413
|
+
scope: 'project',
|
|
414
|
+
projectSlug,
|
|
415
|
+
sourceFileKind: 'mcp.json',
|
|
416
|
+
wrapped: true,
|
|
417
|
+
disabledByBackup: true,
|
|
418
|
+
sources,
|
|
419
|
+
malformed,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
async enumerateUserMcps(sources, malformed) {
|
|
424
|
+
const filePath = getUserMcpFilePath();
|
|
425
|
+
if (!filePath)
|
|
426
|
+
return;
|
|
427
|
+
await readMcpFile({
|
|
428
|
+
absoluteFile: filePath,
|
|
429
|
+
scope: 'user',
|
|
430
|
+
sourceFileKind: USER_FILE_KIND ?? 'mcp.json',
|
|
431
|
+
wrapped: true,
|
|
432
|
+
sources,
|
|
433
|
+
malformed,
|
|
434
|
+
});
|
|
435
|
+
if (DISABLE_STRATEGY === 'backup') {
|
|
436
|
+
await readMcpFile({
|
|
437
|
+
absoluteFile: path.join(getUserHarnessRoot(), 'mcp.disabled.json'),
|
|
438
|
+
scope: 'user',
|
|
439
|
+
sourceFileKind: 'mcp.json',
|
|
440
|
+
wrapped: true,
|
|
441
|
+
disabledByBackup: true,
|
|
442
|
+
sources,
|
|
443
|
+
malformed,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
async enumeratePluginMcps(sources, malformed) {
|
|
448
|
+
let installed = {};
|
|
449
|
+
try {
|
|
450
|
+
const res = await harnessService.read({
|
|
451
|
+
scope: 'user',
|
|
452
|
+
relativePath: 'plugins/installed_plugins.json',
|
|
453
|
+
});
|
|
454
|
+
const trimmed = (res.content ?? '').trim();
|
|
455
|
+
if (trimmed) {
|
|
456
|
+
try {
|
|
457
|
+
installed = JSON.parse(trimmed);
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
catch (err) {
|
|
465
|
+
if (isFileNotFound(err))
|
|
466
|
+
return;
|
|
467
|
+
throw err;
|
|
468
|
+
}
|
|
469
|
+
const plugins = installed.plugins ?? {};
|
|
470
|
+
for (const [pluginKey, value] of Object.entries(plugins)) {
|
|
471
|
+
const entries = Array.isArray(value) ? value : [value];
|
|
472
|
+
for (const entry of entries) {
|
|
473
|
+
if (!entry?.installPath || typeof entry.installPath !== 'string')
|
|
474
|
+
continue;
|
|
475
|
+
const installRoot = path.resolve(entry.installPath);
|
|
476
|
+
const guardedAbs = (abs) => {
|
|
477
|
+
const resolved = path.resolve(abs);
|
|
478
|
+
return resolved === installRoot || resolved.startsWith(installRoot + path.sep);
|
|
479
|
+
};
|
|
480
|
+
// Plugin .mcp.json — uses the UNWRAPPED form on disk per Anthropic's
|
|
481
|
+
// marketplace catalog convention (top-level keys are server names).
|
|
482
|
+
const pluginMcp = path.join(entry.installPath, '.mcp.json');
|
|
483
|
+
if (guardedAbs(pluginMcp)) {
|
|
484
|
+
await readMcpFile({
|
|
485
|
+
absoluteFile: pluginMcp,
|
|
486
|
+
scope: 'plugin',
|
|
487
|
+
pluginKey,
|
|
488
|
+
sourceFileKind: 'mcp.json',
|
|
489
|
+
wrapped: false,
|
|
490
|
+
sources,
|
|
491
|
+
malformed,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
// Plugin manifest's mcpServers field — wrapped form.
|
|
495
|
+
const pluginManifest = path.join(entry.installPath, '.claude-plugin', 'plugin.json');
|
|
496
|
+
if (guardedAbs(pluginManifest)) {
|
|
497
|
+
await readMcpFile({
|
|
498
|
+
absoluteFile: pluginManifest,
|
|
499
|
+
scope: 'plugin',
|
|
500
|
+
pluginKey,
|
|
501
|
+
sourceFileKind: 'plugin.json',
|
|
502
|
+
wrapped: true,
|
|
503
|
+
sources,
|
|
504
|
+
malformed,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
// ---- toggle helpers ---------------------------------------------------
|
|
511
|
+
async toggleEnabled(source, name, enabled, expectedMtime) {
|
|
512
|
+
const editableScope = source.scope;
|
|
513
|
+
const main = await getMainAccessor(editableScope, source.projectSlug);
|
|
514
|
+
if (DISABLE_STRATEGY === 'flag') {
|
|
515
|
+
// Spike A 경로 1 — write `enabled: true|false` directly. enabled=true
|
|
516
|
+
// deletes the key (default) so the file stays clean when re-enabling.
|
|
517
|
+
const current = await main.read();
|
|
518
|
+
const cfg = current.servers[name];
|
|
519
|
+
if (!cfg) {
|
|
520
|
+
throwMapped(HARNESS_ERRORS.HARNESS_MCP_NOT_FOUND.code, `entry not in main: ${name}`);
|
|
521
|
+
}
|
|
522
|
+
const next = { ...cfg };
|
|
523
|
+
if (enabled)
|
|
524
|
+
delete next.enabled;
|
|
525
|
+
else
|
|
526
|
+
next.enabled = false;
|
|
527
|
+
const result = await main.patch(name, next, expectedMtime ?? current.mtime);
|
|
528
|
+
return { success: true, mtime: result.mtime };
|
|
529
|
+
}
|
|
530
|
+
// Spike A 경로 2 — backup file move (default).
|
|
531
|
+
const backup = await getBackupAccessor(editableScope, source.projectSlug);
|
|
532
|
+
if (enabled) {
|
|
533
|
+
// backup → main: read entry from backup, set on main, delete from backup.
|
|
534
|
+
const backupCurrent = await backup.read();
|
|
535
|
+
const cfg = backupCurrent.servers[name];
|
|
536
|
+
if (!cfg) {
|
|
537
|
+
throwMapped(HARNESS_ERRORS.HARNESS_MCP_NOT_FOUND.code, `entry not in backup: ${name}`);
|
|
538
|
+
}
|
|
539
|
+
const mainCurrent = await main.read();
|
|
540
|
+
if (Object.prototype.hasOwnProperty.call(mainCurrent.servers, name)) {
|
|
541
|
+
throwMapped(HARNESS_ERRORS.HARNESS_MCP_NAME_CONFLICT.code, `cannot enable: ${name} already exists in main file`);
|
|
542
|
+
}
|
|
543
|
+
const mainWrite = await main.patch(name, cfg, expectedMtime ?? mainCurrent.mtime);
|
|
544
|
+
try {
|
|
545
|
+
await backup.patch(name, undefined, backupCurrent.mtime);
|
|
546
|
+
}
|
|
547
|
+
catch (err) {
|
|
548
|
+
// Inverse: remove the entry we just added to main so the user is not
|
|
549
|
+
// left with a stale duplicate.
|
|
550
|
+
await main
|
|
551
|
+
.patch(name, undefined, mainWrite.mtime)
|
|
552
|
+
.catch((rollbackErr) => {
|
|
553
|
+
log.warn(`enable rollback failed for ${name}: ${rollbackErr.message}`);
|
|
554
|
+
});
|
|
555
|
+
throw err;
|
|
556
|
+
}
|
|
557
|
+
return { success: true, mtime: mainWrite.mtime };
|
|
558
|
+
}
|
|
559
|
+
// main → backup
|
|
560
|
+
const mainCurrent = await main.read();
|
|
561
|
+
const cfg = mainCurrent.servers[name];
|
|
562
|
+
if (!cfg) {
|
|
563
|
+
throwMapped(HARNESS_ERRORS.HARNESS_MCP_NOT_FOUND.code, `entry not in main: ${name}`);
|
|
564
|
+
}
|
|
565
|
+
const backupCurrent = await backup.read();
|
|
566
|
+
const backupWrite = await backup.patch(name, cfg, backupCurrent.mtime || undefined);
|
|
567
|
+
try {
|
|
568
|
+
const mainWrite = await main.patch(name, undefined, expectedMtime ?? mainCurrent.mtime);
|
|
569
|
+
return { success: true, mtime: mainWrite.mtime };
|
|
570
|
+
}
|
|
571
|
+
catch (err) {
|
|
572
|
+
await backup
|
|
573
|
+
.patch(name, undefined, backupWrite.mtime)
|
|
574
|
+
.catch((rollbackErr) => {
|
|
575
|
+
log.warn(`disable rollback failed for ${name}: ${rollbackErr.message}`);
|
|
576
|
+
});
|
|
577
|
+
throw err;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
async function readMcpFile(args) {
|
|
582
|
+
let stat;
|
|
583
|
+
try {
|
|
584
|
+
stat = await fs.stat(args.absoluteFile);
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
return; // missing file is normal
|
|
588
|
+
}
|
|
589
|
+
if (!stat.isFile())
|
|
590
|
+
return;
|
|
591
|
+
let text;
|
|
592
|
+
try {
|
|
593
|
+
text = await fs.readFile(args.absoluteFile, 'utf-8');
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
const trimmed = text.trim();
|
|
599
|
+
if (trimmed === '')
|
|
600
|
+
return;
|
|
601
|
+
const parsed = safeParseJsonc(trimmed);
|
|
602
|
+
if (parsed === null) {
|
|
603
|
+
args.malformed.push({
|
|
604
|
+
scope: args.scope,
|
|
605
|
+
absoluteFile: args.absoluteFile,
|
|
606
|
+
pluginKey: args.pluginKey,
|
|
607
|
+
projectSlug: args.projectSlug,
|
|
608
|
+
serverName: '*',
|
|
609
|
+
reason: 'failed to parse JSON',
|
|
610
|
+
});
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
const servers = extractServers(parsed, args.wrapped);
|
|
614
|
+
if (!servers) {
|
|
615
|
+
args.malformed.push({
|
|
616
|
+
scope: args.scope,
|
|
617
|
+
absoluteFile: args.absoluteFile,
|
|
618
|
+
pluginKey: args.pluginKey,
|
|
619
|
+
projectSlug: args.projectSlug,
|
|
620
|
+
serverName: '*',
|
|
621
|
+
reason: args.wrapped
|
|
622
|
+
? 'mcpServers field missing or invalid'
|
|
623
|
+
: 'expected an object of server entries',
|
|
624
|
+
});
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
for (const [name, config] of Object.entries(servers)) {
|
|
628
|
+
if (!config || typeof config !== 'object') {
|
|
629
|
+
args.malformed.push({
|
|
630
|
+
scope: args.scope,
|
|
631
|
+
absoluteFile: args.absoluteFile,
|
|
632
|
+
pluginKey: args.pluginKey,
|
|
633
|
+
projectSlug: args.projectSlug,
|
|
634
|
+
serverName: name,
|
|
635
|
+
reason: 'server entry is not an object',
|
|
636
|
+
});
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
const source = {
|
|
640
|
+
scope: args.scope,
|
|
641
|
+
absoluteFile: args.absoluteFile,
|
|
642
|
+
pluginKey: args.pluginKey,
|
|
643
|
+
projectSlug: args.projectSlug,
|
|
644
|
+
sourceFileKind: args.sourceFileKind,
|
|
645
|
+
config,
|
|
646
|
+
mtime: stat.mtime.toISOString(),
|
|
647
|
+
disabledByBackup: args.disabledByBackup === true,
|
|
648
|
+
};
|
|
649
|
+
const list = args.sources.get(name);
|
|
650
|
+
if (list)
|
|
651
|
+
list.push(source);
|
|
652
|
+
else
|
|
653
|
+
args.sources.set(name, [source]);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
async function readServerEntry(source, name) {
|
|
657
|
+
let stat;
|
|
658
|
+
try {
|
|
659
|
+
stat = await fs.stat(source.absoluteFile);
|
|
660
|
+
}
|
|
661
|
+
catch (err) {
|
|
662
|
+
if (err.code === 'ENOENT') {
|
|
663
|
+
throwMapped(HARNESS_ERRORS.HARNESS_MCP_NOT_FOUND.code, `MCP file not found: ${source.absoluteFile}`);
|
|
664
|
+
}
|
|
665
|
+
throw err;
|
|
666
|
+
}
|
|
667
|
+
if (!stat.isFile()) {
|
|
668
|
+
throwMapped(HARNESS_ERRORS.HARNESS_MCP_NOT_FOUND.code, 'MCP source is not a regular file');
|
|
669
|
+
}
|
|
670
|
+
const text = await fs.readFile(source.absoluteFile, 'utf-8');
|
|
671
|
+
const parsed = safeParseJsonc(text);
|
|
672
|
+
if (parsed === null) {
|
|
673
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'failed to parse MCP file');
|
|
674
|
+
}
|
|
675
|
+
// Plugin .mcp.json is unwrapped; everything else (incl. plugin.json) is wrapped.
|
|
676
|
+
const wrapped = !(source.scope === 'plugin' && source.sourceFileKind === 'mcp.json');
|
|
677
|
+
const servers = extractServers(parsed, wrapped);
|
|
678
|
+
if (!servers || !servers[name]) {
|
|
679
|
+
throwMapped(HARNESS_ERRORS.HARNESS_MCP_NOT_FOUND.code, `server not found: ${name}`);
|
|
680
|
+
}
|
|
681
|
+
const config = servers[name];
|
|
682
|
+
const raw = JSON.stringify(config, null, 2);
|
|
683
|
+
return { config, raw, mtime: stat.mtime.toISOString() };
|
|
684
|
+
}
|
|
685
|
+
function extractServers(parsed, wrapped) {
|
|
686
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
687
|
+
return null;
|
|
688
|
+
const obj = parsed;
|
|
689
|
+
let servers;
|
|
690
|
+
if (wrapped) {
|
|
691
|
+
servers = obj.mcpServers;
|
|
692
|
+
if (servers === undefined)
|
|
693
|
+
return {};
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
servers = obj;
|
|
697
|
+
}
|
|
698
|
+
if (!servers || typeof servers !== 'object' || Array.isArray(servers))
|
|
699
|
+
return null;
|
|
700
|
+
const result = {};
|
|
701
|
+
for (const [name, value] of Object.entries(servers)) {
|
|
702
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
703
|
+
continue;
|
|
704
|
+
result[name] = value;
|
|
705
|
+
}
|
|
706
|
+
return result;
|
|
707
|
+
}
|
|
708
|
+
function safeParseJsonc(text) {
|
|
709
|
+
try {
|
|
710
|
+
return JSON.parse(text);
|
|
711
|
+
}
|
|
712
|
+
catch {
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
export async function resolveSourceLocation(input) {
|
|
717
|
+
if (input.scope === 'project') {
|
|
718
|
+
if (!input.projectSlug) {
|
|
719
|
+
throwMapped(HARNESS_ERRORS.HARNESS_ROOT_MISSING.code, 'projectSlug required for scope=project');
|
|
720
|
+
}
|
|
721
|
+
const filePath = await getProjectMcpFilePath(input.projectSlug);
|
|
722
|
+
return {
|
|
723
|
+
scope: 'project',
|
|
724
|
+
absoluteFile: filePath,
|
|
725
|
+
projectSlug: input.projectSlug,
|
|
726
|
+
sourceFileKind: 'mcp.json',
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
if (input.scope === 'user') {
|
|
730
|
+
const filePath = getUserMcpFilePath();
|
|
731
|
+
if (!filePath) {
|
|
732
|
+
throwMapped(HARNESS_ERRORS.HARNESS_ROOT_MISSING.code, 'global MCP file is not configured');
|
|
733
|
+
}
|
|
734
|
+
return {
|
|
735
|
+
scope: 'user',
|
|
736
|
+
absoluteFile: filePath,
|
|
737
|
+
sourceFileKind: USER_FILE_KIND ?? 'mcp.json',
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
// plugin
|
|
741
|
+
if (!input.pluginKey) {
|
|
742
|
+
throwMapped(HARNESS_ERRORS.HARNESS_ROOT_MISSING.code, 'pluginKey required for scope=plugin');
|
|
743
|
+
}
|
|
744
|
+
const installPath = await readPluginInstallPath(input.pluginKey);
|
|
745
|
+
if (!installPath) {
|
|
746
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PLUGIN_NOT_FOUND.code, `plugin not installed: ${input.pluginKey}`);
|
|
747
|
+
}
|
|
748
|
+
const fileKind = input.fileKind ?? 'mcp.json';
|
|
749
|
+
const fileName = fileKind === 'plugin.json' ? path.join('.claude-plugin', 'plugin.json') : '.mcp.json';
|
|
750
|
+
const absoluteFile = path.join(installPath, fileName);
|
|
751
|
+
// Containment guard for dev-installed (out-of-tree) plugins.
|
|
752
|
+
const root = path.resolve(installPath);
|
|
753
|
+
const abs = path.resolve(absoluteFile);
|
|
754
|
+
if (abs !== root && !abs.startsWith(root + path.sep)) {
|
|
755
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PATH_DENIED.code, 'plugin file escapes installPath');
|
|
756
|
+
}
|
|
757
|
+
return {
|
|
758
|
+
scope: 'plugin',
|
|
759
|
+
absoluteFile,
|
|
760
|
+
pluginKey: input.pluginKey,
|
|
761
|
+
sourceFileKind: fileKind,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
async function readPluginInstallPath(pluginKey) {
|
|
765
|
+
try {
|
|
766
|
+
const res = await harnessService.read({
|
|
767
|
+
scope: 'user',
|
|
768
|
+
relativePath: 'plugins/installed_plugins.json',
|
|
769
|
+
});
|
|
770
|
+
const trimmed = (res.content ?? '').trim();
|
|
771
|
+
if (!trimmed)
|
|
772
|
+
return undefined;
|
|
773
|
+
const parsed = JSON.parse(trimmed);
|
|
774
|
+
const raw = parsed.plugins?.[pluginKey];
|
|
775
|
+
if (!raw)
|
|
776
|
+
return undefined;
|
|
777
|
+
const entries = Array.isArray(raw) ? raw : [raw];
|
|
778
|
+
const first = entries.find((e) => typeof e?.installPath === 'string');
|
|
779
|
+
return first?.installPath;
|
|
780
|
+
}
|
|
781
|
+
catch (err) {
|
|
782
|
+
if (isFileNotFound(err))
|
|
783
|
+
return undefined;
|
|
784
|
+
throw err;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
function computeEnabled(source) {
|
|
788
|
+
if (DISABLE_STRATEGY === 'flag') {
|
|
789
|
+
return source.config.enabled !== false;
|
|
790
|
+
}
|
|
791
|
+
return !source.disabledByBackup;
|
|
792
|
+
}
|
|
793
|
+
function validateConfigShape(config) {
|
|
794
|
+
const type = resolveType(config);
|
|
795
|
+
if (type === 'stdio') {
|
|
796
|
+
if (!config.command || typeof config.command !== 'string') {
|
|
797
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'command is required for stdio');
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
else {
|
|
801
|
+
if (!config.url || typeof config.url !== 'string') {
|
|
802
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, `url is required for ${type}`);
|
|
803
|
+
}
|
|
804
|
+
if (type !== 'http' && config.headers && Object.keys(config.headers).length > 0) {
|
|
805
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'headers only allowed for http');
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
export const harnessMcpService = new HarnessMcpService();
|
|
810
|
+
export const SPIKE_RESULTS = {
|
|
811
|
+
disableStrategy: DISABLE_STRATEGY,
|
|
812
|
+
userFileKind: USER_FILE_KIND,
|
|
813
|
+
};
|
|
814
|
+
//# sourceMappingURL=harnessMcpService.js.map
|