praxis-agent 0.1.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/LICENSE +21 -0
- package/README.md +534 -0
- package/dist/application/background-agent-manager.d.ts +66 -0
- package/dist/application/background-agent-manager.d.ts.map +1 -0
- package/dist/application/background-agent-manager.js +362 -0
- package/dist/application/background-agent-manager.js.map +1 -0
- package/dist/application/background-bash-manager.d.ts +54 -0
- package/dist/application/background-bash-manager.d.ts.map +1 -0
- package/dist/application/background-bash-manager.js +354 -0
- package/dist/application/background-bash-manager.js.map +1 -0
- package/dist/application/background-task-id.d.ts +5 -0
- package/dist/application/background-task-id.d.ts.map +1 -0
- package/dist/application/background-task-id.js +25 -0
- package/dist/application/background-task-id.js.map +1 -0
- package/dist/application/claude-project-purge.d.ts +50 -0
- package/dist/application/claude-project-purge.d.ts.map +1 -0
- package/dist/application/claude-project-purge.js +459 -0
- package/dist/application/claude-project-purge.js.map +1 -0
- package/dist/application/managed-worktree.d.ts +15 -0
- package/dist/application/managed-worktree.d.ts.map +1 -0
- package/dist/application/managed-worktree.js +127 -0
- package/dist/application/managed-worktree.js.map +1 -0
- package/dist/application/model-compactor.d.ts +8 -0
- package/dist/application/model-compactor.d.ts.map +1 -0
- package/dist/application/model-compactor.js +70 -0
- package/dist/application/model-compactor.js.map +1 -0
- package/dist/application/scheduled-prompt-manager.d.ts +69 -0
- package/dist/application/scheduled-prompt-manager.d.ts.map +1 -0
- package/dist/application/scheduled-prompt-manager.js +387 -0
- package/dist/application/scheduled-prompt-manager.js.map +1 -0
- package/dist/application/session-service.d.ts +142 -0
- package/dist/application/session-service.d.ts.map +1 -0
- package/dist/application/session-service.js +1690 -0
- package/dist/application/session-service.js.map +1 -0
- package/dist/application/session-worktree.d.ts +55 -0
- package/dist/application/session-worktree.d.ts.map +1 -0
- package/dist/application/session-worktree.js +311 -0
- package/dist/application/session-worktree.js.map +1 -0
- package/dist/application/subagent-service.d.ts +98 -0
- package/dist/application/subagent-service.d.ts.map +1 -0
- package/dist/application/subagent-service.js +1305 -0
- package/dist/application/subagent-service.js.map +1 -0
- package/dist/application/tool-use-summary.d.ts +8 -0
- package/dist/application/tool-use-summary.d.ts.map +1 -0
- package/dist/application/tool-use-summary.js +53 -0
- package/dist/application/tool-use-summary.js.map +1 -0
- package/dist/application/top-level-agent-manager.d.ts +58 -0
- package/dist/application/top-level-agent-manager.d.ts.map +1 -0
- package/dist/application/top-level-agent-manager.js +907 -0
- package/dist/application/top-level-agent-manager.js.map +1 -0
- package/dist/application/workflow-manager.d.ts +61 -0
- package/dist/application/workflow-manager.d.ts.map +1 -0
- package/dist/application/workflow-manager.js +582 -0
- package/dist/application/workflow-manager.js.map +1 -0
- package/dist/application/workflow-meta.d.ts +18 -0
- package/dist/application/workflow-meta.d.ts.map +1 -0
- package/dist/application/workflow-meta.js +146 -0
- package/dist/application/workflow-meta.js.map +1 -0
- package/dist/application/workflow-runtime.d.ts +29 -0
- package/dist/application/workflow-runtime.d.ts.map +1 -0
- package/dist/application/workflow-runtime.js +176 -0
- package/dist/application/workflow-runtime.js.map +1 -0
- package/dist/application/workflow-worktree.d.ts +9 -0
- package/dist/application/workflow-worktree.d.ts.map +1 -0
- package/dist/application/workflow-worktree.js +11 -0
- package/dist/application/workflow-worktree.js.map +1 -0
- package/dist/cli/agents-dashboard.d.ts +37 -0
- package/dist/cli/agents-dashboard.d.ts.map +1 -0
- package/dist/cli/agents-dashboard.js +408 -0
- package/dist/cli/agents-dashboard.js.map +1 -0
- package/dist/cli/controls.d.ts +15 -0
- package/dist/cli/controls.d.ts.map +1 -0
- package/dist/cli/controls.js +219 -0
- package/dist/cli/controls.js.map +1 -0
- package/dist/cli/debug.d.ts +13 -0
- package/dist/cli/debug.d.ts.map +1 -0
- package/dist/cli/debug.js +37 -0
- package/dist/cli/debug.js.map +1 -0
- package/dist/cli/interactive.d.ts +63 -0
- package/dist/cli/interactive.d.ts.map +1 -0
- package/dist/cli/interactive.js +600 -0
- package/dist/cli/interactive.js.map +1 -0
- package/dist/cli/protocol.d.ts +251 -0
- package/dist/cli/protocol.d.ts.map +1 -0
- package/dist/cli/protocol.js +2146 -0
- package/dist/cli/protocol.js.map +1 -0
- package/dist/cli.d.ts +116 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +3901 -0
- package/dist/cli.js.map +1 -0
- package/dist/compatibility/claude/compaction.d.ts +19 -0
- package/dist/compatibility/claude/compaction.d.ts.map +1 -0
- package/dist/compatibility/claude/compaction.js +82 -0
- package/dist/compatibility/claude/compaction.js.map +1 -0
- package/dist/compatibility/claude/context.d.ts +24 -0
- package/dist/compatibility/claude/context.d.ts.map +1 -0
- package/dist/compatibility/claude/context.js +92 -0
- package/dist/compatibility/claude/context.js.map +1 -0
- package/dist/compatibility/claude/dynamic-context.d.ts +20 -0
- package/dist/compatibility/claude/dynamic-context.d.ts.map +1 -0
- package/dist/compatibility/claude/dynamic-context.js +168 -0
- package/dist/compatibility/claude/dynamic-context.js.map +1 -0
- package/dist/compatibility/claude/file-history.d.ts +14 -0
- package/dist/compatibility/claude/file-history.d.ts.map +1 -0
- package/dist/compatibility/claude/file-history.js +340 -0
- package/dist/compatibility/claude/file-history.js.map +1 -0
- package/dist/compatibility/claude/file-resources.d.ts +28 -0
- package/dist/compatibility/claude/file-resources.d.ts.map +1 -0
- package/dist/compatibility/claude/file-resources.js +168 -0
- package/dist/compatibility/claude/file-resources.js.map +1 -0
- package/dist/compatibility/claude/fork.d.ts +9 -0
- package/dist/compatibility/claude/fork.d.ts.map +1 -0
- package/dist/compatibility/claude/fork.js +225 -0
- package/dist/compatibility/claude/fork.js.map +1 -0
- package/dist/compatibility/claude/history.d.ts +4 -0
- package/dist/compatibility/claude/history.d.ts.map +1 -0
- package/dist/compatibility/claude/history.js +86 -0
- package/dist/compatibility/claude/history.js.map +1 -0
- package/dist/compatibility/claude/ownership.d.ts +87 -0
- package/dist/compatibility/claude/ownership.d.ts.map +1 -0
- package/dist/compatibility/claude/ownership.js +100 -0
- package/dist/compatibility/claude/ownership.js.map +1 -0
- package/dist/compatibility/claude/paths.d.ts +17 -0
- package/dist/compatibility/claude/paths.d.ts.map +1 -0
- package/dist/compatibility/claude/paths.js +54 -0
- package/dist/compatibility/claude/paths.js.map +1 -0
- package/dist/compatibility/claude/pr-links.d.ts +25 -0
- package/dist/compatibility/claude/pr-links.d.ts.map +1 -0
- package/dist/compatibility/claude/pr-links.js +80 -0
- package/dist/compatibility/claude/pr-links.js.map +1 -0
- package/dist/compatibility/claude/projection.d.ts +11 -0
- package/dist/compatibility/claude/projection.d.ts.map +1 -0
- package/dist/compatibility/claude/projection.js +325 -0
- package/dist/compatibility/claude/projection.js.map +1 -0
- package/dist/compatibility/claude/schema.d.ts +18 -0
- package/dist/compatibility/claude/schema.d.ts.map +1 -0
- package/dist/compatibility/claude/schema.js +983 -0
- package/dist/compatibility/claude/schema.js.map +1 -0
- package/dist/compatibility/claude/shared-resources.d.ts +51 -0
- package/dist/compatibility/claude/shared-resources.d.ts.map +1 -0
- package/dist/compatibility/claude/shared-resources.js +382 -0
- package/dist/compatibility/claude/shared-resources.js.map +1 -0
- package/dist/compatibility/claude/sidechain.d.ts +50 -0
- package/dist/compatibility/claude/sidechain.d.ts.map +1 -0
- package/dist/compatibility/claude/sidechain.js +90 -0
- package/dist/compatibility/claude/sidechain.js.map +1 -0
- package/dist/compatibility/claude/tool-links.d.ts +13 -0
- package/dist/compatibility/claude/tool-links.d.ts.map +1 -0
- package/dist/compatibility/claude/tool-links.js +61 -0
- package/dist/compatibility/claude/tool-links.js.map +1 -0
- package/dist/compatibility/claude/translation.d.ts +71 -0
- package/dist/compatibility/claude/translation.d.ts.map +1 -0
- package/dist/compatibility/claude/translation.js +443 -0
- package/dist/compatibility/claude/translation.js.map +1 -0
- package/dist/compatibility/claude/workflow-replay.d.ts +10 -0
- package/dist/compatibility/claude/workflow-replay.d.ts.map +1 -0
- package/dist/compatibility/claude/workflow-replay.js +66 -0
- package/dist/compatibility/claude/workflow-replay.js.map +1 -0
- package/dist/compatibility/claude/workflow.d.ts +33 -0
- package/dist/compatibility/claude/workflow.d.ts.map +1 -0
- package/dist/compatibility/claude/workflow.js +76 -0
- package/dist/compatibility/claude/workflow.js.map +1 -0
- package/dist/core/compaction.d.ts +16 -0
- package/dist/core/compaction.d.ts.map +1 -0
- package/dist/core/compaction.js +2 -0
- package/dist/core/compaction.js.map +1 -0
- package/dist/core/context-budget.d.ts +28 -0
- package/dist/core/context-budget.d.ts.map +1 -0
- package/dist/core/context-budget.js +108 -0
- package/dist/core/context-budget.js.map +1 -0
- package/dist/core/context.d.ts +16 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/context.js +13 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/runtime.d.ts +394 -0
- package/dist/core/runtime.d.ts.map +1 -0
- package/dist/core/runtime.js +507 -0
- package/dist/core/runtime.js.map +1 -0
- package/dist/core/usage.d.ts +26 -0
- package/dist/core/usage.d.ts.map +1 -0
- package/dist/core/usage.js +133 -0
- package/dist/core/usage.js.map +1 -0
- package/dist/extensions/claude-extension-tools.d.ts +16 -0
- package/dist/extensions/claude-extension-tools.d.ts.map +1 -0
- package/dist/extensions/claude-extension-tools.js +78 -0
- package/dist/extensions/claude-extension-tools.js.map +1 -0
- package/dist/extensions/claude-extensions.d.ts +41 -0
- package/dist/extensions/claude-extensions.d.ts.map +1 -0
- package/dist/extensions/claude-extensions.js +270 -0
- package/dist/extensions/claude-extensions.js.map +1 -0
- package/dist/hooks/claude-hook-tools.d.ts +26 -0
- package/dist/hooks/claude-hook-tools.d.ts.map +1 -0
- package/dist/hooks/claude-hook-tools.js +129 -0
- package/dist/hooks/claude-hook-tools.js.map +1 -0
- package/dist/hooks/claude-hooks.d.ts +87 -0
- package/dist/hooks/claude-hooks.d.ts.map +1 -0
- package/dist/hooks/claude-hooks.js +411 -0
- package/dist/hooks/claude-hooks.js.map +1 -0
- package/dist/maintenance/doctor.d.ts +33 -0
- package/dist/maintenance/doctor.d.ts.map +1 -0
- package/dist/maintenance/doctor.js +391 -0
- package/dist/maintenance/doctor.js.map +1 -0
- package/dist/maintenance/self-update.d.ts +31 -0
- package/dist/maintenance/self-update.d.ts.map +1 -0
- package/dist/maintenance/self-update.js +81 -0
- package/dist/maintenance/self-update.js.map +1 -0
- package/dist/mcp/claude-mcp-management.d.ts +28 -0
- package/dist/mcp/claude-mcp-management.d.ts.map +1 -0
- package/dist/mcp/claude-mcp-management.js +169 -0
- package/dist/mcp/claude-mcp-management.js.map +1 -0
- package/dist/mcp/claude-mcp-oauth.d.ts +113 -0
- package/dist/mcp/claude-mcp-oauth.d.ts.map +1 -0
- package/dist/mcp/claude-mcp-oauth.js +1053 -0
- package/dist/mcp/claude-mcp-oauth.js.map +1 -0
- package/dist/mcp/claude-mcp-tools.d.ts +98 -0
- package/dist/mcp/claude-mcp-tools.d.ts.map +1 -0
- package/dist/mcp/claude-mcp-tools.js +1246 -0
- package/dist/mcp/claude-mcp-tools.js.map +1 -0
- package/dist/mcp/praxis-mcp-server.d.ts +35 -0
- package/dist/mcp/praxis-mcp-server.d.ts.map +1 -0
- package/dist/mcp/praxis-mcp-server.js +520 -0
- package/dist/mcp/praxis-mcp-server.js.map +1 -0
- package/dist/permissions/claude-auto-classifier.d.ts +20 -0
- package/dist/permissions/claude-auto-classifier.d.ts.map +1 -0
- package/dist/permissions/claude-auto-classifier.js +140 -0
- package/dist/permissions/claude-auto-classifier.js.map +1 -0
- package/dist/permissions/claude-permission-resolver.d.ts +30 -0
- package/dist/permissions/claude-permission-resolver.d.ts.map +1 -0
- package/dist/permissions/claude-permission-resolver.js +334 -0
- package/dist/permissions/claude-permission-resolver.js.map +1 -0
- package/dist/persistence/claude-job-store.d.ts +68 -0
- package/dist/persistence/claude-job-store.d.ts.map +1 -0
- package/dist/persistence/claude-job-store.js +311 -0
- package/dist/persistence/claude-job-store.js.map +1 -0
- package/dist/persistence/claude-scheduled-task-store.d.ts +29 -0
- package/dist/persistence/claude-scheduled-task-store.d.ts.map +1 -0
- package/dist/persistence/claude-scheduled-task-store.js +163 -0
- package/dist/persistence/claude-scheduled-task-store.js.map +1 -0
- package/dist/persistence/claude-sidechain-store.d.ts +18 -0
- package/dist/persistence/claude-sidechain-store.d.ts.map +1 -0
- package/dist/persistence/claude-sidechain-store.js +107 -0
- package/dist/persistence/claude-sidechain-store.js.map +1 -0
- package/dist/persistence/claude-task-store.d.ts +72 -0
- package/dist/persistence/claude-task-store.d.ts.map +1 -0
- package/dist/persistence/claude-task-store.js +440 -0
- package/dist/persistence/claude-task-store.js.map +1 -0
- package/dist/persistence/claude-transcript-store.d.ts +85 -0
- package/dist/persistence/claude-transcript-store.d.ts.map +1 -0
- package/dist/persistence/claude-transcript-store.js +385 -0
- package/dist/persistence/claude-transcript-store.js.map +1 -0
- package/dist/persistence/claude-workflow-store.d.ts +41 -0
- package/dist/persistence/claude-workflow-store.d.ts.map +1 -0
- package/dist/persistence/claude-workflow-store.js +263 -0
- package/dist/persistence/claude-workflow-store.js.map +1 -0
- package/dist/persistence/in-memory-sidechain-store.d.ts +12 -0
- package/dist/persistence/in-memory-sidechain-store.d.ts.map +1 -0
- package/dist/persistence/in-memory-sidechain-store.js +30 -0
- package/dist/persistence/in-memory-sidechain-store.js.map +1 -0
- package/dist/persistence/in-memory-transcript-store.d.ts +15 -0
- package/dist/persistence/in-memory-transcript-store.d.ts.map +1 -0
- package/dist/persistence/in-memory-transcript-store.js +95 -0
- package/dist/persistence/in-memory-transcript-store.js.map +1 -0
- package/dist/platform/atomic-write.d.ts +5 -0
- package/dist/platform/atomic-write.d.ts.map +1 -0
- package/dist/platform/atomic-write.js +25 -0
- package/dist/platform/atomic-write.js.map +1 -0
- package/dist/platform/bounded-process-runner.d.ts +28 -0
- package/dist/platform/bounded-process-runner.d.ts.map +1 -0
- package/dist/platform/bounded-process-runner.js +209 -0
- package/dist/platform/bounded-process-runner.js.map +1 -0
- package/dist/platform/claude-version.d.ts +5 -0
- package/dist/platform/claude-version.d.ts.map +1 -0
- package/dist/platform/claude-version.js +17 -0
- package/dist/platform/claude-version.js.map +1 -0
- package/dist/platform/command-shell.d.ts +3 -0
- package/dist/platform/command-shell.d.ts.map +1 -0
- package/dist/platform/command-shell.js +15 -0
- package/dist/platform/command-shell.js.map +1 -0
- package/dist/platform/exclusive-file-lease.d.ts +15 -0
- package/dist/platform/exclusive-file-lease.d.ts.map +1 -0
- package/dist/platform/exclusive-file-lease.js +265 -0
- package/dist/platform/exclusive-file-lease.js.map +1 -0
- package/dist/platform/sensitive-data.d.ts +8 -0
- package/dist/platform/sensitive-data.d.ts.map +1 -0
- package/dist/platform/sensitive-data.js +138 -0
- package/dist/platform/sensitive-data.js.map +1 -0
- package/dist/platform/tmux-worktree.d.ts +30 -0
- package/dist/platform/tmux-worktree.d.ts.map +1 -0
- package/dist/platform/tmux-worktree.js +138 -0
- package/dist/platform/tmux-worktree.js.map +1 -0
- package/dist/plugins/claude-plugin-eval-discovery.d.ts +20 -0
- package/dist/plugins/claude-plugin-eval-discovery.d.ts.map +1 -0
- package/dist/plugins/claude-plugin-eval-discovery.js +128 -0
- package/dist/plugins/claude-plugin-eval-discovery.js.map +1 -0
- package/dist/plugins/claude-plugin-eval-graders.d.ts +49 -0
- package/dist/plugins/claude-plugin-eval-graders.d.ts.map +1 -0
- package/dist/plugins/claude-plugin-eval-graders.js +137 -0
- package/dist/plugins/claude-plugin-eval-graders.js.map +1 -0
- package/dist/plugins/claude-plugin-eval-runner.d.ts +50 -0
- package/dist/plugins/claude-plugin-eval-runner.d.ts.map +1 -0
- package/dist/plugins/claude-plugin-eval-runner.js +189 -0
- package/dist/plugins/claude-plugin-eval-runner.js.map +1 -0
- package/dist/plugins/claude-plugin-eval-schema.d.ts +80 -0
- package/dist/plugins/claude-plugin-eval-schema.d.ts.map +1 -0
- package/dist/plugins/claude-plugin-eval-schema.js +436 -0
- package/dist/plugins/claude-plugin-eval-schema.js.map +1 -0
- package/dist/plugins/claude-plugin-eval.d.ts +66 -0
- package/dist/plugins/claude-plugin-eval.d.ts.map +1 -0
- package/dist/plugins/claude-plugin-eval.js +438 -0
- package/dist/plugins/claude-plugin-eval.js.map +1 -0
- package/dist/plugins/claude-plugin-maintenance.d.ts +39 -0
- package/dist/plugins/claude-plugin-maintenance.d.ts.map +1 -0
- package/dist/plugins/claude-plugin-maintenance.js +336 -0
- package/dist/plugins/claude-plugin-maintenance.js.map +1 -0
- package/dist/plugins/claude-plugin-marketplace.d.ts +97 -0
- package/dist/plugins/claude-plugin-marketplace.d.ts.map +1 -0
- package/dist/plugins/claude-plugin-marketplace.js +1433 -0
- package/dist/plugins/claude-plugin-marketplace.js.map +1 -0
- package/dist/plugins/claude-plugin-mcpb.d.ts +71 -0
- package/dist/plugins/claude-plugin-mcpb.d.ts.map +1 -0
- package/dist/plugins/claude-plugin-mcpb.js +885 -0
- package/dist/plugins/claude-plugin-mcpb.js.map +1 -0
- package/dist/plugins/claude-plugin-runtime.d.ts +120 -0
- package/dist/plugins/claude-plugin-runtime.d.ts.map +1 -0
- package/dist/plugins/claude-plugin-runtime.js +1263 -0
- package/dist/plugins/claude-plugin-runtime.js.map +1 -0
- package/dist/providers/anthropic-compatible.d.ts +35 -0
- package/dist/providers/anthropic-compatible.d.ts.map +1 -0
- package/dist/providers/anthropic-compatible.js +733 -0
- package/dist/providers/anthropic-compatible.js.map +1 -0
- package/dist/providers/environment.d.ts +14 -0
- package/dist/providers/environment.d.ts.map +1 -0
- package/dist/providers/environment.js +67 -0
- package/dist/providers/environment.js.map +1 -0
- package/dist/providers/fallback-provider.d.ts +17 -0
- package/dist/providers/fallback-provider.d.ts.map +1 -0
- package/dist/providers/fallback-provider.js +70 -0
- package/dist/providers/fallback-provider.js.map +1 -0
- package/dist/providers/openai-compatible.d.ts +30 -0
- package/dist/providers/openai-compatible.d.ts.map +1 -0
- package/dist/providers/openai-compatible.js +409 -0
- package/dist/providers/openai-compatible.js.map +1 -0
- package/dist/tools/claude-interactive-tools.d.ts +58 -0
- package/dist/tools/claude-interactive-tools.d.ts.map +1 -0
- package/dist/tools/claude-interactive-tools.js +390 -0
- package/dist/tools/claude-interactive-tools.js.map +1 -0
- package/dist/tools/claude-lsp-formatters.d.ts +2 -0
- package/dist/tools/claude-lsp-formatters.d.ts.map +1 -0
- package/dist/tools/claude-lsp-formatters.js +291 -0
- package/dist/tools/claude-lsp-formatters.js.map +1 -0
- package/dist/tools/claude-lsp-tool.d.ts +22 -0
- package/dist/tools/claude-lsp-tool.d.ts.map +1 -0
- package/dist/tools/claude-lsp-tool.js +744 -0
- package/dist/tools/claude-lsp-tool.js.map +1 -0
- package/dist/tools/claude-scheduled-tools.d.ts +20 -0
- package/dist/tools/claude-scheduled-tools.d.ts.map +1 -0
- package/dist/tools/claude-scheduled-tools.js +344 -0
- package/dist/tools/claude-scheduled-tools.js.map +1 -0
- package/dist/tools/claude-task-tools.d.ts +30 -0
- package/dist/tools/claude-task-tools.d.ts.map +1 -0
- package/dist/tools/claude-task-tools.js +503 -0
- package/dist/tools/claude-task-tools.js.map +1 -0
- package/dist/tools/claude-user-message.d.ts +16 -0
- package/dist/tools/claude-user-message.d.ts.map +1 -0
- package/dist/tools/claude-user-message.js +98 -0
- package/dist/tools/claude-user-message.js.map +1 -0
- package/dist/tools/claude-workflow-tools.d.ts +27 -0
- package/dist/tools/claude-workflow-tools.d.ts.map +1 -0
- package/dist/tools/claude-workflow-tools.js +257 -0
- package/dist/tools/claude-workflow-tools.js.map +1 -0
- package/dist/tools/claude-worktree-tools.d.ts +17 -0
- package/dist/tools/claude-worktree-tools.d.ts.map +1 -0
- package/dist/tools/claude-worktree-tools.js +135 -0
- package/dist/tools/claude-worktree-tools.js.map +1 -0
- package/dist/tools/filtered-tool-registry.d.ts +16 -0
- package/dist/tools/filtered-tool-registry.d.ts.map +1 -0
- package/dist/tools/filtered-tool-registry.js +51 -0
- package/dist/tools/filtered-tool-registry.js.map +1 -0
- package/dist/tools/glob.d.ts +9 -0
- package/dist/tools/glob.d.ts.map +1 -0
- package/dist/tools/glob.js +88 -0
- package/dist/tools/glob.js.map +1 -0
- package/dist/tools/local-tools.d.ts +47 -0
- package/dist/tools/local-tools.d.ts.map +1 -0
- package/dist/tools/local-tools.js +1082 -0
- package/dist/tools/local-tools.js.map +1 -0
- package/dist/tools/notebook.d.ts +22 -0
- package/dist/tools/notebook.d.ts.map +1 -0
- package/dist/tools/notebook.js +154 -0
- package/dist/tools/notebook.js.map +1 -0
- package/dist/tools/web.d.ts +48 -0
- package/dist/tools/web.d.ts.map +1 -0
- package/dist/tools/web.js +726 -0
- package/dist/tools/web.js.map +1 -0
- package/package.json +145 -0
|
@@ -0,0 +1,1246 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { mkdir, mkdtemp, open, rm } from 'node:fs/promises';
|
|
3
|
+
import { homedir, tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
6
|
+
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
7
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
8
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
9
|
+
import { ElicitRequestSchema, ElicitationCompleteNotificationSchema, PromptListChangedNotificationSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
10
|
+
import { redactSensitiveError, redactSensitiveText, redactSensitiveValue, sanitizeChildEnvironment, sensitiveEnvironmentValues, } from '../platform/sensitive-data.js';
|
|
11
|
+
import { loadMcpOAuthProvider, mcpOAuthServerIdentity, } from './claude-mcp-oauth.js';
|
|
12
|
+
const MAX_TOOL_PAGES = 100;
|
|
13
|
+
const MAX_TOOLS = 10_000;
|
|
14
|
+
const MAX_RESOURCE_PAGES = 100;
|
|
15
|
+
const MAX_PROMPT_PAGES = 100;
|
|
16
|
+
function normalizeMcpName(name) {
|
|
17
|
+
return name.replace(/[^a-zA-Z0-9_-]/gu, '_');
|
|
18
|
+
}
|
|
19
|
+
function sanitizeMcpUnicode(value) {
|
|
20
|
+
return value
|
|
21
|
+
.normalize('NFKC')
|
|
22
|
+
.replace(/[\p{Cf}\p{Co}\p{Cn}]/gu, '')
|
|
23
|
+
.replace(/[\u200B-\u200F\u202A-\u202E\u2066-\u2069\uFEFF\uE000-\uF8FF]/gu, '');
|
|
24
|
+
}
|
|
25
|
+
const MAX_RESOURCES = 10_000;
|
|
26
|
+
const MAX_PROMPTS = 10_000;
|
|
27
|
+
const MAX_RESOURCE_BYTES = 25 * 1024 * 1024;
|
|
28
|
+
const DISCOVERY_TIMEOUT_MS = 10_000;
|
|
29
|
+
const RESOURCE_TIMEOUT_MS = 30_000;
|
|
30
|
+
const NO_MCP_RESOURCES = 'No resources found. MCP servers may still provide tools even if they have no resources.';
|
|
31
|
+
const MCP_RESOURCE_TOOL_DEFINITIONS = [
|
|
32
|
+
{
|
|
33
|
+
name: 'ListMcpResourcesTool',
|
|
34
|
+
description: 'list resources from connected MCP servers',
|
|
35
|
+
inputSchema: {
|
|
36
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: {
|
|
39
|
+
server: {
|
|
40
|
+
description: 'Optional server name to filter resources by',
|
|
41
|
+
type: 'string',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
additionalProperties: false,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'ReadMcpResourceDirTool',
|
|
49
|
+
description: 'list the children of an MCP directory resource',
|
|
50
|
+
inputSchema: {
|
|
51
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {
|
|
54
|
+
server: { description: 'The MCP server name', type: 'string' },
|
|
55
|
+
uri: {
|
|
56
|
+
description: 'The directory resource URI to list',
|
|
57
|
+
type: 'string',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
required: ['server', 'uri'],
|
|
61
|
+
additionalProperties: false,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'ReadMcpResourceTool',
|
|
66
|
+
description: 'read a specific MCP resource by URI',
|
|
67
|
+
inputSchema: {
|
|
68
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: {
|
|
71
|
+
server: { description: 'The MCP server name', type: 'string' },
|
|
72
|
+
uri: { description: 'The resource URI to read', type: 'string' },
|
|
73
|
+
},
|
|
74
|
+
required: ['server', 'uri'],
|
|
75
|
+
additionalProperties: false,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
function isRecord(value) {
|
|
80
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
81
|
+
}
|
|
82
|
+
function stringRecord(value, label) {
|
|
83
|
+
if (value === undefined)
|
|
84
|
+
return {};
|
|
85
|
+
if (!isRecord(value) ||
|
|
86
|
+
Object.values(value).some((item) => typeof item !== 'string')) {
|
|
87
|
+
throw new Error(`${label} must contain only string values`);
|
|
88
|
+
}
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
function parseServerConfig(name, value, path) {
|
|
92
|
+
if (!isRecord(value))
|
|
93
|
+
throw new Error(`Invalid MCP server ${name}: ${path}`);
|
|
94
|
+
if (value.type !== undefined &&
|
|
95
|
+
value.type !== 'stdio' &&
|
|
96
|
+
value.type !== 'http' &&
|
|
97
|
+
value.type !== 'sse') {
|
|
98
|
+
throw new Error(`Unsupported MCP server transport ${name}: ${path}`);
|
|
99
|
+
}
|
|
100
|
+
if (value.type === 'http' ||
|
|
101
|
+
value.type === 'sse' ||
|
|
102
|
+
value.url !== undefined) {
|
|
103
|
+
if (value.type !== undefined &&
|
|
104
|
+
value.type !== 'http' &&
|
|
105
|
+
value.type !== 'sse') {
|
|
106
|
+
throw new Error(`Unsupported MCP server transport ${name}: ${path}`);
|
|
107
|
+
}
|
|
108
|
+
if (typeof value.url !== 'string' || value.url.length === 0) {
|
|
109
|
+
throw new Error(`Invalid MCP server URL ${name}: ${path}`);
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
type: value.type === 'sse' ? 'sse' : 'http',
|
|
113
|
+
url: value.url,
|
|
114
|
+
headers: stringRecord(value.headers, `MCP server ${name} headers`),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (typeof value.command !== 'string' || value.command.length === 0) {
|
|
118
|
+
throw new Error(`Invalid MCP server command ${name}: ${path}`);
|
|
119
|
+
}
|
|
120
|
+
const args = value.args ?? [];
|
|
121
|
+
if (!Array.isArray(args) || args.some((item) => typeof item !== 'string')) {
|
|
122
|
+
throw new Error(`MCP server ${name} args must contain only strings`);
|
|
123
|
+
}
|
|
124
|
+
if (value.cwd !== undefined && typeof value.cwd !== 'string') {
|
|
125
|
+
throw new Error(`MCP server ${name} cwd must be a string`);
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
type: 'stdio',
|
|
129
|
+
command: value.command,
|
|
130
|
+
args,
|
|
131
|
+
env: stringRecord(value.env, `MCP server ${name} env`),
|
|
132
|
+
...(typeof value.cwd === 'string' ? { cwd: value.cwd } : {}),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function configuredServers(resources, onWarning) {
|
|
136
|
+
const servers = new Map();
|
|
137
|
+
for (const resource of resources) {
|
|
138
|
+
if (!isRecord(resource.value))
|
|
139
|
+
continue;
|
|
140
|
+
const value = resource.value.mcpServers;
|
|
141
|
+
if (value === undefined)
|
|
142
|
+
continue;
|
|
143
|
+
if (!isRecord(value)) {
|
|
144
|
+
onWarning?.(`Invalid Claude MCP resource: ${resource.path}`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
for (const [name, config] of Object.entries(value)) {
|
|
148
|
+
if (name.length === 0) {
|
|
149
|
+
onWarning?.(`Invalid empty MCP server name: ${resource.path}`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
servers.set(name, {
|
|
153
|
+
name,
|
|
154
|
+
value: config,
|
|
155
|
+
path: resource.path,
|
|
156
|
+
plugin: resource.plugin === true,
|
|
157
|
+
...(resource.sensitiveValues === undefined
|
|
158
|
+
? {}
|
|
159
|
+
: { sensitiveValues: resource.sensitiveValues }),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const configured = [...servers.values()];
|
|
164
|
+
const manualSignatures = new Set(configured
|
|
165
|
+
.filter((server) => !server.plugin)
|
|
166
|
+
.map((server) => mcpServerSignature(server.value))
|
|
167
|
+
.filter((signature) => signature !== undefined));
|
|
168
|
+
const pluginSignatures = new Set();
|
|
169
|
+
return configured.filter((server) => {
|
|
170
|
+
if (!server.plugin)
|
|
171
|
+
return true;
|
|
172
|
+
const signature = mcpServerSignature(server.value);
|
|
173
|
+
if (signature === undefined)
|
|
174
|
+
return true;
|
|
175
|
+
if (manualSignatures.has(signature) || pluginSignatures.has(signature)) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
pluginSignatures.add(signature);
|
|
179
|
+
return true;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function mcpServerSignature(value) {
|
|
183
|
+
if (!isRecord(value))
|
|
184
|
+
return undefined;
|
|
185
|
+
if (typeof value.url === 'string') {
|
|
186
|
+
let url = value.url;
|
|
187
|
+
if (url.includes('/v2/session_ingress/shttp/mcp/') ||
|
|
188
|
+
url.includes('/v2/ccr-sessions/')) {
|
|
189
|
+
try {
|
|
190
|
+
url = new URL(url).searchParams.get('mcp_url') ?? url;
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// Invalid URLs are validated later; preserve their literal signature.
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return `url:${url}`;
|
|
197
|
+
}
|
|
198
|
+
if (typeof value.command !== 'string')
|
|
199
|
+
return undefined;
|
|
200
|
+
const args = value.args ?? [];
|
|
201
|
+
if (!Array.isArray(args) || !args.every((arg) => typeof arg === 'string')) {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
return `stdio:${JSON.stringify([value.command, ...args])}`;
|
|
205
|
+
}
|
|
206
|
+
export function validateClaudeMcpConfiguration(resources) {
|
|
207
|
+
const warnings = [];
|
|
208
|
+
const servers = configuredServers(resources, (message) => warnings.push(message)).map((server) => {
|
|
209
|
+
const config = parseServerConfig(server.name, server.value, server.path);
|
|
210
|
+
if (config.type !== 'stdio') {
|
|
211
|
+
let endpoint;
|
|
212
|
+
try {
|
|
213
|
+
endpoint = new URL(config.url);
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
throw new Error(`Invalid MCP server URL ${server.name}: ${server.path}`, {
|
|
217
|
+
cause: error,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (endpoint.protocol !== 'http:' && endpoint.protocol !== 'https:') {
|
|
221
|
+
throw new Error(`MCP server URL must use http or https ${server.name}: ${server.path}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
name: server.name,
|
|
226
|
+
path: server.path,
|
|
227
|
+
transport: config.type,
|
|
228
|
+
...(config.type === 'stdio'
|
|
229
|
+
? {
|
|
230
|
+
command: config.command,
|
|
231
|
+
...(config.cwd === undefined ? {} : { cwd: config.cwd }),
|
|
232
|
+
}
|
|
233
|
+
: {}),
|
|
234
|
+
};
|
|
235
|
+
});
|
|
236
|
+
return { servers, warnings };
|
|
237
|
+
}
|
|
238
|
+
async function transport(serverName, config, cwd, configRoot) {
|
|
239
|
+
if (config.type === 'stdio') {
|
|
240
|
+
return new StdioClientTransport({
|
|
241
|
+
command: config.command,
|
|
242
|
+
args: config.args,
|
|
243
|
+
env: sanitizeChildEnvironment(config.env),
|
|
244
|
+
cwd: config.cwd ?? cwd,
|
|
245
|
+
stderr: 'pipe',
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
const requestInit = { headers: config.headers };
|
|
249
|
+
const identity = mcpOAuthServerIdentity(serverName, config);
|
|
250
|
+
const authProvider = await loadMcpOAuthProvider(configRoot, identity);
|
|
251
|
+
return config.type === 'sse'
|
|
252
|
+
? new SSEClientTransport(new URL(config.url), {
|
|
253
|
+
requestInit,
|
|
254
|
+
...(authProvider ? { authProvider } : {}),
|
|
255
|
+
})
|
|
256
|
+
: new StreamableHTTPClientTransport(new URL(config.url), {
|
|
257
|
+
requestInit,
|
|
258
|
+
...(authProvider ? { authProvider } : {}),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
function toolContent(result, sensitiveValues) {
|
|
262
|
+
if (!Array.isArray(result.content)) {
|
|
263
|
+
throw new Error('MCP tool result content must be an array');
|
|
264
|
+
}
|
|
265
|
+
const parts = result.content.map((item) => isRecord(item) && item.type === 'text' && typeof item.text === 'string'
|
|
266
|
+
? item.text
|
|
267
|
+
: JSON.stringify(item));
|
|
268
|
+
if (result.structuredContent !== undefined) {
|
|
269
|
+
parts.push(JSON.stringify(result.structuredContent));
|
|
270
|
+
}
|
|
271
|
+
return redactSensitiveText(parts.join('\n'), sensitiveValues);
|
|
272
|
+
}
|
|
273
|
+
const INVALID_PERMISSION_RESULT = "The permission prompt tool returned an invalid permission result. Expected {behavior: 'allow', updatedInput?: object} or {behavior: 'deny', message: string}.";
|
|
274
|
+
function invalidPermissionResult() {
|
|
275
|
+
return { behavior: 'deny', message: INVALID_PERMISSION_RESULT };
|
|
276
|
+
}
|
|
277
|
+
function parsePermissionResult(source) {
|
|
278
|
+
const value = JSON.parse(source);
|
|
279
|
+
if (!isRecord(value))
|
|
280
|
+
return invalidPermissionResult();
|
|
281
|
+
if (value.behavior === 'allow') {
|
|
282
|
+
if (value.updatedInput === undefined)
|
|
283
|
+
return { behavior: 'allow' };
|
|
284
|
+
if (!isRecord(value.updatedInput))
|
|
285
|
+
return invalidPermissionResult();
|
|
286
|
+
return { behavior: 'allow', updatedInput: value.updatedInput };
|
|
287
|
+
}
|
|
288
|
+
if (value.behavior === 'deny' && typeof value.message === 'string') {
|
|
289
|
+
return {
|
|
290
|
+
behavior: 'deny',
|
|
291
|
+
message: value.message,
|
|
292
|
+
...(typeof value.interrupt === 'boolean'
|
|
293
|
+
? { interrupt: value.interrupt }
|
|
294
|
+
: {}),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
return invalidPermissionResult();
|
|
298
|
+
}
|
|
299
|
+
function configSensitiveValues(config, additional = []) {
|
|
300
|
+
return [
|
|
301
|
+
...new Set([
|
|
302
|
+
...sensitiveEnvironmentValues(process.env, config.type === 'stdio' ? config.env : config.headers),
|
|
303
|
+
...additional,
|
|
304
|
+
]),
|
|
305
|
+
]
|
|
306
|
+
.filter((value) => value.length > 0)
|
|
307
|
+
.sort((left, right) => right.length - left.length);
|
|
308
|
+
}
|
|
309
|
+
function requiredString(input, name) {
|
|
310
|
+
const value = input[name];
|
|
311
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
312
|
+
throw new Error(`${name} must be a non-empty string`);
|
|
313
|
+
}
|
|
314
|
+
return value;
|
|
315
|
+
}
|
|
316
|
+
function optionalString(input, name) {
|
|
317
|
+
const value = input[name];
|
|
318
|
+
if (value === undefined)
|
|
319
|
+
return undefined;
|
|
320
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
321
|
+
throw new Error(`${name} must be a non-empty string`);
|
|
322
|
+
}
|
|
323
|
+
return value;
|
|
324
|
+
}
|
|
325
|
+
function requestSignal(signal, timeout) {
|
|
326
|
+
const timeoutSignal = AbortSignal.timeout(timeout);
|
|
327
|
+
return signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
328
|
+
}
|
|
329
|
+
function resourceExtension(mimeType) {
|
|
330
|
+
switch (mimeType) {
|
|
331
|
+
case 'image/png':
|
|
332
|
+
return '.png';
|
|
333
|
+
case 'image/jpeg':
|
|
334
|
+
return '.jpg';
|
|
335
|
+
case 'image/gif':
|
|
336
|
+
return '.gif';
|
|
337
|
+
case 'image/webp':
|
|
338
|
+
return '.webp';
|
|
339
|
+
case 'application/pdf':
|
|
340
|
+
return '.pdf';
|
|
341
|
+
case 'application/json':
|
|
342
|
+
return '.json';
|
|
343
|
+
case 'audio/wav':
|
|
344
|
+
return '.wav';
|
|
345
|
+
case 'audio/mpeg':
|
|
346
|
+
return '.mp3';
|
|
347
|
+
case 'audio/ogg':
|
|
348
|
+
return '.ogg';
|
|
349
|
+
case 'audio/flac':
|
|
350
|
+
return '.flac';
|
|
351
|
+
case 'text/plain':
|
|
352
|
+
return '.txt';
|
|
353
|
+
default:
|
|
354
|
+
return '.bin';
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function decodeResourceBlob(value) {
|
|
358
|
+
if (value.length > Math.ceil((MAX_RESOURCE_BYTES * 4) / 3) + 4) {
|
|
359
|
+
throw new Error(`MCP resource exceeded ${MAX_RESOURCE_BYTES} bytes`);
|
|
360
|
+
}
|
|
361
|
+
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) {
|
|
362
|
+
throw new Error('MCP resource blob must be valid base64');
|
|
363
|
+
}
|
|
364
|
+
const bytes = Buffer.from(value, 'base64');
|
|
365
|
+
if (bytes.length > MAX_RESOURCE_BYTES) {
|
|
366
|
+
throw new Error(`MCP resource exceeded ${MAX_RESOURCE_BYTES} bytes`);
|
|
367
|
+
}
|
|
368
|
+
return bytes;
|
|
369
|
+
}
|
|
370
|
+
async function writeResourceBlob(directory, bytes, extension, index) {
|
|
371
|
+
await mkdir(directory, { recursive: true });
|
|
372
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
373
|
+
const filePath = join(directory, `mcp-resource-${Date.now()}-${index}-${randomBytes(3).toString('hex')}${extension}`);
|
|
374
|
+
let handle;
|
|
375
|
+
let created = false;
|
|
376
|
+
try {
|
|
377
|
+
handle = await open(filePath, 'wx', 0o600);
|
|
378
|
+
created = true;
|
|
379
|
+
await handle.writeFile(bytes);
|
|
380
|
+
await handle.sync();
|
|
381
|
+
await handle.close();
|
|
382
|
+
return filePath;
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
await handle?.close().catch(() => undefined);
|
|
386
|
+
if (created)
|
|
387
|
+
await rm(filePath, { force: true }).catch(() => undefined);
|
|
388
|
+
if (error.code !== 'EEXIST')
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
throw new Error('Could not reserve MCP resource output path');
|
|
393
|
+
}
|
|
394
|
+
const MCP_IMAGE_MEDIA_TYPES = new Set([
|
|
395
|
+
'image/png',
|
|
396
|
+
'image/jpeg',
|
|
397
|
+
'image/gif',
|
|
398
|
+
'image/webp',
|
|
399
|
+
]);
|
|
400
|
+
async function writeToolBlob(directory, serverName, bytes, extension) {
|
|
401
|
+
await mkdir(directory, { recursive: true });
|
|
402
|
+
const safeServer = serverName.replaceAll(/[^A-Za-z0-9_-]/gu, '-');
|
|
403
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
404
|
+
const filePath = join(directory, `mcp-${safeServer}-blob-${Date.now()}-${randomBytes(3).toString('hex')}${extension}`);
|
|
405
|
+
let handle;
|
|
406
|
+
let created = false;
|
|
407
|
+
try {
|
|
408
|
+
handle = await open(filePath, 'wx', 0o600);
|
|
409
|
+
created = true;
|
|
410
|
+
await handle.writeFile(bytes);
|
|
411
|
+
await handle.sync();
|
|
412
|
+
await handle.close();
|
|
413
|
+
return filePath;
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
await handle?.close().catch(() => undefined);
|
|
417
|
+
if (created)
|
|
418
|
+
await rm(filePath, { force: true }).catch(() => undefined);
|
|
419
|
+
if (error.code !== 'EEXIST')
|
|
420
|
+
throw error;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
throw new Error('Could not reserve MCP tool blob output path');
|
|
424
|
+
}
|
|
425
|
+
function mcpTextBlock(text, sensitiveValues) {
|
|
426
|
+
return { type: 'text', text: redactSensitiveText(text, sensitiveValues) };
|
|
427
|
+
}
|
|
428
|
+
async function mcpBinaryText(options, sensitiveValues, createdFiles) {
|
|
429
|
+
if (!options.context.toolResultDirectory) {
|
|
430
|
+
throw new Error('MCP binary tool result output directory is unavailable');
|
|
431
|
+
}
|
|
432
|
+
const filePath = await writeToolBlob(options.context.toolResultDirectory, options.serverName, options.bytes, resourceExtension(options.mimeType));
|
|
433
|
+
createdFiles.push(filePath);
|
|
434
|
+
const origin = options.kind === 'Audio'
|
|
435
|
+
? `Audio from ${options.serverName}`
|
|
436
|
+
: `Resource from ${options.serverName} at ${options.uri}`;
|
|
437
|
+
return mcpTextBlock(`[${origin}] Binary content (${options.mimeType}, ${options.bytes.length} bytes) saved to ${filePath}`, sensitiveValues);
|
|
438
|
+
}
|
|
439
|
+
async function mcpToolResultUnchecked(serverName, result, context, sensitiveValues, createdFiles) {
|
|
440
|
+
if (!Array.isArray(result.content)) {
|
|
441
|
+
throw new Error('MCP tool result content must be an array');
|
|
442
|
+
}
|
|
443
|
+
const blocks = [];
|
|
444
|
+
let resultBytes = 0;
|
|
445
|
+
const consumeBytes = (bytes) => {
|
|
446
|
+
resultBytes += bytes;
|
|
447
|
+
if (resultBytes > MAX_RESOURCE_BYTES) {
|
|
448
|
+
throw new Error(`MCP tool result exceeded ${MAX_RESOURCE_BYTES} bytes`);
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
const structuredContent = result.structuredContent === undefined
|
|
452
|
+
? undefined
|
|
453
|
+
: redactSensitiveValue(result.structuredContent, sensitiveValues);
|
|
454
|
+
for (const item of result.content) {
|
|
455
|
+
if (!isRecord(item) || typeof item.type !== 'string') {
|
|
456
|
+
throw new Error('Invalid MCP tool result content block');
|
|
457
|
+
}
|
|
458
|
+
if (item.type === 'text') {
|
|
459
|
+
if (typeof item.text !== 'string') {
|
|
460
|
+
throw new Error('MCP text result requires text');
|
|
461
|
+
}
|
|
462
|
+
consumeBytes(Buffer.byteLength(item.text));
|
|
463
|
+
if (structuredContent === undefined) {
|
|
464
|
+
blocks.push(mcpTextBlock(item.text, sensitiveValues));
|
|
465
|
+
}
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
if (item.type === 'image') {
|
|
469
|
+
if (typeof item.mimeType !== 'string' ||
|
|
470
|
+
!MCP_IMAGE_MEDIA_TYPES.has(item.mimeType) ||
|
|
471
|
+
typeof item.data !== 'string') {
|
|
472
|
+
throw new Error('MCP image result is invalid or unsupported');
|
|
473
|
+
}
|
|
474
|
+
const bytes = decodeResourceBlob(item.data);
|
|
475
|
+
consumeBytes(bytes.length);
|
|
476
|
+
blocks.push({
|
|
477
|
+
type: 'image',
|
|
478
|
+
mediaType: item.mimeType,
|
|
479
|
+
data: item.data,
|
|
480
|
+
});
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (item.type === 'audio') {
|
|
484
|
+
if (typeof item.mimeType !== 'string' || typeof item.data !== 'string') {
|
|
485
|
+
throw new Error('MCP audio result is invalid');
|
|
486
|
+
}
|
|
487
|
+
const bytes = decodeResourceBlob(item.data);
|
|
488
|
+
consumeBytes(bytes.length);
|
|
489
|
+
blocks.push(await mcpBinaryText({
|
|
490
|
+
kind: 'Audio',
|
|
491
|
+
serverName,
|
|
492
|
+
mimeType: item.mimeType,
|
|
493
|
+
bytes,
|
|
494
|
+
context,
|
|
495
|
+
}, sensitiveValues, createdFiles));
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
if (item.type === 'resource_link') {
|
|
499
|
+
if (typeof item.name !== 'string' || typeof item.uri !== 'string') {
|
|
500
|
+
throw new Error('MCP resource link result is invalid');
|
|
501
|
+
}
|
|
502
|
+
consumeBytes(Buffer.byteLength(item.name) +
|
|
503
|
+
Buffer.byteLength(item.uri) +
|
|
504
|
+
(typeof item.description === 'string'
|
|
505
|
+
? Buffer.byteLength(item.description)
|
|
506
|
+
: 0));
|
|
507
|
+
blocks.push(mcpTextBlock(`[Resource link: ${item.name}] ${item.uri}${typeof item.description === 'string' ? ` (${item.description})` : ''}`, sensitiveValues));
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
if (item.type === 'resource') {
|
|
511
|
+
if (!isRecord(item.resource) || typeof item.resource.uri !== 'string') {
|
|
512
|
+
throw new Error('MCP embedded resource result is invalid');
|
|
513
|
+
}
|
|
514
|
+
const resource = item.resource;
|
|
515
|
+
const resourceUri = String(resource.uri);
|
|
516
|
+
if (typeof resource.text === 'string') {
|
|
517
|
+
consumeBytes(Buffer.byteLength(resourceUri) + Buffer.byteLength(resource.text));
|
|
518
|
+
blocks.push(mcpTextBlock(`[Resource from ${serverName} at ${resourceUri}] ${resource.text}`, sensitiveValues));
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (typeof resource.blob === 'string') {
|
|
522
|
+
const bytes = decodeResourceBlob(resource.blob);
|
|
523
|
+
consumeBytes(bytes.length);
|
|
524
|
+
blocks.push(await mcpBinaryText({
|
|
525
|
+
kind: 'Resource',
|
|
526
|
+
serverName,
|
|
527
|
+
uri: resourceUri,
|
|
528
|
+
mimeType: typeof resource.mimeType === 'string'
|
|
529
|
+
? resource.mimeType
|
|
530
|
+
: 'application/octet-stream',
|
|
531
|
+
bytes,
|
|
532
|
+
context,
|
|
533
|
+
}, sensitiveValues, createdFiles));
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
throw new Error('MCP embedded resource requires text or blob');
|
|
537
|
+
}
|
|
538
|
+
const serialized = JSON.stringify(redactSensitiveValue(item, sensitiveValues));
|
|
539
|
+
consumeBytes(Buffer.byteLength(serialized));
|
|
540
|
+
blocks.push(mcpTextBlock(serialized, sensitiveValues));
|
|
541
|
+
}
|
|
542
|
+
if (structuredContent !== undefined) {
|
|
543
|
+
const serialized = JSON.stringify(structuredContent);
|
|
544
|
+
consumeBytes(Buffer.byteLength(serialized));
|
|
545
|
+
blocks.push(mcpTextBlock(serialized, sensitiveValues));
|
|
546
|
+
}
|
|
547
|
+
const content = blocks
|
|
548
|
+
.filter((block) => block.type === 'text')
|
|
549
|
+
.map((block) => block.text)
|
|
550
|
+
.join('\n');
|
|
551
|
+
if (Buffer.byteLength(content) > MAX_RESOURCE_BYTES) {
|
|
552
|
+
throw new Error(`MCP tool result exceeded ${MAX_RESOURCE_BYTES} bytes`);
|
|
553
|
+
}
|
|
554
|
+
const images = blocks.filter((block) => block.type === 'image');
|
|
555
|
+
return {
|
|
556
|
+
content,
|
|
557
|
+
contentBlocks: blocks,
|
|
558
|
+
...(images.length > 0 ? { images } : {}),
|
|
559
|
+
isError: result.isError === true,
|
|
560
|
+
...(structuredContent === undefined
|
|
561
|
+
? {}
|
|
562
|
+
: { nativeMcpMeta: { structuredContent } }),
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
async function mcpToolResult(serverName, result, context, sensitiveValues) {
|
|
566
|
+
const createdFiles = [];
|
|
567
|
+
try {
|
|
568
|
+
return await mcpToolResultUnchecked(serverName, result, context, sensitiveValues, createdFiles);
|
|
569
|
+
}
|
|
570
|
+
catch (error) {
|
|
571
|
+
await Promise.all(createdFiles.map((filePath) => rm(filePath, { force: true }).catch(() => undefined)));
|
|
572
|
+
throw error;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
function isMissingResourceError(error) {
|
|
576
|
+
return (isRecord(error) &&
|
|
577
|
+
(error.code === -32002 || String(error.message).includes('-32002')));
|
|
578
|
+
}
|
|
579
|
+
async function resourceContent(server, result, context, sensitiveValues) {
|
|
580
|
+
if (!Array.isArray(result.contents)) {
|
|
581
|
+
throw new Error('MCP resource result contents must be an array');
|
|
582
|
+
}
|
|
583
|
+
const contents = [];
|
|
584
|
+
const createdFiles = [];
|
|
585
|
+
let contentBytes = 0;
|
|
586
|
+
const consumeBytes = (bytes) => {
|
|
587
|
+
contentBytes += bytes;
|
|
588
|
+
if (contentBytes > MAX_RESOURCE_BYTES) {
|
|
589
|
+
throw new Error(`MCP resource result exceeded ${MAX_RESOURCE_BYTES} bytes`);
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
try {
|
|
593
|
+
for (let index = 0; index < result.contents.length; index += 1) {
|
|
594
|
+
const item = result.contents[index];
|
|
595
|
+
if (!isRecord(item) || typeof item.uri !== 'string') {
|
|
596
|
+
throw new Error('Invalid MCP resource content');
|
|
597
|
+
}
|
|
598
|
+
if (typeof item.text === 'string') {
|
|
599
|
+
consumeBytes(Buffer.byteLength(item.text));
|
|
600
|
+
contents.push({
|
|
601
|
+
uri: item.uri,
|
|
602
|
+
...(typeof item.mimeType === 'string'
|
|
603
|
+
? { mimeType: item.mimeType }
|
|
604
|
+
: {}),
|
|
605
|
+
text: item.text,
|
|
606
|
+
});
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
if (typeof item.blob !== 'string') {
|
|
610
|
+
throw new Error('MCP resource content requires text or blob');
|
|
611
|
+
}
|
|
612
|
+
if (!context.toolResultDirectory) {
|
|
613
|
+
throw new Error('MCP binary resource output directory is unavailable');
|
|
614
|
+
}
|
|
615
|
+
const bytes = decodeResourceBlob(item.blob);
|
|
616
|
+
consumeBytes(bytes.length);
|
|
617
|
+
const filePath = await writeResourceBlob(context.toolResultDirectory, bytes, resourceExtension(item.mimeType), index);
|
|
618
|
+
createdFiles.push(filePath);
|
|
619
|
+
const mimeType = typeof item.mimeType === 'string'
|
|
620
|
+
? item.mimeType
|
|
621
|
+
: 'application/octet-stream';
|
|
622
|
+
contents.push({
|
|
623
|
+
uri: item.uri,
|
|
624
|
+
...(typeof item.mimeType === 'string'
|
|
625
|
+
? { mimeType: item.mimeType }
|
|
626
|
+
: {}),
|
|
627
|
+
blobSavedTo: filePath,
|
|
628
|
+
text: `[Resource from ${server} at ${item.uri}] Binary content (${mimeType}, ${bytes.length} bytes) saved to ${filePath}`,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
const serialized = JSON.stringify(redactSensitiveValue({ contents }, sensitiveValues));
|
|
632
|
+
if (Buffer.byteLength(serialized) > MAX_RESOURCE_BYTES) {
|
|
633
|
+
throw new Error(`MCP resource result exceeded ${MAX_RESOURCE_BYTES} bytes`);
|
|
634
|
+
}
|
|
635
|
+
return serialized;
|
|
636
|
+
}
|
|
637
|
+
catch (error) {
|
|
638
|
+
await Promise.allSettled(createdFiles.map((filePath) => rm(filePath, { force: true })));
|
|
639
|
+
throw error;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
export class ClaudeMcpToolRegistry {
|
|
643
|
+
options;
|
|
644
|
+
tools = new Map();
|
|
645
|
+
reservedTools = new Set();
|
|
646
|
+
resourceServers = new Map();
|
|
647
|
+
promptServers = new Map();
|
|
648
|
+
statuses = new Map();
|
|
649
|
+
clients = new Set();
|
|
650
|
+
serverClients = new Map();
|
|
651
|
+
reconnectableServers = new Map();
|
|
652
|
+
reconnectingServers = new Map();
|
|
653
|
+
promptOperations = new Set();
|
|
654
|
+
promptResultDirectoryPromise;
|
|
655
|
+
closePromise;
|
|
656
|
+
generation = 0;
|
|
657
|
+
closed = false;
|
|
658
|
+
constructor(options) {
|
|
659
|
+
this.options = options;
|
|
660
|
+
}
|
|
661
|
+
static async connect(options) {
|
|
662
|
+
const registry = new ClaudeMcpToolRegistry(options);
|
|
663
|
+
const ambientSensitiveValues = sensitiveEnvironmentValues(process.env);
|
|
664
|
+
const warn = (message) => options.onWarning?.(redactSensitiveText(message, ambientSensitiveValues));
|
|
665
|
+
try {
|
|
666
|
+
for (const server of configuredServers(options.resources, warn)) {
|
|
667
|
+
registry.statuses.set(server.name, {
|
|
668
|
+
name: server.name,
|
|
669
|
+
status: 'failed',
|
|
670
|
+
});
|
|
671
|
+
let config;
|
|
672
|
+
try {
|
|
673
|
+
config = parseServerConfig(server.name, server.value, server.path);
|
|
674
|
+
}
|
|
675
|
+
catch (error) {
|
|
676
|
+
warn(`MCP server ${server.name} unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
registry.reconnectableServers.set(server.name, {
|
|
680
|
+
config,
|
|
681
|
+
sensitiveValues: server.sensitiveValues ?? [],
|
|
682
|
+
});
|
|
683
|
+
await registry.connectServer(server.name, config, server.sensitiveValues);
|
|
684
|
+
}
|
|
685
|
+
registry.publishPrompts();
|
|
686
|
+
}
|
|
687
|
+
catch (error) {
|
|
688
|
+
await registry.close();
|
|
689
|
+
throw error;
|
|
690
|
+
}
|
|
691
|
+
return registry;
|
|
692
|
+
}
|
|
693
|
+
definitions() {
|
|
694
|
+
return [
|
|
695
|
+
...this.options.base.definitions(),
|
|
696
|
+
...[...this.tools.entries()]
|
|
697
|
+
.filter(([name]) => !this.reservedTools.has(name))
|
|
698
|
+
.map(([, tool]) => tool.definition),
|
|
699
|
+
...(this.resourceServers.size > 0 ? MCP_RESOURCE_TOOL_DEFINITIONS : []),
|
|
700
|
+
];
|
|
701
|
+
}
|
|
702
|
+
serverStatuses() {
|
|
703
|
+
return [...this.statuses.values()];
|
|
704
|
+
}
|
|
705
|
+
prompts() {
|
|
706
|
+
const prompts = [];
|
|
707
|
+
const names = new Set();
|
|
708
|
+
for (const serverPrompts of this.promptServers.values()) {
|
|
709
|
+
for (const prompt of serverPrompts) {
|
|
710
|
+
if (names.has(prompt.name))
|
|
711
|
+
continue;
|
|
712
|
+
names.add(prompt.name);
|
|
713
|
+
prompts.push({
|
|
714
|
+
name: prompt.name,
|
|
715
|
+
userFacingName: prompt.userFacingName,
|
|
716
|
+
description: prompt.description,
|
|
717
|
+
argumentNames: prompt.argumentNames,
|
|
718
|
+
invoke: (argumentsText, options) => this.trackPromptOperation(this.invokePrompt(prompt, argumentsText, options?.signal, true, options?.toolResultDirectory)),
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return prompts;
|
|
723
|
+
}
|
|
724
|
+
permissionPrompt(name) {
|
|
725
|
+
if (!this.tools.has(name)) {
|
|
726
|
+
const available = [...this.tools.keys()].join(', ') || 'none';
|
|
727
|
+
throw new Error(`MCP tool ${name} (from --permission-prompt-tool) not found. Available MCP tools: ${available}`);
|
|
728
|
+
}
|
|
729
|
+
this.reservedTools.add(name);
|
|
730
|
+
return async (call, originalCall = call) => {
|
|
731
|
+
const tool = this.tools.get(name);
|
|
732
|
+
if (!tool)
|
|
733
|
+
return invalidPermissionResult();
|
|
734
|
+
let result;
|
|
735
|
+
try {
|
|
736
|
+
result = await tool.client.callTool({
|
|
737
|
+
name: tool.toolName,
|
|
738
|
+
arguments: {
|
|
739
|
+
tool_name: originalCall.name,
|
|
740
|
+
input: originalCall.input,
|
|
741
|
+
tool_use_id: originalCall.id,
|
|
742
|
+
},
|
|
743
|
+
_meta: { 'claudecode/toolUseId': originalCall.id },
|
|
744
|
+
}, undefined, this.options.signal ? { signal: this.options.signal } : undefined);
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
return invalidPermissionResult();
|
|
748
|
+
}
|
|
749
|
+
if (!isRecord(result) || result.isError === true) {
|
|
750
|
+
return invalidPermissionResult();
|
|
751
|
+
}
|
|
752
|
+
try {
|
|
753
|
+
return parsePermissionResult(toolContent(result, tool.sensitiveValues));
|
|
754
|
+
}
|
|
755
|
+
catch {
|
|
756
|
+
return invalidPermissionResult();
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
async prepare(call, context) {
|
|
761
|
+
return this.tools.has(call.name) ||
|
|
762
|
+
MCP_RESOURCE_TOOL_DEFINITIONS.some((definition) => definition.name === call.name)
|
|
763
|
+
? call
|
|
764
|
+
: this.options.base.prepare(call, context);
|
|
765
|
+
}
|
|
766
|
+
async execute(call, context) {
|
|
767
|
+
if (call.name === 'ListMcpResourcesTool') {
|
|
768
|
+
return this.listResources(call.input);
|
|
769
|
+
}
|
|
770
|
+
if (call.name === 'ReadMcpResourceDirTool') {
|
|
771
|
+
this.resourceServer(requiredString(call.input, 'server'));
|
|
772
|
+
requiredString(call.input, 'uri');
|
|
773
|
+
return {
|
|
774
|
+
content: 'Directory listing is not enabled in this build.',
|
|
775
|
+
isError: false,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
if (call.name === 'ReadMcpResourceTool') {
|
|
779
|
+
return this.readResource(call.input, context);
|
|
780
|
+
}
|
|
781
|
+
const tool = this.tools.get(call.name);
|
|
782
|
+
if (!tool)
|
|
783
|
+
return this.options.base.execute(call, context);
|
|
784
|
+
let result;
|
|
785
|
+
try {
|
|
786
|
+
result = await tool.client.callTool({ name: tool.toolName, arguments: call.input }, undefined, context.signal ? { signal: context.signal } : undefined);
|
|
787
|
+
}
|
|
788
|
+
catch (error) {
|
|
789
|
+
throw redactSensitiveError(error, tool.sensitiveValues);
|
|
790
|
+
}
|
|
791
|
+
if (!isRecord(result))
|
|
792
|
+
throw new Error('Invalid MCP tool result');
|
|
793
|
+
return mcpToolResult(tool.serverName, result, context, tool.sensitiveValues);
|
|
794
|
+
}
|
|
795
|
+
close() {
|
|
796
|
+
if (this.closePromise)
|
|
797
|
+
return this.closePromise;
|
|
798
|
+
this.closed = true;
|
|
799
|
+
this.generation += 1;
|
|
800
|
+
this.closePromise = this.finishClose();
|
|
801
|
+
return this.closePromise;
|
|
802
|
+
}
|
|
803
|
+
async finishClose() {
|
|
804
|
+
await Promise.allSettled([...this.clients].map((client) => client.close()));
|
|
805
|
+
await Promise.allSettled([
|
|
806
|
+
...this.reconnectingServers.values(),
|
|
807
|
+
...this.promptOperations,
|
|
808
|
+
]);
|
|
809
|
+
await Promise.allSettled([...this.clients].map((client) => client.close()));
|
|
810
|
+
this.clients.clear();
|
|
811
|
+
this.serverClients.clear();
|
|
812
|
+
const directory = await this.promptResultDirectoryPromise?.catch(() => undefined);
|
|
813
|
+
if (directory)
|
|
814
|
+
await rm(directory, { recursive: true, force: true });
|
|
815
|
+
}
|
|
816
|
+
listResources(input) {
|
|
817
|
+
const requestedServer = optionalString(input, 'server');
|
|
818
|
+
let servers;
|
|
819
|
+
if (requestedServer) {
|
|
820
|
+
if (!this.statuses.has(requestedServer)) {
|
|
821
|
+
this.resourceServer(requestedServer);
|
|
822
|
+
}
|
|
823
|
+
const connected = this.resourceServers.get(requestedServer);
|
|
824
|
+
if (!connected)
|
|
825
|
+
return { content: NO_MCP_RESOURCES, isError: false };
|
|
826
|
+
servers = [[requestedServer, connected]];
|
|
827
|
+
}
|
|
828
|
+
else {
|
|
829
|
+
servers = [...this.resourceServers.entries()];
|
|
830
|
+
}
|
|
831
|
+
const resources = servers.flatMap(([server, connected]) => connected.resources.map((resource) => redactSensitiveValue({ ...resource, server }, connected.sensitiveValues)));
|
|
832
|
+
const serialized = resources.length > 0 ? JSON.stringify(resources) : null;
|
|
833
|
+
if (serialized !== null &&
|
|
834
|
+
Buffer.byteLength(serialized) > MAX_RESOURCE_BYTES) {
|
|
835
|
+
throw new Error(`MCP resource list exceeded ${MAX_RESOURCE_BYTES} bytes`);
|
|
836
|
+
}
|
|
837
|
+
return {
|
|
838
|
+
content: serialized ?? NO_MCP_RESOURCES,
|
|
839
|
+
isError: false,
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
async readResource(input, context) {
|
|
843
|
+
const serverName = requiredString(input, 'server');
|
|
844
|
+
const uri = requiredString(input, 'uri');
|
|
845
|
+
const server = this.resourceServer(serverName);
|
|
846
|
+
let result;
|
|
847
|
+
try {
|
|
848
|
+
result = await server.client.readResource({ uri }, {
|
|
849
|
+
timeout: RESOURCE_TIMEOUT_MS,
|
|
850
|
+
signal: requestSignal(context.signal, RESOURCE_TIMEOUT_MS),
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
catch (error) {
|
|
854
|
+
if (context.signal?.aborted)
|
|
855
|
+
throw error;
|
|
856
|
+
if (isMissingResourceError(error)) {
|
|
857
|
+
return {
|
|
858
|
+
content: `Resource not found: ${uri} \u2014 it may have been deleted or the URI is stale. Re-run ListMcpResourcesTool to refresh.`,
|
|
859
|
+
isError: false,
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
throw redactSensitiveError(error, server.sensitiveValues);
|
|
863
|
+
}
|
|
864
|
+
if (!isRecord(result))
|
|
865
|
+
throw new Error('Invalid MCP resource result');
|
|
866
|
+
return {
|
|
867
|
+
content: await resourceContent(serverName, result, context, server.sensitiveValues),
|
|
868
|
+
isError: false,
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
resourceServer(name) {
|
|
872
|
+
const server = this.resourceServers.get(name);
|
|
873
|
+
if (server)
|
|
874
|
+
return server;
|
|
875
|
+
if (this.statuses.get(name)?.status === 'failed') {
|
|
876
|
+
throw new Error(`Server "${name}" is not connected`);
|
|
877
|
+
}
|
|
878
|
+
if (this.statuses.get(name)?.status === 'connected') {
|
|
879
|
+
throw new Error(`Server "${name}" does not support resources`);
|
|
880
|
+
}
|
|
881
|
+
const available = [...this.statuses.keys()].join(', ');
|
|
882
|
+
throw new Error(`Server "${name}" not found. Available servers: ${available}`);
|
|
883
|
+
}
|
|
884
|
+
async connectServer(serverName, config, additionalSensitiveValues = [], expectedGeneration = this.generation) {
|
|
885
|
+
this.assertOpenGeneration(expectedGeneration);
|
|
886
|
+
this.statuses.set(serverName, { name: serverName, status: 'failed' });
|
|
887
|
+
const sensitiveValues = configSensitiveValues(config, additionalSensitiveValues);
|
|
888
|
+
const client = new Client({ name: 'praxis', version: '0.1.0' }, {
|
|
889
|
+
capabilities: {
|
|
890
|
+
elicitation: {
|
|
891
|
+
form: { applyDefaults: true },
|
|
892
|
+
url: {},
|
|
893
|
+
},
|
|
894
|
+
},
|
|
895
|
+
});
|
|
896
|
+
this.clients.add(client);
|
|
897
|
+
client.onclose = () => {
|
|
898
|
+
this.clients.delete(client);
|
|
899
|
+
if (this.closed || this.serverClients.get(serverName) !== client)
|
|
900
|
+
return;
|
|
901
|
+
this.statuses.set(serverName, { name: serverName, status: 'failed' });
|
|
902
|
+
};
|
|
903
|
+
client.setRequestHandler(ElicitRequestSchema, async (request) => {
|
|
904
|
+
if (!this.options.onElicitation)
|
|
905
|
+
return { action: 'decline' };
|
|
906
|
+
return (await this.options.onElicitation({
|
|
907
|
+
serverName,
|
|
908
|
+
message: redactSensitiveText(request.params.message, sensitiveValues),
|
|
909
|
+
...(request.params.mode ? { mode: request.params.mode } : {}),
|
|
910
|
+
...('url' in request.params
|
|
911
|
+
? { url: redactSensitiveText(request.params.url, sensitiveValues) }
|
|
912
|
+
: {}),
|
|
913
|
+
...('elicitationId' in request.params
|
|
914
|
+
? { elicitationId: request.params.elicitationId }
|
|
915
|
+
: {}),
|
|
916
|
+
...('requestedSchema' in request.params
|
|
917
|
+
? {
|
|
918
|
+
requestedSchema: redactSensitiveValue(request.params.requestedSchema, sensitiveValues),
|
|
919
|
+
}
|
|
920
|
+
: {}),
|
|
921
|
+
}));
|
|
922
|
+
});
|
|
923
|
+
client.setNotificationHandler(ElicitationCompleteNotificationSchema, async (notification) => {
|
|
924
|
+
this.options.eventSink?.({
|
|
925
|
+
type: 'elicitation-complete',
|
|
926
|
+
mcpServerName: serverName,
|
|
927
|
+
elicitationId: notification.params.elicitationId,
|
|
928
|
+
});
|
|
929
|
+
});
|
|
930
|
+
const discoverySignal = requestSignal(this.options.signal, DISCOVERY_TIMEOUT_MS);
|
|
931
|
+
try {
|
|
932
|
+
const configRoot = this.options.configRoot ??
|
|
933
|
+
process.env.CLAUDE_CONFIG_DIR ??
|
|
934
|
+
join(homedir(), '.claude');
|
|
935
|
+
await client.connect((await transport(serverName, config, this.options.cwd, configRoot)), {
|
|
936
|
+
timeout: DISCOVERY_TIMEOUT_MS,
|
|
937
|
+
signal: discoverySignal,
|
|
938
|
+
});
|
|
939
|
+
const capabilities = client.getServerCapabilities();
|
|
940
|
+
const tools = capabilities?.tools
|
|
941
|
+
? await this.discoverTools(client, serverName, sensitiveValues)
|
|
942
|
+
: [];
|
|
943
|
+
const resources = capabilities?.resources
|
|
944
|
+
? await this.discoverResources(client, serverName, sensitiveValues)
|
|
945
|
+
: [];
|
|
946
|
+
const prompts = capabilities?.prompts
|
|
947
|
+
? await this.discoverPrompts(client, serverName, sensitiveValues)
|
|
948
|
+
: [];
|
|
949
|
+
const connectedTools = new Map();
|
|
950
|
+
for (const tool of tools) {
|
|
951
|
+
const name = `mcp__${normalizeMcpName(serverName)}__${normalizeMcpName(tool.name)}`;
|
|
952
|
+
if (redactSensitiveText(name, sensitiveValues) !== name) {
|
|
953
|
+
throw new Error('MCP tool name contains sensitive data');
|
|
954
|
+
}
|
|
955
|
+
if (this.tools.has(name) || connectedTools.has(name)) {
|
|
956
|
+
throw new Error(`Duplicate MCP tool ${name}`);
|
|
957
|
+
}
|
|
958
|
+
connectedTools.set(name, {
|
|
959
|
+
client,
|
|
960
|
+
serverName,
|
|
961
|
+
toolName: tool.name,
|
|
962
|
+
sensitiveValues,
|
|
963
|
+
definition: {
|
|
964
|
+
name,
|
|
965
|
+
description: redactSensitiveText(tool.description ?? `MCP tool ${tool.name} from ${serverName}`, sensitiveValues),
|
|
966
|
+
inputSchema: redactSensitiveValue(tool.inputSchema, sensitiveValues),
|
|
967
|
+
},
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
this.assertOpenGeneration(expectedGeneration);
|
|
971
|
+
this.serverClients.set(serverName, client);
|
|
972
|
+
for (const [name, tool] of connectedTools)
|
|
973
|
+
this.tools.set(name, tool);
|
|
974
|
+
if (capabilities?.resources) {
|
|
975
|
+
this.resourceServers.set(serverName, {
|
|
976
|
+
client,
|
|
977
|
+
resources,
|
|
978
|
+
sensitiveValues,
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
if (capabilities?.prompts) {
|
|
982
|
+
this.promptServers.set(serverName, prompts);
|
|
983
|
+
if (capabilities.prompts.listChanged) {
|
|
984
|
+
client.setNotificationHandler(PromptListChangedNotificationSchema, async () => {
|
|
985
|
+
const refreshed = await this.discoverPrompts(client, serverName, sensitiveValues);
|
|
986
|
+
if (this.closed || this.serverClients.get(serverName) !== client)
|
|
987
|
+
return;
|
|
988
|
+
this.promptServers.set(serverName, refreshed);
|
|
989
|
+
this.publishPrompts();
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
this.statuses.set(serverName, { name: serverName, status: 'connected' });
|
|
994
|
+
}
|
|
995
|
+
catch (error) {
|
|
996
|
+
await client.close().catch(() => undefined);
|
|
997
|
+
this.clients.delete(client);
|
|
998
|
+
if (this.options.signal?.aborted ||
|
|
999
|
+
this.closed ||
|
|
1000
|
+
expectedGeneration !== this.generation)
|
|
1001
|
+
throw error;
|
|
1002
|
+
this.options.onWarning?.(redactSensitiveText(`MCP server ${serverName} unavailable: ${error instanceof Error ? error.message : String(error)}`, sensitiveValues));
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
async discoverTools(client, serverName, sensitiveValues) {
|
|
1006
|
+
try {
|
|
1007
|
+
const tools = [];
|
|
1008
|
+
let cursor;
|
|
1009
|
+
const cursors = new Set();
|
|
1010
|
+
let pages = 0;
|
|
1011
|
+
do {
|
|
1012
|
+
if (cursor && cursors.has(cursor)) {
|
|
1013
|
+
throw new Error('Repeated MCP tools cursor');
|
|
1014
|
+
}
|
|
1015
|
+
if (cursor)
|
|
1016
|
+
cursors.add(cursor);
|
|
1017
|
+
if (++pages > MAX_TOOL_PAGES) {
|
|
1018
|
+
throw new Error('MCP tools page limit exceeded');
|
|
1019
|
+
}
|
|
1020
|
+
const page = await client.listTools(cursor ? { cursor } : undefined, {
|
|
1021
|
+
timeout: DISCOVERY_TIMEOUT_MS,
|
|
1022
|
+
signal: requestSignal(this.options.signal, DISCOVERY_TIMEOUT_MS),
|
|
1023
|
+
});
|
|
1024
|
+
tools.push(...page.tools);
|
|
1025
|
+
if (tools.length > MAX_TOOLS) {
|
|
1026
|
+
throw new Error('MCP tool limit exceeded');
|
|
1027
|
+
}
|
|
1028
|
+
cursor = page.nextCursor;
|
|
1029
|
+
} while (cursor);
|
|
1030
|
+
return tools;
|
|
1031
|
+
}
|
|
1032
|
+
catch (error) {
|
|
1033
|
+
if (this.options.signal?.aborted)
|
|
1034
|
+
throw error;
|
|
1035
|
+
this.warnDiscovery(serverName, 'tools', error, sensitiveValues);
|
|
1036
|
+
return [];
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
async discoverResources(client, serverName, sensitiveValues) {
|
|
1040
|
+
try {
|
|
1041
|
+
const resources = [];
|
|
1042
|
+
let cursor;
|
|
1043
|
+
const cursors = new Set();
|
|
1044
|
+
let pages = 0;
|
|
1045
|
+
do {
|
|
1046
|
+
if (cursor && cursors.has(cursor)) {
|
|
1047
|
+
throw new Error('Repeated MCP resources cursor');
|
|
1048
|
+
}
|
|
1049
|
+
if (cursor)
|
|
1050
|
+
cursors.add(cursor);
|
|
1051
|
+
if (++pages > MAX_RESOURCE_PAGES) {
|
|
1052
|
+
throw new Error('MCP resources page limit exceeded');
|
|
1053
|
+
}
|
|
1054
|
+
const page = await client.listResources(cursor ? { cursor } : undefined, {
|
|
1055
|
+
timeout: DISCOVERY_TIMEOUT_MS,
|
|
1056
|
+
signal: requestSignal(this.options.signal, DISCOVERY_TIMEOUT_MS),
|
|
1057
|
+
});
|
|
1058
|
+
resources.push(...page.resources.map((resource) => ({ ...resource })));
|
|
1059
|
+
if (resources.length > MAX_RESOURCES) {
|
|
1060
|
+
throw new Error('MCP resource limit exceeded');
|
|
1061
|
+
}
|
|
1062
|
+
cursor = page.nextCursor;
|
|
1063
|
+
} while (cursor);
|
|
1064
|
+
return resources;
|
|
1065
|
+
}
|
|
1066
|
+
catch (error) {
|
|
1067
|
+
if (this.options.signal?.aborted)
|
|
1068
|
+
throw error;
|
|
1069
|
+
this.warnDiscovery(serverName, 'resources', error, sensitiveValues);
|
|
1070
|
+
return [];
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
async discoverPrompts(client, serverName, sensitiveValues) {
|
|
1074
|
+
try {
|
|
1075
|
+
const prompts = [];
|
|
1076
|
+
let cursor;
|
|
1077
|
+
const cursors = new Set();
|
|
1078
|
+
let pages = 0;
|
|
1079
|
+
do {
|
|
1080
|
+
if (cursor && cursors.has(cursor)) {
|
|
1081
|
+
throw new Error('Repeated MCP prompts cursor');
|
|
1082
|
+
}
|
|
1083
|
+
if (cursor)
|
|
1084
|
+
cursors.add(cursor);
|
|
1085
|
+
if (++pages > MAX_PROMPT_PAGES) {
|
|
1086
|
+
throw new Error('MCP prompts page limit exceeded');
|
|
1087
|
+
}
|
|
1088
|
+
const page = await client.listPrompts(cursor ? { cursor } : undefined, {
|
|
1089
|
+
timeout: DISCOVERY_TIMEOUT_MS,
|
|
1090
|
+
signal: requestSignal(this.options.signal, DISCOVERY_TIMEOUT_MS),
|
|
1091
|
+
});
|
|
1092
|
+
prompts.push(...page.prompts);
|
|
1093
|
+
if (prompts.length > MAX_PROMPTS) {
|
|
1094
|
+
throw new Error('MCP prompt limit exceeded');
|
|
1095
|
+
}
|
|
1096
|
+
cursor = page.nextCursor;
|
|
1097
|
+
} while (cursor);
|
|
1098
|
+
return prompts.map((prompt) => {
|
|
1099
|
+
const promptName = sanitizeMcpUnicode(prompt.name);
|
|
1100
|
+
const name = `mcp__${normalizeMcpName(serverName)}__${promptName}`;
|
|
1101
|
+
return {
|
|
1102
|
+
client,
|
|
1103
|
+
serverName,
|
|
1104
|
+
rawPromptName: prompt.name,
|
|
1105
|
+
rawArgumentNames: (prompt.arguments ?? []).map((argument) => argument.name),
|
|
1106
|
+
promptName,
|
|
1107
|
+
name,
|
|
1108
|
+
userFacingName: `${serverName}:${promptName} (MCP)`,
|
|
1109
|
+
description: redactSensitiveText(sanitizeMcpUnicode(prompt.description ?? ''), sensitiveValues),
|
|
1110
|
+
argumentNames: (prompt.arguments ?? []).map((argument) => sanitizeMcpUnicode(argument.name)),
|
|
1111
|
+
sensitiveValues,
|
|
1112
|
+
};
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
catch (error) {
|
|
1116
|
+
if (this.options.signal?.aborted)
|
|
1117
|
+
throw error;
|
|
1118
|
+
this.warnDiscovery(serverName, 'prompts', error, sensitiveValues);
|
|
1119
|
+
return [];
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
async invokePrompt(prompt, argumentsText, signal, allowReconnect = true, toolResultDirectory) {
|
|
1123
|
+
if (this.closed)
|
|
1124
|
+
throw new Error('MCP registry is closed');
|
|
1125
|
+
prompt = await this.ensurePromptConnected(prompt);
|
|
1126
|
+
const values = argumentsText.split(' ');
|
|
1127
|
+
const argumentsRecord = {};
|
|
1128
|
+
const argumentCount = Math.min(prompt.rawArgumentNames.length, values.length);
|
|
1129
|
+
for (let index = 0; index < argumentCount; index += 1) {
|
|
1130
|
+
const name = prompt.rawArgumentNames[index];
|
|
1131
|
+
const value = values[index];
|
|
1132
|
+
if (name !== undefined && value !== undefined) {
|
|
1133
|
+
argumentsRecord[name] = value;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
let result;
|
|
1137
|
+
try {
|
|
1138
|
+
result = await prompt.client.getPrompt({ name: prompt.rawPromptName, arguments: argumentsRecord }, {
|
|
1139
|
+
timeout: RESOURCE_TIMEOUT_MS,
|
|
1140
|
+
signal: requestSignal(signal ?? this.options.signal, RESOURCE_TIMEOUT_MS),
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
catch (error) {
|
|
1144
|
+
if (!signal?.aborted &&
|
|
1145
|
+
!this.options.signal?.aborted &&
|
|
1146
|
+
this.statuses.get(prompt.serverName)?.status === 'failed' &&
|
|
1147
|
+
allowReconnect) {
|
|
1148
|
+
prompt = await this.ensurePromptConnected(prompt, true);
|
|
1149
|
+
return this.invokePrompt(prompt, argumentsText, signal, false, toolResultDirectory);
|
|
1150
|
+
}
|
|
1151
|
+
throw redactSensitiveError(error, prompt.sensitiveValues);
|
|
1152
|
+
}
|
|
1153
|
+
const promptResultDirectory = toolResultDirectory ?? (await this.promptResultDirectory());
|
|
1154
|
+
const converted = await mcpToolResult(prompt.serverName, { content: result.messages.map((message) => message.content) }, {
|
|
1155
|
+
cwd: this.options.cwd,
|
|
1156
|
+
toolResultDirectory: promptResultDirectory,
|
|
1157
|
+
...(signal ? { signal } : {}),
|
|
1158
|
+
}, prompt.sensitiveValues);
|
|
1159
|
+
return {
|
|
1160
|
+
text: converted.content,
|
|
1161
|
+
contentBlocks: converted.contentBlocks ?? [],
|
|
1162
|
+
images: converted.images ?? [],
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
async ensurePromptConnected(prompt, force = false) {
|
|
1166
|
+
const currentClient = this.serverClients.get(prompt.serverName);
|
|
1167
|
+
if (!force &&
|
|
1168
|
+
currentClient === prompt.client &&
|
|
1169
|
+
this.statuses.get(prompt.serverName)?.status === 'connected') {
|
|
1170
|
+
return prompt;
|
|
1171
|
+
}
|
|
1172
|
+
let reconnecting = this.reconnectingServers.get(prompt.serverName);
|
|
1173
|
+
if (!reconnecting) {
|
|
1174
|
+
reconnecting = this.reconnectServer(prompt.serverName);
|
|
1175
|
+
this.reconnectingServers.set(prompt.serverName, reconnecting);
|
|
1176
|
+
const clearReconnect = () => {
|
|
1177
|
+
if (this.reconnectingServers.get(prompt.serverName) === reconnecting) {
|
|
1178
|
+
this.reconnectingServers.delete(prompt.serverName);
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
void reconnecting.then(clearReconnect, clearReconnect);
|
|
1182
|
+
}
|
|
1183
|
+
await reconnecting;
|
|
1184
|
+
const refreshed = this.promptServers
|
|
1185
|
+
.get(prompt.serverName)
|
|
1186
|
+
?.find((candidate) => candidate.rawPromptName === prompt.rawPromptName);
|
|
1187
|
+
if (!refreshed) {
|
|
1188
|
+
throw new Error(`MCP prompt ${prompt.serverName}:${prompt.promptName} is unavailable after reconnect`);
|
|
1189
|
+
}
|
|
1190
|
+
return refreshed;
|
|
1191
|
+
}
|
|
1192
|
+
async reconnectServer(serverName) {
|
|
1193
|
+
if (this.closed)
|
|
1194
|
+
throw new Error('MCP registry is closed');
|
|
1195
|
+
const expectedGeneration = this.generation;
|
|
1196
|
+
const reconnectable = this.reconnectableServers.get(serverName);
|
|
1197
|
+
if (!reconnectable)
|
|
1198
|
+
throw new Error(`MCP server ${serverName} cannot reconnect`);
|
|
1199
|
+
const previous = this.serverClients.get(serverName);
|
|
1200
|
+
this.serverClients.delete(serverName);
|
|
1201
|
+
await previous?.close().catch(() => undefined);
|
|
1202
|
+
if (previous)
|
|
1203
|
+
this.clients.delete(previous);
|
|
1204
|
+
this.assertOpenGeneration(expectedGeneration);
|
|
1205
|
+
for (const [name, tool] of this.tools) {
|
|
1206
|
+
if (tool.serverName === serverName)
|
|
1207
|
+
this.tools.delete(name);
|
|
1208
|
+
}
|
|
1209
|
+
this.resourceServers.delete(serverName);
|
|
1210
|
+
await this.connectServer(serverName, reconnectable.config, reconnectable.sensitiveValues, expectedGeneration);
|
|
1211
|
+
if (this.statuses.get(serverName)?.status !== 'connected') {
|
|
1212
|
+
throw new Error(`MCP server ${serverName} could not reconnect`);
|
|
1213
|
+
}
|
|
1214
|
+
this.publishPrompts();
|
|
1215
|
+
}
|
|
1216
|
+
publishPrompts() {
|
|
1217
|
+
if (!this.closed)
|
|
1218
|
+
this.options.onPromptsChanged?.(this.prompts());
|
|
1219
|
+
}
|
|
1220
|
+
assertOpenGeneration(expectedGeneration) {
|
|
1221
|
+
if (this.closed || expectedGeneration !== this.generation) {
|
|
1222
|
+
throw new Error('MCP registry is closed');
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
trackPromptOperation(operation) {
|
|
1226
|
+
this.promptOperations.add(operation);
|
|
1227
|
+
const clear = () => this.promptOperations.delete(operation);
|
|
1228
|
+
void operation.then(clear, clear);
|
|
1229
|
+
return operation;
|
|
1230
|
+
}
|
|
1231
|
+
async promptResultDirectory() {
|
|
1232
|
+
if (this.closed)
|
|
1233
|
+
throw new Error('MCP registry is closed');
|
|
1234
|
+
this.promptResultDirectoryPromise ??= mkdtemp(join(tmpdir(), 'praxis-mcp-prompts-'));
|
|
1235
|
+
const directory = await this.promptResultDirectoryPromise;
|
|
1236
|
+
if (this.closed) {
|
|
1237
|
+
await rm(directory, { recursive: true, force: true });
|
|
1238
|
+
throw new Error('MCP registry is closed');
|
|
1239
|
+
}
|
|
1240
|
+
return directory;
|
|
1241
|
+
}
|
|
1242
|
+
warnDiscovery(serverName, kind, error, sensitiveValues) {
|
|
1243
|
+
this.options.onWarning?.(redactSensitiveText(`MCP server ${serverName} ${kind} unavailable: ${error instanceof Error ? error.message : String(error)}`, sensitiveValues));
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
//# sourceMappingURL=claude-mcp-tools.js.map
|