apex-code 0.0.1-alpha.8 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +231 -0
- package/README.md +9 -8
- package/dist/cli/args.d.ts +26 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +72 -10
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/file-processor.d.ts.map +1 -1
- package/dist/cli/file-processor.js +2 -1
- package/dist/cli/file-processor.js.map +1 -1
- package/dist/cli/mcp-command.d.ts +9 -0
- package/dist/cli/mcp-command.d.ts.map +1 -0
- package/dist/cli/mcp-command.js +53 -0
- package/dist/cli/mcp-command.js.map +1 -0
- package/dist/cli/project-trust.d.ts.map +1 -1
- package/dist/cli/project-trust.js +3 -1
- package/dist/cli/project-trust.js.map +1 -1
- package/dist/cli/startup-ui.d.ts.map +1 -1
- package/dist/cli/startup-ui.js +2 -1
- package/dist/cli/startup-ui.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +94 -5
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +3 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +45 -18
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +3 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +1 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +183 -15
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +728 -117
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +6 -7
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +7 -0
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/checkpoints/git-checkpoints.d.ts +39 -0
- package/dist/core/checkpoints/git-checkpoints.d.ts.map +1 -0
- package/dist/core/checkpoints/git-checkpoints.js +251 -0
- package/dist/core/checkpoints/git-checkpoints.js.map +1 -0
- package/dist/core/checkpoints/session-checkpoints.d.ts +23 -0
- package/dist/core/checkpoints/session-checkpoints.d.ts.map +1 -0
- package/dist/core/checkpoints/session-checkpoints.js +36 -0
- package/dist/core/checkpoints/session-checkpoints.js.map +1 -0
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +7 -3
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +9 -3
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +59 -36
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/defaults.d.ts +1 -0
- package/dist/core/defaults.d.ts.map +1 -1
- package/dist/core/defaults.js +9 -0
- package/dist/core/defaults.js.map +1 -1
- package/dist/core/delegation/runtime.d.ts +6 -5
- package/dist/core/delegation/runtime.d.ts.map +1 -1
- package/dist/core/delegation/runtime.js +4 -3
- package/dist/core/delegation/runtime.js.map +1 -1
- package/dist/core/durable-state/sqlite.d.ts.map +1 -1
- package/dist/core/durable-state/sqlite.js +26 -2
- package/dist/core/durable-state/sqlite.js.map +1 -1
- package/dist/core/exec.d.ts.map +1 -1
- package/dist/core/exec.js +5 -2
- package/dist/core/exec.js.map +1 -1
- package/dist/core/extensions/index.d.ts +2 -2
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +123 -50
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +5 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +45 -1
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/source-runtime.d.ts +14 -0
- package/dist/core/extensions/source-runtime.d.ts.map +1 -0
- package/dist/core/extensions/source-runtime.js +56 -0
- package/dist/core/extensions/source-runtime.js.map +1 -0
- package/dist/core/extensions/types.d.ts +60 -11
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js +3 -0
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts +1 -1
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +1 -1
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/formatter-lifecycle.d.ts +69 -0
- package/dist/core/formatter-lifecycle.d.ts.map +1 -0
- package/dist/core/formatter-lifecycle.js +360 -0
- package/dist/core/formatter-lifecycle.js.map +1 -0
- package/dist/core/hooks/command-handler.d.ts +19 -0
- package/dist/core/hooks/command-handler.d.ts.map +1 -0
- package/dist/core/hooks/command-handler.js +112 -0
- package/dist/core/hooks/command-handler.js.map +1 -0
- package/dist/core/hooks/http-handler.d.ts +10 -0
- package/dist/core/hooks/http-handler.d.ts.map +1 -0
- package/dist/core/hooks/http-handler.js +36 -0
- package/dist/core/hooks/http-handler.js.map +1 -0
- package/dist/core/hooks/loader.d.ts +29 -0
- package/dist/core/hooks/loader.d.ts.map +1 -0
- package/dist/core/hooks/loader.js +139 -0
- package/dist/core/hooks/loader.js.map +1 -0
- package/dist/core/hooks/runtime.d.ts +13 -0
- package/dist/core/hooks/runtime.d.ts.map +1 -0
- package/dist/core/hooks/runtime.js +63 -0
- package/dist/core/hooks/runtime.js.map +1 -0
- package/dist/core/hooks/types.d.ts +83 -0
- package/dist/core/hooks/types.d.ts.map +1 -0
- package/dist/core/hooks/types.js +24 -0
- package/dist/core/hooks/types.js.map +1 -0
- package/dist/core/keybindings.d.ts +20 -19
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +26 -5
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/mcp/config.d.ts +16 -0
- package/dist/core/mcp/config.d.ts.map +1 -0
- package/dist/core/mcp/config.js +166 -0
- package/dist/core/mcp/config.js.map +1 -0
- package/dist/core/mcp/connector.d.ts +35 -0
- package/dist/core/mcp/connector.d.ts.map +1 -0
- package/dist/core/mcp/connector.js +105 -0
- package/dist/core/mcp/connector.js.map +1 -0
- package/dist/core/mcp/contract.d.ts +19 -0
- package/dist/core/mcp/contract.d.ts.map +1 -0
- package/dist/core/mcp/contract.js +89 -0
- package/dist/core/mcp/contract.js.map +1 -0
- package/dist/core/mcp/mcp-tool.d.ts +32 -0
- package/dist/core/mcp/mcp-tool.d.ts.map +1 -0
- package/dist/core/mcp/mcp-tool.js +171 -0
- package/dist/core/mcp/mcp-tool.js.map +1 -0
- package/dist/core/mcp/metadata-cache.d.ts +29 -0
- package/dist/core/mcp/metadata-cache.d.ts.map +1 -0
- package/dist/core/mcp/metadata-cache.js +99 -0
- package/dist/core/mcp/metadata-cache.js.map +1 -0
- package/dist/core/mcp/oauth/authorize.d.ts +21 -0
- package/dist/core/mcp/oauth/authorize.d.ts.map +1 -0
- package/dist/core/mcp/oauth/authorize.js +32 -0
- package/dist/core/mcp/oauth/authorize.js.map +1 -0
- package/dist/core/mcp/oauth/discover.d.ts +27 -0
- package/dist/core/mcp/oauth/discover.d.ts.map +1 -0
- package/dist/core/mcp/oauth/discover.js +116 -0
- package/dist/core/mcp/oauth/discover.js.map +1 -0
- package/dist/core/mcp/oauth/flow.d.ts +31 -0
- package/dist/core/mcp/oauth/flow.d.ts.map +1 -0
- package/dist/core/mcp/oauth/flow.js +175 -0
- package/dist/core/mcp/oauth/flow.js.map +1 -0
- package/dist/core/mcp/oauth/mcp-token.d.ts +51 -0
- package/dist/core/mcp/oauth/mcp-token.d.ts.map +1 -0
- package/dist/core/mcp/oauth/mcp-token.js +129 -0
- package/dist/core/mcp/oauth/mcp-token.js.map +1 -0
- package/dist/core/mcp/oauth/pkce.d.ts +12 -0
- package/dist/core/mcp/oauth/pkce.d.ts.map +1 -0
- package/dist/core/mcp/oauth/pkce.js +11 -0
- package/dist/core/mcp/oauth/pkce.js.map +1 -0
- package/dist/core/mcp/oauth/register.d.ts +16 -0
- package/dist/core/mcp/oauth/register.d.ts.map +1 -0
- package/dist/core/mcp/oauth/register.js +42 -0
- package/dist/core/mcp/oauth/register.js.map +1 -0
- package/dist/core/mcp/oauth/token-client.d.ts +33 -0
- package/dist/core/mcp/oauth/token-client.d.ts.map +1 -0
- package/dist/core/mcp/oauth/token-client.js +65 -0
- package/dist/core/mcp/oauth/token-client.js.map +1 -0
- package/dist/core/mcp/runtime.d.ts +20 -0
- package/dist/core/mcp/runtime.d.ts.map +1 -0
- package/dist/core/mcp/runtime.js +37 -0
- package/dist/core/mcp/runtime.js.map +1 -0
- package/dist/core/mcp/schema.d.ts +22 -0
- package/dist/core/mcp/schema.d.ts.map +1 -0
- package/dist/core/mcp/schema.js +20 -0
- package/dist/core/mcp/schema.js.map +1 -0
- package/dist/core/mcp/server-manager.d.ts +43 -0
- package/dist/core/mcp/server-manager.d.ts.map +1 -0
- package/dist/core/mcp/server-manager.js +142 -0
- package/dist/core/mcp/server-manager.js.map +1 -0
- package/dist/core/mcp/types.d.ts +100 -0
- package/dist/core/mcp/types.d.ts.map +1 -0
- package/dist/core/mcp/types.js +8 -0
- package/dist/core/mcp/types.js.map +1 -0
- package/dist/core/model-config.d.ts +5 -0
- package/dist/core/model-config.d.ts.map +1 -1
- package/dist/core/model-config.js +3 -1
- package/dist/core/model-config.js.map +1 -1
- package/dist/core/model-resolver.d.ts +1 -0
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +14 -8
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/models-store.d.ts.map +1 -1
- package/dist/core/models-store.js +2 -1
- package/dist/core/models-store.js.map +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +22 -14
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/permissions/gate.d.ts.map +1 -1
- package/dist/core/permissions/gate.js +45 -4
- package/dist/core/permissions/gate.js.map +1 -1
- package/dist/core/permissions/operations.d.ts +20 -0
- package/dist/core/permissions/operations.d.ts.map +1 -0
- package/dist/core/permissions/operations.js +9 -0
- package/dist/core/permissions/operations.js.map +1 -0
- package/dist/core/permissions/policy-command.d.ts +42 -0
- package/dist/core/permissions/policy-command.d.ts.map +1 -0
- package/dist/core/permissions/policy-command.js +44 -0
- package/dist/core/permissions/policy-command.js.map +1 -0
- package/dist/core/permissions/responder.d.ts +49 -2
- package/dist/core/permissions/responder.d.ts.map +1 -1
- package/dist/core/permissions/responder.js +20 -8
- package/dist/core/permissions/responder.js.map +1 -1
- package/dist/core/permissions/rules.d.ts.map +1 -1
- package/dist/core/permissions/rules.js +11 -1
- package/dist/core/permissions/rules.js.map +1 -1
- package/dist/core/permissions/store.d.ts +18 -0
- package/dist/core/permissions/store.d.ts.map +1 -1
- package/dist/core/permissions/store.js +89 -8
- package/dist/core/permissions/store.js.map +1 -1
- package/dist/core/pi-manifest.d.ts.map +1 -1
- package/dist/core/pi-manifest.js +2 -1
- package/dist/core/pi-manifest.js.map +1 -1
- package/dist/core/policy-executor.d.ts +47 -0
- package/dist/core/policy-executor.d.ts.map +1 -0
- package/dist/core/policy-executor.js +228 -0
- package/dist/core/policy-executor.js.map +1 -0
- package/dist/core/policy-loader.d.ts +72 -0
- package/dist/core/policy-loader.d.ts.map +1 -0
- package/dist/core/policy-loader.js +195 -0
- package/dist/core/policy-loader.js.map +1 -0
- package/dist/core/project-trust.d.ts +1 -1
- package/dist/core/project-trust.d.ts.map +1 -1
- package/dist/core/project-trust.js.map +1 -1
- package/dist/core/remote-catalog-provider.d.ts.map +1 -1
- package/dist/core/remote-catalog-provider.js +2 -1
- package/dist/core/remote-catalog-provider.js.map +1 -1
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +3 -2
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sandbox/bwrap-arguments.d.ts +41 -0
- package/dist/core/sandbox/bwrap-arguments.d.ts.map +1 -0
- package/dist/core/sandbox/bwrap-arguments.js +136 -0
- package/dist/core/sandbox/bwrap-arguments.js.map +1 -0
- package/dist/core/sandbox/child-entry.d.ts.map +1 -1
- package/dist/core/sandbox/child-entry.js +3 -1
- package/dist/core/sandbox/child-entry.js.map +1 -1
- package/dist/core/sandbox/cli-launch.d.ts +84 -4
- package/dist/core/sandbox/cli-launch.d.ts.map +1 -1
- package/dist/core/sandbox/cli-launch.js +115 -16
- package/dist/core/sandbox/cli-launch.js.map +1 -1
- package/dist/core/sandbox/cli-supervisor.d.ts +2 -0
- package/dist/core/sandbox/cli-supervisor.d.ts.map +1 -1
- package/dist/core/sandbox/cli-supervisor.js +50 -2
- package/dist/core/sandbox/cli-supervisor.js.map +1 -1
- package/dist/core/sandbox/full-access.d.ts +28 -0
- package/dist/core/sandbox/full-access.d.ts.map +1 -0
- package/dist/core/sandbox/full-access.js +63 -0
- package/dist/core/sandbox/full-access.js.map +1 -0
- package/dist/core/sandbox/git-identity.d.ts +57 -0
- package/dist/core/sandbox/git-identity.d.ts.map +1 -0
- package/dist/core/sandbox/git-identity.js +78 -0
- package/dist/core/sandbox/git-identity.js.map +1 -0
- package/dist/core/sandbox/host-approval.d.ts +42 -0
- package/dist/core/sandbox/host-approval.d.ts.map +1 -0
- package/dist/core/sandbox/host-approval.js +99 -0
- package/dist/core/sandbox/host-approval.js.map +1 -0
- package/dist/core/sandbox/linux-backend.d.ts +14 -0
- package/dist/core/sandbox/linux-backend.d.ts.map +1 -1
- package/dist/core/sandbox/linux-backend.js +158 -80
- package/dist/core/sandbox/linux-backend.js.map +1 -1
- package/dist/core/sandbox/macos-backend.d.ts +5 -0
- package/dist/core/sandbox/macos-backend.d.ts.map +1 -1
- package/dist/core/sandbox/macos-backend.js +164 -5
- package/dist/core/sandbox/macos-backend.js.map +1 -1
- package/dist/core/sandbox/network-proxy.d.ts +19 -0
- package/dist/core/sandbox/network-proxy.d.ts.map +1 -1
- package/dist/core/sandbox/network-proxy.js +93 -19
- package/dist/core/sandbox/network-proxy.js.map +1 -1
- package/dist/core/sandbox/policy.d.ts +8 -0
- package/dist/core/sandbox/policy.d.ts.map +1 -1
- package/dist/core/sandbox/policy.js +29 -12
- package/dist/core/sandbox/policy.js.map +1 -1
- package/dist/core/sandbox/profiles.d.ts +25 -0
- package/dist/core/sandbox/profiles.d.ts.map +1 -0
- package/dist/core/sandbox/profiles.js +23 -0
- package/dist/core/sandbox/profiles.js.map +1 -0
- package/dist/core/sandbox/rpc/command-client.d.ts +28 -0
- package/dist/core/sandbox/rpc/command-client.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/command-client.js +86 -0
- package/dist/core/sandbox/rpc/command-client.js.map +1 -0
- package/dist/core/sandbox/rpc/command-proxy.d.ts +48 -0
- package/dist/core/sandbox/rpc/command-proxy.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/command-proxy.js +120 -0
- package/dist/core/sandbox/rpc/command-proxy.js.map +1 -0
- package/dist/core/sandbox/rpc/credential-proxy.d.ts.map +1 -1
- package/dist/core/sandbox/rpc/credential-proxy.js +2 -96
- package/dist/core/sandbox/rpc/credential-proxy.js.map +1 -1
- package/dist/core/sandbox/rpc/framing.d.ts +35 -0
- package/dist/core/sandbox/rpc/framing.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/framing.js +114 -0
- package/dist/core/sandbox/rpc/framing.js.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-helper.d.ts +47 -0
- package/dist/core/sandbox/rpc/git-credential-helper.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-helper.js +222 -0
- package/dist/core/sandbox/rpc/git-credential-helper.js.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.d.ts +66 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.js +209 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.js.map +1 -0
- package/dist/core/sandbox/supervisor-temp.d.ts +18 -0
- package/dist/core/sandbox/supervisor-temp.d.ts.map +1 -0
- package/dist/core/sandbox/supervisor-temp.js +21 -0
- package/dist/core/sandbox/supervisor-temp.js.map +1 -0
- package/dist/core/sandbox/supervisor.d.ts +12 -1
- package/dist/core/sandbox/supervisor.d.ts.map +1 -1
- package/dist/core/sandbox/supervisor.js +15 -1
- package/dist/core/sandbox/supervisor.js.map +1 -1
- package/dist/core/sandbox/terminal-handoff.d.ts +68 -0
- package/dist/core/sandbox/terminal-handoff.d.ts.map +1 -0
- package/dist/core/sandbox/terminal-handoff.js +221 -0
- package/dist/core/sandbox/terminal-handoff.js.map +1 -0
- package/dist/core/sandbox/terminal-size.d.ts +13 -0
- package/dist/core/sandbox/terminal-size.d.ts.map +1 -1
- package/dist/core/sandbox/terminal-size.js +23 -2
- package/dist/core/sandbox/terminal-size.js.map +1 -1
- package/dist/core/sdk.d.ts +24 -2
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +63 -4
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-export.d.ts +4 -0
- package/dist/core/session-export.d.ts.map +1 -0
- package/dist/core/session-export.js +32 -0
- package/dist/core/session-export.js.map +1 -0
- package/dist/core/session-manager.d.ts +13 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +34 -6
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-diagnostics.d.ts +9 -0
- package/dist/core/settings-diagnostics.d.ts.map +1 -0
- package/dist/core/settings-diagnostics.js +21 -0
- package/dist/core/settings-diagnostics.js.map +1 -0
- package/dist/core/settings-manager.d.ts +172 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +160 -11
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +51 -33
- package/dist/core/skills.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +4 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +8 -2
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/background-shell.d.ts +39 -0
- package/dist/core/tools/background-shell.d.ts.map +1 -0
- package/dist/core/tools/background-shell.js +81 -0
- package/dist/core/tools/background-shell.js.map +1 -0
- package/dist/core/tools/bash-command-segments.d.ts.map +1 -1
- package/dist/core/tools/bash-command-segments.js +2 -0
- package/dist/core/tools/bash-command-segments.js.map +1 -1
- package/dist/core/tools/bash.d.ts +44 -16
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +264 -32
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/contract-snapshot.d.ts +58 -0
- package/dist/core/tools/contract-snapshot.d.ts.map +1 -0
- package/dist/core/tools/contract-snapshot.js +51 -0
- package/dist/core/tools/contract-snapshot.js.map +1 -0
- package/dist/core/tools/contract.d.ts +53 -5
- package/dist/core/tools/contract.d.ts.map +1 -1
- package/dist/core/tools/contract.js +22 -2
- package/dist/core/tools/contract.js.map +1 -1
- package/dist/core/tools/delegate.d.ts +6 -4
- package/dist/core/tools/delegate.d.ts.map +1 -1
- package/dist/core/tools/delegate.js +21 -11
- package/dist/core/tools/delegate.js.map +1 -1
- package/dist/core/tools/edit-diff.d.ts +48 -5
- package/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/core/tools/edit-diff.js +217 -8
- package/dist/core/tools/edit-diff.js.map +1 -1
- package/dist/core/tools/edit.d.ts +8 -0
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +72 -10
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +5 -2
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +5 -2
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +14 -2
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +12 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +5 -2
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/lsp.d.ts +19 -1
- package/dist/core/tools/lsp.d.ts.map +1 -1
- package/dist/core/tools/lsp.js +163 -10
- package/dist/core/tools/lsp.js.map +1 -1
- package/dist/core/tools/path-permission.d.ts +3 -0
- package/dist/core/tools/path-permission.d.ts.map +1 -1
- package/dist/core/tools/path-permission.js +16 -4
- package/dist/core/tools/path-permission.js.map +1 -1
- package/dist/core/tools/path-utils.d.ts +27 -2
- package/dist/core/tools/path-utils.d.ts.map +1 -1
- package/dist/core/tools/path-utils.js +221 -4
- package/dist/core/tools/path-utils.js.map +1 -1
- package/dist/core/tools/powershell.d.ts +16 -0
- package/dist/core/tools/powershell.d.ts.map +1 -0
- package/dist/core/tools/powershell.js +39 -0
- package/dist/core/tools/powershell.js.map +1 -0
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +20 -11
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/test.d.ts +54 -9
- package/dist/core/tools/test.d.ts.map +1 -1
- package/dist/core/tools/test.js +239 -19
- package/dist/core/tools/test.js.map +1 -1
- package/dist/core/tools/write.d.ts +7 -0
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +64 -9
- package/dist/core/tools/write.js.map +1 -1
- package/dist/core/trust-manager.d.ts.map +1 -1
- package/dist/core/trust-manager.js +2 -1
- package/dist/core/trust-manager.js.map +1 -1
- package/dist/core/verification-lifecycle.d.ts +77 -0
- package/dist/core/verification-lifecycle.d.ts.map +1 -0
- package/dist/core/verification-lifecycle.js +146 -0
- package/dist/core/verification-lifecycle.js.map +1 -0
- package/dist/core/workspace/artifacts.d.ts +45 -0
- package/dist/core/workspace/artifacts.d.ts.map +1 -0
- package/dist/core/workspace/artifacts.js +165 -0
- package/dist/core/workspace/artifacts.js.map +1 -0
- package/dist/core/workspace/comparison.d.ts +27 -0
- package/dist/core/workspace/comparison.d.ts.map +1 -0
- package/dist/core/workspace/comparison.js +94 -0
- package/dist/core/workspace/comparison.js.map +1 -0
- package/dist/core/workspace/git-observer.d.ts +48 -0
- package/dist/core/workspace/git-observer.d.ts.map +1 -0
- package/dist/core/workspace/git-observer.js +346 -0
- package/dist/core/workspace/git-observer.js.map +1 -0
- package/dist/core/workspace/projection.d.ts +3 -0
- package/dist/core/workspace/projection.d.ts.map +1 -0
- package/dist/core/workspace/projection.js +56 -0
- package/dist/core/workspace/projection.js.map +1 -0
- package/dist/core/workspace/state.d.ts +133 -0
- package/dist/core/workspace/state.d.ts.map +1 -0
- package/dist/core/workspace/state.js +96 -0
- package/dist/core/workspace/state.js.map +1 -0
- package/dist/extensions/llama/client.d.ts +6 -0
- package/dist/extensions/llama/client.d.ts.map +1 -1
- package/dist/extensions/llama/client.js +7 -0
- package/dist/extensions/llama/client.js.map +1 -1
- package/dist/extensions/llama/index.d.ts.map +1 -1
- package/dist/extensions/llama/index.js +2 -0
- package/dist/extensions/llama/index.js.map +1 -1
- package/dist/extensions/llama/provider.d.ts +3 -1
- package/dist/extensions/llama/provider.d.ts.map +1 -1
- package/dist/extensions/llama/provider.js +29 -4
- package/dist/extensions/llama/provider.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +6 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +58 -23
- package/dist/main.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +4 -3
- package/dist/migrations.js.map +1 -1
- package/dist/modes/acp/server.d.ts +55 -0
- package/dist/modes/acp/server.d.ts.map +1 -0
- package/dist/modes/acp/server.js +244 -0
- package/dist/modes/acp/server.js.map +1 -0
- package/dist/modes/acp/translate.d.ts +37 -0
- package/dist/modes/acp/translate.d.ts.map +1 -0
- package/dist/modes/acp/translate.js +50 -0
- package/dist/modes/acp/translate.js.map +1 -0
- package/dist/modes/index.d.ts +1 -0
- package/dist/modes/index.d.ts.map +1 -1
- package/dist/modes/index.js +1 -0
- package/dist/modes/index.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +29 -0
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +182 -23
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts +7 -0
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +20 -0
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts +9 -4
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +20 -9
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/extension-selector.d.ts +7 -1
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +7 -3
- package/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +19 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +70 -10
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +10 -3
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +56 -14
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/permission-preview.d.ts +15 -0
- package/dist/modes/interactive/components/permission-preview.d.ts.map +1 -0
- package/dist/modes/interactive/components/permission-preview.js +33 -0
- package/dist/modes/interactive/components/permission-preview.js.map +1 -0
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +2 -2
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +9 -2
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +128 -56
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-submenu.d.ts +72 -0
- package/dist/modes/interactive/components/settings-submenu.d.ts.map +1 -0
- package/dist/modes/interactive/components/settings-submenu.js +165 -0
- package/dist/modes/interactive/components/settings-submenu.js.map +1 -0
- package/dist/modes/interactive/components/thinking-selector.d.ts +15 -3
- package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.js +74 -17
- package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +23 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +64 -10
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/external-editor.d.ts.map +1 -1
- package/dist/modes/interactive/external-editor.js +2 -1
- package/dist/modes/interactive/external-editor.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +45 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +395 -43
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts +12 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +20 -9
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/json-event.d.ts +16 -13
- package/dist/modes/json-event.d.ts.map +1 -1
- package/dist/modes/json-event.js +20 -6
- package/dist/modes/json-event.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +27 -12
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +7 -0
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +7 -0
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +3 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +12 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/package-manager-cli.d.ts +1 -0
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +196 -10
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +2 -1
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/management-http.d.ts +5 -3
- package/dist/utils/management-http.d.ts.map +1 -1
- package/dist/utils/management-http.js +14 -10
- package/dist/utils/management-http.js.map +1 -1
- package/dist/utils/shell.d.ts +3 -0
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +23 -9
- package/dist/utils/shell.js.map +1 -1
- package/dist/utils/syntax-highlight.d.ts +1 -0
- package/dist/utils/syntax-highlight.d.ts.map +1 -1
- package/dist/utils/syntax-highlight.js +60 -1
- package/dist/utils/syntax-highlight.js.map +1 -1
- package/dist/utils/text.d.ts +8 -0
- package/dist/utils/text.d.ts.map +1 -0
- package/dist/utils/text.js +9 -0
- package/dist/utils/text.js.map +1 -0
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +6 -4
- package/dist/utils/version-check.js.map +1 -1
- package/docs/compaction.md +19 -0
- package/docs/custom-provider.md +12 -9
- package/docs/environment-variables.md +1 -1
- package/docs/extensions.md +35 -7
- package/docs/json.md +7 -2
- package/docs/keybindings.md +9 -9
- package/docs/llama-cpp.md +2 -0
- package/docs/models.md +8 -2
- package/docs/packages.md +2 -2
- package/docs/quickstart.md +1 -1
- package/docs/rpc.md +32 -3
- package/docs/sdk.md +32 -2
- package/docs/settings.md +23 -7
- package/docs/skills.md +4 -3
- package/docs/terminal-setup.md +23 -7
- package/docs/usage.md +10 -5
- package/docs/windows.md +24 -2
- package/examples/extensions/border-status-editor.ts +1 -1
- package/examples/extensions/built-in-tool-renderer.ts +8 -4
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/git-checkpoint.ts +24 -36
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/minimal-mode.ts +2 -2
- package/examples/extensions/notify.ts +3 -1
- package/examples/extensions/permission-gate.ts +1 -1
- package/examples/extensions/plan-mode/index.ts +1 -1
- package/examples/extensions/rpc-demo.ts +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/subagent/README.md +1 -1
- package/examples/extensions/subagent/index.ts +6 -1
- package/examples/extensions/titlebar-spinner.ts +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/rpc-extension-ui.ts +1 -1
- package/examples/sdk/06-extensions.ts +1 -1
- package/examples/sdk/README.md +1 -1
- package/npm-shrinkwrap.json +439 -405
- package/package.json +17 -17
- package/dist/server/create-harness.d.ts +0 -26
- package/dist/server/create-harness.d.ts.map +0 -1
- package/dist/server/create-harness.js +0 -107
- package/dist/server/create-harness.js.map +0 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MCP SDK lives here and nowhere else.
|
|
3
|
+
*
|
|
4
|
+
* Everything above this file talks to `McpConnection`, so the lifecycle logic, the
|
|
5
|
+
* proxy tool, and their tests never import the SDK or start a process. This module is
|
|
6
|
+
* the boundary where an external protocol becomes an internal type.
|
|
7
|
+
*/
|
|
8
|
+
import type { CredentialStore } from "@earendil-works/pi-ai";
|
|
9
|
+
import type { FetchLike } from "./oauth/discover.ts";
|
|
10
|
+
import type { McpConnection, McpConnector, McpServerConfig } from "./types.ts";
|
|
11
|
+
/**
|
|
12
|
+
* Authorization for one HTTP connection, in precedence order: a static configured
|
|
13
|
+
* header, then a named bearer env var, then the credential store's OAuth token.
|
|
14
|
+
* Later wins, because each later source is the more deliberately configured one.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveAuthorizationHeaders(server: McpServerConfig, oauthAuthorization?: string): Record<string, string>;
|
|
17
|
+
export declare function connectMcpServer(server: McpServerConfig, oauthAuthorization?: string): Promise<McpConnection>;
|
|
18
|
+
export interface SessionMcpConnectorOptions {
|
|
19
|
+
/**
|
|
20
|
+
* The session's credential store: `AuthStorage` on the host, `SandboxAuthStorage`
|
|
21
|
+
* in a sandboxed child (reads from the projection, writes through the supervisor
|
|
22
|
+
* channel). OAuth-configured servers without one fail closed.
|
|
23
|
+
*/
|
|
24
|
+
credentials?: CredentialStore;
|
|
25
|
+
now?: () => number;
|
|
26
|
+
fetch?: FetchLike;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The session connector: resolves OAuth tokens for servers configured with
|
|
30
|
+
* `auth: "oauth"`, and behaves exactly like `connectMcpServer` for everything else.
|
|
31
|
+
* A missing authorization throws before the transport is constructed, so no OAuth
|
|
32
|
+
* endpoint is ever contacted from a tool call path (spec 2026-09-01-mcp-oauth).
|
|
33
|
+
*/
|
|
34
|
+
export declare function createSessionMcpConnector(options?: SessionMcpConnectorOptions): McpConnector;
|
|
35
|
+
//# sourceMappingURL=connector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connector.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/connector.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAI7D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,KAAK,EAA6B,aAAa,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE1G;;;;GAIG;AACH,wBAAgB,2BAA2B,CAC1C,MAAM,EAAE,eAAe,EACvB,kBAAkB,CAAC,EAAE,MAAM,GACzB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAOxB;AAwCD,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,eAAe,EAAE,kBAAkB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAqBnH;AAED,MAAM,WAAW,0BAA0B;IAC1C;;;;OAIG;IACH,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,GAAE,0BAA+B,GAAG,YAAY,CAchG","sourcesContent":["/**\n * The MCP SDK lives here and nowhere else.\n *\n * Everything above this file talks to `McpConnection`, so the lifecycle logic, the\n * proxy tool, and their tests never import the SDK or start a process. This module is\n * the boundary where an external protocol becomes an internal type.\n */\n\nimport type { CredentialStore } from \"@earendil-works/pi-ai\";\nimport { Client, StreamableHTTPClientTransport } from \"@modelcontextprotocol/client\";\nimport { StdioClientTransport } from \"@modelcontextprotocol/client/stdio\";\nimport { APP_NAME, VERSION } from \"../../config.ts\";\nimport type { FetchLike } from \"./oauth/discover.ts\";\nimport { authRequired, ensureFreshServerToken } from \"./oauth/mcp-token.ts\";\nimport type { CachedTool, McpCallResult, McpConnection, McpConnector, McpServerConfig } from \"./types.ts\";\n\n/**\n * Authorization for one HTTP connection, in precedence order: a static configured\n * header, then a named bearer env var, then the credential store's OAuth token.\n * Later wins, because each later source is the more deliberately configured one.\n */\nexport function resolveAuthorizationHeaders(\n\tserver: McpServerConfig,\n\toauthAuthorization?: string,\n): Record<string, string> {\n\tif (server.transport.kind !== \"http\") return {};\n\tconst headers = { ...server.transport.headers };\n\tconst token = server.transport.bearerTokenEnv ? process.env[server.transport.bearerTokenEnv] : undefined;\n\tif (token) headers.Authorization = `Bearer ${token}`;\n\tif (oauthAuthorization) headers.Authorization = oauthAuthorization;\n\treturn headers;\n}\n\nfunction createTransport(server: McpServerConfig, oauthAuthorization?: string) {\n\tif (server.transport.kind === \"stdio\") {\n\t\treturn new StdioClientTransport({\n\t\t\tcommand: server.transport.command,\n\t\t\targs: server.transport.args,\n\t\t\tenv: { ...process.env, ...server.transport.env } as Record<string, string>,\n\t\t\tcwd: server.transport.cwd,\n\t\t});\n\t}\n\n\treturn new StreamableHTTPClientTransport(new URL(server.transport.url), {\n\t\trequestInit: { headers: resolveAuthorizationHeaders(server, oauthAuthorization) },\n\t});\n}\n\n/**\n * Server content arrives as a heterogeneous array. Only text is carried forward; an\n * image or an embedded resource is named rather than dropped silently, so a caller\n * can tell the difference between \"returned nothing\" and \"returned something we do\n * not render yet\".\n */\nfunction toCallResult(raw: unknown): McpCallResult {\n\tconst result = raw as { content?: unknown; isError?: unknown };\n\tconst parts = Array.isArray(result?.content) ? result.content : [];\n\tconst content = parts.map((part) => {\n\t\tconst item = part as { type?: unknown; text?: unknown };\n\t\tif (item?.type === \"text\" && typeof item.text === \"string\") {\n\t\t\treturn { type: \"text\" as const, text: item.text };\n\t\t}\n\t\treturn { type: \"text\" as const, text: `[${String(item?.type ?? \"unknown\")} content omitted]` };\n\t});\n\n\treturn {\n\t\tcontent: content.length > 0 ? content : [{ type: \"text\" as const, text: \"(no content)\" }],\n\t\tisError: result?.isError === true,\n\t};\n}\n\nexport async function connectMcpServer(server: McpServerConfig, oauthAuthorization?: string): Promise<McpConnection> {\n\tconst client = new Client({ name: APP_NAME, version: VERSION });\n\tawait client.connect(createTransport(server, oauthAuthorization));\n\n\treturn {\n\t\tasync listTools(): Promise<CachedTool[]> {\n\t\t\tconst { tools } = await client.listTools();\n\t\t\treturn tools.map((tool) => ({\n\t\t\t\tserver: server.name,\n\t\t\t\tname: tool.name,\n\t\t\t\tdescription: tool.description ?? \"\",\n\t\t\t\tinputSchema: tool.inputSchema,\n\t\t\t}));\n\t\t},\n\t\tasync callTool(name: string, args: unknown): Promise<McpCallResult> {\n\t\t\treturn toCallResult(await client.callTool({ name, arguments: (args ?? {}) as Record<string, unknown> }));\n\t\t},\n\t\tasync close(): Promise<void> {\n\t\t\tawait client.close();\n\t\t},\n\t};\n}\n\nexport interface SessionMcpConnectorOptions {\n\t/**\n\t * The session's credential store: `AuthStorage` on the host, `SandboxAuthStorage`\n\t * in a sandboxed child (reads from the projection, writes through the supervisor\n\t * channel). OAuth-configured servers without one fail closed.\n\t */\n\tcredentials?: CredentialStore;\n\tnow?: () => number;\n\tfetch?: FetchLike;\n}\n\n/**\n * The session connector: resolves OAuth tokens for servers configured with\n * `auth: \"oauth\"`, and behaves exactly like `connectMcpServer` for everything else.\n * A missing authorization throws before the transport is constructed, so no OAuth\n * endpoint is ever contacted from a tool call path (spec 2026-09-01-mcp-oauth).\n */\nexport function createSessionMcpConnector(options: SessionMcpConnectorOptions = {}): McpConnector {\n\treturn async (server) => {\n\t\tif (server.transport.kind === \"http\" && server.auth === \"oauth\") {\n\t\t\tif (!options.credentials) throw authRequired(server.name);\n\t\t\tconst accessToken = await ensureFreshServerToken({\n\t\t\t\tserver,\n\t\t\t\tcredentials: options.credentials,\n\t\t\t\tnow: options.now,\n\t\t\t\tfetch: options.fetch,\n\t\t\t});\n\t\t\treturn connectMcpServer(server, `Bearer ${accessToken}`);\n\t\t}\n\t\treturn connectMcpServer(server);\n\t};\n}\n"]}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MCP SDK lives here and nowhere else.
|
|
3
|
+
*
|
|
4
|
+
* Everything above this file talks to `McpConnection`, so the lifecycle logic, the
|
|
5
|
+
* proxy tool, and their tests never import the SDK or start a process. This module is
|
|
6
|
+
* the boundary where an external protocol becomes an internal type.
|
|
7
|
+
*/
|
|
8
|
+
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
9
|
+
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
10
|
+
import { APP_NAME, VERSION } from "../../config.js";
|
|
11
|
+
import { authRequired, ensureFreshServerToken } from "./oauth/mcp-token.js";
|
|
12
|
+
/**
|
|
13
|
+
* Authorization for one HTTP connection, in precedence order: a static configured
|
|
14
|
+
* header, then a named bearer env var, then the credential store's OAuth token.
|
|
15
|
+
* Later wins, because each later source is the more deliberately configured one.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveAuthorizationHeaders(server, oauthAuthorization) {
|
|
18
|
+
if (server.transport.kind !== "http")
|
|
19
|
+
return {};
|
|
20
|
+
const headers = { ...server.transport.headers };
|
|
21
|
+
const token = server.transport.bearerTokenEnv ? process.env[server.transport.bearerTokenEnv] : undefined;
|
|
22
|
+
if (token)
|
|
23
|
+
headers.Authorization = `Bearer ${token}`;
|
|
24
|
+
if (oauthAuthorization)
|
|
25
|
+
headers.Authorization = oauthAuthorization;
|
|
26
|
+
return headers;
|
|
27
|
+
}
|
|
28
|
+
function createTransport(server, oauthAuthorization) {
|
|
29
|
+
if (server.transport.kind === "stdio") {
|
|
30
|
+
return new StdioClientTransport({
|
|
31
|
+
command: server.transport.command,
|
|
32
|
+
args: server.transport.args,
|
|
33
|
+
env: { ...process.env, ...server.transport.env },
|
|
34
|
+
cwd: server.transport.cwd,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return new StreamableHTTPClientTransport(new URL(server.transport.url), {
|
|
38
|
+
requestInit: { headers: resolveAuthorizationHeaders(server, oauthAuthorization) },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Server content arrives as a heterogeneous array. Only text is carried forward; an
|
|
43
|
+
* image or an embedded resource is named rather than dropped silently, so a caller
|
|
44
|
+
* can tell the difference between "returned nothing" and "returned something we do
|
|
45
|
+
* not render yet".
|
|
46
|
+
*/
|
|
47
|
+
function toCallResult(raw) {
|
|
48
|
+
const result = raw;
|
|
49
|
+
const parts = Array.isArray(result?.content) ? result.content : [];
|
|
50
|
+
const content = parts.map((part) => {
|
|
51
|
+
const item = part;
|
|
52
|
+
if (item?.type === "text" && typeof item.text === "string") {
|
|
53
|
+
return { type: "text", text: item.text };
|
|
54
|
+
}
|
|
55
|
+
return { type: "text", text: `[${String(item?.type ?? "unknown")} content omitted]` };
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
content: content.length > 0 ? content : [{ type: "text", text: "(no content)" }],
|
|
59
|
+
isError: result?.isError === true,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export async function connectMcpServer(server, oauthAuthorization) {
|
|
63
|
+
const client = new Client({ name: APP_NAME, version: VERSION });
|
|
64
|
+
await client.connect(createTransport(server, oauthAuthorization));
|
|
65
|
+
return {
|
|
66
|
+
async listTools() {
|
|
67
|
+
const { tools } = await client.listTools();
|
|
68
|
+
return tools.map((tool) => ({
|
|
69
|
+
server: server.name,
|
|
70
|
+
name: tool.name,
|
|
71
|
+
description: tool.description ?? "",
|
|
72
|
+
inputSchema: tool.inputSchema,
|
|
73
|
+
}));
|
|
74
|
+
},
|
|
75
|
+
async callTool(name, args) {
|
|
76
|
+
return toCallResult(await client.callTool({ name, arguments: (args ?? {}) }));
|
|
77
|
+
},
|
|
78
|
+
async close() {
|
|
79
|
+
await client.close();
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The session connector: resolves OAuth tokens for servers configured with
|
|
85
|
+
* `auth: "oauth"`, and behaves exactly like `connectMcpServer` for everything else.
|
|
86
|
+
* A missing authorization throws before the transport is constructed, so no OAuth
|
|
87
|
+
* endpoint is ever contacted from a tool call path (spec 2026-09-01-mcp-oauth).
|
|
88
|
+
*/
|
|
89
|
+
export function createSessionMcpConnector(options = {}) {
|
|
90
|
+
return async (server) => {
|
|
91
|
+
if (server.transport.kind === "http" && server.auth === "oauth") {
|
|
92
|
+
if (!options.credentials)
|
|
93
|
+
throw authRequired(server.name);
|
|
94
|
+
const accessToken = await ensureFreshServerToken({
|
|
95
|
+
server,
|
|
96
|
+
credentials: options.credentials,
|
|
97
|
+
now: options.now,
|
|
98
|
+
fetch: options.fetch,
|
|
99
|
+
});
|
|
100
|
+
return connectMcpServer(server, `Bearer ${accessToken}`);
|
|
101
|
+
}
|
|
102
|
+
return connectMcpServer(server);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=connector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connector.js","sourceRoot":"","sources":["../../../src/core/mcp/connector.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The MCP SDK lives here and nowhere else.\n *\n * Everything above this file talks to `McpConnection`, so the lifecycle logic, the\n * proxy tool, and their tests never import the SDK or start a process. This module is\n * the boundary where an external protocol becomes an internal type.\n */\n\nimport type { CredentialStore } from \"@earendil-works/pi-ai\";\nimport { Client, StreamableHTTPClientTransport } from \"@modelcontextprotocol/client\";\nimport { StdioClientTransport } from \"@modelcontextprotocol/client/stdio\";\nimport { APP_NAME, VERSION } from \"../../config.ts\";\nimport type { FetchLike } from \"./oauth/discover.ts\";\nimport { authRequired, ensureFreshServerToken } from \"./oauth/mcp-token.ts\";\nimport type { CachedTool, McpCallResult, McpConnection, McpConnector, McpServerConfig } from \"./types.ts\";\n\n/**\n * Authorization for one HTTP connection, in precedence order: a static configured\n * header, then a named bearer env var, then the credential store's OAuth token.\n * Later wins, because each later source is the more deliberately configured one.\n */\nexport function resolveAuthorizationHeaders(\n\tserver: McpServerConfig,\n\toauthAuthorization?: string,\n): Record<string, string> {\n\tif (server.transport.kind !== \"http\") return {};\n\tconst headers = { ...server.transport.headers };\n\tconst token = server.transport.bearerTokenEnv ? process.env[server.transport.bearerTokenEnv] : undefined;\n\tif (token) headers.Authorization = `Bearer ${token}`;\n\tif (oauthAuthorization) headers.Authorization = oauthAuthorization;\n\treturn headers;\n}\n\nfunction createTransport(server: McpServerConfig, oauthAuthorization?: string) {\n\tif (server.transport.kind === \"stdio\") {\n\t\treturn new StdioClientTransport({\n\t\t\tcommand: server.transport.command,\n\t\t\targs: server.transport.args,\n\t\t\tenv: { ...process.env, ...server.transport.env } as Record<string, string>,\n\t\t\tcwd: server.transport.cwd,\n\t\t});\n\t}\n\n\treturn new StreamableHTTPClientTransport(new URL(server.transport.url), {\n\t\trequestInit: { headers: resolveAuthorizationHeaders(server, oauthAuthorization) },\n\t});\n}\n\n/**\n * Server content arrives as a heterogeneous array. Only text is carried forward; an\n * image or an embedded resource is named rather than dropped silently, so a caller\n * can tell the difference between \"returned nothing\" and \"returned something we do\n * not render yet\".\n */\nfunction toCallResult(raw: unknown): McpCallResult {\n\tconst result = raw as { content?: unknown; isError?: unknown };\n\tconst parts = Array.isArray(result?.content) ? result.content : [];\n\tconst content = parts.map((part) => {\n\t\tconst item = part as { type?: unknown; text?: unknown };\n\t\tif (item?.type === \"text\" && typeof item.text === \"string\") {\n\t\t\treturn { type: \"text\" as const, text: item.text };\n\t\t}\n\t\treturn { type: \"text\" as const, text: `[${String(item?.type ?? \"unknown\")} content omitted]` };\n\t});\n\n\treturn {\n\t\tcontent: content.length > 0 ? content : [{ type: \"text\" as const, text: \"(no content)\" }],\n\t\tisError: result?.isError === true,\n\t};\n}\n\nexport async function connectMcpServer(server: McpServerConfig, oauthAuthorization?: string): Promise<McpConnection> {\n\tconst client = new Client({ name: APP_NAME, version: VERSION });\n\tawait client.connect(createTransport(server, oauthAuthorization));\n\n\treturn {\n\t\tasync listTools(): Promise<CachedTool[]> {\n\t\t\tconst { tools } = await client.listTools();\n\t\t\treturn tools.map((tool) => ({\n\t\t\t\tserver: server.name,\n\t\t\t\tname: tool.name,\n\t\t\t\tdescription: tool.description ?? \"\",\n\t\t\t\tinputSchema: tool.inputSchema,\n\t\t\t}));\n\t\t},\n\t\tasync callTool(name: string, args: unknown): Promise<McpCallResult> {\n\t\t\treturn toCallResult(await client.callTool({ name, arguments: (args ?? {}) as Record<string, unknown> }));\n\t\t},\n\t\tasync close(): Promise<void> {\n\t\t\tawait client.close();\n\t\t},\n\t};\n}\n\nexport interface SessionMcpConnectorOptions {\n\t/**\n\t * The session's credential store: `AuthStorage` on the host, `SandboxAuthStorage`\n\t * in a sandboxed child (reads from the projection, writes through the supervisor\n\t * channel). OAuth-configured servers without one fail closed.\n\t */\n\tcredentials?: CredentialStore;\n\tnow?: () => number;\n\tfetch?: FetchLike;\n}\n\n/**\n * The session connector: resolves OAuth tokens for servers configured with\n * `auth: \"oauth\"`, and behaves exactly like `connectMcpServer` for everything else.\n * A missing authorization throws before the transport is constructed, so no OAuth\n * endpoint is ever contacted from a tool call path (spec 2026-09-01-mcp-oauth).\n */\nexport function createSessionMcpConnector(options: SessionMcpConnectorOptions = {}): McpConnector {\n\treturn async (server) => {\n\t\tif (server.transport.kind === \"http\" && server.auth === \"oauth\") {\n\t\t\tif (!options.credentials) throw authRequired(server.name);\n\t\t\tconst accessToken = await ensureFreshServerToken({\n\t\t\t\tserver,\n\t\t\t\tcredentials: options.credentials,\n\t\t\t\tnow: options.now,\n\t\t\t\tfetch: options.fetch,\n\t\t\t});\n\t\t\treturn connectMcpServer(server, `Bearer ${accessToken}`);\n\t\t}\n\t\treturn connectMcpServer(server);\n\t};\n}\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tool contract for the `mcp` proxy tool (ADR 0010).
|
|
3
|
+
*
|
|
4
|
+
* This module is the reason MCP is built into Apex Code rather than adopted as an
|
|
5
|
+
* extension. A tool registered from outside the repo cannot supply a contract, so it
|
|
6
|
+
* resolves `UNCLASSIFIED`: every capability, `ask` by default, and rule matching by
|
|
7
|
+
* exact serialized arguments. Under that fallback a second call with different
|
|
8
|
+
* arguments prompts again and no result is ever evictable.
|
|
9
|
+
*
|
|
10
|
+
* The grammar is owned here and nowhere else. The permission engine resolves
|
|
11
|
+
* precedence between rules and never learns what one means.
|
|
12
|
+
*/
|
|
13
|
+
import type { ToolContract } from "../tools/contract.ts";
|
|
14
|
+
import type { McpToolDetails, mcpToolSchema } from "./schema.ts";
|
|
15
|
+
import type { McpServerConfig } from "./types.ts";
|
|
16
|
+
/** Rule covering the metadata actions, which read the local cache and reach no server. */
|
|
17
|
+
export declare const METADATA_RULE = "Mcp(metadata)";
|
|
18
|
+
export declare function createMcpToolContract(servers: ReadonlyMap<string, McpServerConfig>): ToolContract<typeof mcpToolSchema, McpToolDetails>;
|
|
19
|
+
//# sourceMappingURL=contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAc,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAErE,OAAO,KAAK,EAAE,cAAc,EAAiB,aAAa,EAAE,MAAM,aAAa,CAAC;AAChF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,0FAA0F;AAC1F,eAAO,MAAM,aAAa,kBAAkB,CAAC;AA8D7C,wBAAgB,qBAAqB,CACpC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,GAC3C,YAAY,CAAC,OAAO,aAAa,EAAE,cAAc,CAAC,CAYpD","sourcesContent":["/**\n * The tool contract for the `mcp` proxy tool (ADR 0010).\n *\n * This module is the reason MCP is built into Apex Code rather than adopted as an\n * extension. A tool registered from outside the repo cannot supply a contract, so it\n * resolves `UNCLASSIFIED`: every capability, `ask` by default, and rule matching by\n * exact serialized arguments. Under that fallback a second call with different\n * arguments prompts again and no result is ever evictable.\n *\n * The grammar is owned here and nowhere else. The permission engine resolves\n * precedence between rules and never learns what one means.\n */\n\nimport type { Capability, ToolContract } from \"../tools/contract.ts\";\nimport { ALL_CAPABILITIES } from \"../tools/contract.ts\";\nimport type { McpToolDetails, McpToolParams, mcpToolSchema } from \"./schema.ts\";\nimport type { McpServerConfig } from \"./types.ts\";\n\n/** Rule covering the metadata actions, which read the local cache and reach no server. */\nexport const METADATA_RULE = \"Mcp(metadata)\";\n\nconst RULE_PATTERN = /^Mcp\\(([^():*\\s]+):([^():\\s]+)\\)$/;\n\nfunction isCall(params: McpToolParams): params is McpToolParams & { tool: string } {\n\treturn typeof params.tool === \"string\" && params.tool.length > 0;\n}\n\n/** `<server>:<tool>`, the form the model calls and the form a rule names. */\nfunction splitQualifiedName(qualified: string): { server: string; tool: string } | undefined {\n\tconst index = qualified.indexOf(\":\");\n\tif (index <= 0 || index === qualified.length - 1) return undefined;\n\treturn { server: qualified.slice(0, index), tool: qualified.slice(index + 1) };\n}\n\nfunction matches(ruleContent: string, params: McpToolParams): boolean {\n\tif (ruleContent === METADATA_RULE) return !isCall(params);\n\tif (!isCall(params)) return false;\n\n\tconst rule = RULE_PATTERN.exec(ruleContent);\n\tif (!rule) return false;\n\tconst [, ruleServer, ruleTool] = rule;\n\n\tconst target = splitQualifiedName(params.tool);\n\tif (!target) return false;\n\n\tif (ruleServer !== target.server) return false;\n\treturn ruleTool === \"*\" || ruleTool === target.tool;\n}\n\nfunction ruleForCall(params: McpToolParams): string | null {\n\tif (!isCall(params)) return METADATA_RULE;\n\tconst target = splitQualifiedName(params.tool);\n\treturn target ? `Mcp(${target.server}:${target.tool})` : null;\n}\n\nfunction describe(ruleContent: string): string {\n\tif (ruleContent === METADATA_RULE) return \"Searching and describing MCP tools (no server is contacted)\";\n\n\tconst rule = RULE_PATTERN.exec(ruleContent);\n\tif (!rule) return `Unrecognized MCP rule: ${ruleContent}`;\n\tconst [, server, tool] = rule;\n\treturn tool === \"*\"\n\t\t? `Calling any tool on the \"${server}\" MCP server`\n\t\t: `Calling \"${tool}\" on the \"${server}\" MCP server`;\n}\n\n/**\n * One proxy tool carries one capability set, and that set feeds the delegation\n * ceiling (ADR 0008) and mode resolution. The union across configured servers is the\n * only sound answer: narrower would understate what a call can do, and a fixed full\n * set would ignore what a user took the trouble to declare.\n */\nfunction unionCapabilities(servers: Iterable<McpServerConfig>): ReadonlySet<Capability> {\n\tconst union = new Set<Capability>();\n\tfor (const server of servers) {\n\t\tfor (const capability of server.capabilities) union.add(capability);\n\t\tif (union.size === ALL_CAPABILITIES.size) return ALL_CAPABILITIES;\n\t}\n\treturn union;\n}\n\nexport function createMcpToolContract(\n\tservers: ReadonlyMap<string, McpServerConfig>,\n): ToolContract<typeof mcpToolSchema, McpToolDetails> {\n\treturn {\n\t\tcapabilities: unionCapabilities(servers.values()),\n\t\tpermission: {\n\t\t\tdefaultBehavior: \"ask\",\n\t\t\tmatches,\n\t\t\tdescribe,\n\t\t\truleForCall,\n\t\t},\n\t\tcontext: { resultRecoverable: false, deferSchema: true },\n\t\tevidence: { emits: new Set(), capture: () => [] },\n\t};\n}\n"]}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tool contract for the `mcp` proxy tool (ADR 0010).
|
|
3
|
+
*
|
|
4
|
+
* This module is the reason MCP is built into Apex Code rather than adopted as an
|
|
5
|
+
* extension. A tool registered from outside the repo cannot supply a contract, so it
|
|
6
|
+
* resolves `UNCLASSIFIED`: every capability, `ask` by default, and rule matching by
|
|
7
|
+
* exact serialized arguments. Under that fallback a second call with different
|
|
8
|
+
* arguments prompts again and no result is ever evictable.
|
|
9
|
+
*
|
|
10
|
+
* The grammar is owned here and nowhere else. The permission engine resolves
|
|
11
|
+
* precedence between rules and never learns what one means.
|
|
12
|
+
*/
|
|
13
|
+
import { ALL_CAPABILITIES } from "../tools/contract.js";
|
|
14
|
+
/** Rule covering the metadata actions, which read the local cache and reach no server. */
|
|
15
|
+
export const METADATA_RULE = "Mcp(metadata)";
|
|
16
|
+
const RULE_PATTERN = /^Mcp\(([^():*\s]+):([^():\s]+)\)$/;
|
|
17
|
+
function isCall(params) {
|
|
18
|
+
return typeof params.tool === "string" && params.tool.length > 0;
|
|
19
|
+
}
|
|
20
|
+
/** `<server>:<tool>`, the form the model calls and the form a rule names. */
|
|
21
|
+
function splitQualifiedName(qualified) {
|
|
22
|
+
const index = qualified.indexOf(":");
|
|
23
|
+
if (index <= 0 || index === qualified.length - 1)
|
|
24
|
+
return undefined;
|
|
25
|
+
return { server: qualified.slice(0, index), tool: qualified.slice(index + 1) };
|
|
26
|
+
}
|
|
27
|
+
function matches(ruleContent, params) {
|
|
28
|
+
if (ruleContent === METADATA_RULE)
|
|
29
|
+
return !isCall(params);
|
|
30
|
+
if (!isCall(params))
|
|
31
|
+
return false;
|
|
32
|
+
const rule = RULE_PATTERN.exec(ruleContent);
|
|
33
|
+
if (!rule)
|
|
34
|
+
return false;
|
|
35
|
+
const [, ruleServer, ruleTool] = rule;
|
|
36
|
+
const target = splitQualifiedName(params.tool);
|
|
37
|
+
if (!target)
|
|
38
|
+
return false;
|
|
39
|
+
if (ruleServer !== target.server)
|
|
40
|
+
return false;
|
|
41
|
+
return ruleTool === "*" || ruleTool === target.tool;
|
|
42
|
+
}
|
|
43
|
+
function ruleForCall(params) {
|
|
44
|
+
if (!isCall(params))
|
|
45
|
+
return METADATA_RULE;
|
|
46
|
+
const target = splitQualifiedName(params.tool);
|
|
47
|
+
return target ? `Mcp(${target.server}:${target.tool})` : null;
|
|
48
|
+
}
|
|
49
|
+
function describe(ruleContent) {
|
|
50
|
+
if (ruleContent === METADATA_RULE)
|
|
51
|
+
return "Searching and describing MCP tools (no server is contacted)";
|
|
52
|
+
const rule = RULE_PATTERN.exec(ruleContent);
|
|
53
|
+
if (!rule)
|
|
54
|
+
return `Unrecognized MCP rule: ${ruleContent}`;
|
|
55
|
+
const [, server, tool] = rule;
|
|
56
|
+
return tool === "*"
|
|
57
|
+
? `Calling any tool on the "${server}" MCP server`
|
|
58
|
+
: `Calling "${tool}" on the "${server}" MCP server`;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* One proxy tool carries one capability set, and that set feeds the delegation
|
|
62
|
+
* ceiling (ADR 0008) and mode resolution. The union across configured servers is the
|
|
63
|
+
* only sound answer: narrower would understate what a call can do, and a fixed full
|
|
64
|
+
* set would ignore what a user took the trouble to declare.
|
|
65
|
+
*/
|
|
66
|
+
function unionCapabilities(servers) {
|
|
67
|
+
const union = new Set();
|
|
68
|
+
for (const server of servers) {
|
|
69
|
+
for (const capability of server.capabilities)
|
|
70
|
+
union.add(capability);
|
|
71
|
+
if (union.size === ALL_CAPABILITIES.size)
|
|
72
|
+
return ALL_CAPABILITIES;
|
|
73
|
+
}
|
|
74
|
+
return union;
|
|
75
|
+
}
|
|
76
|
+
export function createMcpToolContract(servers) {
|
|
77
|
+
return {
|
|
78
|
+
capabilities: unionCapabilities(servers.values()),
|
|
79
|
+
permission: {
|
|
80
|
+
defaultBehavior: "ask",
|
|
81
|
+
matches,
|
|
82
|
+
describe,
|
|
83
|
+
ruleForCall,
|
|
84
|
+
},
|
|
85
|
+
context: { resultRecoverable: false, deferSchema: true },
|
|
86
|
+
evidence: { emits: new Set(), capture: () => [] },
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../../../src/core/mcp/contract.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The tool contract for the `mcp` proxy tool (ADR 0010).\n *\n * This module is the reason MCP is built into Apex Code rather than adopted as an\n * extension. A tool registered from outside the repo cannot supply a contract, so it\n * resolves `UNCLASSIFIED`: every capability, `ask` by default, and rule matching by\n * exact serialized arguments. Under that fallback a second call with different\n * arguments prompts again and no result is ever evictable.\n *\n * The grammar is owned here and nowhere else. The permission engine resolves\n * precedence between rules and never learns what one means.\n */\n\nimport type { Capability, ToolContract } from \"../tools/contract.ts\";\nimport { ALL_CAPABILITIES } from \"../tools/contract.ts\";\nimport type { McpToolDetails, McpToolParams, mcpToolSchema } from \"./schema.ts\";\nimport type { McpServerConfig } from \"./types.ts\";\n\n/** Rule covering the metadata actions, which read the local cache and reach no server. */\nexport const METADATA_RULE = \"Mcp(metadata)\";\n\nconst RULE_PATTERN = /^Mcp\\(([^():*\\s]+):([^():\\s]+)\\)$/;\n\nfunction isCall(params: McpToolParams): params is McpToolParams & { tool: string } {\n\treturn typeof params.tool === \"string\" && params.tool.length > 0;\n}\n\n/** `<server>:<tool>`, the form the model calls and the form a rule names. */\nfunction splitQualifiedName(qualified: string): { server: string; tool: string } | undefined {\n\tconst index = qualified.indexOf(\":\");\n\tif (index <= 0 || index === qualified.length - 1) return undefined;\n\treturn { server: qualified.slice(0, index), tool: qualified.slice(index + 1) };\n}\n\nfunction matches(ruleContent: string, params: McpToolParams): boolean {\n\tif (ruleContent === METADATA_RULE) return !isCall(params);\n\tif (!isCall(params)) return false;\n\n\tconst rule = RULE_PATTERN.exec(ruleContent);\n\tif (!rule) return false;\n\tconst [, ruleServer, ruleTool] = rule;\n\n\tconst target = splitQualifiedName(params.tool);\n\tif (!target) return false;\n\n\tif (ruleServer !== target.server) return false;\n\treturn ruleTool === \"*\" || ruleTool === target.tool;\n}\n\nfunction ruleForCall(params: McpToolParams): string | null {\n\tif (!isCall(params)) return METADATA_RULE;\n\tconst target = splitQualifiedName(params.tool);\n\treturn target ? `Mcp(${target.server}:${target.tool})` : null;\n}\n\nfunction describe(ruleContent: string): string {\n\tif (ruleContent === METADATA_RULE) return \"Searching and describing MCP tools (no server is contacted)\";\n\n\tconst rule = RULE_PATTERN.exec(ruleContent);\n\tif (!rule) return `Unrecognized MCP rule: ${ruleContent}`;\n\tconst [, server, tool] = rule;\n\treturn tool === \"*\"\n\t\t? `Calling any tool on the \"${server}\" MCP server`\n\t\t: `Calling \"${tool}\" on the \"${server}\" MCP server`;\n}\n\n/**\n * One proxy tool carries one capability set, and that set feeds the delegation\n * ceiling (ADR 0008) and mode resolution. The union across configured servers is the\n * only sound answer: narrower would understate what a call can do, and a fixed full\n * set would ignore what a user took the trouble to declare.\n */\nfunction unionCapabilities(servers: Iterable<McpServerConfig>): ReadonlySet<Capability> {\n\tconst union = new Set<Capability>();\n\tfor (const server of servers) {\n\t\tfor (const capability of server.capabilities) union.add(capability);\n\t\tif (union.size === ALL_CAPABILITIES.size) return ALL_CAPABILITIES;\n\t}\n\treturn union;\n}\n\nexport function createMcpToolContract(\n\tservers: ReadonlyMap<string, McpServerConfig>,\n): ToolContract<typeof mcpToolSchema, McpToolDetails> {\n\treturn {\n\t\tcapabilities: unionCapabilities(servers.values()),\n\t\tpermission: {\n\t\t\tdefaultBehavior: \"ask\",\n\t\t\tmatches,\n\t\t\tdescribe,\n\t\t\truleForCall,\n\t\t},\n\t\tcontext: { resultRecoverable: false, deferSchema: true },\n\t\tevidence: { emits: new Set(), capture: () => [] },\n\t};\n}\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One tool for every MCP server, instead of one tool per server tool.
|
|
3
|
+
*
|
|
4
|
+
* Registering each server tool individually costs 150-300 prompt tokens apiece, and
|
|
5
|
+
* two ordinary servers would exceed the whole static-prefix budget before a
|
|
6
|
+
* conversation starts. The proxy costs one announced name and this description,
|
|
7
|
+
* because its schema defers.
|
|
8
|
+
*
|
|
9
|
+
* `search` and `describe` read the disk cache and reach no server. Only a `tool` call
|
|
10
|
+
* connects, which is what makes discovery free and is why the two have separate
|
|
11
|
+
* permission rules (ADR 0025).
|
|
12
|
+
*/
|
|
13
|
+
import type { ApexToolDefinition } from "../tools/contract.ts";
|
|
14
|
+
import type { McpMetadataCache } from "./metadata-cache.ts";
|
|
15
|
+
import { type McpToolDetails, mcpToolSchema } from "./schema.ts";
|
|
16
|
+
import type { McpServerManager } from "./server-manager.ts";
|
|
17
|
+
import type { McpServerConfig } from "./types.ts";
|
|
18
|
+
export declare const MCP_TOOL_NAME = "mcp";
|
|
19
|
+
export interface McpToolOptions {
|
|
20
|
+
servers: ReadonlyMap<string, McpServerConfig>;
|
|
21
|
+
cache: McpMetadataCache;
|
|
22
|
+
manager: McpServerManager;
|
|
23
|
+
}
|
|
24
|
+
export declare function createMcpToolDefinition(options: McpToolOptions): ApexToolDefinition<typeof mcpToolSchema, McpToolDetails>;
|
|
25
|
+
export declare function createMcpTool(options: McpToolOptions): import("apex-code-agent-core").AgentTool<import("typebox").TObject<{
|
|
26
|
+
search: import("typebox").TOptional<import("typebox").TString>;
|
|
27
|
+
server: import("typebox").TOptional<import("typebox").TString>;
|
|
28
|
+
describe: import("typebox").TOptional<import("typebox").TString>;
|
|
29
|
+
tool: import("typebox").TOptional<import("typebox").TString>;
|
|
30
|
+
args: import("typebox").TOptional<import("typebox").TUnknown>;
|
|
31
|
+
}>, McpToolDetails>;
|
|
32
|
+
//# sourceMappingURL=mcp-tool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-tool.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/mcp-tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAG/D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,EAAE,KAAK,cAAc,EAAsB,aAAa,EAAE,MAAM,aAAa,CAAC;AACrF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EAAc,eAAe,EAAE,MAAM,YAAY,CAAC;AAE9D,eAAO,MAAM,aAAa,QAAQ,CAAC;AAwCnC,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC9C,KAAK,EAAE,gBAAgB,CAAC;IACxB,OAAO,EAAE,gBAAgB,CAAC;CAC1B;AAED,wBAAgB,uBAAuB,CACtC,OAAO,EAAE,cAAc,GACrB,kBAAkB,CAAC,OAAO,aAAa,EAAE,cAAc,CAAC,CA0I1D;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc;;;;;;oBAEpD","sourcesContent":["/**\n * One tool for every MCP server, instead of one tool per server tool.\n *\n * Registering each server tool individually costs 150-300 prompt tokens apiece, and\n * two ordinary servers would exceed the whole static-prefix budget before a\n * conversation starts. The proxy costs one announced name and this description,\n * because its schema defers.\n *\n * `search` and `describe` read the disk cache and reach no server. Only a `tool` call\n * connects, which is what makes discovery free and is why the two have separate\n * permission rules (ADR 0025).\n */\n\nimport type { AgentToolResult } from \"apex-code-agent-core\";\nimport type { ApexToolDefinition } from \"../tools/contract.ts\";\nimport { wrapToolDefinition } from \"../tools/tool-definition-wrapper.ts\";\nimport { createMcpToolContract } from \"./contract.ts\";\nimport type { McpMetadataCache } from \"./metadata-cache.ts\";\nimport { McpOAuthRefreshError, McpOAuthRequiredError } from \"./oauth/mcp-token.ts\";\nimport { type McpToolDetails, type McpToolParams, mcpToolSchema } from \"./schema.ts\";\nimport type { McpServerManager } from \"./server-manager.ts\";\nimport type { CachedTool, McpServerConfig } from \"./types.ts\";\n\nexport const MCP_TOOL_NAME = \"mcp\";\n\nconst MAX_RESULTS = 25;\n\nconst DESCRIPTION =\n\t\"Use tools provided by configured MCP servers. \" +\n\t'Search with {\"search\":\"keyword\"}, list one server with {\"server\":\"name\"}, ' +\n\t'see a tool\\'s full schema with {\"describe\":\"server:tool\"}, ' +\n\t'and call one with {\"tool\":\"server:tool\",\"args\":{...}}. ' +\n\t\"Search and describe read a local cache and never start a server, so discovery is free. \" +\n\t\"Always describe a tool before calling it for the first time.\";\n\nfunction text(body: string): AgentToolResult<McpToolDetails>[\"content\"] {\n\treturn [{ type: \"text\", text: body }];\n}\n\nfunction qualified(tool: CachedTool): string {\n\treturn `${tool.server}:${tool.name}`;\n}\n\n/** Substring scoring, weighted to the name, which is what the model searched for. */\nfunction score(tool: CachedTool, needle: string): number {\n\tconst name = qualified(tool).toLowerCase();\n\tconst description = tool.description.toLowerCase();\n\tif (name === needle) return 100;\n\tif (name.includes(needle)) return 50;\n\tif (description.includes(needle)) return 10;\n\treturn 0;\n}\n\nfunction renderList(tools: readonly CachedTool[]): string {\n\treturn tools.map((tool) => `${qualified(tool)}: ${tool.description}`).join(\"\\n\");\n}\n\nfunction nearest(tools: readonly CachedTool[], target: string): CachedTool[] {\n\tconst needle = target.toLowerCase();\n\tconst stem = needle.slice(0, Math.max(3, Math.floor(needle.length * 0.6)));\n\treturn tools.filter((tool) => qualified(tool).toLowerCase().includes(stem)).slice(0, 5);\n}\n\nexport interface McpToolOptions {\n\tservers: ReadonlyMap<string, McpServerConfig>;\n\tcache: McpMetadataCache;\n\tmanager: McpServerManager;\n}\n\nexport function createMcpToolDefinition(\n\toptions: McpToolOptions,\n): ApexToolDefinition<typeof mcpToolSchema, McpToolDetails> {\n\tconst { servers, cache, manager } = options;\n\n\treturn {\n\t\tname: MCP_TOOL_NAME,\n\t\tlabel: \"mcp\",\n\t\tdescription: DESCRIPTION,\n\t\tpromptSnippet: \"Call tools on configured MCP servers\",\n\t\tparameters: mcpToolSchema,\n\t\tcontract: createMcpToolContract(servers),\n\t\tasync execute(_id, params: McpToolParams): Promise<AgentToolResult<McpToolDetails>> {\n\t\t\tif (params.tool) return callTool(params.tool, params.args);\n\t\t\tif (params.describe) return describeTool(params.describe);\n\t\t\tif (params.search !== undefined) return searchTools(params.search, params.server);\n\t\t\tif (params.server) return listServer(params.server);\n\n\t\t\treturn {\n\t\t\t\tcontent: text('Provide one of \"search\", \"server\", \"describe\", or \"tool\".'),\n\t\t\t\tdetails: { action: \"search\", server: undefined, tool: undefined },\n\t\t\t};\n\t\t},\n\t};\n\n\tfunction searchTools(needle: string, serverFilter?: string): AgentToolResult<McpToolDetails> {\n\t\tconst pool = serverFilter ? cache.all().filter((tool) => tool.server === serverFilter) : cache.all();\n\t\tconst lowered = needle.toLowerCase();\n\t\tconst ranked = pool\n\t\t\t.map((tool) => ({ tool, rank: score(tool, lowered) }))\n\t\t\t.filter((entry) => entry.rank > 0)\n\t\t\t.sort((a, b) => b.rank - a.rank)\n\t\t\t.slice(0, MAX_RESULTS)\n\t\t\t.map((entry) => entry.tool);\n\n\t\treturn {\n\t\t\tcontent: text(\n\t\t\t\tranked.length > 0\n\t\t\t\t\t? renderList(ranked)\n\t\t\t\t\t: `No cached MCP tool matches \"${needle}\". Configured servers: ${[...servers.keys()].join(\", \") || \"none\"}.`,\n\t\t\t),\n\t\t\tdetails: { action: \"search\", server: serverFilter, tool: undefined },\n\t\t};\n\t}\n\n\tfunction listServer(name: string): AgentToolResult<McpToolDetails> {\n\t\tif (!servers.has(name)) {\n\t\t\treturn {\n\t\t\t\tcontent: text(\n\t\t\t\t\t`MCP server \"${name}\" is not configured. Configured servers: ${[...servers.keys()].join(\", \") || \"none\"}.`,\n\t\t\t\t),\n\t\t\t\tdetails: { action: \"list\", server: name, tool: undefined },\n\t\t\t};\n\t\t}\n\n\t\tconst tools = cache.all().filter((tool) => tool.server === name);\n\t\treturn {\n\t\t\tcontent: text(\n\t\t\t\ttools.length > 0\n\t\t\t\t\t? renderList(tools)\n\t\t\t\t\t: `No cached tools for \"${name}\" yet. Tools are cached the first time the server is called, so call a known tool on it, or set \"lifecycle\": \"eager\" for this server in .mcp.json to have them cached at startup.`,\n\t\t\t),\n\t\t\tdetails: { action: \"list\", server: name, tool: undefined },\n\t\t};\n\t}\n\n\tfunction describeTool(target: string): AgentToolResult<McpToolDetails> {\n\t\tconst found = cache.all().find((tool) => qualified(tool) === target);\n\t\tif (!found) {\n\t\t\tconst suggestions = nearest(cache.all(), target);\n\t\t\treturn {\n\t\t\t\tcontent: text(\n\t\t\t\t\tsuggestions.length > 0\n\t\t\t\t\t\t? `No cached tool named \"${target}\". Did you mean:\\n${renderList(suggestions)}`\n\t\t\t\t\t\t: `No cached tool named \"${target}\". Search with {\"search\":\"keyword\"}.`,\n\t\t\t\t),\n\t\t\t\tdetails: { action: \"describe\", server: undefined, tool: target },\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tcontent: text(\n\t\t\t\t`${qualified(found)}\\n${found.description}\\n\\nInput schema:\\n${JSON.stringify(found.inputSchema, null, 2)}`,\n\t\t\t),\n\t\t\tdetails: { action: \"describe\", server: found.server, tool: target },\n\t\t};\n\t}\n\n\tasync function callTool(target: string, args: unknown): Promise<AgentToolResult<McpToolDetails>> {\n\t\tconst separator = target.indexOf(\":\");\n\t\tconst details: McpToolDetails = { action: \"call\", server: undefined, tool: target };\n\t\tif (separator <= 0) {\n\t\t\treturn { content: text(`Tool must be qualified as \"server:tool\". Received \"${target}\".`), details };\n\t\t}\n\n\t\tconst serverName = target.slice(0, separator);\n\t\tconst toolName = target.slice(separator + 1);\n\t\tdetails.server = serverName;\n\n\t\tif (!servers.has(serverName)) {\n\t\t\treturn {\n\t\t\t\tcontent: text(\n\t\t\t\t\t`MCP server \"${serverName}\" is not configured. Configured servers: ${[...servers.keys()].join(\", \") || \"none\"}.`,\n\t\t\t\t),\n\t\t\t\tdetails,\n\t\t\t};\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = await manager.withConnection(serverName, async (connection) => {\n\t\t\t\t// An empty list is a fact about the server, not a failed read. Keeping the\n\t\t\t\t// previous entry would leave search advertising tools that no longer exist.\n\t\t\t\tconst fresh = await connection.listTools();\n\t\t\t\tcache.set(servers.get(serverName) as McpServerConfig, fresh);\n\t\t\t\tcache.save();\n\t\t\t\treturn connection.callTool(toolName, args);\n\t\t\t});\n\n\t\t\t// `AgentToolResult` has no error flag; a server-reported failure is marked in\n\t\t\t// the text so the model can see it failed without the turn being aborted.\n\t\t\tconst content = result.isError\n\t\t\t\t? text(`Tool reported an error:\\n${result.content.map((part) => part.text).join(\"\\n\")}`)\n\t\t\t\t: result.content;\n\t\t\treturn { content, details };\n\t\t} catch (error) {\n\t\t\t// OAuth failures already carry the one action that helps (`apex-code mcp auth\n\t\t\t// <server>`); the generic \"unavailable\" wrapper would bury it.\n\t\t\tif (error instanceof McpOAuthRequiredError || error instanceof McpOAuthRefreshError) {\n\t\t\t\treturn { content: text(error.message), details };\n\t\t\t}\n\t\t\t// A failed server is reported, not retried: the manager holds it in `failed`\n\t\t\t// with a backoff window so one broken server cannot cost every call a spawn.\n\t\t\treturn {\n\t\t\t\tcontent: text(\n\t\t\t\t\t`MCP server \"${serverName}\" is unavailable: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\t),\n\t\t\t\tdetails,\n\t\t\t};\n\t\t}\n\t}\n}\n\nexport function createMcpTool(options: McpToolOptions) {\n\treturn wrapToolDefinition(createMcpToolDefinition(options));\n}\n"]}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One tool for every MCP server, instead of one tool per server tool.
|
|
3
|
+
*
|
|
4
|
+
* Registering each server tool individually costs 150-300 prompt tokens apiece, and
|
|
5
|
+
* two ordinary servers would exceed the whole static-prefix budget before a
|
|
6
|
+
* conversation starts. The proxy costs one announced name and this description,
|
|
7
|
+
* because its schema defers.
|
|
8
|
+
*
|
|
9
|
+
* `search` and `describe` read the disk cache and reach no server. Only a `tool` call
|
|
10
|
+
* connects, which is what makes discovery free and is why the two have separate
|
|
11
|
+
* permission rules (ADR 0025).
|
|
12
|
+
*/
|
|
13
|
+
import { wrapToolDefinition } from "../tools/tool-definition-wrapper.js";
|
|
14
|
+
import { createMcpToolContract } from "./contract.js";
|
|
15
|
+
import { McpOAuthRefreshError, McpOAuthRequiredError } from "./oauth/mcp-token.js";
|
|
16
|
+
import { mcpToolSchema } from "./schema.js";
|
|
17
|
+
export const MCP_TOOL_NAME = "mcp";
|
|
18
|
+
const MAX_RESULTS = 25;
|
|
19
|
+
const DESCRIPTION = "Use tools provided by configured MCP servers. " +
|
|
20
|
+
'Search with {"search":"keyword"}, list one server with {"server":"name"}, ' +
|
|
21
|
+
'see a tool\'s full schema with {"describe":"server:tool"}, ' +
|
|
22
|
+
'and call one with {"tool":"server:tool","args":{...}}. ' +
|
|
23
|
+
"Search and describe read a local cache and never start a server, so discovery is free. " +
|
|
24
|
+
"Always describe a tool before calling it for the first time.";
|
|
25
|
+
function text(body) {
|
|
26
|
+
return [{ type: "text", text: body }];
|
|
27
|
+
}
|
|
28
|
+
function qualified(tool) {
|
|
29
|
+
return `${tool.server}:${tool.name}`;
|
|
30
|
+
}
|
|
31
|
+
/** Substring scoring, weighted to the name, which is what the model searched for. */
|
|
32
|
+
function score(tool, needle) {
|
|
33
|
+
const name = qualified(tool).toLowerCase();
|
|
34
|
+
const description = tool.description.toLowerCase();
|
|
35
|
+
if (name === needle)
|
|
36
|
+
return 100;
|
|
37
|
+
if (name.includes(needle))
|
|
38
|
+
return 50;
|
|
39
|
+
if (description.includes(needle))
|
|
40
|
+
return 10;
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
function renderList(tools) {
|
|
44
|
+
return tools.map((tool) => `${qualified(tool)}: ${tool.description}`).join("\n");
|
|
45
|
+
}
|
|
46
|
+
function nearest(tools, target) {
|
|
47
|
+
const needle = target.toLowerCase();
|
|
48
|
+
const stem = needle.slice(0, Math.max(3, Math.floor(needle.length * 0.6)));
|
|
49
|
+
return tools.filter((tool) => qualified(tool).toLowerCase().includes(stem)).slice(0, 5);
|
|
50
|
+
}
|
|
51
|
+
export function createMcpToolDefinition(options) {
|
|
52
|
+
const { servers, cache, manager } = options;
|
|
53
|
+
return {
|
|
54
|
+
name: MCP_TOOL_NAME,
|
|
55
|
+
label: "mcp",
|
|
56
|
+
description: DESCRIPTION,
|
|
57
|
+
promptSnippet: "Call tools on configured MCP servers",
|
|
58
|
+
parameters: mcpToolSchema,
|
|
59
|
+
contract: createMcpToolContract(servers),
|
|
60
|
+
async execute(_id, params) {
|
|
61
|
+
if (params.tool)
|
|
62
|
+
return callTool(params.tool, params.args);
|
|
63
|
+
if (params.describe)
|
|
64
|
+
return describeTool(params.describe);
|
|
65
|
+
if (params.search !== undefined)
|
|
66
|
+
return searchTools(params.search, params.server);
|
|
67
|
+
if (params.server)
|
|
68
|
+
return listServer(params.server);
|
|
69
|
+
return {
|
|
70
|
+
content: text('Provide one of "search", "server", "describe", or "tool".'),
|
|
71
|
+
details: { action: "search", server: undefined, tool: undefined },
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
function searchTools(needle, serverFilter) {
|
|
76
|
+
const pool = serverFilter ? cache.all().filter((tool) => tool.server === serverFilter) : cache.all();
|
|
77
|
+
const lowered = needle.toLowerCase();
|
|
78
|
+
const ranked = pool
|
|
79
|
+
.map((tool) => ({ tool, rank: score(tool, lowered) }))
|
|
80
|
+
.filter((entry) => entry.rank > 0)
|
|
81
|
+
.sort((a, b) => b.rank - a.rank)
|
|
82
|
+
.slice(0, MAX_RESULTS)
|
|
83
|
+
.map((entry) => entry.tool);
|
|
84
|
+
return {
|
|
85
|
+
content: text(ranked.length > 0
|
|
86
|
+
? renderList(ranked)
|
|
87
|
+
: `No cached MCP tool matches "${needle}". Configured servers: ${[...servers.keys()].join(", ") || "none"}.`),
|
|
88
|
+
details: { action: "search", server: serverFilter, tool: undefined },
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function listServer(name) {
|
|
92
|
+
if (!servers.has(name)) {
|
|
93
|
+
return {
|
|
94
|
+
content: text(`MCP server "${name}" is not configured. Configured servers: ${[...servers.keys()].join(", ") || "none"}.`),
|
|
95
|
+
details: { action: "list", server: name, tool: undefined },
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const tools = cache.all().filter((tool) => tool.server === name);
|
|
99
|
+
return {
|
|
100
|
+
content: text(tools.length > 0
|
|
101
|
+
? renderList(tools)
|
|
102
|
+
: `No cached tools for "${name}" yet. Tools are cached the first time the server is called, so call a known tool on it, or set "lifecycle": "eager" for this server in .mcp.json to have them cached at startup.`),
|
|
103
|
+
details: { action: "list", server: name, tool: undefined },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function describeTool(target) {
|
|
107
|
+
const found = cache.all().find((tool) => qualified(tool) === target);
|
|
108
|
+
if (!found) {
|
|
109
|
+
const suggestions = nearest(cache.all(), target);
|
|
110
|
+
return {
|
|
111
|
+
content: text(suggestions.length > 0
|
|
112
|
+
? `No cached tool named "${target}". Did you mean:\n${renderList(suggestions)}`
|
|
113
|
+
: `No cached tool named "${target}". Search with {"search":"keyword"}.`),
|
|
114
|
+
details: { action: "describe", server: undefined, tool: target },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
content: text(`${qualified(found)}\n${found.description}\n\nInput schema:\n${JSON.stringify(found.inputSchema, null, 2)}`),
|
|
119
|
+
details: { action: "describe", server: found.server, tool: target },
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async function callTool(target, args) {
|
|
123
|
+
const separator = target.indexOf(":");
|
|
124
|
+
const details = { action: "call", server: undefined, tool: target };
|
|
125
|
+
if (separator <= 0) {
|
|
126
|
+
return { content: text(`Tool must be qualified as "server:tool". Received "${target}".`), details };
|
|
127
|
+
}
|
|
128
|
+
const serverName = target.slice(0, separator);
|
|
129
|
+
const toolName = target.slice(separator + 1);
|
|
130
|
+
details.server = serverName;
|
|
131
|
+
if (!servers.has(serverName)) {
|
|
132
|
+
return {
|
|
133
|
+
content: text(`MCP server "${serverName}" is not configured. Configured servers: ${[...servers.keys()].join(", ") || "none"}.`),
|
|
134
|
+
details,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const result = await manager.withConnection(serverName, async (connection) => {
|
|
139
|
+
// An empty list is a fact about the server, not a failed read. Keeping the
|
|
140
|
+
// previous entry would leave search advertising tools that no longer exist.
|
|
141
|
+
const fresh = await connection.listTools();
|
|
142
|
+
cache.set(servers.get(serverName), fresh);
|
|
143
|
+
cache.save();
|
|
144
|
+
return connection.callTool(toolName, args);
|
|
145
|
+
});
|
|
146
|
+
// `AgentToolResult` has no error flag; a server-reported failure is marked in
|
|
147
|
+
// the text so the model can see it failed without the turn being aborted.
|
|
148
|
+
const content = result.isError
|
|
149
|
+
? text(`Tool reported an error:\n${result.content.map((part) => part.text).join("\n")}`)
|
|
150
|
+
: result.content;
|
|
151
|
+
return { content, details };
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
// OAuth failures already carry the one action that helps (`apex-code mcp auth
|
|
155
|
+
// <server>`); the generic "unavailable" wrapper would bury it.
|
|
156
|
+
if (error instanceof McpOAuthRequiredError || error instanceof McpOAuthRefreshError) {
|
|
157
|
+
return { content: text(error.message), details };
|
|
158
|
+
}
|
|
159
|
+
// A failed server is reported, not retried: the manager holds it in `failed`
|
|
160
|
+
// with a backoff window so one broken server cannot cost every call a spawn.
|
|
161
|
+
return {
|
|
162
|
+
content: text(`MCP server "${serverName}" is unavailable: ${error instanceof Error ? error.message : String(error)}`),
|
|
163
|
+
details,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export function createMcpTool(options) {
|
|
169
|
+
return wrapToolDefinition(createMcpToolDefinition(options));
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=mcp-tool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-tool.js","sourceRoot":"","sources":["../../../src/core/mcp/mcp-tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AACzE,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAEtD,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AACnF,OAAO,EAA2C,aAAa,EAAE,MAAM,aAAa,CAAC;AAIrF,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,CAAC;AAEnC,MAAM,WAAW,GAAG,EAAE,CAAC;AAEvB,MAAM,WAAW,GAChB,gDAAgD;IAChD,4EAA4E;IAC5E,6DAA6D;IAC7D,yDAAyD;IACzD,yFAAyF;IACzF,8DAA8D,CAAC;AAEhE,SAAS,IAAI,CAAC,IAAY,EAA8C;IACvE,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;AAAA,CACtC;AAED,SAAS,SAAS,CAAC,IAAgB,EAAU;IAC5C,OAAO,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;AAAA,CACrC;AAED,qFAAqF;AACrF,SAAS,KAAK,CAAC,IAAgB,EAAE,MAAc,EAAU;IACxD,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;IAC3C,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC,WAAW,EAAE,CAAC;IACnD,IAAI,IAAI,KAAK,MAAM;QAAE,OAAO,GAAG,CAAC;IAChC,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,CAAC;IACrC,IAAI,WAAW,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,CAAC;IAC5C,OAAO,CAAC,CAAC;AAAA,CACT;AAED,SAAS,UAAU,CAAC,KAA4B,EAAU;IACzD,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAAA,CACjF;AAED,SAAS,OAAO,CAAC,KAA4B,EAAE,MAAc,EAAgB;IAC5E,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;IACpC,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;IAC3E,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAAA,CACxF;AAQD,MAAM,UAAU,uBAAuB,CACtC,OAAuB,EACoC;IAC3D,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;IAE5C,OAAO;QACN,IAAI,EAAE,aAAa;QACnB,KAAK,EAAE,KAAK;QACZ,WAAW,EAAE,WAAW;QACxB,aAAa,EAAE,sCAAsC;QACrD,UAAU,EAAE,aAAa;QACzB,QAAQ,EAAE,qBAAqB,CAAC,OAAO,CAAC;QACxC,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,MAAqB,EAA4C;YACnF,IAAI,MAAM,CAAC,IAAI;gBAAE,OAAO,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;YAC3D,IAAI,MAAM,CAAC,QAAQ;gBAAE,OAAO,YAAY,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YAC1D,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS;gBAAE,OAAO,WAAW,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;YAClF,IAAI,MAAM,CAAC,MAAM;gBAAE,OAAO,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAEpD,OAAO;gBACN,OAAO,EAAE,IAAI,CAAC,2DAA2D,CAAC;gBAC1E,OAAO,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE;aACjE,CAAC;QAAA,CACF;KACD,CAAC;IAEF,SAAS,WAAW,CAAC,MAAc,EAAE,YAAqB,EAAmC;QAC5F,MAAM,IAAI,GAAG,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,YAAY,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC;QACrG,MAAM,OAAO,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;QACrC,MAAM,MAAM,GAAG,IAAI;aACjB,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;aACrD,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC;aACjC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC;aAC/B,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC;aACrB,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAE7B,OAAO;YACN,OAAO,EAAE,IAAI,CACZ,MAAM,CAAC,MAAM,GAAG,CAAC;gBAChB,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC;gBACpB,CAAC,CAAC,+BAA+B,MAAM,0BAA0B,CAAC,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,GAAG,CAC7G;YACD,OAAO,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,SAAS,EAAE;SACpE,CAAC;IAAA,CACF;IAED,SAAS,UAAU,CAAC,IAAY,EAAmC;QAClE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACxB,OAAO;gBACN,OAAO,EAAE,IAAI,CACZ,eAAe,IAAI,4CAA4C,CAAC,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,GAAG,CAC1G;gBACD,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE;aAC1D,CAAC;QACH,CAAC;QAED,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC;QACjE,OAAO;YACN,OAAO,EAAE,IAAI,CACZ,KAAK,CAAC,MAAM,GAAG,CAAC;gBACf,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC;gBACnB,CAAC,CAAC,wBAAwB,IAAI,mLAAmL,CAClN;YACD,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE;SAC1D,CAAC;IAAA,CACF;IAED,SAAS,YAAY,CAAC,MAAc,EAAmC;QACtE,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,CAAC;QACrE,IAAI,CAAC,KAAK,EAAE,CAAC;YACZ,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC,CAAC;YACjD,OAAO;gBACN,OAAO,EAAE,IAAI,CACZ,WAAW,CAAC,MAAM,GAAG,CAAC;oBACrB,CAAC,CAAC,yBAAyB,MAAM,qBAAqB,UAAU,CAAC,WAAW,CAAC,EAAE;oBAC/E,CAAC,CAAC,yBAAyB,MAAM,sCAAsC,CACxE;gBACD,OAAO,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE;aAChE,CAAC;QACH,CAAC;QAED,OAAO;YACN,OAAO,EAAE,IAAI,CACZ,GAAG,SAAS,CAAC,KAAK,CAAC,KAAK,KAAK,CAAC,WAAW,sBAAsB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAC3G;YACD,OAAO,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE;SACnE,CAAC;IAAA,CACF;IAED,KAAK,UAAU,QAAQ,CAAC,MAAc,EAAE,IAAa,EAA4C;QAChG,MAAM,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACtC,MAAM,OAAO,GAAmB,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QACpF,IAAI,SAAS,IAAI,CAAC,EAAE,CAAC;YACpB,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,sDAAsD,MAAM,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC;QACrG,CAAC;QAED,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;QAC9C,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC;QAC7C,OAAO,CAAC,MAAM,GAAG,UAAU,CAAC;QAE5B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;YAC9B,OAAO;gBACN,OAAO,EAAE,IAAI,CACZ,eAAe,UAAU,4CAA4C,CAAC,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,GAAG,CAChH;gBACD,OAAO;aACP,CAAC;QACH,CAAC;QAED,IAAI,CAAC;YACJ,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,cAAc,CAAC,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,CAAC;gBAC7E,2EAA2E;gBAC3E,4EAA4E;gBAC5E,MAAM,KAAK,GAAG,MAAM,UAAU,CAAC,SAAS,EAAE,CAAC;gBAC3C,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAoB,EAAE,KAAK,CAAC,CAAC;gBAC7D,KAAK,CAAC,IAAI,EAAE,CAAC;gBACb,OAAO,UAAU,CAAC,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;YAAA,CAC3C,CAAC,CAAC;YAEH,8EAA8E;YAC9E,0EAA0E;YAC1E,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO;gBAC7B,CAAC,CAAC,IAAI,CAAC,4BAA4B,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBACxF,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;YAClB,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;QAC7B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,8EAA8E;YAC9E,+DAA+D;YAC/D,IAAI,KAAK,YAAY,qBAAqB,IAAI,KAAK,YAAY,oBAAoB,EAAE,CAAC;gBACrF,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC;YAClD,CAAC;YACD,6EAA6E;YAC7E,6EAA6E;YAC7E,OAAO;gBACN,OAAO,EAAE,IAAI,CACZ,eAAe,UAAU,qBAAqB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CACtG;gBACD,OAAO;aACP,CAAC;QACH,CAAC;IAAA,CACD;AAAA,CACD;AAED,MAAM,UAAU,aAAa,CAAC,OAAuB,EAAE;IACtD,OAAO,kBAAkB,CAAC,uBAAuB,CAAC,OAAO,CAAC,CAAC,CAAC;AAAA,CAC5D","sourcesContent":["/**\n * One tool for every MCP server, instead of one tool per server tool.\n *\n * Registering each server tool individually costs 150-300 prompt tokens apiece, and\n * two ordinary servers would exceed the whole static-prefix budget before a\n * conversation starts. The proxy costs one announced name and this description,\n * because its schema defers.\n *\n * `search` and `describe` read the disk cache and reach no server. Only a `tool` call\n * connects, which is what makes discovery free and is why the two have separate\n * permission rules (ADR 0025).\n */\n\nimport type { AgentToolResult } from \"apex-code-agent-core\";\nimport type { ApexToolDefinition } from \"../tools/contract.ts\";\nimport { wrapToolDefinition } from \"../tools/tool-definition-wrapper.ts\";\nimport { createMcpToolContract } from \"./contract.ts\";\nimport type { McpMetadataCache } from \"./metadata-cache.ts\";\nimport { McpOAuthRefreshError, McpOAuthRequiredError } from \"./oauth/mcp-token.ts\";\nimport { type McpToolDetails, type McpToolParams, mcpToolSchema } from \"./schema.ts\";\nimport type { McpServerManager } from \"./server-manager.ts\";\nimport type { CachedTool, McpServerConfig } from \"./types.ts\";\n\nexport const MCP_TOOL_NAME = \"mcp\";\n\nconst MAX_RESULTS = 25;\n\nconst DESCRIPTION =\n\t\"Use tools provided by configured MCP servers. \" +\n\t'Search with {\"search\":\"keyword\"}, list one server with {\"server\":\"name\"}, ' +\n\t'see a tool\\'s full schema with {\"describe\":\"server:tool\"}, ' +\n\t'and call one with {\"tool\":\"server:tool\",\"args\":{...}}. ' +\n\t\"Search and describe read a local cache and never start a server, so discovery is free. \" +\n\t\"Always describe a tool before calling it for the first time.\";\n\nfunction text(body: string): AgentToolResult<McpToolDetails>[\"content\"] {\n\treturn [{ type: \"text\", text: body }];\n}\n\nfunction qualified(tool: CachedTool): string {\n\treturn `${tool.server}:${tool.name}`;\n}\n\n/** Substring scoring, weighted to the name, which is what the model searched for. */\nfunction score(tool: CachedTool, needle: string): number {\n\tconst name = qualified(tool).toLowerCase();\n\tconst description = tool.description.toLowerCase();\n\tif (name === needle) return 100;\n\tif (name.includes(needle)) return 50;\n\tif (description.includes(needle)) return 10;\n\treturn 0;\n}\n\nfunction renderList(tools: readonly CachedTool[]): string {\n\treturn tools.map((tool) => `${qualified(tool)}: ${tool.description}`).join(\"\\n\");\n}\n\nfunction nearest(tools: readonly CachedTool[], target: string): CachedTool[] {\n\tconst needle = target.toLowerCase();\n\tconst stem = needle.slice(0, Math.max(3, Math.floor(needle.length * 0.6)));\n\treturn tools.filter((tool) => qualified(tool).toLowerCase().includes(stem)).slice(0, 5);\n}\n\nexport interface McpToolOptions {\n\tservers: ReadonlyMap<string, McpServerConfig>;\n\tcache: McpMetadataCache;\n\tmanager: McpServerManager;\n}\n\nexport function createMcpToolDefinition(\n\toptions: McpToolOptions,\n): ApexToolDefinition<typeof mcpToolSchema, McpToolDetails> {\n\tconst { servers, cache, manager } = options;\n\n\treturn {\n\t\tname: MCP_TOOL_NAME,\n\t\tlabel: \"mcp\",\n\t\tdescription: DESCRIPTION,\n\t\tpromptSnippet: \"Call tools on configured MCP servers\",\n\t\tparameters: mcpToolSchema,\n\t\tcontract: createMcpToolContract(servers),\n\t\tasync execute(_id, params: McpToolParams): Promise<AgentToolResult<McpToolDetails>> {\n\t\t\tif (params.tool) return callTool(params.tool, params.args);\n\t\t\tif (params.describe) return describeTool(params.describe);\n\t\t\tif (params.search !== undefined) return searchTools(params.search, params.server);\n\t\t\tif (params.server) return listServer(params.server);\n\n\t\t\treturn {\n\t\t\t\tcontent: text('Provide one of \"search\", \"server\", \"describe\", or \"tool\".'),\n\t\t\t\tdetails: { action: \"search\", server: undefined, tool: undefined },\n\t\t\t};\n\t\t},\n\t};\n\n\tfunction searchTools(needle: string, serverFilter?: string): AgentToolResult<McpToolDetails> {\n\t\tconst pool = serverFilter ? cache.all().filter((tool) => tool.server === serverFilter) : cache.all();\n\t\tconst lowered = needle.toLowerCase();\n\t\tconst ranked = pool\n\t\t\t.map((tool) => ({ tool, rank: score(tool, lowered) }))\n\t\t\t.filter((entry) => entry.rank > 0)\n\t\t\t.sort((a, b) => b.rank - a.rank)\n\t\t\t.slice(0, MAX_RESULTS)\n\t\t\t.map((entry) => entry.tool);\n\n\t\treturn {\n\t\t\tcontent: text(\n\t\t\t\tranked.length > 0\n\t\t\t\t\t? renderList(ranked)\n\t\t\t\t\t: `No cached MCP tool matches \"${needle}\". Configured servers: ${[...servers.keys()].join(\", \") || \"none\"}.`,\n\t\t\t),\n\t\t\tdetails: { action: \"search\", server: serverFilter, tool: undefined },\n\t\t};\n\t}\n\n\tfunction listServer(name: string): AgentToolResult<McpToolDetails> {\n\t\tif (!servers.has(name)) {\n\t\t\treturn {\n\t\t\t\tcontent: text(\n\t\t\t\t\t`MCP server \"${name}\" is not configured. Configured servers: ${[...servers.keys()].join(\", \") || \"none\"}.`,\n\t\t\t\t),\n\t\t\t\tdetails: { action: \"list\", server: name, tool: undefined },\n\t\t\t};\n\t\t}\n\n\t\tconst tools = cache.all().filter((tool) => tool.server === name);\n\t\treturn {\n\t\t\tcontent: text(\n\t\t\t\ttools.length > 0\n\t\t\t\t\t? renderList(tools)\n\t\t\t\t\t: `No cached tools for \"${name}\" yet. Tools are cached the first time the server is called, so call a known tool on it, or set \"lifecycle\": \"eager\" for this server in .mcp.json to have them cached at startup.`,\n\t\t\t),\n\t\t\tdetails: { action: \"list\", server: name, tool: undefined },\n\t\t};\n\t}\n\n\tfunction describeTool(target: string): AgentToolResult<McpToolDetails> {\n\t\tconst found = cache.all().find((tool) => qualified(tool) === target);\n\t\tif (!found) {\n\t\t\tconst suggestions = nearest(cache.all(), target);\n\t\t\treturn {\n\t\t\t\tcontent: text(\n\t\t\t\t\tsuggestions.length > 0\n\t\t\t\t\t\t? `No cached tool named \"${target}\". Did you mean:\\n${renderList(suggestions)}`\n\t\t\t\t\t\t: `No cached tool named \"${target}\". Search with {\"search\":\"keyword\"}.`,\n\t\t\t\t),\n\t\t\t\tdetails: { action: \"describe\", server: undefined, tool: target },\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tcontent: text(\n\t\t\t\t`${qualified(found)}\\n${found.description}\\n\\nInput schema:\\n${JSON.stringify(found.inputSchema, null, 2)}`,\n\t\t\t),\n\t\t\tdetails: { action: \"describe\", server: found.server, tool: target },\n\t\t};\n\t}\n\n\tasync function callTool(target: string, args: unknown): Promise<AgentToolResult<McpToolDetails>> {\n\t\tconst separator = target.indexOf(\":\");\n\t\tconst details: McpToolDetails = { action: \"call\", server: undefined, tool: target };\n\t\tif (separator <= 0) {\n\t\t\treturn { content: text(`Tool must be qualified as \"server:tool\". Received \"${target}\".`), details };\n\t\t}\n\n\t\tconst serverName = target.slice(0, separator);\n\t\tconst toolName = target.slice(separator + 1);\n\t\tdetails.server = serverName;\n\n\t\tif (!servers.has(serverName)) {\n\t\t\treturn {\n\t\t\t\tcontent: text(\n\t\t\t\t\t`MCP server \"${serverName}\" is not configured. Configured servers: ${[...servers.keys()].join(\", \") || \"none\"}.`,\n\t\t\t\t),\n\t\t\t\tdetails,\n\t\t\t};\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = await manager.withConnection(serverName, async (connection) => {\n\t\t\t\t// An empty list is a fact about the server, not a failed read. Keeping the\n\t\t\t\t// previous entry would leave search advertising tools that no longer exist.\n\t\t\t\tconst fresh = await connection.listTools();\n\t\t\t\tcache.set(servers.get(serverName) as McpServerConfig, fresh);\n\t\t\t\tcache.save();\n\t\t\t\treturn connection.callTool(toolName, args);\n\t\t\t});\n\n\t\t\t// `AgentToolResult` has no error flag; a server-reported failure is marked in\n\t\t\t// the text so the model can see it failed without the turn being aborted.\n\t\t\tconst content = result.isError\n\t\t\t\t? text(`Tool reported an error:\\n${result.content.map((part) => part.text).join(\"\\n\")}`)\n\t\t\t\t: result.content;\n\t\t\treturn { content, details };\n\t\t} catch (error) {\n\t\t\t// OAuth failures already carry the one action that helps (`apex-code mcp auth\n\t\t\t// <server>`); the generic \"unavailable\" wrapper would bury it.\n\t\t\tif (error instanceof McpOAuthRequiredError || error instanceof McpOAuthRefreshError) {\n\t\t\t\treturn { content: text(error.message), details };\n\t\t\t}\n\t\t\t// A failed server is reported, not retried: the manager holds it in `failed`\n\t\t\t// with a backoff window so one broken server cannot cost every call a spawn.\n\t\t\treturn {\n\t\t\t\tcontent: text(\n\t\t\t\t\t`MCP server \"${serverName}\" is unavailable: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\t),\n\t\t\t\tdetails,\n\t\t\t};\n\t\t}\n\t}\n}\n\nexport function createMcpTool(options: McpToolOptions) {\n\treturn wrapToolDefinition(createMcpToolDefinition(options));\n}\n"]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent MCP tool metadata, so search and describe answer without a running
|
|
3
|
+
* server. Everything the cache holds is producible from a past connection, which is
|
|
4
|
+
* what makes "no server needed to search" a property rather than a convention.
|
|
5
|
+
*
|
|
6
|
+
* One file for every server, not one per server: the whole cache is read once at
|
|
7
|
+
* session start and written whole, and a handful of servers never makes that costly.
|
|
8
|
+
*/
|
|
9
|
+
import type { CachedTool, McpServerConfig } from "./types.ts";
|
|
10
|
+
export declare function mcpCachePath(): string;
|
|
11
|
+
/**
|
|
12
|
+
* The name is part of the key, not only the launch spec. Two servers can share a
|
|
13
|
+
* command (the same binary run twice under different names) and would otherwise
|
|
14
|
+
* collide, the second silently overwriting the first's cached tools.
|
|
15
|
+
*/
|
|
16
|
+
export declare function serverCacheKey(server: McpServerConfig): string;
|
|
17
|
+
export declare class McpMetadataCache {
|
|
18
|
+
private file;
|
|
19
|
+
private readonly path;
|
|
20
|
+
constructor(path?: string);
|
|
21
|
+
private static read;
|
|
22
|
+
get(server: McpServerConfig): CachedTool[];
|
|
23
|
+
all(): CachedTool[];
|
|
24
|
+
set(server: McpServerConfig, tools: readonly CachedTool[]): void;
|
|
25
|
+
/** Forget every server absent from `configured`, so a removed server stops appearing in search. */
|
|
26
|
+
prune(configured: readonly McpServerConfig[]): void;
|
|
27
|
+
save(): void;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=metadata-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata-cache.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/metadata-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAgB,MAAM,YAAY,CAAC;AAW5E,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAwBD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAG9D;AAED,qBAAa,gBAAgB;IAC5B,OAAO,CAAC,IAAI,CAAY;IACxB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAE9B,YAAY,IAAI,GAAE,MAAuB,EAGxC;IAED,OAAO,CAAC,MAAM,CAAC,IAAI;IAanB,GAAG,CAAC,MAAM,EAAE,eAAe,GAAG,UAAU,EAAE,CAEzC;IAED,GAAG,IAAI,UAAU,EAAE,CAElB;IAED,GAAG,CAAC,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,IAAI,CAE/D;IAED,mGAAmG;IACnG,KAAK,CAAC,UAAU,EAAE,SAAS,eAAe,EAAE,GAAG,IAAI,CAKlD;IAED,IAAI,IAAI,IAAI,CAUX;CACD","sourcesContent":["/**\n * Persistent MCP tool metadata, so search and describe answer without a running\n * server. Everything the cache holds is producible from a past connection, which is\n * what makes \"no server needed to search\" a property rather than a convention.\n *\n * One file for every server, not one per server: the whole cache is read once at\n * session start and written whole, and a handful of servers never makes that costly.\n */\n\nimport { createHash } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { getAgentDir } from \"../../config.ts\";\nimport type { CachedTool, McpServerConfig, McpTransport } from \"./types.ts\";\n\ninterface CacheFile {\n\tversion: 1;\n\tservers: Record<string, { name: string; tools: CachedTool[] }>;\n}\n\nconst EMPTY: CacheFile = { version: 1, servers: {} };\n\nconst SEPARATOR = \"|\";\n\nexport function mcpCachePath(): string {\n\treturn join(getAgentDir(), \"mcp\", \"metadata.json\");\n}\n\n/**\n * Everything that can change which tools a server publishes, and nothing that cannot.\n * A version bump in `args` produces a new key and therefore reads no stale entries,\n * while `capabilities` or `idleTimeoutMinutes` leave the key alone.\n */\nfunction transportIdentity(transport: McpTransport): string {\n\tif (transport.kind === \"stdio\") {\n\t\t// Env values, not just keys: a server that switches which token or mode it runs\n\t\t// under can publish a different tool list under the same command line.\n\t\tconst env = Object.entries(transport.env)\n\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t.map(([key, value]) => `${key}=${value}`)\n\t\t\t.join(\",\");\n\t\treturn [\"stdio\", transport.command, transport.args.join(\" \"), env, transport.cwd ?? \"\"].join(SEPARATOR);\n\t}\n\tconst headers = Object.entries(transport.headers)\n\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t.map(([key, value]) => `${key}=${value}`)\n\t\t.join(\",\");\n\treturn [\"http\", transport.url, headers, transport.bearerTokenEnv ?? \"\"].join(SEPARATOR);\n}\n\n/**\n * The name is part of the key, not only the launch spec. Two servers can share a\n * command (the same binary run twice under different names) and would otherwise\n * collide, the second silently overwriting the first's cached tools.\n */\nexport function serverCacheKey(server: McpServerConfig): string {\n\tconst identity = [server.name, transportIdentity(server.transport)].join(SEPARATOR);\n\treturn createHash(\"sha256\").update(identity).digest(\"hex\").slice(0, 32);\n}\n\nexport class McpMetadataCache {\n\tprivate file: CacheFile;\n\tprivate readonly path: string;\n\n\tconstructor(path: string = mcpCachePath()) {\n\t\tthis.path = path;\n\t\tthis.file = McpMetadataCache.read(path);\n\t}\n\n\tprivate static read(path: string): CacheFile {\n\t\tif (!existsSync(path)) return structuredClone(EMPTY);\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(readFileSync(path, \"utf-8\")) as CacheFile;\n\t\t\tif (parsed?.version !== 1 || typeof parsed.servers !== \"object\" || parsed.servers === null) {\n\t\t\t\treturn structuredClone(EMPTY);\n\t\t\t}\n\t\t\treturn parsed;\n\t\t} catch {\n\t\t\treturn structuredClone(EMPTY);\n\t\t}\n\t}\n\n\tget(server: McpServerConfig): CachedTool[] {\n\t\treturn this.file.servers[serverCacheKey(server)]?.tools ?? [];\n\t}\n\n\tall(): CachedTool[] {\n\t\treturn Object.values(this.file.servers).flatMap((entry) => entry.tools);\n\t}\n\n\tset(server: McpServerConfig, tools: readonly CachedTool[]): void {\n\t\tthis.file.servers[serverCacheKey(server)] = { name: server.name, tools: [...tools] };\n\t}\n\n\t/** Forget every server absent from `configured`, so a removed server stops appearing in search. */\n\tprune(configured: readonly McpServerConfig[]): void {\n\t\tconst live = new Set(configured.map(serverCacheKey));\n\t\tfor (const key of Object.keys(this.file.servers)) {\n\t\t\tif (!live.has(key)) delete this.file.servers[key];\n\t\t}\n\t}\n\n\tsave(): void {\n\t\tconst directory = dirname(this.path);\n\t\tmkdirSync(directory, { recursive: true });\n\t\tconst temporary = join(directory, `.metadata.${process.pid}.tmp`);\n\t\ttry {\n\t\t\twriteFileSync(temporary, JSON.stringify(this.file));\n\t\t\trenameSync(temporary, this.path);\n\t\t} catch {\n\t\t\trmSync(temporary, { force: true });\n\t\t}\n\t}\n}\n"]}
|