open-multi-agent-kit 0.98.5 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +4 -3
- package/dist/bun/cli.d.ts.map +1 -1
- package/dist/bun/cli.js +1 -0
- package/dist/bun/cli.js.map +1 -1
- package/dist/bun/register-bundled-coding-agent.d.ts +10 -0
- package/dist/bun/register-bundled-coding-agent.d.ts.map +1 -0
- package/dist/bun/register-bundled-coding-agent.js +12 -0
- package/dist/bun/register-bundled-coding-agent.js.map +1 -0
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +1 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +2 -1
- package/dist/cli/help.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +14 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/neo-cli.d.ts +9 -0
- package/dist/commands/neo-cli.d.ts.map +1 -0
- package/dist/commands/neo-cli.js +61 -0
- package/dist/commands/neo-cli.js.map +1 -0
- package/dist/commands/verified-run-cli.d.ts.map +1 -1
- package/dist/commands/verified-run-cli.js +2 -2
- package/dist/commands/verified-run-cli.js.map +1 -1
- package/dist/core/agent-session.d.ts +29 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +126 -32
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/bundled-skills.d.ts +4 -0
- package/dist/core/bundled-skills.d.ts.map +1 -0
- package/dist/core/bundled-skills.js +32 -0
- package/dist/core/bundled-skills.js.map +1 -0
- package/dist/core/cli-diagnostics.d.ts +6 -0
- package/dist/core/cli-diagnostics.d.ts.map +1 -0
- package/dist/core/cli-diagnostics.js +20 -0
- package/dist/core/cli-diagnostics.js.map +1 -0
- package/dist/core/context-budget-headroom.d.ts +12 -0
- package/dist/core/context-budget-headroom.d.ts.map +1 -1
- package/dist/core/context-budget-headroom.js +35 -0
- package/dist/core/context-budget-headroom.js.map +1 -1
- package/dist/core/context-budget-v2-input-validation.d.ts +4 -0
- package/dist/core/context-budget-v2-input-validation.d.ts.map +1 -0
- package/dist/core/context-budget-v2-input-validation.js +79 -0
- package/dist/core/context-budget-v2-input-validation.js.map +1 -0
- package/dist/core/context-budget-v2-observability.d.ts +15 -0
- package/dist/core/context-budget-v2-observability.d.ts.map +1 -0
- package/dist/core/context-budget-v2-observability.js +35 -0
- package/dist/core/context-budget-v2-observability.js.map +1 -0
- package/dist/core/context-budget-v2-planned-items.d.ts +5 -0
- package/dist/core/context-budget-v2-planned-items.d.ts.map +1 -0
- package/dist/core/context-budget-v2-planned-items.js +40 -0
- package/dist/core/context-budget-v2-planned-items.js.map +1 -0
- package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
- package/dist/core/context-budget-v2-planner.js +57 -38
- package/dist/core/context-budget-v2-planner.js.map +1 -1
- package/dist/core/context-budget-v2-selection.d.ts +19 -3
- package/dist/core/context-budget-v2-selection.d.ts.map +1 -1
- package/dist/core/context-budget-v2-selection.js +35 -57
- package/dist/core/context-budget-v2-selection.js.map +1 -1
- package/dist/core/context-budget-v2-tiers.d.ts.map +1 -1
- package/dist/core/context-budget-v2-tiers.js +9 -42
- package/dist/core/context-budget-v2-tiers.js.map +1 -1
- package/dist/core/context-budget-v2-types.d.ts +8 -1
- package/dist/core/context-budget-v2-types.d.ts.map +1 -1
- package/dist/core/context-budget-v2-types.js.map +1 -1
- package/dist/core/devin-harness-dispatch.d.ts +12 -0
- package/dist/core/devin-harness-dispatch.d.ts.map +1 -0
- package/dist/core/devin-harness-dispatch.js +12 -0
- package/dist/core/devin-harness-dispatch.js.map +1 -0
- package/dist/core/devin-harness.d.ts +53 -0
- package/dist/core/devin-harness.d.ts.map +1 -0
- package/dist/core/devin-harness.js +112 -0
- package/dist/core/devin-harness.js.map +1 -0
- package/dist/core/domain-dispatch.d.ts +4 -1
- package/dist/core/domain-dispatch.d.ts.map +1 -1
- package/dist/core/domain-dispatch.js +5 -0
- package/dist/core/domain-dispatch.js.map +1 -1
- package/dist/core/domain-loadouts-provider-harness.d.ts +12 -0
- package/dist/core/domain-loadouts-provider-harness.d.ts.map +1 -0
- package/dist/core/domain-loadouts-provider-harness.js +122 -0
- package/dist/core/domain-loadouts-provider-harness.js.map +1 -0
- package/dist/core/domain-loadouts.d.ts +2 -33
- package/dist/core/domain-loadouts.d.ts.map +1 -1
- package/dist/core/domain-loadouts.js +3 -56
- package/dist/core/domain-loadouts.js.map +1 -1
- package/dist/core/domain-profile.d.ts +40 -0
- package/dist/core/domain-profile.d.ts.map +1 -0
- package/dist/core/domain-profile.js +7 -0
- package/dist/core/domain-profile.js.map +1 -0
- package/dist/core/extensions/bundled-virtual-modules.d.ts +15 -0
- package/dist/core/extensions/bundled-virtual-modules.d.ts.map +1 -0
- package/dist/core/extensions/bundled-virtual-modules.js +63 -0
- package/dist/core/extensions/bundled-virtual-modules.js.map +1 -0
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +2 -42
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +1 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +6 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +10 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/grok-harness-dispatch.d.ts +6 -20
- package/dist/core/grok-harness-dispatch.d.ts.map +1 -1
- package/dist/core/grok-harness-dispatch.js +6 -55
- package/dist/core/grok-harness-dispatch.js.map +1 -1
- package/dist/core/grok-harness.d.ts +7 -9
- package/dist/core/grok-harness.d.ts.map +1 -1
- package/dist/core/grok-harness.js +9 -23
- package/dist/core/grok-harness.js.map +1 -1
- package/dist/core/harness-skills.d.ts +20 -0
- package/dist/core/harness-skills.d.ts.map +1 -0
- package/dist/core/harness-skills.js +36 -0
- package/dist/core/harness-skills.js.map +1 -0
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/loadout-runtime-state.d.ts +25 -0
- package/dist/core/loadout-runtime-state.d.ts.map +1 -0
- package/dist/core/loadout-runtime-state.js +39 -0
- package/dist/core/loadout-runtime-state.js.map +1 -0
- package/dist/core/loadout-runtime.d.ts +4 -20
- package/dist/core/loadout-runtime.d.ts.map +1 -1
- package/dist/core/loadout-runtime.js +6 -13
- package/dist/core/loadout-runtime.js.map +1 -1
- package/dist/core/mcp/client.d.ts +27 -6
- package/dist/core/mcp/client.d.ts.map +1 -1
- package/dist/core/mcp/client.js +80 -20
- package/dist/core/mcp/client.js.map +1 -1
- package/dist/core/mcp/manager.d.ts +10 -1
- package/dist/core/mcp/manager.d.ts.map +1 -1
- package/dist/core/mcp/manager.js +68 -9
- package/dist/core/mcp/manager.js.map +1 -1
- package/dist/core/mcp/protocol.d.ts +9 -3
- package/dist/core/mcp/protocol.d.ts.map +1 -1
- package/dist/core/mcp/protocol.js +61 -15
- package/dist/core/mcp/protocol.js.map +1 -1
- package/dist/core/mcp/stdio-transport.d.ts +12 -1
- package/dist/core/mcp/stdio-transport.d.ts.map +1 -1
- package/dist/core/mcp/stdio-transport.js +30 -2
- package/dist/core/mcp/stdio-transport.js.map +1 -1
- package/dist/core/mcp-descriptor-injection.d.ts +26 -0
- package/dist/core/mcp-descriptor-injection.d.ts.map +1 -0
- package/dist/core/mcp-descriptor-injection.js +25 -0
- package/dist/core/mcp-descriptor-injection.js.map +1 -0
- package/dist/core/mcp-public-presets.d.ts +1 -3
- package/dist/core/mcp-public-presets.d.ts.map +1 -1
- package/dist/core/mcp-public-presets.js +3 -15
- package/dist/core/mcp-public-presets.js.map +1 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +24 -3
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts +1 -41
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +2 -49
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/neo/catalog.d.ts +20 -0
- package/dist/core/neo/catalog.d.ts.map +1 -0
- package/dist/core/neo/catalog.js +50 -0
- package/dist/core/neo/catalog.js.map +1 -0
- package/dist/core/neo/setup.d.ts +3 -0
- package/dist/core/neo/setup.d.ts.map +1 -0
- package/dist/core/neo/setup.js +47 -0
- package/dist/core/neo/setup.js.map +1 -0
- package/dist/core/provider-default-models.d.ts +43 -0
- package/dist/core/provider-default-models.d.ts.map +1 -0
- package/dist/core/provider-default-models.js +51 -0
- package/dist/core/provider-default-models.js.map +1 -0
- package/dist/core/provider-display-names.d.ts.map +1 -1
- package/dist/core/provider-display-names.js +2 -0
- package/dist/core/provider-display-names.js.map +1 -1
- package/dist/core/provider-error-classification.d.ts +57 -0
- package/dist/core/provider-error-classification.d.ts.map +1 -0
- package/dist/core/provider-error-classification.js +103 -0
- package/dist/core/provider-error-classification.js.map +1 -0
- package/dist/core/provider-harness-dispatch.d.ts +63 -0
- package/dist/core/provider-harness-dispatch.d.ts.map +1 -0
- package/dist/core/provider-harness-dispatch.js +60 -0
- package/dist/core/provider-harness-dispatch.js.map +1 -0
- package/dist/core/provider-resilience.d.ts +10 -0
- package/dist/core/provider-resilience.d.ts.map +1 -1
- package/dist/core/provider-resilience.js +36 -3
- package/dist/core/provider-resilience.js.map +1 -1
- package/dist/core/provider-usage-commandcode.d.ts +9 -0
- package/dist/core/provider-usage-commandcode.d.ts.map +1 -0
- package/dist/core/provider-usage-commandcode.js +198 -0
- package/dist/core/provider-usage-commandcode.js.map +1 -0
- package/dist/core/provider-usage-devin.d.ts +18 -0
- package/dist/core/provider-usage-devin.d.ts.map +1 -0
- package/dist/core/provider-usage-devin.js +71 -0
- package/dist/core/provider-usage-devin.js.map +1 -0
- package/dist/core/provider-usage-text.d.ts +5 -0
- package/dist/core/provider-usage-text.d.ts.map +1 -0
- package/dist/core/provider-usage-text.js +15 -0
- package/dist/core/provider-usage-text.js.map +1 -0
- package/dist/core/provider-usage-types.d.ts +2 -1
- package/dist/core/provider-usage-types.d.ts.map +1 -1
- package/dist/core/provider-usage-types.js.map +1 -1
- package/dist/core/provider-usage.d.ts +1 -2
- package/dist/core/provider-usage.d.ts.map +1 -1
- package/dist/core/provider-usage.js +18 -7
- package/dist/core/provider-usage.js.map +1 -1
- package/dist/core/resource-admission.d.ts +36 -0
- package/dist/core/resource-admission.d.ts.map +1 -1
- package/dist/core/resource-admission.js +59 -0
- package/dist/core/resource-admission.js.map +1 -1
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +2 -2
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/run-execution-api.d.ts +1 -1
- package/dist/core/run-execution-api.d.ts.map +1 -1
- package/dist/core/run-execution-api.js.map +1 -1
- package/dist/core/run-usage-ledger.d.ts +47 -0
- package/dist/core/run-usage-ledger.d.ts.map +1 -0
- package/dist/core/run-usage-ledger.js +162 -0
- package/dist/core/run-usage-ledger.js.map +1 -0
- package/dist/core/run-usage-operation.d.ts +8 -0
- package/dist/core/run-usage-operation.d.ts.map +1 -0
- package/dist/core/run-usage-operation.js +12 -0
- package/dist/core/run-usage-operation.js.map +1 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +20 -16
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-failure-cause.d.ts.map +1 -1
- package/dist/core/session-failure-cause.js +14 -5
- package/dist/core/session-failure-cause.js.map +1 -1
- package/dist/core/session-prompt-lifecycle.d.ts +6 -0
- package/dist/core/session-prompt-lifecycle.d.ts.map +1 -1
- package/dist/core/session-prompt-lifecycle.js +47 -2
- package/dist/core/session-prompt-lifecycle.js.map +1 -1
- package/dist/core/session-termination.d.ts.map +1 -1
- package/dist/core/session-termination.js +4 -2
- package/dist/core/session-termination.js.map +1 -1
- package/dist/core/subagent-lane-authority.d.ts +28 -0
- package/dist/core/subagent-lane-authority.d.ts.map +1 -0
- package/dist/core/subagent-lane-authority.js +119 -0
- package/dist/core/subagent-lane-authority.js.map +1 -0
- package/dist/core/subagent-lane-contract.d.ts +114 -0
- package/dist/core/subagent-lane-contract.d.ts.map +1 -0
- package/dist/core/subagent-lane-contract.js +18 -0
- package/dist/core/subagent-lane-contract.js.map +1 -0
- package/dist/core/subagent-lane-launcher.d.ts +3 -6
- package/dist/core/subagent-lane-launcher.d.ts.map +1 -1
- package/dist/core/subagent-lane-launcher.js +53 -12
- package/dist/core/subagent-lane-launcher.js.map +1 -1
- package/dist/core/subagent-orchestration.d.ts +4 -17
- package/dist/core/subagent-orchestration.d.ts.map +1 -1
- package/dist/core/subagent-orchestration.js.map +1 -1
- package/dist/core/verified-run/broker.d.ts.map +1 -1
- package/dist/core/verified-run/broker.js +4 -1
- package/dist/core/verified-run/broker.js.map +1 -1
- package/dist/core/verified-run/dag-phase.d.ts +1 -1
- package/dist/core/verified-run/dag-phase.d.ts.map +1 -1
- package/dist/core/verified-run/dag-phase.js +46 -8
- package/dist/core/verified-run/dag-phase.js.map +1 -1
- package/dist/core/verified-run/dag-projection.d.ts.map +1 -1
- package/dist/core/verified-run/dag-projection.js +25 -12
- package/dist/core/verified-run/dag-projection.js.map +1 -1
- package/dist/core/verified-run/dag-types.d.ts +11 -0
- package/dist/core/verified-run/dag-types.d.ts.map +1 -1
- package/dist/core/verified-run/dag-types.js.map +1 -1
- package/dist/core/verified-run/event-parser.d.ts.map +1 -1
- package/dist/core/verified-run/event-parser.js +1 -0
- package/dist/core/verified-run/event-parser.js.map +1 -1
- package/dist/core/verified-run/owned-execution.d.ts +1 -0
- package/dist/core/verified-run/owned-execution.d.ts.map +1 -1
- package/dist/core/verified-run/owned-execution.js +7 -1
- package/dist/core/verified-run/owned-execution.js.map +1 -1
- package/dist/core/verified-run/process-projection.d.ts +13 -0
- package/dist/core/verified-run/process-projection.d.ts.map +1 -0
- package/dist/core/verified-run/process-projection.js +82 -0
- package/dist/core/verified-run/process-projection.js.map +1 -0
- package/dist/core/verified-run/projection.d.ts.map +1 -1
- package/dist/core/verified-run/projection.js +11 -55
- package/dist/core/verified-run/projection.js.map +1 -1
- package/dist/core/verified-run/run-types.d.ts +1 -0
- package/dist/core/verified-run/run-types.d.ts.map +1 -1
- package/dist/core/verified-run/run-types.js.map +1 -1
- package/dist/core/verified-run/task-execution.d.ts +9 -0
- package/dist/core/verified-run/task-execution.d.ts.map +1 -0
- package/dist/core/verified-run/task-execution.js +29 -0
- package/dist/core/verified-run/task-execution.js.map +1 -0
- package/dist/core/verified-run/writer-projection.d.ts.map +1 -1
- package/dist/core/verified-run/writer-projection.js +6 -5
- package/dist/core/verified-run/writer-projection.js.map +1 -1
- package/dist/core/workload-permit-pool.d.ts +1 -1
- package/dist/core/workload-permit-pool.d.ts.map +1 -1
- package/dist/core/workload-permit-pool.js +3 -0
- package/dist/core/workload-permit-pool.js.map +1 -1
- package/dist/guardrails/strict-evidence-approval-adapter.d.ts +34 -0
- package/dist/guardrails/strict-evidence-approval-adapter.d.ts.map +1 -0
- package/dist/guardrails/strict-evidence-approval-adapter.js +70 -0
- package/dist/guardrails/strict-evidence-approval-adapter.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +11 -18
- package/dist/main.js.map +1 -1
- package/dist/modes/acp/acp-agent.d.ts +24 -0
- package/dist/modes/acp/acp-agent.d.ts.map +1 -0
- package/dist/modes/acp/acp-agent.js +133 -0
- package/dist/modes/acp/acp-agent.js.map +1 -0
- package/dist/modes/acp/acp-mode.d.ts +4 -0
- package/dist/modes/acp/acp-mode.d.ts.map +1 -0
- package/dist/modes/acp/acp-mode.js +34 -0
- package/dist/modes/acp/acp-mode.js.map +1 -0
- package/dist/modes/acp/acp-session.d.ts +4 -0
- package/dist/modes/acp/acp-session.d.ts.map +1 -0
- package/dist/modes/acp/acp-session.js +76 -0
- package/dist/modes/acp/acp-session.js.map +1 -0
- package/dist/modes/acp/acp-transport.d.ts +5 -0
- package/dist/modes/acp/acp-transport.d.ts.map +1 -0
- package/dist/modes/acp/acp-transport.js +90 -0
- package/dist/modes/acp/acp-transport.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +3 -5
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/resource-description.d.ts +3 -0
- package/dist/modes/interactive/resource-description.d.ts.map +1 -0
- package/dist/modes/interactive/resource-description.js +15 -0
- package/dist/modes/interactive/resource-description.js.map +1 -0
- package/docs/audit-revalidation-2026-09-17.md +77 -0
- package/docs/devin-harness.md +150 -0
- package/docs/docs.json +4 -0
- package/docs/ecraf-normalization.md +83 -0
- package/docs/environment-variables.md +2 -1
- package/docs/index.md +1 -0
- package/docs/loadout-domains/README.md +2 -1
- package/docs/loadout-domains/devin-harness.md +72 -0
- package/docs/metrics.md +57 -16
- package/docs/model-catalog-refresh.md +51 -1
- package/docs/models.md +1 -1
- package/docs/neo.md +134 -0
- package/docs/providers.md +143 -1
- package/docs/release-audit-0.98.5.md +17 -0
- package/docs/release-audit-0.99.0.md +68 -0
- package/docs/run-protocol.md +4 -3
- package/docs/run-usage-ledger.md +56 -0
- package/docs/runtime-algorithms.md +48 -1
- package/docs/sdk.md +9 -5
- package/docs/settings.md +1 -1
- package/docs/skills.md +1 -1
- package/docs/startup-resource-labels-testing.md +47 -0
- package/docs/tb21-audit.md +18 -4
- package/docs/usage.md +7 -1
- package/docs/verified-run-remaining-design.md +881 -0
- package/docs/verified-run-testing.md +91 -0
- package/docs/verified-run.md +30 -9
- 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/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/subagent/adaptive-agent-runtime.ts +14 -1
- package/examples/extensions/subagent/graph-result.ts +48 -0
- package/examples/extensions/subagent/index.ts +246 -111
- package/examples/extensions/subagent/managed-process-tree.ts +42 -0
- package/examples/extensions/subagent/managed-process.test.ts +24 -0
- package/examples/extensions/subagent/managed-process.ts +93 -112
- package/examples/extensions/subagent/subagent-runtime-types.ts +17 -1
- package/examples/extensions/subagent/subagent-stream.ts +161 -0
- package/examples/extensions/terminal-browser/README.md +54 -0
- package/examples/extensions/terminal-browser/bridge-protocol.ts +29 -0
- package/examples/extensions/terminal-browser/bridge.ts +219 -0
- package/examples/extensions/terminal-browser/browser-surface.ts +288 -0
- package/examples/extensions/terminal-browser/index.ts +215 -0
- package/examples/extensions/terminal-browser/placeholders.ts +49 -0
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +18 -18
- package/package.json +8 -7
- package/resources/neo/skills/omk-browser/SKILL.md +32 -0
- package/resources/neo/skills/omk-code-review/SKILL.md +24 -0
- package/resources/neo/skills/omk-computeruse/SKILL.md +34 -0
- package/resources/neo/skills/omk-mcp-setup/SKILL.md +48 -0
- package/resources/neo/skills/omk-research/SKILL.md +24 -0
- package/resources/neo/skills/omk-site/SKILL.md +28 -0
|
@@ -14,10 +14,23 @@
|
|
|
14
14
|
import { type StdioTransportOptions } from "./stdio-transport.ts";
|
|
15
15
|
/** Protocol revision this client implements. */
|
|
16
16
|
export declare const MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
17
|
+
/**
|
|
18
|
+
* Protocol revisions this client accepts from a server. The handshake fails
|
|
19
|
+
* closed on any other value instead of guessing at forward compatibility.
|
|
20
|
+
*/
|
|
21
|
+
export declare const SUPPORTED_PROTOCOL_VERSIONS: readonly string[];
|
|
17
22
|
/** Default per-request deadline. */
|
|
18
23
|
export declare const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
19
24
|
/** Default handshake deadline. Servers that install on first run need more room than a normal call. */
|
|
20
25
|
export declare const DEFAULT_HANDSHAKE_TIMEOUT_MS = 60000;
|
|
26
|
+
/** Page ceiling for `tools/list` so a looping cursor cannot stall a session forever. */
|
|
27
|
+
export declare const MAX_LIST_PAGES = 128;
|
|
28
|
+
/** Tool ceiling across all `tools/list` pages for one server. */
|
|
29
|
+
export declare const MAX_LIST_TOOLS = 4096;
|
|
30
|
+
/** Overall `tools/list` deadline; per-request timeouts do not bound the total. */
|
|
31
|
+
export declare const LIST_TOOLS_TIMEOUT_MS = 120000;
|
|
32
|
+
/** In-flight request ceiling per server. */
|
|
33
|
+
export declare const MAX_PENDING_REQUESTS = 128;
|
|
21
34
|
export interface McpToolSchema {
|
|
22
35
|
readonly name: string;
|
|
23
36
|
readonly description?: string;
|
|
@@ -28,12 +41,11 @@ export interface McpTextBlock {
|
|
|
28
41
|
readonly type: "text";
|
|
29
42
|
readonly text: string;
|
|
30
43
|
}
|
|
31
|
-
export
|
|
44
|
+
export type McpContentBlock = McpTextBlock | {
|
|
32
45
|
readonly type: "image";
|
|
33
46
|
readonly data: string;
|
|
34
47
|
readonly mimeType: string;
|
|
35
|
-
}
|
|
36
|
-
export type McpContentBlock = McpTextBlock | McpImageBlock | {
|
|
48
|
+
} | {
|
|
37
49
|
readonly type: string;
|
|
38
50
|
readonly [key: string]: unknown;
|
|
39
51
|
};
|
|
@@ -68,26 +80,33 @@ export declare class McpClient {
|
|
|
68
80
|
private closed;
|
|
69
81
|
private exitReason;
|
|
70
82
|
private info;
|
|
83
|
+
private lastProtocolErrorReason;
|
|
71
84
|
private readonly options;
|
|
72
85
|
constructor(options: McpClientOptions);
|
|
73
86
|
get name(): string;
|
|
74
87
|
get serverInfo(): McpServerInfo;
|
|
75
88
|
get ready(): boolean;
|
|
76
|
-
/** Why the server is unusable, when it is. */
|
|
77
|
-
get failure(): string | undefined;
|
|
78
89
|
/**
|
|
79
90
|
* Spawn the server and complete the MCP handshake. Safe to await once; a
|
|
80
91
|
* second call after a failure re-throws the recorded reason rather than
|
|
81
92
|
* silently reusing a dead process.
|
|
82
93
|
*/
|
|
83
94
|
connect(): Promise<void>;
|
|
95
|
+
private assertInitializeResult;
|
|
84
96
|
/**
|
|
85
97
|
* Protocol-level liveness probe. Rejects when the server is closed, dead,
|
|
86
98
|
* or fails to answer within `timeoutMs`, so callers can tell a silently
|
|
87
99
|
* killed process apart from a genuinely connected one.
|
|
88
100
|
*/
|
|
89
101
|
ping(timeoutMs?: number): Promise<void>;
|
|
90
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* List the server's tools. Requires a completed handshake.
|
|
104
|
+
*
|
|
105
|
+
* Pagination is bounded three ways — an overall deadline, a page count, and
|
|
106
|
+
* a tool count — and a repeated cursor is a protocol error, so a misbehaving
|
|
107
|
+
* server cannot keep the listing loop alive indefinitely. Duplicate tool
|
|
108
|
+
* names in one listing are rejected instead of silently overwritten.
|
|
109
|
+
*/
|
|
91
110
|
listTools(): Promise<McpToolSchema[]>;
|
|
92
111
|
/**
|
|
93
112
|
* Invoke a tool. A protocol-level failure rejects; a tool-level failure
|
|
@@ -97,6 +116,8 @@ export declare class McpClient {
|
|
|
97
116
|
callTool(name: string, args: unknown, timeoutMs?: number): Promise<McpToolCallResult>;
|
|
98
117
|
/** Terminate the server and reject anything still in flight. */
|
|
99
118
|
close(): void;
|
|
119
|
+
/** Why the server is unusable, when it is; includes the latest decode failure. */
|
|
120
|
+
get failure(): string | undefined;
|
|
100
121
|
private assertReady;
|
|
101
122
|
private request;
|
|
102
123
|
private handleMessage;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/client.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal MCP client: initialize handshake, `tools/list`, `tools/call`.\n *\n * Deliberately dependency-free. The upstream SDK carries a full server\n * implementation, resource/prompt/sampling surfaces, and its own transport\n * stack; OMK needs a client for three methods, so the client is ~200 lines of\n * request correlation over a transport this repo already owns.\n *\n * Fail-closed contract:\n * - Every request has a deadline. A hung server rejects, it does not stall a turn.\n * - A transport exit rejects every in-flight request with the server's stderr tail.\n * - Calling before a completed handshake throws instead of guessing.\n */\n\nimport {\n\tformatJsonRpcError,\n\tisJsonRpcResponse,\n\ttype JsonRpcId,\n\ttype JsonRpcMessage,\n\ttype JsonRpcResponse,\n} from \"./protocol.ts\";\nimport { McpStdioTransport, type StdioTransportOptions } from \"./stdio-transport.ts\";\n\n/** Protocol revision this client implements. */\nexport const MCP_PROTOCOL_VERSION = \"2025-06-18\";\n/** Default per-request deadline. */\nexport const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;\n/** Default handshake deadline. Servers that install on first run need more room than a normal call. */\nexport const DEFAULT_HANDSHAKE_TIMEOUT_MS = 60_000;\n\nexport interface McpToolSchema {\n\treadonly name: string;\n\treadonly description?: string;\n\treadonly inputSchema?: Record<string, unknown>;\n\treadonly title?: string;\n}\n\nexport interface McpTextBlock {\n\treadonly type: \"text\";\n\treadonly text: string;\n}\n\nexport interface McpImageBlock {\n\treadonly type: \"image\";\n\treadonly data: string;\n\treadonly mimeType: string;\n}\n\nexport type McpContentBlock = McpTextBlock | McpImageBlock | { readonly type: string; readonly [key: string]: unknown };\n\nexport interface McpToolCallResult {\n\treadonly content: readonly McpContentBlock[];\n\treadonly isError: boolean;\n\treadonly structuredContent?: unknown;\n}\n\nexport interface McpServerInfo {\n\treadonly name?: string;\n\treadonly version?: string;\n}\n\nexport interface McpClientOptions {\n\t/** Server label used in error messages. */\n\treadonly name: string;\n\treadonly transport: StdioTransportOptions;\n\treadonly requestTimeoutMs?: number;\n\treadonly handshakeTimeoutMs?: number;\n\t/** Client identity reported during the handshake. */\n\treadonly clientInfo?: { readonly name: string; readonly version: string };\n}\n\ninterface PendingRequest {\n\treadonly resolve: (result: unknown) => void;\n\treadonly reject: (error: Error) => void;\n\treadonly timer: ReturnType<typeof setTimeout>;\n\treadonly method: string;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport class McpClient {\n\tprivate readonly transport: McpStdioTransport;\n\tprivate readonly pending = new Map<JsonRpcId, PendingRequest>();\n\tprivate readonly requestTimeoutMs: number;\n\tprivate readonly handshakeTimeoutMs: number;\n\tprivate nextId = 1;\n\tprivate initialized = false;\n\tprivate closed = false;\n\tprivate exitReason: string | undefined;\n\tprivate info: McpServerInfo = {};\n\tprivate readonly options: McpClientOptions;\n\n\tconstructor(options: McpClientOptions) {\n\t\tthis.options = options;\n\t\tthis.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n\t\tthis.handshakeTimeoutMs = options.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;\n\t\tthis.transport = new McpStdioTransport(options.transport, {\n\t\t\tonMessage: (message) => this.handleMessage(message),\n\t\t\tonExit: ({ code, signal }) => this.handleExit(code, signal),\n\t\t});\n\t}\n\n\tget name(): string {\n\t\treturn this.options.name;\n\t}\n\n\tget serverInfo(): McpServerInfo {\n\t\treturn this.info;\n\t}\n\n\tget ready(): boolean {\n\t\treturn this.initialized && !this.closed;\n\t}\n\n\t/** Why the server is unusable, when it is. */\n\tget failure(): string | undefined {\n\t\treturn this.exitReason;\n\t}\n\n\t/**\n\t * Spawn the server and complete the MCP handshake. Safe to await once; a\n\t * second call after a failure re-throws the recorded reason rather than\n\t * silently reusing a dead process.\n\t */\n\tasync connect(): Promise<void> {\n\t\tif (this.closed) throw new Error(this.exitReason ?? `MCP server \"${this.options.name}\" is closed`);\n\t\tif (this.initialized) return;\n\t\tthis.transport.start();\n\n\t\tconst result = await this.request(\n\t\t\t\"initialize\",\n\t\t\t{\n\t\t\t\tprotocolVersion: MCP_PROTOCOL_VERSION,\n\t\t\t\tcapabilities: {},\n\t\t\t\tclientInfo: this.options.clientInfo ?? { name: \"omk\", version: \"0.0.0\" },\n\t\t\t},\n\t\t\tthis.handshakeTimeoutMs,\n\t\t);\n\t\tif (isRecord(result) && isRecord(result.serverInfo)) {\n\t\t\tconst { name, version } = result.serverInfo;\n\t\t\tthis.info = {\n\t\t\t\tname: typeof name === \"string\" ? name : undefined,\n\t\t\t\tversion: typeof version === \"string\" ? version : undefined,\n\t\t\t};\n\t\t}\n\t\tthis.initialized = true;\n\t\tthis.transport.send({ jsonrpc: \"2.0\", method: \"notifications/initialized\" });\n\t}\n\n\t/**\n\t * Protocol-level liveness probe. Rejects when the server is closed, dead,\n\t * or fails to answer within `timeoutMs`, so callers can tell a silently\n\t * killed process apart from a genuinely connected one.\n\t */\n\tasync ping(timeoutMs?: number): Promise<void> {\n\t\tthis.assertReady();\n\t\tawait this.request(\"ping\", {}, timeoutMs);\n\t}\n\n\t/** List the server's tools. Requires a completed handshake. */\n\tasync listTools(): Promise<McpToolSchema[]> {\n\t\tthis.assertReady();\n\t\tconst tools: McpToolSchema[] = [];\n\t\tlet cursor: string | undefined;\n\t\tdo {\n\t\t\tconst result = await this.request(\"tools/list\", cursor ? { cursor } : {});\n\t\t\tif (!isRecord(result) || !Array.isArray(result.tools)) return tools;\n\t\t\tfor (const raw of result.tools) {\n\t\t\t\tif (!isRecord(raw) || typeof raw.name !== \"string\" || raw.name.length === 0) continue;\n\t\t\t\ttools.push({\n\t\t\t\t\tname: raw.name,\n\t\t\t\t\tdescription: typeof raw.description === \"string\" ? raw.description : undefined,\n\t\t\t\t\ttitle: typeof raw.title === \"string\" ? raw.title : undefined,\n\t\t\t\t\tinputSchema: isRecord(raw.inputSchema) ? raw.inputSchema : undefined,\n\t\t\t\t});\n\t\t\t}\n\t\t\tcursor = typeof result.nextCursor === \"string\" && result.nextCursor.length > 0 ? result.nextCursor : undefined;\n\t\t} while (cursor);\n\t\treturn tools;\n\t}\n\n\t/**\n\t * Invoke a tool. A protocol-level failure rejects; a tool-level failure\n\t * resolves with `isError: true`, matching MCP semantics so the model sees\n\t * the server's own error text instead of a harness exception.\n\t */\n\tasync callTool(name: string, args: unknown, timeoutMs?: number): Promise<McpToolCallResult> {\n\t\tthis.assertReady();\n\t\tconst result = await this.request(\"tools/call\", { name, arguments: args ?? {} }, timeoutMs);\n\t\tif (!isRecord(result)) return { content: [], isError: false };\n\t\tconst content = Array.isArray(result.content) ? (result.content as McpContentBlock[]) : [];\n\t\treturn {\n\t\t\tcontent,\n\t\t\tisError: result.isError === true,\n\t\t\tstructuredContent: result.structuredContent,\n\t\t};\n\t}\n\n\t/** Terminate the server and reject anything still in flight. */\n\tclose(): void {\n\t\tif (this.closed) return;\n\t\tthis.transport.close();\n\t\tthis.handleExit(null, null);\n\t}\n\n\tprivate assertReady(): void {\n\t\tif (this.closed) throw new Error(this.exitReason ?? `MCP server \"${this.options.name}\" is closed`);\n\t\tif (!this.initialized) throw new Error(`MCP server \"${this.options.name}\" is not initialized`);\n\t}\n\n\tprivate request(method: string, params: unknown, timeoutMs?: number): Promise<unknown> {\n\t\tif (this.closed) {\n\t\t\treturn Promise.reject(new Error(this.exitReason ?? `MCP server \"${this.options.name}\" is closed`));\n\t\t}\n\t\tconst id = this.nextId++;\n\t\tconst effectiveTimeout = timeoutMs ?? this.requestTimeoutMs;\n\t\treturn new Promise<unknown>((resolve, reject) => {\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tthis.pending.delete(id);\n\t\t\t\treject(new Error(`MCP server \"${this.options.name}\" timed out after ${effectiveTimeout}ms on ${method}`));\n\t\t\t}, effectiveTimeout);\n\t\t\ttimer.unref?.();\n\t\t\tthis.pending.set(id, { resolve, reject, timer, method });\n\n\t\t\tconst sent = this.transport.send({ jsonrpc: \"2.0\", id, method, params });\n\t\t\tif (!sent) {\n\t\t\t\tthis.pending.delete(id);\n\t\t\t\tclearTimeout(timer);\n\t\t\t\treject(new Error(`MCP server \"${this.options.name}\" is not writable (${method})`));\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate handleMessage(message: JsonRpcMessage): void {\n\t\tif (!isJsonRpcResponse(message)) return; // Server-initiated requests/notifications are not used yet.\n\t\tconst response: JsonRpcResponse = message;\n\t\tconst pending = this.pending.get(response.id);\n\t\tif (!pending) return;\n\t\tthis.pending.delete(response.id);\n\t\tclearTimeout(pending.timer);\n\t\tif (response.error) {\n\t\t\tpending.reject(new Error(`MCP server \"${this.options.name}\": ${formatJsonRpcError(response.error)}`));\n\t\t\treturn;\n\t\t}\n\t\tpending.resolve(response.result);\n\t}\n\n\tprivate handleExit(code: number | null, signal: string | null): void {\n\t\tif (this.closed) return;\n\t\tthis.closed = true;\n\t\tthis.initialized = false;\n\t\tconst detail = signal ? `signal ${signal}` : code === null ? \"spawn failure\" : `exit code ${code}`;\n\t\tconst stderr = this.transport.stderr.trim();\n\t\tthis.exitReason = `MCP server \"${this.options.name}\" stopped (${detail})${stderr ? `: ${stderr}` : \"\"}`;\n\t\tconst reason = new Error(this.exitReason);\n\t\tfor (const [id, pending] of this.pending) {\n\t\t\tclearTimeout(pending.timer);\n\t\t\tthis.pending.delete(id);\n\t\t\tpending.reject(reason);\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/client.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal MCP client: initialize handshake, `tools/list`, `tools/call`.\n *\n * Deliberately dependency-free. The upstream SDK carries a full server\n * implementation, resource/prompt/sampling surfaces, and its own transport\n * stack; OMK needs a client for three methods, so the client is ~200 lines of\n * request correlation over a transport this repo already owns.\n *\n * Fail-closed contract:\n * - Every request has a deadline. A hung server rejects, it does not stall a turn.\n * - A transport exit rejects every in-flight request with the server's stderr tail.\n * - Calling before a completed handshake throws instead of guessing.\n */\n\nimport {\n\tformatJsonRpcError,\n\tisJsonRpcResponse,\n\ttype JsonRpcId,\n\ttype JsonRpcMessage,\n\ttype JsonRpcResponse,\n} from \"./protocol.ts\";\nimport { McpStdioTransport, type StdioTransportOptions } from \"./stdio-transport.ts\";\n\n/** Protocol revision this client implements. */\nexport const MCP_PROTOCOL_VERSION = \"2025-06-18\";\n/**\n * Protocol revisions this client accepts from a server. The handshake fails\n * closed on any other value instead of guessing at forward compatibility.\n */\nexport const SUPPORTED_PROTOCOL_VERSIONS: readonly string[] = [\"2024-11-05\", MCP_PROTOCOL_VERSION];\n/** Default per-request deadline. */\nexport const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;\n/** Default handshake deadline. Servers that install on first run need more room than a normal call. */\nexport const DEFAULT_HANDSHAKE_TIMEOUT_MS = 60_000;\n/** Page ceiling for `tools/list` so a looping cursor cannot stall a session forever. */\nexport const MAX_LIST_PAGES = 128;\n/** Tool ceiling across all `tools/list` pages for one server. */\nexport const MAX_LIST_TOOLS = 4096;\n/** Overall `tools/list` deadline; per-request timeouts do not bound the total. */\nexport const LIST_TOOLS_TIMEOUT_MS = 120_000;\n/** In-flight request ceiling per server. */\nexport const MAX_PENDING_REQUESTS = 128;\n\nexport interface McpToolSchema {\n\treadonly name: string;\n\treadonly description?: string;\n\treadonly inputSchema?: Record<string, unknown>;\n\treadonly title?: string;\n}\n\nexport interface McpTextBlock {\n\treadonly type: \"text\";\n\treadonly text: string;\n}\n\nexport type McpContentBlock =\n\t| McpTextBlock\n\t| { readonly type: \"image\"; readonly data: string; readonly mimeType: string }\n\t| { readonly type: string; readonly [key: string]: unknown };\n\nexport interface McpToolCallResult {\n\treadonly content: readonly McpContentBlock[];\n\treadonly isError: boolean;\n\treadonly structuredContent?: unknown;\n}\n\nexport interface McpServerInfo {\n\treadonly name?: string;\n\treadonly version?: string;\n}\n\nexport interface McpClientOptions {\n\t/** Server label used in error messages. */\n\treadonly name: string;\n\treadonly transport: StdioTransportOptions;\n\treadonly requestTimeoutMs?: number;\n\treadonly handshakeTimeoutMs?: number;\n\t/** Client identity reported during the handshake. */\n\treadonly clientInfo?: { readonly name: string; readonly version: string };\n}\n\ninterface PendingRequest {\n\treadonly resolve: (result: unknown) => void;\n\treadonly reject: (error: Error) => void;\n\treadonly timer: ReturnType<typeof setTimeout>;\n\treadonly method: string;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport class McpClient {\n\tprivate readonly transport: McpStdioTransport;\n\tprivate readonly pending = new Map<JsonRpcId, PendingRequest>();\n\tprivate readonly requestTimeoutMs: number;\n\tprivate readonly handshakeTimeoutMs: number;\n\tprivate nextId = 1;\n\tprivate initialized = false;\n\tprivate closed = false;\n\tprivate exitReason: string | undefined;\n\tprivate info: McpServerInfo = {};\n\tprivate lastProtocolErrorReason: string | undefined;\n\tprivate readonly options: McpClientOptions;\n\n\tconstructor(options: McpClientOptions) {\n\t\tthis.options = options;\n\t\tthis.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n\t\tthis.handshakeTimeoutMs = options.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;\n\t\tthis.transport = new McpStdioTransport(options.transport, {\n\t\t\tonMessage: (message) => this.handleMessage(message),\n\t\t\tonDecodeError: (reason) => {\n\t\t\t\tthis.lastProtocolErrorReason = reason;\n\t\t\t},\n\t\t\tonExit: ({ code, signal }) => this.handleExit(code, signal),\n\t\t});\n\t}\n\n\tget name(): string {\n\t\treturn this.options.name;\n\t}\n\n\tget serverInfo(): McpServerInfo {\n\t\treturn this.info;\n\t}\n\n\tget ready(): boolean {\n\t\treturn this.initialized && !this.closed;\n\t}\n\n\t/**\n\t * Spawn the server and complete the MCP handshake. Safe to await once; a\n\t * second call after a failure re-throws the recorded reason rather than\n\t * silently reusing a dead process.\n\t */\n\tasync connect(): Promise<void> {\n\t\tif (this.closed) throw new Error(this.exitReason ?? `MCP server \"${this.options.name}\" is closed`);\n\t\tif (this.initialized) return;\n\t\tthis.transport.start();\n\n\t\tconst result = await this.request(\n\t\t\t\"initialize\",\n\t\t\t{\n\t\t\t\tprotocolVersion: MCP_PROTOCOL_VERSION,\n\t\t\t\tcapabilities: {},\n\t\t\t\tclientInfo: this.options.clientInfo ?? { name: \"omk\", version: \"0.0.0\" },\n\t\t\t},\n\t\t\tthis.handshakeTimeoutMs,\n\t\t);\n\t\tthis.assertInitializeResult(result);\n\t\tthis.initialized = true;\n\t\tthis.transport.send({ jsonrpc: \"2.0\", method: \"notifications/initialized\" });\n\t}\n\n\t// The handshake is a negotiation, not a formality: reject a response that\n\t// does not carry a protocol version this client actually supports.\n\tprivate assertInitializeResult(result: unknown): void {\n\t\tif (!isRecord(result) || typeof result.protocolVersion !== \"string\") {\n\t\t\tthrow new Error(\n\t\t\t\t`MCP server \"${this.options.name}\" returned an invalid initialize result (missing protocolVersion)`,\n\t\t\t);\n\t\t}\n\t\tif (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {\n\t\t\tthrow new Error(\n\t\t\t\t`MCP server \"${this.options.name}\" requested unsupported protocol version \"${result.protocolVersion}\"`,\n\t\t\t);\n\t\t}\n\t\tif (isRecord(result.serverInfo)) {\n\t\t\tconst { name, version } = result.serverInfo;\n\t\t\tthis.info = {\n\t\t\t\tname: typeof name === \"string\" ? name : undefined,\n\t\t\t\tversion: typeof version === \"string\" ? version : undefined,\n\t\t\t};\n\t\t}\n\t}\n\n\t/**\n\t * Protocol-level liveness probe. Rejects when the server is closed, dead,\n\t * or fails to answer within `timeoutMs`, so callers can tell a silently\n\t * killed process apart from a genuinely connected one.\n\t */\n\tasync ping(timeoutMs?: number): Promise<void> {\n\t\tthis.assertReady();\n\t\tawait this.request(\"ping\", {}, timeoutMs);\n\t}\n\n\t/**\n\t * List the server's tools. Requires a completed handshake.\n\t *\n\t * Pagination is bounded three ways — an overall deadline, a page count, and\n\t * a tool count — and a repeated cursor is a protocol error, so a misbehaving\n\t * server cannot keep the listing loop alive indefinitely. Duplicate tool\n\t * names in one listing are rejected instead of silently overwritten.\n\t */\n\tasync listTools(): Promise<McpToolSchema[]> {\n\t\tthis.assertReady();\n\t\tconst tools: McpToolSchema[] = [];\n\t\tconst seenNames = new Set<string>();\n\t\tconst seenCursors = new Set<string>();\n\t\tconst deadline = Date.now() + LIST_TOOLS_TIMEOUT_MS;\n\t\tlet cursor: string | undefined;\n\t\tconst fail = (what: string): never => {\n\t\t\tthrow new Error(`MCP server \"${this.options.name}\" ${what}`);\n\t\t};\n\t\tfor (let page = 0; page < MAX_LIST_PAGES; page++) {\n\t\t\tconst remainingMs = deadline - Date.now();\n\t\t\tif (remainingMs <= 0) fail(`tools/list exceeded ${LIST_TOOLS_TIMEOUT_MS}ms`);\n\t\t\tconst result = await this.request(\n\t\t\t\t\"tools/list\",\n\t\t\t\tcursor ? { cursor } : {},\n\t\t\t\tMath.min(remainingMs, this.requestTimeoutMs),\n\t\t\t);\n\t\t\tconst record = isRecord(result) ? result : fail(\"returned an invalid tools/list result\");\n\t\t\tconst rawTools = Array.isArray(record.tools) ? record.tools : fail(\"returned an invalid tools/list result\");\n\t\t\tfor (const raw of rawTools) {\n\t\t\t\tif (!isRecord(raw) || typeof raw.name !== \"string\" || raw.name.length === 0) continue;\n\t\t\t\tif (seenNames.has(raw.name)) fail(`listed duplicate tool \"${raw.name}\"`);\n\t\t\t\tseenNames.add(raw.name);\n\t\t\t\ttools.push({\n\t\t\t\t\tname: raw.name,\n\t\t\t\t\tdescription: typeof raw.description === \"string\" ? raw.description : undefined,\n\t\t\t\t\ttitle: typeof raw.title === \"string\" ? raw.title : undefined,\n\t\t\t\t\tinputSchema: isRecord(raw.inputSchema) ? raw.inputSchema : undefined,\n\t\t\t\t});\n\t\t\t\tif (tools.length > MAX_LIST_TOOLS) fail(`exceeded ${MAX_LIST_TOOLS} tools`);\n\t\t\t}\n\t\t\tconst next =\n\t\t\t\ttypeof record.nextCursor === \"string\" && record.nextCursor.length > 0 ? record.nextCursor : undefined;\n\t\t\tif (!next) return tools;\n\t\t\tif (seenCursors.has(next)) fail(\"repeated tools/list cursor\");\n\t\t\tseenCursors.add(next);\n\t\t\tcursor = next;\n\t\t}\n\t\treturn fail(`exceeded ${MAX_LIST_PAGES} tools/list pages`);\n\t}\n\n\t/**\n\t * Invoke a tool. A protocol-level failure rejects; a tool-level failure\n\t * resolves with `isError: true`, matching MCP semantics so the model sees\n\t * the server's own error text instead of a harness exception.\n\t */\n\tasync callTool(name: string, args: unknown, timeoutMs?: number): Promise<McpToolCallResult> {\n\t\tthis.assertReady();\n\t\tconst result = await this.request(\"tools/call\", { name, arguments: args ?? {} }, timeoutMs);\n\t\t// A non-object result is a protocol violation, not an empty success —\n\t\t// normalizing it would hide a broken server from the caller.\n\t\tif (!isRecord(result)) {\n\t\t\tthrow new Error(`MCP server \"${this.options.name}\" returned an invalid tools/call result`);\n\t\t}\n\t\tconst content = Array.isArray(result.content) ? (result.content as McpContentBlock[]) : [];\n\t\treturn {\n\t\t\tcontent,\n\t\t\tisError: result.isError === true,\n\t\t\tstructuredContent: result.structuredContent,\n\t\t};\n\t}\n\n\t/** Terminate the server and reject anything still in flight. */\n\tclose(): void {\n\t\tif (this.closed) return;\n\t\tthis.transport.close();\n\t\tthis.handleExit(null, null);\n\t}\n\n\t/** Why the server is unusable, when it is; includes the latest decode failure. */\n\tget failure(): string | undefined {\n\t\treturn this.exitReason ?? this.lastProtocolErrorReason;\n\t}\n\n\tprivate assertReady(): void {\n\t\tif (this.closed) throw new Error(this.exitReason ?? `MCP server \"${this.options.name}\" is closed`);\n\t\tif (!this.initialized) throw new Error(`MCP server \"${this.options.name}\" is not initialized`);\n\t}\n\n\tprivate request(method: string, params: unknown, timeoutMs?: number): Promise<unknown> {\n\t\tif (this.closed) {\n\t\t\treturn Promise.reject(new Error(this.exitReason ?? `MCP server \"${this.options.name}\" is closed`));\n\t\t}\n\t\tif (this.pending.size >= MAX_PENDING_REQUESTS) {\n\t\t\treturn Promise.reject(\n\t\t\t\tnew Error(`MCP server \"${this.options.name}\" has ${MAX_PENDING_REQUESTS} pending requests (${method})`),\n\t\t\t);\n\t\t}\n\t\tconst id = this.nextId++;\n\t\tconst effectiveTimeout = timeoutMs ?? this.requestTimeoutMs;\n\t\treturn new Promise<unknown>((resolve, reject) => {\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tthis.pending.delete(id);\n\t\t\t\treject(new Error(`MCP server \"${this.options.name}\" timed out after ${effectiveTimeout}ms on ${method}`));\n\t\t\t}, effectiveTimeout);\n\t\t\ttimer.unref?.();\n\t\t\tthis.pending.set(id, { resolve, reject, timer, method });\n\t\t\tif (!this.transport.send({ jsonrpc: \"2.0\", id, method, params })) {\n\t\t\t\tthis.pending.delete(id);\n\t\t\t\tclearTimeout(timer);\n\t\t\t\treject(new Error(`MCP server \"${this.options.name}\" is not writable (${method})`));\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate handleMessage(message: JsonRpcMessage): void {\n\t\tif (!isJsonRpcResponse(message)) return; // Server-initiated requests/notifications are not used yet.\n\t\tconst response: JsonRpcResponse = message;\n\t\tconst pending = this.pending.get(response.id);\n\t\tif (!pending) return;\n\t\tthis.pending.delete(response.id);\n\t\tclearTimeout(pending.timer);\n\t\tif (response.error) {\n\t\t\tpending.reject(new Error(`MCP server \"${this.options.name}\": ${formatJsonRpcError(response.error)}`));\n\t\t\treturn;\n\t\t}\n\t\tpending.resolve(response.result);\n\t}\n\n\tprivate handleExit(code: number | null, signal: string | null): void {\n\t\tif (this.closed) return;\n\t\tthis.closed = true;\n\t\tthis.initialized = false;\n\t\tconst detail = signal ? `signal ${signal}` : code === null ? \"spawn failure\" : `exit code ${code}`;\n\t\tconst stderr = this.transport.stderr.trim();\n\t\tthis.exitReason = `MCP server \"${this.options.name}\" stopped (${detail})${stderr ? `: ${stderr}` : \"\"}`;\n\t\tconst reason = new Error(this.exitReason);\n\t\tfor (const [id, pending] of this.pending) {\n\t\t\tclearTimeout(pending.timer);\n\t\t\tthis.pending.delete(id);\n\t\t\tpending.reject(reason);\n\t\t}\n\t}\n}\n"]}
|
package/dist/core/mcp/client.js
CHANGED
|
@@ -15,10 +15,23 @@ import { formatJsonRpcError, isJsonRpcResponse, } from "./protocol.js";
|
|
|
15
15
|
import { McpStdioTransport } from "./stdio-transport.js";
|
|
16
16
|
/** Protocol revision this client implements. */
|
|
17
17
|
export const MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
18
|
+
/**
|
|
19
|
+
* Protocol revisions this client accepts from a server. The handshake fails
|
|
20
|
+
* closed on any other value instead of guessing at forward compatibility.
|
|
21
|
+
*/
|
|
22
|
+
export const SUPPORTED_PROTOCOL_VERSIONS = ["2024-11-05", MCP_PROTOCOL_VERSION];
|
|
18
23
|
/** Default per-request deadline. */
|
|
19
24
|
export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
20
25
|
/** Default handshake deadline. Servers that install on first run need more room than a normal call. */
|
|
21
26
|
export const DEFAULT_HANDSHAKE_TIMEOUT_MS = 60_000;
|
|
27
|
+
/** Page ceiling for `tools/list` so a looping cursor cannot stall a session forever. */
|
|
28
|
+
export const MAX_LIST_PAGES = 128;
|
|
29
|
+
/** Tool ceiling across all `tools/list` pages for one server. */
|
|
30
|
+
export const MAX_LIST_TOOLS = 4096;
|
|
31
|
+
/** Overall `tools/list` deadline; per-request timeouts do not bound the total. */
|
|
32
|
+
export const LIST_TOOLS_TIMEOUT_MS = 120_000;
|
|
33
|
+
/** In-flight request ceiling per server. */
|
|
34
|
+
export const MAX_PENDING_REQUESTS = 128;
|
|
22
35
|
function isRecord(value) {
|
|
23
36
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
37
|
}
|
|
@@ -32,6 +45,7 @@ export class McpClient {
|
|
|
32
45
|
closed = false;
|
|
33
46
|
exitReason;
|
|
34
47
|
info = {};
|
|
48
|
+
lastProtocolErrorReason;
|
|
35
49
|
options;
|
|
36
50
|
constructor(options) {
|
|
37
51
|
this.options = options;
|
|
@@ -39,6 +53,9 @@ export class McpClient {
|
|
|
39
53
|
this.handshakeTimeoutMs = options.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;
|
|
40
54
|
this.transport = new McpStdioTransport(options.transport, {
|
|
41
55
|
onMessage: (message) => this.handleMessage(message),
|
|
56
|
+
onDecodeError: (reason) => {
|
|
57
|
+
this.lastProtocolErrorReason = reason;
|
|
58
|
+
},
|
|
42
59
|
onExit: ({ code, signal }) => this.handleExit(code, signal),
|
|
43
60
|
});
|
|
44
61
|
}
|
|
@@ -51,10 +68,6 @@ export class McpClient {
|
|
|
51
68
|
get ready() {
|
|
52
69
|
return this.initialized && !this.closed;
|
|
53
70
|
}
|
|
54
|
-
/** Why the server is unusable, when it is. */
|
|
55
|
-
get failure() {
|
|
56
|
-
return this.exitReason;
|
|
57
|
-
}
|
|
58
71
|
/**
|
|
59
72
|
* Spawn the server and complete the MCP handshake. Safe to await once; a
|
|
60
73
|
* second call after a failure re-throws the recorded reason rather than
|
|
@@ -71,15 +84,26 @@ export class McpClient {
|
|
|
71
84
|
capabilities: {},
|
|
72
85
|
clientInfo: this.options.clientInfo ?? { name: "omk", version: "0.0.0" },
|
|
73
86
|
}, this.handshakeTimeoutMs);
|
|
74
|
-
|
|
87
|
+
this.assertInitializeResult(result);
|
|
88
|
+
this.initialized = true;
|
|
89
|
+
this.transport.send({ jsonrpc: "2.0", method: "notifications/initialized" });
|
|
90
|
+
}
|
|
91
|
+
// The handshake is a negotiation, not a formality: reject a response that
|
|
92
|
+
// does not carry a protocol version this client actually supports.
|
|
93
|
+
assertInitializeResult(result) {
|
|
94
|
+
if (!isRecord(result) || typeof result.protocolVersion !== "string") {
|
|
95
|
+
throw new Error(`MCP server "${this.options.name}" returned an invalid initialize result (missing protocolVersion)`);
|
|
96
|
+
}
|
|
97
|
+
if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {
|
|
98
|
+
throw new Error(`MCP server "${this.options.name}" requested unsupported protocol version "${result.protocolVersion}"`);
|
|
99
|
+
}
|
|
100
|
+
if (isRecord(result.serverInfo)) {
|
|
75
101
|
const { name, version } = result.serverInfo;
|
|
76
102
|
this.info = {
|
|
77
103
|
name: typeof name === "string" ? name : undefined,
|
|
78
104
|
version: typeof version === "string" ? version : undefined,
|
|
79
105
|
};
|
|
80
106
|
}
|
|
81
|
-
this.initialized = true;
|
|
82
|
-
this.transport.send({ jsonrpc: "2.0", method: "notifications/initialized" });
|
|
83
107
|
}
|
|
84
108
|
/**
|
|
85
109
|
* Protocol-level liveness probe. Rejects when the server is closed, dead,
|
|
@@ -90,28 +114,55 @@ export class McpClient {
|
|
|
90
114
|
this.assertReady();
|
|
91
115
|
await this.request("ping", {}, timeoutMs);
|
|
92
116
|
}
|
|
93
|
-
/**
|
|
117
|
+
/**
|
|
118
|
+
* List the server's tools. Requires a completed handshake.
|
|
119
|
+
*
|
|
120
|
+
* Pagination is bounded three ways — an overall deadline, a page count, and
|
|
121
|
+
* a tool count — and a repeated cursor is a protocol error, so a misbehaving
|
|
122
|
+
* server cannot keep the listing loop alive indefinitely. Duplicate tool
|
|
123
|
+
* names in one listing are rejected instead of silently overwritten.
|
|
124
|
+
*/
|
|
94
125
|
async listTools() {
|
|
95
126
|
this.assertReady();
|
|
96
127
|
const tools = [];
|
|
128
|
+
const seenNames = new Set();
|
|
129
|
+
const seenCursors = new Set();
|
|
130
|
+
const deadline = Date.now() + LIST_TOOLS_TIMEOUT_MS;
|
|
97
131
|
let cursor;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
132
|
+
const fail = (what) => {
|
|
133
|
+
throw new Error(`MCP server "${this.options.name}" ${what}`);
|
|
134
|
+
};
|
|
135
|
+
for (let page = 0; page < MAX_LIST_PAGES; page++) {
|
|
136
|
+
const remainingMs = deadline - Date.now();
|
|
137
|
+
if (remainingMs <= 0)
|
|
138
|
+
fail(`tools/list exceeded ${LIST_TOOLS_TIMEOUT_MS}ms`);
|
|
139
|
+
const result = await this.request("tools/list", cursor ? { cursor } : {}, Math.min(remainingMs, this.requestTimeoutMs));
|
|
140
|
+
const record = isRecord(result) ? result : fail("returned an invalid tools/list result");
|
|
141
|
+
const rawTools = Array.isArray(record.tools) ? record.tools : fail("returned an invalid tools/list result");
|
|
142
|
+
for (const raw of rawTools) {
|
|
103
143
|
if (!isRecord(raw) || typeof raw.name !== "string" || raw.name.length === 0)
|
|
104
144
|
continue;
|
|
145
|
+
if (seenNames.has(raw.name))
|
|
146
|
+
fail(`listed duplicate tool "${raw.name}"`);
|
|
147
|
+
seenNames.add(raw.name);
|
|
105
148
|
tools.push({
|
|
106
149
|
name: raw.name,
|
|
107
150
|
description: typeof raw.description === "string" ? raw.description : undefined,
|
|
108
151
|
title: typeof raw.title === "string" ? raw.title : undefined,
|
|
109
152
|
inputSchema: isRecord(raw.inputSchema) ? raw.inputSchema : undefined,
|
|
110
153
|
});
|
|
154
|
+
if (tools.length > MAX_LIST_TOOLS)
|
|
155
|
+
fail(`exceeded ${MAX_LIST_TOOLS} tools`);
|
|
111
156
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
157
|
+
const next = typeof record.nextCursor === "string" && record.nextCursor.length > 0 ? record.nextCursor : undefined;
|
|
158
|
+
if (!next)
|
|
159
|
+
return tools;
|
|
160
|
+
if (seenCursors.has(next))
|
|
161
|
+
fail("repeated tools/list cursor");
|
|
162
|
+
seenCursors.add(next);
|
|
163
|
+
cursor = next;
|
|
164
|
+
}
|
|
165
|
+
return fail(`exceeded ${MAX_LIST_PAGES} tools/list pages`);
|
|
115
166
|
}
|
|
116
167
|
/**
|
|
117
168
|
* Invoke a tool. A protocol-level failure rejects; a tool-level failure
|
|
@@ -121,8 +172,11 @@ export class McpClient {
|
|
|
121
172
|
async callTool(name, args, timeoutMs) {
|
|
122
173
|
this.assertReady();
|
|
123
174
|
const result = await this.request("tools/call", { name, arguments: args ?? {} }, timeoutMs);
|
|
124
|
-
|
|
125
|
-
|
|
175
|
+
// A non-object result is a protocol violation, not an empty success —
|
|
176
|
+
// normalizing it would hide a broken server from the caller.
|
|
177
|
+
if (!isRecord(result)) {
|
|
178
|
+
throw new Error(`MCP server "${this.options.name}" returned an invalid tools/call result`);
|
|
179
|
+
}
|
|
126
180
|
const content = Array.isArray(result.content) ? result.content : [];
|
|
127
181
|
return {
|
|
128
182
|
content,
|
|
@@ -137,6 +191,10 @@ export class McpClient {
|
|
|
137
191
|
this.transport.close();
|
|
138
192
|
this.handleExit(null, null);
|
|
139
193
|
}
|
|
194
|
+
/** Why the server is unusable, when it is; includes the latest decode failure. */
|
|
195
|
+
get failure() {
|
|
196
|
+
return this.exitReason ?? this.lastProtocolErrorReason;
|
|
197
|
+
}
|
|
140
198
|
assertReady() {
|
|
141
199
|
if (this.closed)
|
|
142
200
|
throw new Error(this.exitReason ?? `MCP server "${this.options.name}" is closed`);
|
|
@@ -147,6 +205,9 @@ export class McpClient {
|
|
|
147
205
|
if (this.closed) {
|
|
148
206
|
return Promise.reject(new Error(this.exitReason ?? `MCP server "${this.options.name}" is closed`));
|
|
149
207
|
}
|
|
208
|
+
if (this.pending.size >= MAX_PENDING_REQUESTS) {
|
|
209
|
+
return Promise.reject(new Error(`MCP server "${this.options.name}" has ${MAX_PENDING_REQUESTS} pending requests (${method})`));
|
|
210
|
+
}
|
|
150
211
|
const id = this.nextId++;
|
|
151
212
|
const effectiveTimeout = timeoutMs ?? this.requestTimeoutMs;
|
|
152
213
|
return new Promise((resolve, reject) => {
|
|
@@ -156,8 +217,7 @@ export class McpClient {
|
|
|
156
217
|
}, effectiveTimeout);
|
|
157
218
|
timer.unref?.();
|
|
158
219
|
this.pending.set(id, { resolve, reject, timer, method });
|
|
159
|
-
|
|
160
|
-
if (!sent) {
|
|
220
|
+
if (!this.transport.send({ jsonrpc: "2.0", id, method, params })) {
|
|
161
221
|
this.pending.delete(id);
|
|
162
222
|
clearTimeout(timer);
|
|
163
223
|
reject(new Error(`MCP server "${this.options.name}" is not writable (${method})`));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/core/mcp/client.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal MCP client: initialize handshake, `tools/list`, `tools/call`.\n *\n * Deliberately dependency-free. The upstream SDK carries a full server\n * implementation, resource/prompt/sampling surfaces, and its own transport\n * stack; OMK needs a client for three methods, so the client is ~200 lines of\n * request correlation over a transport this repo already owns.\n *\n * Fail-closed contract:\n * - Every request has a deadline. A hung server rejects, it does not stall a turn.\n * - A transport exit rejects every in-flight request with the server's stderr tail.\n * - Calling before a completed handshake throws instead of guessing.\n */\n\nimport {\n\tformatJsonRpcError,\n\tisJsonRpcResponse,\n\ttype JsonRpcId,\n\ttype JsonRpcMessage,\n\ttype JsonRpcResponse,\n} from \"./protocol.ts\";\nimport { McpStdioTransport, type StdioTransportOptions } from \"./stdio-transport.ts\";\n\n/** Protocol revision this client implements. */\nexport const MCP_PROTOCOL_VERSION = \"2025-06-18\";\n/** Default per-request deadline. */\nexport const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;\n/** Default handshake deadline. Servers that install on first run need more room than a normal call. */\nexport const DEFAULT_HANDSHAKE_TIMEOUT_MS = 60_000;\n\nexport interface McpToolSchema {\n\treadonly name: string;\n\treadonly description?: string;\n\treadonly inputSchema?: Record<string, unknown>;\n\treadonly title?: string;\n}\n\nexport interface McpTextBlock {\n\treadonly type: \"text\";\n\treadonly text: string;\n}\n\nexport interface McpImageBlock {\n\treadonly type: \"image\";\n\treadonly data: string;\n\treadonly mimeType: string;\n}\n\nexport type McpContentBlock = McpTextBlock | McpImageBlock | { readonly type: string; readonly [key: string]: unknown };\n\nexport interface McpToolCallResult {\n\treadonly content: readonly McpContentBlock[];\n\treadonly isError: boolean;\n\treadonly structuredContent?: unknown;\n}\n\nexport interface McpServerInfo {\n\treadonly name?: string;\n\treadonly version?: string;\n}\n\nexport interface McpClientOptions {\n\t/** Server label used in error messages. */\n\treadonly name: string;\n\treadonly transport: StdioTransportOptions;\n\treadonly requestTimeoutMs?: number;\n\treadonly handshakeTimeoutMs?: number;\n\t/** Client identity reported during the handshake. */\n\treadonly clientInfo?: { readonly name: string; readonly version: string };\n}\n\ninterface PendingRequest {\n\treadonly resolve: (result: unknown) => void;\n\treadonly reject: (error: Error) => void;\n\treadonly timer: ReturnType<typeof setTimeout>;\n\treadonly method: string;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport class McpClient {\n\tprivate readonly transport: McpStdioTransport;\n\tprivate readonly pending = new Map<JsonRpcId, PendingRequest>();\n\tprivate readonly requestTimeoutMs: number;\n\tprivate readonly handshakeTimeoutMs: number;\n\tprivate nextId = 1;\n\tprivate initialized = false;\n\tprivate closed = false;\n\tprivate exitReason: string | undefined;\n\tprivate info: McpServerInfo = {};\n\tprivate readonly options: McpClientOptions;\n\n\tconstructor(options: McpClientOptions) {\n\t\tthis.options = options;\n\t\tthis.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n\t\tthis.handshakeTimeoutMs = options.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;\n\t\tthis.transport = new McpStdioTransport(options.transport, {\n\t\t\tonMessage: (message) => this.handleMessage(message),\n\t\t\tonExit: ({ code, signal }) => this.handleExit(code, signal),\n\t\t});\n\t}\n\n\tget name(): string {\n\t\treturn this.options.name;\n\t}\n\n\tget serverInfo(): McpServerInfo {\n\t\treturn this.info;\n\t}\n\n\tget ready(): boolean {\n\t\treturn this.initialized && !this.closed;\n\t}\n\n\t/** Why the server is unusable, when it is. */\n\tget failure(): string | undefined {\n\t\treturn this.exitReason;\n\t}\n\n\t/**\n\t * Spawn the server and complete the MCP handshake. Safe to await once; a\n\t * second call after a failure re-throws the recorded reason rather than\n\t * silently reusing a dead process.\n\t */\n\tasync connect(): Promise<void> {\n\t\tif (this.closed) throw new Error(this.exitReason ?? `MCP server \"${this.options.name}\" is closed`);\n\t\tif (this.initialized) return;\n\t\tthis.transport.start();\n\n\t\tconst result = await this.request(\n\t\t\t\"initialize\",\n\t\t\t{\n\t\t\t\tprotocolVersion: MCP_PROTOCOL_VERSION,\n\t\t\t\tcapabilities: {},\n\t\t\t\tclientInfo: this.options.clientInfo ?? { name: \"omk\", version: \"0.0.0\" },\n\t\t\t},\n\t\t\tthis.handshakeTimeoutMs,\n\t\t);\n\t\tif (isRecord(result) && isRecord(result.serverInfo)) {\n\t\t\tconst { name, version } = result.serverInfo;\n\t\t\tthis.info = {\n\t\t\t\tname: typeof name === \"string\" ? name : undefined,\n\t\t\t\tversion: typeof version === \"string\" ? version : undefined,\n\t\t\t};\n\t\t}\n\t\tthis.initialized = true;\n\t\tthis.transport.send({ jsonrpc: \"2.0\", method: \"notifications/initialized\" });\n\t}\n\n\t/**\n\t * Protocol-level liveness probe. Rejects when the server is closed, dead,\n\t * or fails to answer within `timeoutMs`, so callers can tell a silently\n\t * killed process apart from a genuinely connected one.\n\t */\n\tasync ping(timeoutMs?: number): Promise<void> {\n\t\tthis.assertReady();\n\t\tawait this.request(\"ping\", {}, timeoutMs);\n\t}\n\n\t/** List the server's tools. Requires a completed handshake. */\n\tasync listTools(): Promise<McpToolSchema[]> {\n\t\tthis.assertReady();\n\t\tconst tools: McpToolSchema[] = [];\n\t\tlet cursor: string | undefined;\n\t\tdo {\n\t\t\tconst result = await this.request(\"tools/list\", cursor ? { cursor } : {});\n\t\t\tif (!isRecord(result) || !Array.isArray(result.tools)) return tools;\n\t\t\tfor (const raw of result.tools) {\n\t\t\t\tif (!isRecord(raw) || typeof raw.name !== \"string\" || raw.name.length === 0) continue;\n\t\t\t\ttools.push({\n\t\t\t\t\tname: raw.name,\n\t\t\t\t\tdescription: typeof raw.description === \"string\" ? raw.description : undefined,\n\t\t\t\t\ttitle: typeof raw.title === \"string\" ? raw.title : undefined,\n\t\t\t\t\tinputSchema: isRecord(raw.inputSchema) ? raw.inputSchema : undefined,\n\t\t\t\t});\n\t\t\t}\n\t\t\tcursor = typeof result.nextCursor === \"string\" && result.nextCursor.length > 0 ? result.nextCursor : undefined;\n\t\t} while (cursor);\n\t\treturn tools;\n\t}\n\n\t/**\n\t * Invoke a tool. A protocol-level failure rejects; a tool-level failure\n\t * resolves with `isError: true`, matching MCP semantics so the model sees\n\t * the server's own error text instead of a harness exception.\n\t */\n\tasync callTool(name: string, args: unknown, timeoutMs?: number): Promise<McpToolCallResult> {\n\t\tthis.assertReady();\n\t\tconst result = await this.request(\"tools/call\", { name, arguments: args ?? {} }, timeoutMs);\n\t\tif (!isRecord(result)) return { content: [], isError: false };\n\t\tconst content = Array.isArray(result.content) ? (result.content as McpContentBlock[]) : [];\n\t\treturn {\n\t\t\tcontent,\n\t\t\tisError: result.isError === true,\n\t\t\tstructuredContent: result.structuredContent,\n\t\t};\n\t}\n\n\t/** Terminate the server and reject anything still in flight. */\n\tclose(): void {\n\t\tif (this.closed) return;\n\t\tthis.transport.close();\n\t\tthis.handleExit(null, null);\n\t}\n\n\tprivate assertReady(): void {\n\t\tif (this.closed) throw new Error(this.exitReason ?? `MCP server \"${this.options.name}\" is closed`);\n\t\tif (!this.initialized) throw new Error(`MCP server \"${this.options.name}\" is not initialized`);\n\t}\n\n\tprivate request(method: string, params: unknown, timeoutMs?: number): Promise<unknown> {\n\t\tif (this.closed) {\n\t\t\treturn Promise.reject(new Error(this.exitReason ?? `MCP server \"${this.options.name}\" is closed`));\n\t\t}\n\t\tconst id = this.nextId++;\n\t\tconst effectiveTimeout = timeoutMs ?? this.requestTimeoutMs;\n\t\treturn new Promise<unknown>((resolve, reject) => {\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tthis.pending.delete(id);\n\t\t\t\treject(new Error(`MCP server \"${this.options.name}\" timed out after ${effectiveTimeout}ms on ${method}`));\n\t\t\t}, effectiveTimeout);\n\t\t\ttimer.unref?.();\n\t\t\tthis.pending.set(id, { resolve, reject, timer, method });\n\n\t\t\tconst sent = this.transport.send({ jsonrpc: \"2.0\", id, method, params });\n\t\t\tif (!sent) {\n\t\t\t\tthis.pending.delete(id);\n\t\t\t\tclearTimeout(timer);\n\t\t\t\treject(new Error(`MCP server \"${this.options.name}\" is not writable (${method})`));\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate handleMessage(message: JsonRpcMessage): void {\n\t\tif (!isJsonRpcResponse(message)) return; // Server-initiated requests/notifications are not used yet.\n\t\tconst response: JsonRpcResponse = message;\n\t\tconst pending = this.pending.get(response.id);\n\t\tif (!pending) return;\n\t\tthis.pending.delete(response.id);\n\t\tclearTimeout(pending.timer);\n\t\tif (response.error) {\n\t\t\tpending.reject(new Error(`MCP server \"${this.options.name}\": ${formatJsonRpcError(response.error)}`));\n\t\t\treturn;\n\t\t}\n\t\tpending.resolve(response.result);\n\t}\n\n\tprivate handleExit(code: number | null, signal: string | null): void {\n\t\tif (this.closed) return;\n\t\tthis.closed = true;\n\t\tthis.initialized = false;\n\t\tconst detail = signal ? `signal ${signal}` : code === null ? \"spawn failure\" : `exit code ${code}`;\n\t\tconst stderr = this.transport.stderr.trim();\n\t\tthis.exitReason = `MCP server \"${this.options.name}\" stopped (${detail})${stderr ? `: ${stderr}` : \"\"}`;\n\t\tconst reason = new Error(this.exitReason);\n\t\tfor (const [id, pending] of this.pending) {\n\t\t\tclearTimeout(pending.timer);\n\t\t\tthis.pending.delete(id);\n\t\t\tpending.reject(reason);\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/core/mcp/client.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal MCP client: initialize handshake, `tools/list`, `tools/call`.\n *\n * Deliberately dependency-free. The upstream SDK carries a full server\n * implementation, resource/prompt/sampling surfaces, and its own transport\n * stack; OMK needs a client for three methods, so the client is ~200 lines of\n * request correlation over a transport this repo already owns.\n *\n * Fail-closed contract:\n * - Every request has a deadline. A hung server rejects, it does not stall a turn.\n * - A transport exit rejects every in-flight request with the server's stderr tail.\n * - Calling before a completed handshake throws instead of guessing.\n */\n\nimport {\n\tformatJsonRpcError,\n\tisJsonRpcResponse,\n\ttype JsonRpcId,\n\ttype JsonRpcMessage,\n\ttype JsonRpcResponse,\n} from \"./protocol.ts\";\nimport { McpStdioTransport, type StdioTransportOptions } from \"./stdio-transport.ts\";\n\n/** Protocol revision this client implements. */\nexport const MCP_PROTOCOL_VERSION = \"2025-06-18\";\n/**\n * Protocol revisions this client accepts from a server. The handshake fails\n * closed on any other value instead of guessing at forward compatibility.\n */\nexport const SUPPORTED_PROTOCOL_VERSIONS: readonly string[] = [\"2024-11-05\", MCP_PROTOCOL_VERSION];\n/** Default per-request deadline. */\nexport const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;\n/** Default handshake deadline. Servers that install on first run need more room than a normal call. */\nexport const DEFAULT_HANDSHAKE_TIMEOUT_MS = 60_000;\n/** Page ceiling for `tools/list` so a looping cursor cannot stall a session forever. */\nexport const MAX_LIST_PAGES = 128;\n/** Tool ceiling across all `tools/list` pages for one server. */\nexport const MAX_LIST_TOOLS = 4096;\n/** Overall `tools/list` deadline; per-request timeouts do not bound the total. */\nexport const LIST_TOOLS_TIMEOUT_MS = 120_000;\n/** In-flight request ceiling per server. */\nexport const MAX_PENDING_REQUESTS = 128;\n\nexport interface McpToolSchema {\n\treadonly name: string;\n\treadonly description?: string;\n\treadonly inputSchema?: Record<string, unknown>;\n\treadonly title?: string;\n}\n\nexport interface McpTextBlock {\n\treadonly type: \"text\";\n\treadonly text: string;\n}\n\nexport type McpContentBlock =\n\t| McpTextBlock\n\t| { readonly type: \"image\"; readonly data: string; readonly mimeType: string }\n\t| { readonly type: string; readonly [key: string]: unknown };\n\nexport interface McpToolCallResult {\n\treadonly content: readonly McpContentBlock[];\n\treadonly isError: boolean;\n\treadonly structuredContent?: unknown;\n}\n\nexport interface McpServerInfo {\n\treadonly name?: string;\n\treadonly version?: string;\n}\n\nexport interface McpClientOptions {\n\t/** Server label used in error messages. */\n\treadonly name: string;\n\treadonly transport: StdioTransportOptions;\n\treadonly requestTimeoutMs?: number;\n\treadonly handshakeTimeoutMs?: number;\n\t/** Client identity reported during the handshake. */\n\treadonly clientInfo?: { readonly name: string; readonly version: string };\n}\n\ninterface PendingRequest {\n\treadonly resolve: (result: unknown) => void;\n\treadonly reject: (error: Error) => void;\n\treadonly timer: ReturnType<typeof setTimeout>;\n\treadonly method: string;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport class McpClient {\n\tprivate readonly transport: McpStdioTransport;\n\tprivate readonly pending = new Map<JsonRpcId, PendingRequest>();\n\tprivate readonly requestTimeoutMs: number;\n\tprivate readonly handshakeTimeoutMs: number;\n\tprivate nextId = 1;\n\tprivate initialized = false;\n\tprivate closed = false;\n\tprivate exitReason: string | undefined;\n\tprivate info: McpServerInfo = {};\n\tprivate lastProtocolErrorReason: string | undefined;\n\tprivate readonly options: McpClientOptions;\n\n\tconstructor(options: McpClientOptions) {\n\t\tthis.options = options;\n\t\tthis.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n\t\tthis.handshakeTimeoutMs = options.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;\n\t\tthis.transport = new McpStdioTransport(options.transport, {\n\t\t\tonMessage: (message) => this.handleMessage(message),\n\t\t\tonDecodeError: (reason) => {\n\t\t\t\tthis.lastProtocolErrorReason = reason;\n\t\t\t},\n\t\t\tonExit: ({ code, signal }) => this.handleExit(code, signal),\n\t\t});\n\t}\n\n\tget name(): string {\n\t\treturn this.options.name;\n\t}\n\n\tget serverInfo(): McpServerInfo {\n\t\treturn this.info;\n\t}\n\n\tget ready(): boolean {\n\t\treturn this.initialized && !this.closed;\n\t}\n\n\t/**\n\t * Spawn the server and complete the MCP handshake. Safe to await once; a\n\t * second call after a failure re-throws the recorded reason rather than\n\t * silently reusing a dead process.\n\t */\n\tasync connect(): Promise<void> {\n\t\tif (this.closed) throw new Error(this.exitReason ?? `MCP server \"${this.options.name}\" is closed`);\n\t\tif (this.initialized) return;\n\t\tthis.transport.start();\n\n\t\tconst result = await this.request(\n\t\t\t\"initialize\",\n\t\t\t{\n\t\t\t\tprotocolVersion: MCP_PROTOCOL_VERSION,\n\t\t\t\tcapabilities: {},\n\t\t\t\tclientInfo: this.options.clientInfo ?? { name: \"omk\", version: \"0.0.0\" },\n\t\t\t},\n\t\t\tthis.handshakeTimeoutMs,\n\t\t);\n\t\tthis.assertInitializeResult(result);\n\t\tthis.initialized = true;\n\t\tthis.transport.send({ jsonrpc: \"2.0\", method: \"notifications/initialized\" });\n\t}\n\n\t// The handshake is a negotiation, not a formality: reject a response that\n\t// does not carry a protocol version this client actually supports.\n\tprivate assertInitializeResult(result: unknown): void {\n\t\tif (!isRecord(result) || typeof result.protocolVersion !== \"string\") {\n\t\t\tthrow new Error(\n\t\t\t\t`MCP server \"${this.options.name}\" returned an invalid initialize result (missing protocolVersion)`,\n\t\t\t);\n\t\t}\n\t\tif (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {\n\t\t\tthrow new Error(\n\t\t\t\t`MCP server \"${this.options.name}\" requested unsupported protocol version \"${result.protocolVersion}\"`,\n\t\t\t);\n\t\t}\n\t\tif (isRecord(result.serverInfo)) {\n\t\t\tconst { name, version } = result.serverInfo;\n\t\t\tthis.info = {\n\t\t\t\tname: typeof name === \"string\" ? name : undefined,\n\t\t\t\tversion: typeof version === \"string\" ? version : undefined,\n\t\t\t};\n\t\t}\n\t}\n\n\t/**\n\t * Protocol-level liveness probe. Rejects when the server is closed, dead,\n\t * or fails to answer within `timeoutMs`, so callers can tell a silently\n\t * killed process apart from a genuinely connected one.\n\t */\n\tasync ping(timeoutMs?: number): Promise<void> {\n\t\tthis.assertReady();\n\t\tawait this.request(\"ping\", {}, timeoutMs);\n\t}\n\n\t/**\n\t * List the server's tools. Requires a completed handshake.\n\t *\n\t * Pagination is bounded three ways — an overall deadline, a page count, and\n\t * a tool count — and a repeated cursor is a protocol error, so a misbehaving\n\t * server cannot keep the listing loop alive indefinitely. Duplicate tool\n\t * names in one listing are rejected instead of silently overwritten.\n\t */\n\tasync listTools(): Promise<McpToolSchema[]> {\n\t\tthis.assertReady();\n\t\tconst tools: McpToolSchema[] = [];\n\t\tconst seenNames = new Set<string>();\n\t\tconst seenCursors = new Set<string>();\n\t\tconst deadline = Date.now() + LIST_TOOLS_TIMEOUT_MS;\n\t\tlet cursor: string | undefined;\n\t\tconst fail = (what: string): never => {\n\t\t\tthrow new Error(`MCP server \"${this.options.name}\" ${what}`);\n\t\t};\n\t\tfor (let page = 0; page < MAX_LIST_PAGES; page++) {\n\t\t\tconst remainingMs = deadline - Date.now();\n\t\t\tif (remainingMs <= 0) fail(`tools/list exceeded ${LIST_TOOLS_TIMEOUT_MS}ms`);\n\t\t\tconst result = await this.request(\n\t\t\t\t\"tools/list\",\n\t\t\t\tcursor ? { cursor } : {},\n\t\t\t\tMath.min(remainingMs, this.requestTimeoutMs),\n\t\t\t);\n\t\t\tconst record = isRecord(result) ? result : fail(\"returned an invalid tools/list result\");\n\t\t\tconst rawTools = Array.isArray(record.tools) ? record.tools : fail(\"returned an invalid tools/list result\");\n\t\t\tfor (const raw of rawTools) {\n\t\t\t\tif (!isRecord(raw) || typeof raw.name !== \"string\" || raw.name.length === 0) continue;\n\t\t\t\tif (seenNames.has(raw.name)) fail(`listed duplicate tool \"${raw.name}\"`);\n\t\t\t\tseenNames.add(raw.name);\n\t\t\t\ttools.push({\n\t\t\t\t\tname: raw.name,\n\t\t\t\t\tdescription: typeof raw.description === \"string\" ? raw.description : undefined,\n\t\t\t\t\ttitle: typeof raw.title === \"string\" ? raw.title : undefined,\n\t\t\t\t\tinputSchema: isRecord(raw.inputSchema) ? raw.inputSchema : undefined,\n\t\t\t\t});\n\t\t\t\tif (tools.length > MAX_LIST_TOOLS) fail(`exceeded ${MAX_LIST_TOOLS} tools`);\n\t\t\t}\n\t\t\tconst next =\n\t\t\t\ttypeof record.nextCursor === \"string\" && record.nextCursor.length > 0 ? record.nextCursor : undefined;\n\t\t\tif (!next) return tools;\n\t\t\tif (seenCursors.has(next)) fail(\"repeated tools/list cursor\");\n\t\t\tseenCursors.add(next);\n\t\t\tcursor = next;\n\t\t}\n\t\treturn fail(`exceeded ${MAX_LIST_PAGES} tools/list pages`);\n\t}\n\n\t/**\n\t * Invoke a tool. A protocol-level failure rejects; a tool-level failure\n\t * resolves with `isError: true`, matching MCP semantics so the model sees\n\t * the server's own error text instead of a harness exception.\n\t */\n\tasync callTool(name: string, args: unknown, timeoutMs?: number): Promise<McpToolCallResult> {\n\t\tthis.assertReady();\n\t\tconst result = await this.request(\"tools/call\", { name, arguments: args ?? {} }, timeoutMs);\n\t\t// A non-object result is a protocol violation, not an empty success —\n\t\t// normalizing it would hide a broken server from the caller.\n\t\tif (!isRecord(result)) {\n\t\t\tthrow new Error(`MCP server \"${this.options.name}\" returned an invalid tools/call result`);\n\t\t}\n\t\tconst content = Array.isArray(result.content) ? (result.content as McpContentBlock[]) : [];\n\t\treturn {\n\t\t\tcontent,\n\t\t\tisError: result.isError === true,\n\t\t\tstructuredContent: result.structuredContent,\n\t\t};\n\t}\n\n\t/** Terminate the server and reject anything still in flight. */\n\tclose(): void {\n\t\tif (this.closed) return;\n\t\tthis.transport.close();\n\t\tthis.handleExit(null, null);\n\t}\n\n\t/** Why the server is unusable, when it is; includes the latest decode failure. */\n\tget failure(): string | undefined {\n\t\treturn this.exitReason ?? this.lastProtocolErrorReason;\n\t}\n\n\tprivate assertReady(): void {\n\t\tif (this.closed) throw new Error(this.exitReason ?? `MCP server \"${this.options.name}\" is closed`);\n\t\tif (!this.initialized) throw new Error(`MCP server \"${this.options.name}\" is not initialized`);\n\t}\n\n\tprivate request(method: string, params: unknown, timeoutMs?: number): Promise<unknown> {\n\t\tif (this.closed) {\n\t\t\treturn Promise.reject(new Error(this.exitReason ?? `MCP server \"${this.options.name}\" is closed`));\n\t\t}\n\t\tif (this.pending.size >= MAX_PENDING_REQUESTS) {\n\t\t\treturn Promise.reject(\n\t\t\t\tnew Error(`MCP server \"${this.options.name}\" has ${MAX_PENDING_REQUESTS} pending requests (${method})`),\n\t\t\t);\n\t\t}\n\t\tconst id = this.nextId++;\n\t\tconst effectiveTimeout = timeoutMs ?? this.requestTimeoutMs;\n\t\treturn new Promise<unknown>((resolve, reject) => {\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tthis.pending.delete(id);\n\t\t\t\treject(new Error(`MCP server \"${this.options.name}\" timed out after ${effectiveTimeout}ms on ${method}`));\n\t\t\t}, effectiveTimeout);\n\t\t\ttimer.unref?.();\n\t\t\tthis.pending.set(id, { resolve, reject, timer, method });\n\t\t\tif (!this.transport.send({ jsonrpc: \"2.0\", id, method, params })) {\n\t\t\t\tthis.pending.delete(id);\n\t\t\t\tclearTimeout(timer);\n\t\t\t\treject(new Error(`MCP server \"${this.options.name}\" is not writable (${method})`));\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate handleMessage(message: JsonRpcMessage): void {\n\t\tif (!isJsonRpcResponse(message)) return; // Server-initiated requests/notifications are not used yet.\n\t\tconst response: JsonRpcResponse = message;\n\t\tconst pending = this.pending.get(response.id);\n\t\tif (!pending) return;\n\t\tthis.pending.delete(response.id);\n\t\tclearTimeout(pending.timer);\n\t\tif (response.error) {\n\t\t\tpending.reject(new Error(`MCP server \"${this.options.name}\": ${formatJsonRpcError(response.error)}`));\n\t\t\treturn;\n\t\t}\n\t\tpending.resolve(response.result);\n\t}\n\n\tprivate handleExit(code: number | null, signal: string | null): void {\n\t\tif (this.closed) return;\n\t\tthis.closed = true;\n\t\tthis.initialized = false;\n\t\tconst detail = signal ? `signal ${signal}` : code === null ? \"spawn failure\" : `exit code ${code}`;\n\t\tconst stderr = this.transport.stderr.trim();\n\t\tthis.exitReason = `MCP server \"${this.options.name}\" stopped (${detail})${stderr ? `: ${stderr}` : \"\"}`;\n\t\tconst reason = new Error(this.exitReason);\n\t\tfor (const [id, pending] of this.pending) {\n\t\t\tclearTimeout(pending.timer);\n\t\t\tthis.pending.delete(id);\n\t\t\tpending.reject(reason);\n\t\t}\n\t}\n}\n"]}
|
|
@@ -34,6 +34,8 @@ export interface McpServerStatus {
|
|
|
34
34
|
/** Failure reason when `state` is `failed`. Never contains configured env values. */
|
|
35
35
|
readonly error?: string;
|
|
36
36
|
readonly serverVersion?: string;
|
|
37
|
+
/** Tool names excluded because their descriptions carried prompt-injection payloads. */
|
|
38
|
+
readonly quarantinedTools?: readonly string[];
|
|
37
39
|
}
|
|
38
40
|
export interface McpManagerOptions {
|
|
39
41
|
readonly servers: readonly McpServerConfig[];
|
|
@@ -62,7 +64,14 @@ export declare class McpManager {
|
|
|
62
64
|
listToolDefinitions(): Promise<ToolDefinition<TSchema, McpToolDetails>[]>;
|
|
63
65
|
/** Connect a single server by name. Returns its status either way. */
|
|
64
66
|
connect(name: string): Promise<McpServerStatus>;
|
|
65
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Terminate every connected or connecting server. Idempotent.
|
|
69
|
+
*
|
|
70
|
+
* Bumping the generation invalidates any in-flight connect before its late
|
|
71
|
+
* completion can publish tools or flip the state back to `ready`, and the
|
|
72
|
+
* attempt's not-yet-published client is closed alongside the live one so no
|
|
73
|
+
* spawned process outlives the close.
|
|
74
|
+
*/
|
|
66
75
|
close(): void;
|
|
67
76
|
/**
|
|
68
77
|
* Verify that `ready` servers are actually alive with a protocol ping. A
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,EAA2B,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAE1E,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,YAAY,GAAG,OAAO,GAAG,QAAQ,CAAC;AAExE,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,+DAA+D;IAC/D,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,qFAAqF;IACrF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;IAC7C,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1E,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,qFAAqF;IACrF,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,SAAS,CAAC;CACjE;AAWD,qBAAa,UAAU;IACtB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoC;IAC7D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;IAE5C,YAAY,OAAO,EAAE,iBAAiB,EAMrC;IAED,uDAAuD;IACvD,IAAI,WAAW,IAAI,MAAM,EAAE,CAE1B;IAED,6EAA6E;IAC7E,MAAM,IAAI,eAAe,EAAE,CAQ1B;IAED;;;;OAIG;IACG,mBAAmB,IAAI,OAAO,CAAC,cAAc,CAAC,OAAO,EAAE,cAAc,CAAC,EAAE,CAAC,CAY9E;IAED,sEAAsE;IAChE,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAapD;IAED,oDAAoD;IACpD,KAAK,IAAI,IAAI,CAOZ;IAED;;;;;;;;OAQG;IACG,WAAW,CAAC,OAAO,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAa7G;YAEa,WAAW;IAazB,OAAO,CAAC,eAAe;YAiBT,cAAc;CAkC5B","sourcesContent":["/**\n * Lazy, crash-isolated MCP server manager.\n *\n * Design constraints that shaped this:\n * - A workspace can configure 25 servers. Spawning them all at startup would\n * trade a 0.6s cold start for a multi-second one, so connection is lazy and\n * only happens when tools are actually requested.\n * - One broken server must never take a session down. Every connect and every\n * listing is isolated; a failure is recorded as server status and the other\n * servers still contribute tools.\n * - Connect work is deduplicated: concurrent `listToolDefinitions()` calls\n * share one in-flight connect per server.\n */\n\nimport type { TSchema } from \"typebox\";\nimport type { ToolDefinition } from \"../extensions/types.ts\";\nimport { McpClient, type McpClientOptions } from \"./client.ts\";\nimport { createMcpToolDefinition, type McpToolDetails } from \"./tools.ts\";\n\nexport type McpServerState = \"idle\" | \"connecting\" | \"ready\" | \"failed\";\n\nexport interface McpServerConfig {\n\treadonly name: string;\n\treadonly command: string;\n\treadonly args?: readonly string[];\n\treadonly env?: Readonly<Record<string, string>>;\n\treadonly cwd?: string;\n\t/** Skip this server without removing it from configuration. */\n\treadonly disabled?: boolean;\n\treadonly requestTimeoutMs?: number;\n\treadonly handshakeTimeoutMs?: number;\n}\n\nexport interface McpServerStatus {\n\treadonly name: string;\n\treadonly state: McpServerState;\n\treadonly toolCount: number;\n\t/** Failure reason when `state` is `failed`. Never contains configured env values. */\n\treadonly error?: string;\n\treadonly serverVersion?: string;\n}\n\nexport interface McpManagerOptions {\n\treadonly servers: readonly McpServerConfig[];\n\treadonly cwd?: string;\n\treadonly clientInfo?: { readonly name: string; readonly version: string };\n\treadonly callTimeoutMs?: number;\n\t/** Injected for tests so the manager can be exercised without spawning processes. */\n\treadonly createClient?: (options: McpClientOptions) => McpClient;\n}\n\ninterface ServerRuntime {\n\treadonly config: McpServerConfig;\n\tstate: McpServerState;\n\tclient?: McpClient;\n\ttools: ToolDefinition<TSchema, McpToolDetails>[];\n\terror?: string;\n\tconnecting?: Promise<void>;\n}\n\nexport class McpManager {\n\tprivate readonly runtimes = new Map<string, ServerRuntime>();\n\tprivate readonly options: McpManagerOptions;\n\n\tconstructor(options: McpManagerOptions) {\n\t\tthis.options = options;\n\t\tfor (const config of options.servers) {\n\t\t\tif (this.runtimes.has(config.name)) continue; // First definition wins; inventory already applies precedence.\n\t\t\tthis.runtimes.set(config.name, { config, state: \"idle\", tools: [] });\n\t\t}\n\t}\n\n\t/** Configured server names, in configuration order. */\n\tget serverNames(): string[] {\n\t\treturn [...this.runtimes.keys()];\n\t}\n\n\t/** Current status per server. Safe to call before any connection attempt. */\n\tstatus(): McpServerStatus[] {\n\t\treturn [...this.runtimes.values()].map((runtime) => ({\n\t\t\tname: runtime.config.name,\n\t\t\tstate: runtime.state,\n\t\t\ttoolCount: runtime.tools.length,\n\t\t\terror: runtime.error,\n\t\t\tserverVersion: runtime.client?.serverInfo.version,\n\t\t}));\n\t}\n\n\t/**\n\t * Connect every enabled server that has not been attempted yet, then return\n\t * the union of their tools. Servers are connected concurrently and failures\n\t * are isolated, so a partial result is the normal outcome, not an error.\n\t */\n\tasync listToolDefinitions(): Promise<ToolDefinition<TSchema, McpToolDetails>[]> {\n\t\tawait Promise.all([...this.runtimes.values()].map((runtime) => this.ensureConnected(runtime)));\n\t\tconst tools: ToolDefinition<TSchema, McpToolDetails>[] = [];\n\t\tconst seen = new Set<string>();\n\t\tfor (const runtime of this.runtimes.values()) {\n\t\t\tfor (const tool of runtime.tools) {\n\t\t\t\tif (seen.has(tool.name)) continue; // Namespacing makes this rare; keep it deterministic anyway.\n\t\t\t\tseen.add(tool.name);\n\t\t\t\ttools.push(tool);\n\t\t\t}\n\t\t}\n\t\treturn tools;\n\t}\n\n\t/** Connect a single server by name. Returns its status either way. */\n\tasync connect(name: string): Promise<McpServerStatus> {\n\t\tconst runtime = this.runtimes.get(name);\n\t\tif (!runtime) {\n\t\t\treturn { name, state: \"failed\", toolCount: 0, error: `Unknown MCP server \"${name}\"` };\n\t\t}\n\t\tawait this.ensureConnected(runtime);\n\t\treturn {\n\t\t\tname,\n\t\t\tstate: runtime.state,\n\t\t\ttoolCount: runtime.tools.length,\n\t\t\terror: runtime.error,\n\t\t\tserverVersion: runtime.client?.serverInfo.version,\n\t\t};\n\t}\n\n\t/** Terminate every connected server. Idempotent. */\n\tclose(): void {\n\t\tfor (const runtime of this.runtimes.values()) {\n\t\t\truntime.client?.close();\n\t\t\truntime.client = undefined;\n\t\t\truntime.tools = [];\n\t\t\tif (runtime.state === \"ready\" || runtime.state === \"connecting\") runtime.state = \"idle\";\n\t\t}\n\t}\n\n\t/**\n\t * Verify that `ready` servers are actually alive with a protocol ping. A\n\t * failed ping closes the client and marks the server `failed`, so a\n\t * silently killed process stops masquerading as connected. Servers already\n\t * `failed` are re-attempted only when `reconnectFailed` is set — callers\n\t * should gate that behind a slow cadence so a permanently broken server is\n\t * not respawned every probe. Idle servers stay idle: connection is lazy by\n\t * design, and a status probe must not spawn processes.\n\t */\n\tasync checkHealth(options?: { pingTimeoutMs?: number; reconnectFailed?: boolean }): Promise<McpServerStatus[]> {\n\t\tconst work: Promise<void>[] = [];\n\t\tfor (const runtime of this.runtimes.values()) {\n\t\t\tif (runtime.state === \"ready\" && runtime.client) {\n\t\t\t\twork.push(this.pingRuntime(runtime, options?.pingTimeoutMs));\n\t\t\t} else if (runtime.state === \"failed\" && options?.reconnectFailed && !runtime.config.disabled) {\n\t\t\t\truntime.state = \"idle\";\n\t\t\t\truntime.error = undefined;\n\t\t\t\twork.push(this.ensureConnected(runtime));\n\t\t\t}\n\t\t}\n\t\tawait Promise.all(work);\n\t\treturn this.status();\n\t}\n\n\tprivate async pingRuntime(runtime: ServerRuntime, timeoutMs?: number): Promise<void> {\n\t\ttry {\n\t\t\tawait runtime.client?.ping(timeoutMs);\n\t\t} catch (error) {\n\t\t\t// Isolation point: only this server transitions; the rest stay ready.\n\t\t\truntime.client?.close();\n\t\t\truntime.client = undefined;\n\t\t\truntime.tools = [];\n\t\t\truntime.state = \"failed\";\n\t\t\truntime.error = `health check failed: ${error instanceof Error ? error.message : String(error)}`;\n\t\t}\n\t}\n\n\tprivate ensureConnected(runtime: ServerRuntime): Promise<void> {\n\t\tif (runtime.config.disabled) {\n\t\t\truntime.state = \"failed\";\n\t\t\truntime.error = \"disabled by configuration\";\n\t\t\treturn Promise.resolve();\n\t\t}\n\t\tif (runtime.state === \"ready\" || runtime.state === \"failed\") return Promise.resolve();\n\t\tif (runtime.connecting) return runtime.connecting;\n\n\t\truntime.state = \"connecting\";\n\t\tconst attempt = this.connectRuntime(runtime).finally(() => {\n\t\t\truntime.connecting = undefined;\n\t\t});\n\t\truntime.connecting = attempt;\n\t\treturn attempt;\n\t}\n\n\tprivate async connectRuntime(runtime: ServerRuntime): Promise<void> {\n\t\tconst clientOptions: McpClientOptions = {\n\t\t\tname: runtime.config.name,\n\t\t\tclientInfo: this.options.clientInfo,\n\t\t\trequestTimeoutMs: runtime.config.requestTimeoutMs,\n\t\t\thandshakeTimeoutMs: runtime.config.handshakeTimeoutMs,\n\t\t\ttransport: {\n\t\t\t\tcommand: runtime.config.command,\n\t\t\t\targs: runtime.config.args,\n\t\t\t\tenv: runtime.config.env,\n\t\t\t\tcwd: runtime.config.cwd ?? this.options.cwd,\n\t\t\t},\n\t\t};\n\t\tconst client = this.options.createClient?.(clientOptions) ?? new McpClient(clientOptions);\n\t\ttry {\n\t\t\tawait client.connect();\n\t\t\tconst schemas = await client.listTools();\n\t\t\truntime.client = client;\n\t\t\truntime.tools = schemas.map((schema) =>\n\t\t\t\tcreateMcpToolDefinition(runtime.config.name, client, schema, {\n\t\t\t\t\tcallTimeoutMs: this.options.callTimeoutMs,\n\t\t\t\t}),\n\t\t\t);\n\t\t\truntime.state = \"ready\";\n\t\t\truntime.error = undefined;\n\t\t} catch (error) {\n\t\t\t// Isolation point: this server is out, every other server is unaffected.\n\t\t\tclient.close();\n\t\t\truntime.client = undefined;\n\t\t\truntime.tools = [];\n\t\t\truntime.state = \"failed\";\n\t\t\truntime.error = error instanceof Error ? error.message : String(error);\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D,OAAO,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,EAA2B,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAE1E,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,YAAY,GAAG,OAAO,GAAG,QAAQ,CAAC;AAExE,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,+DAA+D;IAC/D,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,qFAAqF;IACrF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,wFAAwF;IACxF,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9C;AAED,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;IAC7C,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1E,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,qFAAqF;IACrF,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,SAAS,CAAC;CACjE;AAiBD,qBAAa,UAAU;IACtB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoC;IAC7D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;IAE5C,YAAY,OAAO,EAAE,iBAAiB,EAMrC;IAED,uDAAuD;IACvD,IAAI,WAAW,IAAI,MAAM,EAAE,CAE1B;IAED,6EAA6E;IAC7E,MAAM,IAAI,eAAe,EAAE,CAS1B;IAED;;;;OAIG;IACG,mBAAmB,IAAI,OAAO,CAAC,cAAc,CAAC,OAAO,EAAE,cAAc,CAAC,EAAE,CAAC,CAY9E;IAED,sEAAsE;IAChE,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAcpD;IAED;;;;;;;OAOG;IACH,KAAK,IAAI,IAAI,CAWZ;IAED;;;;;;;;OAQG;IACG,WAAW,CAAC,OAAO,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAa7G;YAEa,WAAW;IAmBzB,OAAO,CAAC,eAAe;YAqBT,cAAc;CAiE5B","sourcesContent":["/**\n * Lazy, crash-isolated MCP server manager.\n *\n * Design constraints that shaped this:\n * - A workspace can configure 25 servers. Spawning them all at startup would\n * trade a 0.6s cold start for a multi-second one, so connection is lazy and\n * only happens when tools are actually requested.\n * - One broken server must never take a session down. Every connect and every\n * listing is isolated; a failure is recorded as server status and the other\n * servers still contribute tools.\n * - Connect work is deduplicated: concurrent `listToolDefinitions()` calls\n * share one in-flight connect per server.\n */\n\nimport type { TSchema } from \"typebox\";\nimport type { ToolDefinition } from \"../extensions/types.ts\";\nimport { detectMcpDescriptorPromptInjection, MCP_QUARANTINE_PATTERN_SIGNAL_THRESHOLD } from \"../mcp-public-presets.ts\";\nimport { McpClient, type McpClientOptions } from \"./client.ts\";\nimport { createMcpToolDefinition, type McpToolDetails } from \"./tools.ts\";\n\nexport type McpServerState = \"idle\" | \"connecting\" | \"ready\" | \"failed\";\n\nexport interface McpServerConfig {\n\treadonly name: string;\n\treadonly command: string;\n\treadonly args?: readonly string[];\n\treadonly env?: Readonly<Record<string, string>>;\n\treadonly cwd?: string;\n\t/** Skip this server without removing it from configuration. */\n\treadonly disabled?: boolean;\n\treadonly requestTimeoutMs?: number;\n\treadonly handshakeTimeoutMs?: number;\n}\n\nexport interface McpServerStatus {\n\treadonly name: string;\n\treadonly state: McpServerState;\n\treadonly toolCount: number;\n\t/** Failure reason when `state` is `failed`. Never contains configured env values. */\n\treadonly error?: string;\n\treadonly serverVersion?: string;\n\t/** Tool names excluded because their descriptions carried prompt-injection payloads. */\n\treadonly quarantinedTools?: readonly string[];\n}\n\nexport interface McpManagerOptions {\n\treadonly servers: readonly McpServerConfig[];\n\treadonly cwd?: string;\n\treadonly clientInfo?: { readonly name: string; readonly version: string };\n\treadonly callTimeoutMs?: number;\n\t/** Injected for tests so the manager can be exercised without spawning processes. */\n\treadonly createClient?: (options: McpClientOptions) => McpClient;\n}\n\ninterface ServerRuntime {\n\treadonly config: McpServerConfig;\n\tstate: McpServerState;\n\tclient?: McpClient;\n\ttools: ToolDefinition<TSchema, McpToolDetails>[];\n\t/** Descriptor prompt-injection quarantines (namespaced tool names). */\n\tquarantinedTools: string[];\n\terror?: string;\n\tconnecting?: Promise<void>;\n\t/** Client owned by the in-flight connect attempt; not yet published. */\n\tpendingClient?: McpClient;\n\t/** Monotonic generation; a completed attempt may only publish when current. */\n\tgeneration: number;\n}\n\nexport class McpManager {\n\tprivate readonly runtimes = new Map<string, ServerRuntime>();\n\tprivate readonly options: McpManagerOptions;\n\n\tconstructor(options: McpManagerOptions) {\n\t\tthis.options = options;\n\t\tfor (const config of options.servers) {\n\t\t\tif (this.runtimes.has(config.name)) continue; // First definition wins; inventory already applies precedence.\n\t\t\tthis.runtimes.set(config.name, { config, state: \"idle\", tools: [], quarantinedTools: [], generation: 0 });\n\t\t}\n\t}\n\n\t/** Configured server names, in configuration order. */\n\tget serverNames(): string[] {\n\t\treturn [...this.runtimes.keys()];\n\t}\n\n\t/** Current status per server. Safe to call before any connection attempt. */\n\tstatus(): McpServerStatus[] {\n\t\treturn [...this.runtimes.values()].map((runtime) => ({\n\t\t\tname: runtime.config.name,\n\t\t\tstate: runtime.state,\n\t\t\ttoolCount: runtime.tools.length,\n\t\t\terror: runtime.error,\n\t\t\tserverVersion: runtime.client?.serverInfo.version,\n\t\t\t...(runtime.quarantinedTools.length > 0 ? { quarantinedTools: runtime.quarantinedTools } : {}),\n\t\t}));\n\t}\n\n\t/**\n\t * Connect every enabled server that has not been attempted yet, then return\n\t * the union of their tools. Servers are connected concurrently and failures\n\t * are isolated, so a partial result is the normal outcome, not an error.\n\t */\n\tasync listToolDefinitions(): Promise<ToolDefinition<TSchema, McpToolDetails>[]> {\n\t\tawait Promise.all([...this.runtimes.values()].map((runtime) => this.ensureConnected(runtime)));\n\t\tconst tools: ToolDefinition<TSchema, McpToolDetails>[] = [];\n\t\tconst seen = new Set<string>();\n\t\tfor (const runtime of this.runtimes.values()) {\n\t\t\tfor (const tool of runtime.tools) {\n\t\t\t\tif (seen.has(tool.name)) continue; // Namespacing makes this rare; keep it deterministic anyway.\n\t\t\t\tseen.add(tool.name);\n\t\t\t\ttools.push(tool);\n\t\t\t}\n\t\t}\n\t\treturn tools;\n\t}\n\n\t/** Connect a single server by name. Returns its status either way. */\n\tasync connect(name: string): Promise<McpServerStatus> {\n\t\tconst runtime = this.runtimes.get(name);\n\t\tif (!runtime) {\n\t\t\treturn { name, state: \"failed\", toolCount: 0, error: `Unknown MCP server \"${name}\"` };\n\t\t}\n\t\tawait this.ensureConnected(runtime);\n\t\treturn {\n\t\t\tname,\n\t\t\tstate: runtime.state,\n\t\t\ttoolCount: runtime.tools.length,\n\t\t\terror: runtime.error,\n\t\t\tserverVersion: runtime.client?.serverInfo.version,\n\t\t\t...(runtime.quarantinedTools.length > 0 ? { quarantinedTools: runtime.quarantinedTools } : {}),\n\t\t};\n\t}\n\n\t/**\n\t * Terminate every connected or connecting server. Idempotent.\n\t *\n\t * Bumping the generation invalidates any in-flight connect before its late\n\t * completion can publish tools or flip the state back to `ready`, and the\n\t * attempt's not-yet-published client is closed alongside the live one so no\n\t * spawned process outlives the close.\n\t */\n\tclose(): void {\n\t\tfor (const runtime of this.runtimes.values()) {\n\t\t\truntime.generation += 1;\n\t\t\truntime.client?.close();\n\t\t\truntime.client = undefined;\n\t\t\truntime.pendingClient?.close();\n\t\t\truntime.pendingClient = undefined;\n\t\t\truntime.tools = [];\n\t\t\truntime.quarantinedTools = [];\n\t\t\tif (runtime.state === \"ready\" || runtime.state === \"connecting\") runtime.state = \"idle\";\n\t\t}\n\t}\n\n\t/**\n\t * Verify that `ready` servers are actually alive with a protocol ping. A\n\t * failed ping closes the client and marks the server `failed`, so a\n\t * silently killed process stops masquerading as connected. Servers already\n\t * `failed` are re-attempted only when `reconnectFailed` is set — callers\n\t * should gate that behind a slow cadence so a permanently broken server is\n\t * not respawned every probe. Idle servers stay idle: connection is lazy by\n\t * design, and a status probe must not spawn processes.\n\t */\n\tasync checkHealth(options?: { pingTimeoutMs?: number; reconnectFailed?: boolean }): Promise<McpServerStatus[]> {\n\t\tconst work: Promise<void>[] = [];\n\t\tfor (const runtime of this.runtimes.values()) {\n\t\t\tif (runtime.state === \"ready\" && runtime.client) {\n\t\t\t\twork.push(this.pingRuntime(runtime, options?.pingTimeoutMs));\n\t\t\t} else if (runtime.state === \"failed\" && options?.reconnectFailed && !runtime.config.disabled) {\n\t\t\t\truntime.state = \"idle\";\n\t\t\t\truntime.error = undefined;\n\t\t\t\twork.push(this.ensureConnected(runtime));\n\t\t\t}\n\t\t}\n\t\tawait Promise.all(work);\n\t\treturn this.status();\n\t}\n\n\tprivate async pingRuntime(runtime: ServerRuntime, timeoutMs?: number): Promise<void> {\n\t\t// Pin the probed client: a close or reconnect during the ping must not let\n\t\t// this failure path dispose the replacement client.\n\t\tconst client = runtime.client;\n\t\tif (!client) return;\n\t\ttry {\n\t\t\tawait client.ping(timeoutMs);\n\t\t} catch (error) {\n\t\t\tif (runtime.client !== client) return; // A newer owner already handled it.\n\t\t\t// Isolation point: only this server transitions; the rest stay ready.\n\t\t\truntime.client = undefined;\n\t\t\tclient.close();\n\t\t\truntime.tools = [];\n\t\t\truntime.quarantinedTools = [];\n\t\t\truntime.state = \"failed\";\n\t\t\truntime.error = `health check failed: ${error instanceof Error ? error.message : String(error)}`;\n\t\t}\n\t}\n\n\tprivate ensureConnected(runtime: ServerRuntime): Promise<void> {\n\t\tif (runtime.config.disabled) {\n\t\t\truntime.state = \"failed\";\n\t\t\truntime.error = \"disabled by configuration\";\n\t\t\treturn Promise.resolve();\n\t\t}\n\t\tif (runtime.state === \"ready\" || runtime.state === \"failed\") return Promise.resolve();\n\t\tif (runtime.connecting) return runtime.connecting;\n\n\t\truntime.state = \"connecting\";\n\t\tconst generation = runtime.generation;\n\t\tconst attempt = this.connectRuntime(runtime, generation).finally(() => {\n\t\t\t// Only this attempt may clear its own in-flight markers; a newer\n\t\t\t// attempt's `connecting`/`pendingClient` belong to the newer generation.\n\t\t\tif (runtime.connecting === attempt) runtime.connecting = undefined;\n\t\t\tif (runtime.generation === generation) runtime.pendingClient = undefined;\n\t\t});\n\t\truntime.connecting = attempt;\n\t\treturn attempt;\n\t}\n\n\tprivate async connectRuntime(runtime: ServerRuntime, generation: number): Promise<void> {\n\t\tconst clientOptions: McpClientOptions = {\n\t\t\tname: runtime.config.name,\n\t\t\tclientInfo: this.options.clientInfo,\n\t\t\trequestTimeoutMs: runtime.config.requestTimeoutMs,\n\t\t\thandshakeTimeoutMs: runtime.config.handshakeTimeoutMs,\n\t\t\ttransport: {\n\t\t\t\tcommand: runtime.config.command,\n\t\t\t\targs: runtime.config.args,\n\t\t\t\tenv: runtime.config.env,\n\t\t\t\tcwd: runtime.config.cwd ?? this.options.cwd,\n\t\t\t},\n\t\t};\n\t\tconst client = this.options.createClient?.(clientOptions) ?? new McpClient(clientOptions);\n\t\t// Register before the first await so close() can dispose a client whose\n\t\t// connect() promise has not resolved yet.\n\t\truntime.pendingClient = client;\n\t\tconst isCurrent = () => runtime.generation === generation && runtime.pendingClient === client;\n\t\ttry {\n\t\t\tawait client.connect();\n\t\t\tif (!isCurrent()) {\n\t\t\t\t// close() or a newer attempt superseded this one mid-handshake.\n\t\t\t\tclient.close();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst schemas = await client.listTools();\n\t\t\tif (!isCurrent()) {\n\t\t\t\tclient.close();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\truntime.client = client;\n\t\t\truntime.pendingClient = undefined;\n\t\t\t// 서술자 주입 스크리닝: MCP 도구 설명은 신뢰할 수 없는 제3자 입력이다.\n\t\t\t// admitPublicMcpServer와 동일 임계값(0.7)으로 차단 — 수입 시점에서 fail-closed.\n\t\t\tconst definitions = schemas.map((schema) =>\n\t\t\t\tcreateMcpToolDefinition(runtime.config.name, client, schema, {\n\t\t\t\t\tcallTimeoutMs: this.options.callTimeoutMs,\n\t\t\t\t}),\n\t\t\t);\n\t\t\tconst admitted: typeof definitions = [];\n\t\t\tconst quarantined: string[] = [];\n\t\t\tfor (const definition of definitions) {\n\t\t\t\tif (\n\t\t\t\t\tdetectMcpDescriptorPromptInjection(definition.description ?? \"\").patternSignal >\n\t\t\t\t\tMCP_QUARANTINE_PATTERN_SIGNAL_THRESHOLD\n\t\t\t\t) {\n\t\t\t\t\tquarantined.push(definition.name);\n\t\t\t\t} else {\n\t\t\t\t\tadmitted.push(definition);\n\t\t\t\t}\n\t\t\t}\n\t\t\truntime.tools = admitted;\n\t\t\truntime.quarantinedTools = quarantined;\n\t\t\truntime.state = \"ready\";\n\t\t\truntime.error = undefined;\n\t\t} catch (error) {\n\t\t\tclient.close();\n\t\t\tif (!isCurrent()) return; // The current generation owns the state now.\n\t\t\t// Isolation point: this server is out, every other server is unaffected.\n\t\t\truntime.client = undefined;\n\t\t\truntime.tools = [];\n\t\t\truntime.state = \"failed\";\n\t\t\truntime.error = error instanceof Error ? error.message : String(error);\n\t\t}\n\t}\n}\n"]}
|