mixdog 1.1.2 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/package.json +2 -1
- package/scripts/lib/run-node-tests.mjs +9 -0
- package/scripts/prune-desktop-runtime.mjs +3 -0
- package/scripts/test-direct.mjs +4 -2
- package/src/defaults/shipped-history.json +10 -0
- package/src/defaults/skills/pptx/SKILL.md +6 -6
- package/src/defaults/skills/pptx/references/review.md +6 -4
- package/src/defaults/skills/pptx/scripts/source-extract.mjs +1 -1
- package/src/defaults/skills/setup/references/actions.md +18 -5
- package/src/defaults/skills/visualize/SKILL.md +160 -0
- package/src/defaults/skills/visualize/references/craft.md +88 -0
- package/src/headless-command.mjs +56 -3
- package/src/headless-exec.mjs +17 -1
- package/src/help.mjs +2 -0
- package/src/rules/shared/60-verification.md +3 -1
- package/src/rules/shared/70-delivery.md +4 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +1 -0
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +1 -0
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -0
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +26 -5
- package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
- package/src/runtime/agent/orchestrator/config-providers-merge.mjs +17 -11
- package/src/runtime/agent/orchestrator/config-storage.mjs +1 -0
- package/src/runtime/agent/orchestrator/config.mjs +19 -5
- package/src/runtime/agent/orchestrator/context/collect.mjs +2 -1
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +11 -5
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +74 -21
- package/src/runtime/agent/orchestrator/mcp/client-config.mjs +133 -15
- package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +55 -8
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +86 -8
- package/src/runtime/agent/orchestrator/mcp/client.mjs +474 -164
- package/src/runtime/agent/orchestrator/mcp/lazy-output-validator.mjs +25 -0
- package/src/runtime/agent/orchestrator/mcp/plugin-placeholders.mjs +54 -0
- package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +108 -17
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -12
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +337 -0
- package/src/runtime/agent/orchestrator/providers/command-code-oauth-tokens.mjs +159 -0
- package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +7 -4
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +21 -6
- package/src/runtime/agent/orchestrator/providers/effort-rejection.mjs +244 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +41 -28
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +8 -40
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +51 -12
- package/src/runtime/agent/orchestrator/providers/model-cache.mjs +37 -2
- package/src/runtime/agent/orchestrator/providers/model-proxy-bridge.mjs +1080 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +82 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +556 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +251 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +69 -102
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +15 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +6 -5
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +15 -4
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +4 -4
- package/src/runtime/agent/orchestrator/providers/registry.mjs +47 -7
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +10 -0
- package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +36 -3
- package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +15 -11
- package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +15 -55
- package/src/runtime/agent/orchestrator/runtime-core/goal-control.mjs +15 -2
- package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +16 -1
- package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +278 -84
- package/src/runtime/agent/orchestrator/runtime-core/skill-tool-loading.mjs +4 -1
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-schema.mjs +21 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +54 -3
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +10 -3
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +9 -5
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +18 -8
- package/src/runtime/agent/orchestrator/session/catalog-db/index.mjs +284 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +268 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/sqlite-gate.mjs +27 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/worker.mjs +628 -0
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +24 -13
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +13 -5
- package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +362 -0
- package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +253 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +184 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +136 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +128 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +127 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/explicit-skills.mjs +83 -5
- package/src/runtime/agent/orchestrator/session/imported-message.mjs +11 -0
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/diagnostic.mjs +1 -7
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/refusal.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +7 -4
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +21 -10
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +12 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +22 -12
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +9 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +37 -16
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +17 -4
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +90 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +22 -12
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +32 -3
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +27 -0
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +0 -1
- package/src/runtime/agent/orchestrator/session/message-ids.mjs +123 -0
- package/src/runtime/agent/orchestrator/session/message-ids.test-support.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +33 -22
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-finish.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +11 -0
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +817 -0
- package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +36 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +23 -12
- package/src/runtime/agent/orchestrator/session/store-summary-reader-catalog-fixture.mjs +158 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +202 -374
- package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +20 -3
- package/src/runtime/agent/orchestrator/session/synthetic-user-envelope.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +48 -9
- package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +161 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -0
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +52 -2
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +32 -22
- package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +10 -3
- package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-cache-policy.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +203 -33
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-fast-paths.mjs +32 -35
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +8 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +11 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +9 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +83 -68
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +8 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +20 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +16 -7
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +137 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +11 -10
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +21 -11
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +13 -10
- package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +17 -12
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +56 -13
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +58 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/root-resolution.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +25 -1
- package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/patch/apply-patch/route.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/read-redirects.mjs +34 -30
- package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +78 -5
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -0
- package/src/runtime/attachments/store.d.mts +3 -0
- package/src/runtime/attachments/store.mjs +38 -0
- package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +1 -1
- package/src/runtime/channels/lib/webhook/session-dispatch.mjs +16 -4
- package/src/runtime/channels/lib/worker-main.mjs +17 -14
- package/src/runtime/computer-bridge/linux-readiness.mjs +0 -2
- package/src/runtime/effort-judge/judge-client.mjs +56 -18
- package/src/runtime/memory/lib/memory.mjs +1 -1
- package/src/runtime/memory/lib/session-ingest.mjs +2 -2
- package/src/runtime/shared/atomic-file.mjs +33 -6
- package/src/runtime/shared/best-effort-stderr.mjs +8 -0
- package/src/runtime/shared/code-graph-state.mjs +12 -0
- package/src/runtime/shared/defaults-separation.mjs +2 -0
- package/src/runtime/shared/extension-scopes.mjs +5 -3
- package/src/runtime/shared/file-lock.mjs +37 -11
- package/src/runtime/shared/git-runner-worker.mjs +3 -2
- package/src/runtime/shared/git-runner.mjs +4 -3
- package/src/runtime/shared/llm/anthropic-effort.mjs +14 -0
- package/src/runtime/shared/llm/default-context-window.mjs +6 -0
- package/src/runtime/shared/llm/model-catalog-projection.mjs +5 -4
- package/src/runtime/shared/llm/model-catalog.mjs +240 -130
- package/src/runtime/shared/llm/provider-catalog-cache.mjs +36 -28
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +162 -0
- package/src/runtime/shared/llm/provider-seed-rosters.mjs +130 -0
- package/src/runtime/shared/model-proxy-mode.d.mts +3 -0
- package/src/runtime/shared/model-proxy-mode.mjs +19 -0
- package/src/runtime/shared/plugin-manifest.mjs +18 -9
- package/src/runtime/shared/pristine-execution-contract.json +9 -2
- package/src/runtime/shared/pristine-execution.mjs +28 -4
- package/src/runtime/shared/provider-accounts.mjs +125 -21
- package/src/runtime/shared/provider-api-key.mjs +33 -1
- package/src/runtime/shared/schedule-model-ref.mjs +11 -2
- package/src/runtime/shared/session-usage-totals.mjs +19 -0
- package/src/runtime/shared/sharp-electron-linux.mjs +25 -0
- package/src/runtime/shared/shell-display.mjs +9 -3
- package/src/runtime/shared/singleton-owner.mjs +36 -6
- package/src/runtime/shared/skill-mentions.d.mts +10 -0
- package/src/runtime/shared/skill-mentions.mjs +133 -0
- package/src/runtime/shared/skill-script-runtime.mjs +264 -0
- package/src/runtime/shared/statusline/statusline-segments.mjs +68 -14
- package/src/runtime/shared/tool-primitives.mjs +35 -2
- package/src/runtime/shared/tool-surface.mjs +7 -0
- package/src/runtime/shared/tool-work-units.mjs +14 -0
- package/src/runtime/shared/transcript-metadata.mjs +17 -1
- package/src/runtime/shared/turn-changes.mjs +791 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +346 -12
- package/src/runtime/shared/turn-snapshot.mjs +690 -81
- package/src/runtime/shared/turn-worktree-snapshot.mjs +185 -15
- package/src/runtime/shared/worker-requests.mjs +15 -2
- package/src/runtime/shared/worktrees/cache-cleanups.mjs +52 -0
- package/src/runtime/shared/worktrees/contract.mjs +551 -0
- package/src/runtime/shared/worktrees/disk.mjs +57 -0
- package/src/runtime/shared/worktrees/git.mjs +247 -0
- package/src/runtime/shared/worktrees/include.mjs +480 -0
- package/src/runtime/shared/worktrees/lock.mjs +46 -0
- package/src/runtime/shared/worktrees/maintenance.mjs +96 -0
- package/src/runtime/shared/worktrees/manager.mjs +1215 -0
- package/src/runtime/shared/worktrees/merge.mjs +182 -0
- package/src/runtime/shared/worktrees/owner.mjs +46 -0
- package/src/runtime/shared/worktrees/paths.mjs +62 -0
- package/src/runtime/shared/worktrees/registry.mjs +110 -0
- package/src/runtime/shared/worktrees/settings.mjs +84 -0
- package/src/runtime/shared/worktrees/slug.mjs +62 -0
- package/src/runtime/shared/worktrees/test-support.mjs +96 -0
- package/src/runtime/visualize-bridge/client.mjs +181 -0
- package/src/runtime/visualize-bridge/tool-defs.mjs +97 -0
- package/src/session-runtime/automation-agents.mjs +8 -2
- package/src/session-runtime/boot/apis.mjs +90 -0
- package/src/session-runtime/boot/config.mjs +7 -2
- package/src/session-runtime/boot/facade.mjs +21 -0
- package/src/session-runtime/boot/infrastructure.mjs +30 -18
- package/src/session-runtime/boot/session.mjs +1 -0
- package/src/session-runtime/boot/tools.mjs +4 -0
- package/src/session-runtime/config-lifecycle.mjs +10 -1
- package/src/session-runtime/conversation-import/discovery.mjs +698 -0
- package/src/session-runtime/conversation-import/service.mjs +369 -0
- package/src/session-runtime/cwd-plugins/cwd-apply.mjs +21 -6
- package/src/session-runtime/cwd-plugins/plugin-status.mjs +47 -42
- package/src/session-runtime/global-extensions.mjs +2 -2
- package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +27 -2
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +32 -1
- package/src/session-runtime/internal-tool-executor.mjs +9 -1
- package/src/session-runtime/lifecycle/session-catalog.mjs +8 -1
- package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +5 -0
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +21 -3
- package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +16 -0
- package/src/session-runtime/mcp-glue/connect.mjs +68 -16
- package/src/session-runtime/mcp-glue/plugin-servers.mjs +499 -0
- package/src/session-runtime/mcp-glue/server-catalog.mjs +122 -33
- package/src/session-runtime/mcp-glue/server-input.mjs +29 -0
- package/src/session-runtime/mcp-glue.mjs +22 -2
- package/src/session-runtime/model-route/route-persist.mjs +3 -0
- package/src/session-runtime/model-route/set-route.mjs +15 -2
- package/src/session-runtime/new-session-config.mjs +3 -2
- package/src/session-runtime/notification-bus.mjs +18 -3
- package/src/session-runtime/provider-auth/accounts.mjs +33 -15
- package/src/session-runtime/provider-models/row-hydration.mjs +2 -0
- package/src/session-runtime/provider-models/web-search-catalog.mjs +5 -1
- package/src/session-runtime/provider-readiness.mjs +16 -10
- package/src/session-runtime/resource-extension-sync.mjs +45 -7
- package/src/session-runtime/resource-mcp-api.mjs +121 -84
- package/src/session-runtime/resource-plugins-api.mjs +29 -41
- package/src/session-runtime/resource-scope-decorate.mjs +6 -8
- package/src/session-runtime/resource-skills-api.mjs +23 -2
- package/src/session-runtime/route-resolution.mjs +10 -1
- package/src/session-runtime/route-state.mjs +16 -2
- package/src/session-runtime/runtime-core.mjs +5 -0
- package/src/session-runtime/runtime-facade.mjs +8 -0
- package/src/session-runtime/runtime-review-api.mjs +9 -0
- package/src/session-runtime/schedule-session-run.mjs +1 -1
- package/src/session-runtime/services/agent-tool/helpers.mjs +5 -0
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +3 -0
- package/src/session-runtime/services/agent-tool/job-views/job-lookup.mjs +1 -0
- package/src/session-runtime/services/agent-tool/job-views/spawn-meta.mjs +1 -0
- package/src/session-runtime/services/agent-tool/lead-worker-index.mjs +15 -0
- package/src/session-runtime/services/agent-tool/send-flow.mjs +1 -0
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +10 -2
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +19 -7
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +3 -0
- package/src/session-runtime/services/agent-tool/tag-registry.mjs +2 -1
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +5 -0
- package/src/session-runtime/services/agent-tool/worker-rows.mjs +1 -0
- package/src/session-runtime/services/agent-tool.mjs +1 -0
- package/src/session-runtime/services/channel-session-router.mjs +4 -1
- package/src/session-runtime/services/daemon-candidate.mjs +3 -2
- package/src/session-runtime/services/daemon-crash-capture/spawn-capture.mjs +55 -0
- package/src/session-runtime/services/hook-bus/command-handler.mjs +4 -1
- package/src/session-runtime/services/hook-bus/handlers.mjs +7 -3
- package/src/session-runtime/services/provider-admin.mjs +64 -18
- package/src/session-runtime/services/usage-dashboard-model.mjs +3 -0
- package/src/session-runtime/session-create/session-options.mjs +10 -1
- package/src/session-runtime/session-turn-api.mjs +1 -0
- package/src/session-runtime/settings-api.mjs +4 -2
- package/src/session-runtime/settings-builtin-tools-api.mjs +9 -2
- package/src/session-runtime/settings-worktree-api.mjs +17 -0
- package/src/session-runtime/setup-tool/executor.mjs +23 -5
- package/src/session-runtime/setup-tool/extended-actions.mjs +1 -0
- package/src/session-runtime/setup-tool/settings-contract.mjs +4 -1
- package/src/session-runtime/setup-tool/tool-defs.mjs +38 -4
- package/src/session-runtime/skills-api.mjs +7 -3
- package/src/session-runtime/turn/agent-ops.mjs +12 -1
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +14 -3
- package/src/session-runtime/turn/turn-run/snapshot-tracker.mjs +20 -15
- package/src/session-runtime/turn/turn-run/turn-open.mjs +4 -5
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +29 -1
- package/src/session-runtime/turn/turn-run/turn-settle.mjs +6 -1
- package/src/session-runtime/turn/turn-run.mjs +6 -7
- package/src/session-runtime/webhook-session-run.mjs +16 -4
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -3
- package/src/session-runtime/workspace/admission-gate.mjs +197 -0
- package/src/session-runtime/workspace/project-branches.mjs +136 -0
- package/src/session-runtime/workspace/transition-steps.mjs +364 -0
- package/src/session-runtime/workspace/transitions.mjs +651 -0
- package/src/session-runtime/workspace/workspace-state.mjs +112 -0
- package/src/skill-script-entry.mjs +36 -0
- package/src/standalone/channel-transport/http-routes.mjs +17 -0
- package/src/standalone/channel-transport.mjs +13 -1
- package/src/standalone/daemon-desktop-runtime.mjs +1 -0
- package/src/standalone/daemon-owner.mjs +372 -0
- package/src/standalone/daemon-stored-session-views.mjs +15 -4
- package/src/standalone/daemon.mjs +179 -8
- package/src/standalone/desktop-service-registry.mjs +20 -0
- package/src/standalone/local-session-runtime.mjs +11 -0
- package/src/standalone/session-client/attachment-calls.mjs +3 -1
- package/src/standalone/session-client/daemon-unavailable.mjs +62 -0
- package/src/standalone/session-client/lease-heartbeat.mjs +47 -0
- package/src/standalone/session-client.mjs +104 -13
- package/src/standalone/session-protocol.mjs +21 -0
- package/src/standalone/session-proxy.mjs +3 -0
- package/src/standalone/session-service/agent-tree/agent-child.mjs +4 -0
- package/src/standalone/session-service/agent-tree/agent-rehydrate.mjs +1 -0
- package/src/standalone/session-service/entries/lifecycle.mjs +6 -0
- package/src/standalone/session-service/projection/revisions.mjs +2 -0
- package/src/standalone/session-service/projection/transcript-window.mjs +149 -19
- package/src/standalone/session-service/projection/workspace.mjs +14 -0
- package/src/standalone/session-service/session-calls/session-action.mjs +17 -4
- package/src/standalone/session-transport/call-route.mjs +13 -0
- package/src/standalone/session-transport/client-registry/client-grace.mjs +45 -3
- package/src/standalone/session-transport/client-registry/registration-replays.mjs +1 -0
- package/src/standalone/session-transport/client-registry.mjs +20 -3
- package/src/standalone/session-transport/routes.mjs +79 -2
- package/src/standalone/session-transport/stack.mjs +2 -0
- package/src/standalone/session-transport.mjs +24 -1
- package/src/tui/App.jsx +9 -1
- package/src/tui/app/app-view/floating-panel.jsx +9 -1
- package/src/tui/app/app-view.jsx +6 -0
- package/src/tui/app/extension-pickers/plugins-pickers.mjs +35 -16
- package/src/tui/app/message-selector.mjs +2 -1
- package/src/tui/app/model-options.mjs +37 -5
- package/src/tui/app/model-picker/provider-models-list.mjs +2 -2
- package/src/tui/app/model-picker.mjs +4 -2
- package/src/tui/app/skill-mention-palette.mjs +88 -0
- package/src/tui/app/slash-palette.mjs +23 -7
- package/src/tui/app/transcript-row-estimate.mjs +11 -3
- package/src/tui/app/use-prompt-draft-flow.mjs +7 -2
- package/src/tui/app/use-prompt-state.mjs +5 -0
- package/src/tui/components/Message.jsx +22 -1
- package/src/tui/components/PromptInput.jsx +21 -3
- package/src/tui/components/SlashCommandPalette.jsx +12 -3
- package/src/tui/components/TranscriptItem.jsx +9 -1
- package/src/tui/components/prompt-input/key-handler.mjs +21 -8
- package/src/tui/components/user-message-skills.mjs +38 -0
- package/src/tui/dist/index.mjs +839 -207
- package/src/tui/index.jsx +10 -2
- package/src/tui/session/compaction-view.mjs +147 -0
- package/src/tui/session/context-state.mjs +6 -0
- package/src/tui/session/initial-state.mjs +3 -0
- package/src/tui/session/queue-helpers.mjs +7 -2
- package/src/tui/session/quota-resume.mjs +2 -1
- package/src/tui/session/session-api/commands.mjs +10 -2
- package/src/tui/session/session-api/controls.mjs +4 -0
- package/src/tui/session/session-api/extensions.mjs +4 -2
- package/src/tui/session/session-api/intake/rewind.mjs +2 -1
- package/src/tui/session/session-api/integrations.mjs +17 -0
- package/src/tui/session/session-api/lifecycle/dispose.mjs +6 -1
- package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -0
- package/src/tui/session/session-api/lifecycle/resume.mjs +3 -0
- package/src/tui/session/session-api/routes.mjs +10 -1
- package/src/tui/session/session-api/settings.mjs +9 -0
- package/src/tui/session/session-api/worktree.mjs +60 -0
- package/src/tui/session/session-api.mjs +2 -0
- package/src/tui/session/session-flow/auto-clear.mjs +31 -12
- package/src/tui/session/session-flow/drain.mjs +27 -3
- package/src/tui/session/session-flow/session-reset.mjs +4 -3
- package/src/tui/session/session-flow.mjs +2 -0
- package/src/tui/session/transcript-intake.mjs +14 -5
- package/src/tui/session/tui-steering-persist.mjs +25 -9
- package/src/tui/session/turn-stream.mjs +32 -4
- package/src/tui/session/turn-tool-cards.mjs +10 -2
- package/src/tui/session/turn.mjs +56 -10
- package/src/runtime/effort-judge/previous-human-request.mjs +0 -162
- package/src/runtime/effort-judge/verified-context-input.mjs +0 -10
|
@@ -22,9 +22,15 @@ import { createKeyedSingleflight } from './reconnect-singleflight.mjs';
|
|
|
22
22
|
import { createOwnerFairGate } from '../../../shared/owner-fair-gate.mjs';
|
|
23
23
|
import { currentToolExecutionOwner } from '../../../shared/tool-execution-owner.mjs';
|
|
24
24
|
import { positiveInt } from '../../../shared/numbers.mjs';
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
import {
|
|
26
|
+
providerSafeMcpToolName,
|
|
27
|
+
resolveMcpRequestTimeouts,
|
|
28
|
+
resolveMcpTransportKind,
|
|
29
|
+
scrubMcpConnectionMessage,
|
|
30
|
+
} from './client-config.mjs';
|
|
31
|
+
import { createLazyOutputValidator } from './lazy-output-validator.mjs';
|
|
32
|
+
import { appendMcpStderrTail, createMcpTransport } from './client-transport.mjs';
|
|
33
|
+
import { listAllMcpTools, runBoundedHandshake } from './client-handshake.mjs';
|
|
28
34
|
|
|
29
35
|
// Config interpretation (transport kind, URL policy, env expansion, headers,
|
|
30
36
|
// timeouts, log scrubbing) lives in client-config.mjs; re-exported so every
|
|
@@ -37,6 +43,9 @@ export {
|
|
|
37
43
|
resolveMcpHttpHeaders,
|
|
38
44
|
resolveMcpTransportKind,
|
|
39
45
|
resolveMcpStartupTimeoutMs,
|
|
46
|
+
resolveMcpCallTimeoutMs,
|
|
47
|
+
resolveMcpMaxTotalTimeoutMs,
|
|
48
|
+
resolveMcpRequestTimeouts,
|
|
40
49
|
} from './client-config.mjs';
|
|
41
50
|
const servers = new Map();
|
|
42
51
|
const reconnects = createKeyedSingleflight();
|
|
@@ -47,6 +56,8 @@ const MCP_TOOL_NAME_RE = /^mcp__(.+?)__(.+)$/;
|
|
|
47
56
|
const _knownMcpScopes = new Set([DEFAULT_MCP_SCOPE_ID]);
|
|
48
57
|
const _connectAbortGenerations = new Map();
|
|
49
58
|
const _pendingConnects = new Set();
|
|
59
|
+
// scopeId -> root path advertised by that scope's scope-following servers.
|
|
60
|
+
const _scopeRoots = new Map();
|
|
50
61
|
let mcpSdkPromise = null;
|
|
51
62
|
// Memo for mcpToolHasField(name, field) — keyed by `${scopeId}|${toolName}|${field}`.
|
|
52
63
|
// The lookup (regex parse + servers Map get + tools.find + schema property
|
|
@@ -55,8 +66,14 @@ let mcpSdkPromise = null;
|
|
|
55
66
|
// mutation point (connectServer / disconnectAll) so a stale positive or
|
|
56
67
|
// negative can never survive a tools-list change.
|
|
57
68
|
const _mcpToolFieldMemo = new Map();
|
|
69
|
+
let _mcpToolGeneration = 0;
|
|
58
70
|
function _invalidateMcpToolFieldMemo() {
|
|
59
71
|
_mcpToolFieldMemo.clear();
|
|
72
|
+
_mcpToolGeneration += 1;
|
|
73
|
+
}
|
|
74
|
+
/** Read-only counter bumped on every MCP tool registry mutation. */
|
|
75
|
+
export function getMcpToolGeneration() {
|
|
76
|
+
return _mcpToolGeneration;
|
|
60
77
|
}
|
|
61
78
|
function normalizeMcpScopeId(value) {
|
|
62
79
|
const raw = value && typeof value === 'object' ? value.scopeId : value;
|
|
@@ -95,12 +112,15 @@ async function loadMcpSdk() {
|
|
|
95
112
|
import('@modelcontextprotocol/sdk/client/sse.js'),
|
|
96
113
|
import('@modelcontextprotocol/sdk/client/websocket.js'),
|
|
97
114
|
import('@modelcontextprotocol/sdk/types.js'),
|
|
98
|
-
|
|
115
|
+
import('@modelcontextprotocol/sdk/validation/ajv'),
|
|
116
|
+
]).then(([clientMod, stdioMod, httpMod, sseMod, wsMod, typesMod, ajvMod]) => ({
|
|
99
117
|
Client: clientMod.Client,
|
|
118
|
+
AjvJsonSchemaValidator: ajvMod.AjvJsonSchemaValidator,
|
|
100
119
|
StdioClientTransport: stdioMod.StdioClientTransport,
|
|
101
120
|
StreamableHTTPClientTransport: httpMod.StreamableHTTPClientTransport,
|
|
102
121
|
SSEClientTransport: sseMod.SSEClientTransport,
|
|
103
122
|
WebSocketClientTransport: wsMod.WebSocketClientTransport,
|
|
123
|
+
JSONRPCMessageSchema: typesMod.JSONRPCMessageSchema,
|
|
104
124
|
ListRootsRequestSchema: typesMod.ListRootsRequestSchema,
|
|
105
125
|
ToolListChangedNotificationSchema: typesMod.ToolListChangedNotificationSchema,
|
|
106
126
|
}));
|
|
@@ -160,8 +180,8 @@ function mcpFeatureTools(serverName, capabilities, protocolTools) {
|
|
|
160
180
|
const tools = [];
|
|
161
181
|
const used = new Set(protocolTools.map((tool) => tool.name));
|
|
162
182
|
const add = (leaf, description, properties, required, operation) => {
|
|
163
|
-
let name =
|
|
164
|
-
|
|
183
|
+
let name = providerSafeMcpToolName(serverName, leaf);
|
|
184
|
+
for (let suffix = '_'; used.has(name); suffix += '_') name = providerSafeMcpToolName(serverName, leaf + suffix);
|
|
165
185
|
used.add(name);
|
|
166
186
|
tools.push({
|
|
167
187
|
name,
|
|
@@ -226,7 +246,16 @@ function mcpFeatureTools(serverName, capabilities, protocolTools) {
|
|
|
226
246
|
export function getMcpServerStatus(scopeId = DEFAULT_MCP_SCOPE_ID) {
|
|
227
247
|
return scopedServerEntries(scopeId).map(([, server]) => ({
|
|
228
248
|
name: server.name,
|
|
229
|
-
connected:
|
|
249
|
+
connected: server.liveness?.alive !== false,
|
|
250
|
+
...(server.liveness?.alive === false
|
|
251
|
+
? {
|
|
252
|
+
error: appendMcpStderrTail(
|
|
253
|
+
server.liveness.lastError || `MCP server "${server.name}" closed`,
|
|
254
|
+
server.stderrTail?.()
|
|
255
|
+
),
|
|
256
|
+
}
|
|
257
|
+
: {}),
|
|
258
|
+
warnings: Array.isArray(server.warnings) ? server.warnings : [],
|
|
230
259
|
toolCount: Array.isArray(server.tools) ? server.tools.length : 0,
|
|
231
260
|
tools: (server.tools || []).map((tool) => ({
|
|
232
261
|
name: tool.name,
|
|
@@ -296,28 +325,98 @@ export function getMcpServerInstructionsMap(scopeId = DEFAULT_MCP_SCOPE_ID) {
|
|
|
296
325
|
return out;
|
|
297
326
|
}
|
|
298
327
|
|
|
328
|
+
// Close `current` (when registered) and connect `name` afresh with `cfg`.
|
|
329
|
+
// Callers run it inside the per-server reconnect lane. `genAtStart` is the
|
|
330
|
+
// shutdown generation captured BEFORE the caller queued/closed anything: a
|
|
331
|
+
// disconnectAll() landing during the close (or the connect) bumps it, and the
|
|
332
|
+
// replacement must then never register (no resurrection after shutdown).
|
|
333
|
+
async function replaceServerConnection(scopeId, name, current, cfg, genAtStart) {
|
|
334
|
+
const registryKey = mcpServerRegistryKey(scopeId, name);
|
|
335
|
+
if (current) {
|
|
336
|
+
await _closeServer(current);
|
|
337
|
+
if (servers.get(registryKey) === current) servers.delete(registryKey);
|
|
338
|
+
_invalidateMcpToolFieldMemo();
|
|
339
|
+
}
|
|
340
|
+
if (genAtStart !== currentConnectAbortGeneration(scopeId)) {
|
|
341
|
+
throw new Error(`MCP server "${name}" connect aborted by shutdown`);
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
// connectServer re-checks the same generation after loading the SDK and
|
|
345
|
+
// again right before registering, so a shutdown mid-handshake also aborts.
|
|
346
|
+
await connectServer(name, cfg, scopeId, genAtStart);
|
|
347
|
+
} catch (reason) {
|
|
348
|
+
const rawMessage = reason instanceof Error ? reason.message : String(reason);
|
|
349
|
+
throw new Error(scrubMcpConnectionMessage(rawMessage, cfg));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Explicit single-server operations (connect/reconnect/disconnect) never join
|
|
354
|
+
// a failure-driven reconnect that is already in flight — its config may be
|
|
355
|
+
// stale — they queue behind it on the same lane instead, so at most one
|
|
356
|
+
// connect per server is ever running (no doubly registered transports).
|
|
357
|
+
async function runExclusiveServerOp(registryKey, task) {
|
|
358
|
+
for (let pending = reconnects.wait(registryKey); pending; pending = reconnects.wait(registryKey)) {
|
|
359
|
+
await pending.catch(() => {});
|
|
360
|
+
}
|
|
361
|
+
return reconnects.run(registryKey, task);
|
|
362
|
+
}
|
|
363
|
+
|
|
299
364
|
async function reconnectMcpServer(scopeId, serverName, failedServer) {
|
|
300
365
|
const registryKey = mcpServerRegistryKey(scopeId, serverName);
|
|
301
|
-
|
|
366
|
+
// The generation the failed server was registered under, NOT the current
|
|
367
|
+
// one: when disconnectAll() itself caused the failure it has already bumped
|
|
368
|
+
// the generation (and still holds the entry while awaiting the close), so a
|
|
369
|
+
// freshly captured value would bless a post-shutdown reconnect.
|
|
370
|
+
const genAtStart = failedServer.generation ?? currentConnectAbortGeneration(scopeId);
|
|
371
|
+
await reconnects.run(registryKey, async () => {
|
|
302
372
|
const current = servers.get(registryKey);
|
|
303
373
|
// A peer already completed the replacement while this failed call was
|
|
304
374
|
// unwinding. Reuse it immediately instead of closing a fresh transport.
|
|
305
|
-
if (current && current !== failedServer) return
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
await connectServer(serverName, failedServer.cfg, scopeId);
|
|
312
|
-
} catch (reason) {
|
|
313
|
-
const rawMessage = reason instanceof Error ? reason.message : String(reason);
|
|
314
|
-
throw new Error(scrubMcpConnectionMessage(rawMessage, failedServer.cfg));
|
|
375
|
+
if (current && current !== failedServer) return;
|
|
376
|
+
// Unregistered meanwhile (explicit disconnect / shutdown), shut down since
|
|
377
|
+
// it was registered, or being deliberately closed right now: a failed call
|
|
378
|
+
// must not resurrect a server someone deliberately removed.
|
|
379
|
+
if (!current || genAtStart !== currentConnectAbortGeneration(scopeId) || current.liveness?.closing) {
|
|
380
|
+
throw new Error(`MCP server "${serverName}" was disconnected`);
|
|
315
381
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
382
|
+
await replaceServerConnection(scopeId, serverName, current, failedServer.cfg, genAtStart);
|
|
383
|
+
});
|
|
384
|
+
const replacement = servers.get(registryKey);
|
|
385
|
+
if (!replacement) {
|
|
386
|
+
throw new Error(`reconnect succeeded but server "${serverName}" entry is missing from registry`);
|
|
387
|
+
}
|
|
388
|
+
return replacement;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Connect, or reconnect, ONE named MCP server in `scopeId` without touching
|
|
393
|
+
* any other server. A registered instance is closed first; `cfg` defaults to
|
|
394
|
+
* the registered config. `cfg.enabled === false` only disconnects. Resolves
|
|
395
|
+
* true when the server is connected afterwards, false when it was disabled;
|
|
396
|
+
* rejects (credential-scrubbed message) when the connect fails, leaving the
|
|
397
|
+
* server unregistered.
|
|
398
|
+
*/
|
|
399
|
+
export async function reconnectSingleMcpServer(scopeId, name, cfg = undefined) {
|
|
400
|
+
const normalized = normalizeMcpScopeId(scopeId);
|
|
401
|
+
_knownMcpScopes.add(normalized);
|
|
402
|
+
const registryKey = mcpServerRegistryKey(normalized, name);
|
|
403
|
+
// Captured before queuing: a shutdown while this op waits or closes wins.
|
|
404
|
+
const genAtStart = currentConnectAbortGeneration(normalized);
|
|
405
|
+
return runExclusiveServerOp(registryKey, async () => {
|
|
406
|
+
const current = servers.get(registryKey);
|
|
407
|
+
const nextCfg = cfg ?? current?.cfg;
|
|
408
|
+
if (!nextCfg) throw new Error(`MCP server "${name}" has no config to connect with`);
|
|
409
|
+
if (nextCfg.enabled === false) {
|
|
410
|
+
if (current) {
|
|
411
|
+
await _closeServer(current);
|
|
412
|
+
if (servers.get(registryKey) === current) servers.delete(registryKey);
|
|
413
|
+
_invalidateMcpToolFieldMemo();
|
|
414
|
+
}
|
|
415
|
+
closeCallAdmission(normalized, name, `MCP ${name} disabled`);
|
|
416
|
+
return false;
|
|
319
417
|
}
|
|
320
|
-
|
|
418
|
+
await replaceServerConnection(normalized, name, current, nextCfg, genAtStart);
|
|
419
|
+
return true;
|
|
321
420
|
});
|
|
322
421
|
}
|
|
323
422
|
|
|
@@ -328,11 +427,12 @@ async function reconnectMcpServer(scopeId, serverName, failedServer) {
|
|
|
328
427
|
export async function executeMcpTool(name, args, options = {}) {
|
|
329
428
|
const match = name.match(MCP_TOOL_NAME_RE);
|
|
330
429
|
if (!match) throw new Error(`Not an MCP tool name: ${name}`);
|
|
331
|
-
const [, serverName, toolName] = match;
|
|
332
430
|
const scopeId = normalizeMcpScopeId(options);
|
|
333
|
-
const
|
|
334
|
-
if (!
|
|
335
|
-
const
|
|
431
|
+
const target = resolveMcpToolTarget(scopeId, name);
|
|
432
|
+
if (!target) throw new Error(`MCP server "${match[1]}" not connected`);
|
|
433
|
+
const { server, definition } = target;
|
|
434
|
+
const serverName = server.name;
|
|
435
|
+
const toolName = definition?.mcpToolName || match[2];
|
|
336
436
|
const callSignal = options?.signal || null;
|
|
337
437
|
const dispatch = (target) =>
|
|
338
438
|
definition?.mcpOperation
|
|
@@ -366,6 +466,16 @@ export async function executeMcpTool(name, args, options = {}) {
|
|
|
366
466
|
mcpLog(`[mcp-client] Tool call timed out; skipping reconnect retry for "${serverName}/${toolName}".\n`);
|
|
367
467
|
throw firstErr;
|
|
368
468
|
}
|
|
469
|
+
const serverDead = server.liveness?.alive === false;
|
|
470
|
+
if (!serverDead && isMcpServerResponseError(firstErr)) {
|
|
471
|
+
// The server answered (JSON-RPC error): it is alive — nothing to
|
|
472
|
+
// reconnect, and the call already ran its course.
|
|
473
|
+
throw firstErr;
|
|
474
|
+
}
|
|
475
|
+
// Only a failure that proves the request never left this process
|
|
476
|
+
// may be replayed; anything else may already have run server-side.
|
|
477
|
+
const undelivered = isMcpUndeliveredError(firstErr);
|
|
478
|
+
const deadTail = serverDead ? server.stderrTail?.() : '';
|
|
369
479
|
mcpLog(`[mcp-client] Tool call failed, attempting shared reconnect...\n`);
|
|
370
480
|
let retryServer;
|
|
371
481
|
try {
|
|
@@ -382,7 +492,17 @@ export async function executeMcpTool(name, args, options = {}) {
|
|
|
382
492
|
} catch (reconnectErr) {
|
|
383
493
|
if (isMcpCallAbortError(reconnectErr) || callSignal?.aborted) throw reconnectErr;
|
|
384
494
|
const reconnectMsg = reconnectErr instanceof Error ? reconnectErr.message : String(reconnectErr);
|
|
385
|
-
throw new Error(
|
|
495
|
+
throw new Error(
|
|
496
|
+
appendMcpStderrTail(`Tool call failed: ${firstMsg}; reconnect also failed: ${reconnectMsg}`, deadTail)
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
if (!undelivered) {
|
|
500
|
+
throw new Error(
|
|
501
|
+
appendMcpStderrTail(
|
|
502
|
+
`Tool call failed: ${firstMsg}; MCP server "${serverName}" was reconnected for the next call, but this call was not replayed because the request may already have reached the server`,
|
|
503
|
+
deadTail
|
|
504
|
+
)
|
|
505
|
+
);
|
|
386
506
|
}
|
|
387
507
|
// A reconnect that outlived the caller's abort must not dispatch the
|
|
388
508
|
// retry: that would run the side effect after cancellation.
|
|
@@ -410,71 +530,120 @@ export async function executeMcpTool(name, args, options = {}) {
|
|
|
410
530
|
);
|
|
411
531
|
}
|
|
412
532
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
request = server.client.listResourceTemplates(cursor, requestOptions);
|
|
421
|
-
} else if (operation === 'read-resource') {
|
|
422
|
-
request = server.client.readResource({ uri: String(args?.uri || '') }, requestOptions);
|
|
423
|
-
} else if (operation === 'list-prompts') {
|
|
424
|
-
request = server.client.listPrompts(cursor, requestOptions);
|
|
425
|
-
} else if (operation === 'get-prompt') {
|
|
426
|
-
request = server.client.getPrompt(
|
|
533
|
+
const MCP_FEATURE_REQUESTS = {
|
|
534
|
+
'list-resources': (client, args, options) => client.listResources(mcpCursorParams(args), options),
|
|
535
|
+
'list-resource-templates': (client, args, options) => client.listResourceTemplates(mcpCursorParams(args), options),
|
|
536
|
+
'read-resource': (client, args, options) => client.readResource({ uri: String(args?.uri || '') }, options),
|
|
537
|
+
'list-prompts': (client, args, options) => client.listPrompts(mcpCursorParams(args), options),
|
|
538
|
+
'get-prompt': (client, args, options) =>
|
|
539
|
+
client.getPrompt(
|
|
427
540
|
{
|
|
428
541
|
name: String(args?.name || ''),
|
|
429
542
|
arguments:
|
|
430
543
|
args?.arguments && typeof args.arguments === 'object' && !Array.isArray(args.arguments) ? args.arguments : {},
|
|
431
544
|
},
|
|
432
|
-
|
|
433
|
-
)
|
|
434
|
-
|
|
435
|
-
|
|
545
|
+
options
|
|
546
|
+
),
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
function mcpCursorParams(args) {
|
|
550
|
+
return typeof args?.cursor === 'string' && args.cursor ? { cursor: args.cursor } : {};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async function _callMcpFeatureWithTimeout(server, operation, args, signal = null) {
|
|
554
|
+
const issue = MCP_FEATURE_REQUESTS[operation];
|
|
555
|
+
if (!issue) throw new Error(`Unsupported MCP feature operation: ${operation}`);
|
|
556
|
+
const result = await runMcpRequest(server, signal, (options) => issue(server.client, args, options), {
|
|
557
|
+
abortMessage: `MCP feature call aborted (server="${server?.name}", operation="${operation}")`,
|
|
558
|
+
timeoutMessage: (ms) =>
|
|
559
|
+
`MCP feature call timed out after ${ms}ms without progress (server="${server.name}", operation="${operation}")`,
|
|
560
|
+
errorFields: { serverName: server.name, operation },
|
|
561
|
+
});
|
|
562
|
+
return {
|
|
563
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
564
|
+
isError: false,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Our per-call deadline. It is reset by every progress notification, but never
|
|
569
|
+
// beyond maxTotalTimeoutMs from the start. On expiry the call rejects with
|
|
570
|
+
// EMCPTOOLTIMEOUT and `cancelRequest` cancels ONLY this request (the SDK then
|
|
571
|
+
// sends notifications/cancelled); the server and its other in-flight calls
|
|
572
|
+
// are untouched. The caller clears the timer once its own race settles.
|
|
573
|
+
function mcpCallTimeout({ callTimeoutMs, maxTotalTimeoutMs }, message, errorFields, cancelRequest) {
|
|
574
|
+
const startedAt = Date.now();
|
|
575
|
+
let timer = null;
|
|
576
|
+
let rejectTimeout;
|
|
577
|
+
const promise = new Promise((_, reject) => {
|
|
578
|
+
rejectTimeout = reject;
|
|
579
|
+
});
|
|
580
|
+
const fire = () => {
|
|
581
|
+
timer = null;
|
|
582
|
+
const error = Object.assign(new Error(message), { code: 'EMCPTOOLTIMEOUT', ...errorFields });
|
|
583
|
+
rejectTimeout(error);
|
|
584
|
+
cancelRequest(error);
|
|
585
|
+
};
|
|
586
|
+
const arm = (ms) => {
|
|
587
|
+
if (timer) clearTimeout(timer);
|
|
588
|
+
timer = setTimeout(fire, ms);
|
|
589
|
+
if (timer.unref) timer.unref();
|
|
590
|
+
};
|
|
591
|
+
arm(callTimeoutMs);
|
|
592
|
+
return {
|
|
593
|
+
promise,
|
|
594
|
+
clear: () => {
|
|
595
|
+
if (timer) clearTimeout(timer);
|
|
596
|
+
timer = null;
|
|
597
|
+
},
|
|
598
|
+
progress: () => {
|
|
599
|
+
if (!timer) return;
|
|
600
|
+
const remainingTotal = maxTotalTimeoutMs ? maxTotalTimeoutMs - (Date.now() - startedAt) : Infinity;
|
|
601
|
+
if (remainingTotal > 0) arm(Math.min(callTimeoutMs, remainingTotal));
|
|
602
|
+
},
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Issue one SDK request under the per-call limits. The request gets its OWN
|
|
608
|
+
* abort signal (linked to the caller's), our timeout as the SDK `timeout`
|
|
609
|
+
* (or the largest timer when disabled, so the SDK's 60s default never
|
|
610
|
+
* applies), and progress handling: each progress report resets both the SDK
|
|
611
|
+
* timeout and ours, capped by `maxTotalTimeout`.
|
|
612
|
+
*/
|
|
613
|
+
async function runMcpRequest(server, signal, issue, { abortMessage, timeoutMessage, errorFields }) {
|
|
614
|
+
const timeouts = resolveMcpRequestTimeouts(server?.cfg);
|
|
615
|
+
const controller = new AbortController();
|
|
616
|
+
const forwardAbort = () => controller.abort(signal.reason);
|
|
617
|
+
if (signal) {
|
|
618
|
+
allowConcurrentAbortListeners(signal);
|
|
619
|
+
if (signal.aborted) forwardAbort();
|
|
620
|
+
else signal.addEventListener('abort', forwardAbort, { once: true });
|
|
436
621
|
}
|
|
437
|
-
const timeoutMs = resolveMcpCallTimeoutMs(server?.cfg);
|
|
438
|
-
const abortMessage = `MCP feature call aborted (server="${server?.name}", operation="${operation}")`;
|
|
439
622
|
const timeout =
|
|
440
|
-
|
|
623
|
+
timeouts.callTimeoutMs > 0
|
|
441
624
|
? mcpCallTimeout(
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
625
|
+
timeouts,
|
|
626
|
+
timeoutMessage(timeouts.callTimeoutMs),
|
|
627
|
+
{ ...errorFields, timeoutMs: timeouts.callTimeoutMs },
|
|
628
|
+
(error) => controller.abort(error)
|
|
445
629
|
)
|
|
446
630
|
: null;
|
|
631
|
+
const requestOptions = {
|
|
632
|
+
signal: controller.signal,
|
|
633
|
+
timeout: timeouts.sdkTimeoutMs,
|
|
634
|
+
resetTimeoutOnProgress: true,
|
|
635
|
+
onprogress: () => timeout?.progress(),
|
|
636
|
+
...(timeouts.maxTotalTimeoutMs ? { maxTotalTimeout: timeouts.maxTotalTimeoutMs } : {}),
|
|
637
|
+
};
|
|
447
638
|
try {
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
signal,
|
|
451
|
-
abortMessage
|
|
452
|
-
);
|
|
453
|
-
return {
|
|
454
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
455
|
-
isError: false,
|
|
456
|
-
};
|
|
639
|
+
const request = issue(requestOptions);
|
|
640
|
+
return await raceMcpAbort(timeout ? Promise.race([request, timeout.promise]) : request, signal, abortMessage);
|
|
457
641
|
} finally {
|
|
458
642
|
timeout?.clear();
|
|
643
|
+
signal?.removeEventListener?.('abort', forwardAbort);
|
|
459
644
|
}
|
|
460
645
|
}
|
|
461
646
|
|
|
462
|
-
// Server-side per-call deadline. On expiry the server goes through the full
|
|
463
|
-
// tree-shutdown path so a timed-out stdio server never orphans grandchildren
|
|
464
|
-
// (fire-and-forget), and the call rejects with EMCPTOOLTIMEOUT. The caller
|
|
465
|
-
// clears the timer once its own (abort-aware) race settles.
|
|
466
|
-
function mcpCallTimeout(server, timeoutMs, message, errorFields = {}) {
|
|
467
|
-
let timer;
|
|
468
|
-
const promise = new Promise((_, reject) => {
|
|
469
|
-
timer = setTimeout(() => {
|
|
470
|
-
_closeServer(server).catch(() => {});
|
|
471
|
-
reject(Object.assign(new Error(message), { code: 'EMCPTOOLTIMEOUT', ...errorFields }));
|
|
472
|
-
}, timeoutMs);
|
|
473
|
-
if (timer.unref) timer.unref();
|
|
474
|
-
});
|
|
475
|
-
return { promise, clear: () => clearTimeout(timer) };
|
|
476
|
-
}
|
|
477
|
-
|
|
478
647
|
function mcpResourceFilename(uri) {
|
|
479
648
|
const tail = String(uri || '')
|
|
480
649
|
.split(/[?#]/)[0]
|
|
@@ -506,14 +675,21 @@ function mcpCanonicalBase64(data, maxBytes) {
|
|
|
506
675
|
const mcpMiB = (bytes) => `${bytes / 1024 / 1024} MiB`;
|
|
507
676
|
|
|
508
677
|
function mcpImagePart(data, rawMime) {
|
|
509
|
-
const mimeType =
|
|
678
|
+
const mimeType =
|
|
679
|
+
String(rawMime || 'image/png')
|
|
680
|
+
.split(';')[0]
|
|
681
|
+
.trim()
|
|
682
|
+
.toLowerCase() || 'image/png';
|
|
510
683
|
if (!SUPPORTED_IMAGE_MIME_TYPES.has(mimeType)) {
|
|
511
684
|
return { type: 'text', text: `[MCP image omitted: ${mimeType} is not a supported image type]` };
|
|
512
685
|
}
|
|
513
686
|
const canon = mcpCanonicalBase64(data, MAX_IMAGE_BYTES);
|
|
514
687
|
if (!canon) return { type: 'text', text: '[MCP image omitted: empty image data]' };
|
|
515
688
|
if (canon.tooLarge) {
|
|
516
|
-
return {
|
|
689
|
+
return {
|
|
690
|
+
type: 'text',
|
|
691
|
+
text: `[MCP image omitted: ${mimeType} image exceeds the ${mcpMiB(MAX_IMAGE_BYTES)} limit]`,
|
|
692
|
+
};
|
|
517
693
|
}
|
|
518
694
|
return { type: 'image', data: canon.data, mimeType };
|
|
519
695
|
}
|
|
@@ -584,10 +760,19 @@ function mcpAudioPart(data, rawMime) {
|
|
|
584
760
|
const canon = typeof data === 'string' && data ? mcpCanonicalBase64(data, MAX_ATTACHMENT_BLOB_BYTES) : null;
|
|
585
761
|
if (!canon) return { type: 'text', text: `[MCP audio content: ${mimeType}]` };
|
|
586
762
|
if (canon.tooLarge) {
|
|
587
|
-
return {
|
|
763
|
+
return {
|
|
764
|
+
type: 'text',
|
|
765
|
+
text: `[MCP audio omitted: ${mimeType} exceeds the ${mcpMiB(MAX_ATTACHMENT_BLOB_BYTES)} limit]`,
|
|
766
|
+
};
|
|
588
767
|
}
|
|
589
768
|
const subtype = mimeType.split(';')[0].split('/')[1] || '';
|
|
590
|
-
const path = spillMcpBinary(
|
|
769
|
+
const path = spillMcpBinary(
|
|
770
|
+
canon.buffer,
|
|
771
|
+
subtype
|
|
772
|
+
.replace(/[^a-z0-9]/gi, '')
|
|
773
|
+
.slice(0, 8)
|
|
774
|
+
.toLowerCase() || 'bin'
|
|
775
|
+
);
|
|
591
776
|
return {
|
|
592
777
|
type: 'text',
|
|
593
778
|
text: path
|
|
@@ -648,8 +833,32 @@ export async function normalizeMcpToolResult(result) {
|
|
|
648
833
|
return parts ? { content: parts } : text;
|
|
649
834
|
}
|
|
650
835
|
|
|
836
|
+
// JSON-RPC codes the SDK uses for client-side failures (McpError).
|
|
837
|
+
const MCP_CONNECTION_CLOSED_CODE = -32000;
|
|
838
|
+
const MCP_REQUEST_TIMEOUT_CODE = -32001;
|
|
839
|
+
|
|
840
|
+
// Our own deadline, or the SDK's (McpError RequestTimeout, incl. the
|
|
841
|
+
// progress maxTotalTimeout).
|
|
651
842
|
function isMcpToolCallTimeoutError(err) {
|
|
652
|
-
return err?.code === 'EMCPTOOLTIMEOUT';
|
|
843
|
+
return err?.code === 'EMCPTOOLTIMEOUT' || err?.code === MCP_REQUEST_TIMEOUT_CODE;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// The server answered with a JSON-RPC error: proof it is alive.
|
|
847
|
+
function isMcpServerResponseError(err) {
|
|
848
|
+
return (
|
|
849
|
+
typeof err?.code === 'number' && err.code !== MCP_CONNECTION_CLOSED_CODE && err.code !== MCP_REQUEST_TIMEOUT_CODE
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Connection-level failures that prove the request was never delivered: the
|
|
854
|
+
// SDK/transport had no live connection to send on, or the socket was refused
|
|
855
|
+
// before anything was written. ECONNRESET etc. are excluded — the request may
|
|
856
|
+
// have been read before the reset.
|
|
857
|
+
const MCP_UNDELIVERED_CODES = new Set(['ECONNREFUSED', 'ENOTFOUND', 'EHOSTUNREACH', 'ENETUNREACH']);
|
|
858
|
+
function isMcpUndeliveredError(err) {
|
|
859
|
+
if (!err) return false;
|
|
860
|
+
if (/^Not connected$/i.test(String(err.message || '').trim())) return true;
|
|
861
|
+
return MCP_UNDELIVERED_CODES.has(String(err.code || err.cause?.code || ''));
|
|
653
862
|
}
|
|
654
863
|
|
|
655
864
|
// Caller-driven cancellation (hook timeout, aborted turn, closed pane).
|
|
@@ -714,38 +923,22 @@ export function raceMcpAbort(promise, signal, message) {
|
|
|
714
923
|
}
|
|
715
924
|
|
|
716
925
|
async function _callToolWithTimeout(server, toolName, args, signal = null) {
|
|
717
|
-
|
|
718
|
-
//
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
if (!(timeoutMs > 0)) {
|
|
724
|
-
// Server timeout disabled: the caller's signal is then the ONLY bound.
|
|
725
|
-
return raceMcpAbort(
|
|
726
|
-
server.client.callTool({ name: toolName, arguments: args }, undefined, requestOptions),
|
|
727
|
-
signal,
|
|
728
|
-
abortMessage
|
|
729
|
-
);
|
|
730
|
-
}
|
|
731
|
-
const timeout = mcpCallTimeout(
|
|
926
|
+
// The request's signal goes INTO the SDK: an aborted caller (or our own
|
|
927
|
+
// timeout) cancels the live JSON-RPC request (notifications/cancelled)
|
|
928
|
+
// instead of merely walking away from it. The abort race additionally frees
|
|
929
|
+
// the admission slot at once. With the timeout disabled the caller's signal
|
|
930
|
+
// is the ONLY bound.
|
|
931
|
+
return runMcpRequest(
|
|
732
932
|
server,
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
{
|
|
933
|
+
signal,
|
|
934
|
+
(options) => server.client.callTool({ name: toolName, arguments: args }, undefined, options),
|
|
935
|
+
{
|
|
936
|
+
abortMessage: `MCP tool call aborted (server="${server?.name}", tool="${toolName}")`,
|
|
937
|
+
timeoutMessage: (ms) =>
|
|
938
|
+
`MCP tool call timed out after ${ms}ms without progress (server="${server.name}", tool="${toolName}")`,
|
|
939
|
+
errorFields: { serverName: server.name, toolName },
|
|
940
|
+
}
|
|
736
941
|
);
|
|
737
|
-
try {
|
|
738
|
-
return await raceMcpAbort(
|
|
739
|
-
Promise.race([
|
|
740
|
-
server.client.callTool({ name: toolName, arguments: args }, undefined, requestOptions),
|
|
741
|
-
timeout.promise,
|
|
742
|
-
]),
|
|
743
|
-
signal,
|
|
744
|
-
abortMessage
|
|
745
|
-
);
|
|
746
|
-
} finally {
|
|
747
|
-
timeout.clear();
|
|
748
|
-
}
|
|
749
942
|
}
|
|
750
943
|
|
|
751
944
|
function countTextLines(text) {
|
|
@@ -848,12 +1041,7 @@ export function isMcpTool(name) {
|
|
|
848
1041
|
}
|
|
849
1042
|
/** True when the prefixed name exists on a connected MCP server. */
|
|
850
1043
|
export function isRegisteredMcpTool(name, scopeId = DEFAULT_MCP_SCOPE_ID) {
|
|
851
|
-
|
|
852
|
-
if (!match) return false;
|
|
853
|
-
const [, serverName] = match;
|
|
854
|
-
const server = scopedServer(scopeId, serverName);
|
|
855
|
-
if (!server || !Array.isArray(server.tools)) return false;
|
|
856
|
-
return server.tools.some((t) => t?.name === name);
|
|
1044
|
+
return Boolean(resolveMcpToolTarget(scopeId, name)?.definition);
|
|
857
1045
|
}
|
|
858
1046
|
/**
|
|
859
1047
|
* Check whether the inputSchema for an MCP tool declares the given top-level
|
|
@@ -866,9 +1054,7 @@ export function mcpToolHasField(name, field, scopeId = DEFAULT_MCP_SCOPE_ID) {
|
|
|
866
1054
|
const memoKey = `${normalizedScopeId}|${name}|${field}`;
|
|
867
1055
|
const memoized = _mcpToolFieldMemo.get(memoKey);
|
|
868
1056
|
if (memoized !== undefined) return memoized;
|
|
869
|
-
const
|
|
870
|
-
const server = match ? scopedServer(normalizedScopeId, match[1]) : null;
|
|
871
|
-
const tool = server ? server.tools.find((t) => t.name === name) : null;
|
|
1057
|
+
const tool = resolveMcpToolTarget(normalizedScopeId, name)?.definition;
|
|
872
1058
|
const props = tool?.inputSchema?.properties;
|
|
873
1059
|
const result = Boolean(props && Object.hasOwn(props, field));
|
|
874
1060
|
_mcpToolFieldMemo.set(memoKey, result);
|
|
@@ -932,17 +1118,19 @@ export async function disconnectAll(options = undefined) {
|
|
|
932
1118
|
export async function disconnectMcpServer(name, options = {}) {
|
|
933
1119
|
const scopeId = normalizeMcpScopeId(options);
|
|
934
1120
|
const registryKey = mcpServerRegistryKey(scopeId, name);
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1121
|
+
return runExclusiveServerOp(registryKey, async () => {
|
|
1122
|
+
const server = servers.get(registryKey);
|
|
1123
|
+
if (!server) return false;
|
|
1124
|
+
try {
|
|
1125
|
+
await _closeServer(server);
|
|
1126
|
+
} catch {
|
|
1127
|
+
/* ignore */
|
|
1128
|
+
}
|
|
1129
|
+
if (servers.get(registryKey) === server) servers.delete(registryKey);
|
|
1130
|
+
closeCallAdmission(scopeId, name, `MCP ${name} disconnected`);
|
|
1131
|
+
_invalidateMcpToolFieldMemo();
|
|
1132
|
+
return true;
|
|
1133
|
+
});
|
|
946
1134
|
}
|
|
947
1135
|
/**
|
|
948
1136
|
* Close a single server: for stdio transports first shut down the full child
|
|
@@ -952,6 +1140,8 @@ export async function disconnectMcpServer(name, options = {}) {
|
|
|
952
1140
|
* only kills the direct child and discards the pid we need to walk the tree.
|
|
953
1141
|
*/
|
|
954
1142
|
async function _closeServer(server) {
|
|
1143
|
+
// A deliberate close is not a crash: suppress the unexpected-close report.
|
|
1144
|
+
if (server?.liveness) server.liveness.closing = true;
|
|
955
1145
|
const transport = server?.transport;
|
|
956
1146
|
// Live stdio transports expose the spawned ChildProcess on _process.
|
|
957
1147
|
if (transport?._process) {
|
|
@@ -985,8 +1175,15 @@ async function connectServer(
|
|
|
985
1175
|
// abort before creating a transport/child at all.
|
|
986
1176
|
throw new Error(`MCP server "${name}" connect aborted by shutdown`);
|
|
987
1177
|
}
|
|
988
|
-
const client = createMcpClient(sdk, name, cfg);
|
|
989
|
-
const { transport, autoDetectAdvert } = createMcpTransport({
|
|
1178
|
+
const client = createMcpClient(sdk, name, cfg, scopeId);
|
|
1179
|
+
const { transport, autoDetectAdvert, stderrTail, warnings } = createMcpTransport({
|
|
1180
|
+
name,
|
|
1181
|
+
cfg,
|
|
1182
|
+
sdk,
|
|
1183
|
+
log: mcpLog,
|
|
1184
|
+
strictEnv: cfg?._mixdogStrictEnv === true,
|
|
1185
|
+
});
|
|
1186
|
+
const liveness = trackMcpLiveness(client, name, cfg, stderrTail);
|
|
990
1187
|
const pending = { scopeId, name, client, transport };
|
|
991
1188
|
_pendingConnects.add(pending);
|
|
992
1189
|
try {
|
|
@@ -996,11 +1193,12 @@ async function connectServer(
|
|
|
996
1193
|
client,
|
|
997
1194
|
transport,
|
|
998
1195
|
autoDetectAdvert,
|
|
999
|
-
closeServer: _closeServer,
|
|
1196
|
+
closeServer: (target) => _closeServer({ ...target, liveness }),
|
|
1197
|
+
stderrTail,
|
|
1000
1198
|
});
|
|
1001
1199
|
if (!toolsResult || !Array.isArray(toolsResult.tools)) {
|
|
1002
1200
|
try {
|
|
1003
|
-
await _closeServer({ client, transport });
|
|
1201
|
+
await _closeServer({ client, transport, liveness });
|
|
1004
1202
|
} catch {
|
|
1005
1203
|
/* ignore */
|
|
1006
1204
|
}
|
|
@@ -1009,13 +1207,13 @@ async function connectServer(
|
|
|
1009
1207
|
if (genAtStart !== currentConnectAbortGeneration(scopeId)) {
|
|
1010
1208
|
// disconnectAll() ran mid-handshake: never register — tear down.
|
|
1011
1209
|
try {
|
|
1012
|
-
await _closeServer({ client, transport });
|
|
1210
|
+
await _closeServer({ client, transport, liveness });
|
|
1013
1211
|
} catch {
|
|
1014
1212
|
/* ignore */
|
|
1015
1213
|
}
|
|
1016
1214
|
throw new Error(`MCP server "${name}" connect aborted by shutdown`);
|
|
1017
1215
|
}
|
|
1018
|
-
const tools = mcpServerTools(name, capabilities, toolsResult.tools);
|
|
1216
|
+
const tools = mcpServerTools(name, capabilities, toolsResult.tools, cfg);
|
|
1019
1217
|
const registryKey = mcpServerRegistryKey(scopeId, name);
|
|
1020
1218
|
servers.set(registryKey, {
|
|
1021
1219
|
scopeId,
|
|
@@ -1027,6 +1225,9 @@ async function connectServer(
|
|
|
1027
1225
|
cfg,
|
|
1028
1226
|
instructions,
|
|
1029
1227
|
capabilities,
|
|
1228
|
+
liveness,
|
|
1229
|
+
stderrTail,
|
|
1230
|
+
warnings: warnings || [],
|
|
1030
1231
|
generation: genAtStart,
|
|
1031
1232
|
});
|
|
1032
1233
|
if (capabilities?.tools?.listChanged && sdk.ToolListChangedNotificationSchema) {
|
|
@@ -1041,16 +1242,52 @@ async function connectServer(
|
|
|
1041
1242
|
}
|
|
1042
1243
|
}
|
|
1043
1244
|
|
|
1044
|
-
/**
|
|
1045
|
-
|
|
1046
|
-
|
|
1245
|
+
/**
|
|
1246
|
+
* Liveness of one server connection. The SDK calls client.onclose whenever
|
|
1247
|
+
* the transport closes (child exit, socket drop, our own close) — the server
|
|
1248
|
+
* is then dead and getMcpServerStatus reports it disconnected. onerror only
|
|
1249
|
+
* records the last error (parse/send hiccups do not mean the server died).
|
|
1250
|
+
* Handlers already installed on the client are chained, not replaced.
|
|
1251
|
+
*/
|
|
1252
|
+
function trackMcpLiveness(client, name, cfg, stderrTail) {
|
|
1253
|
+
const liveness = { alive: true, closing: false, closedAt: 0, lastError: '' };
|
|
1254
|
+
const priorClose = client.onclose;
|
|
1255
|
+
const priorError = client.onerror;
|
|
1256
|
+
client.onclose = (...args) => {
|
|
1257
|
+
const unexpected = liveness.alive && !liveness.closing;
|
|
1258
|
+
liveness.alive = false;
|
|
1259
|
+
liveness.closedAt = Date.now();
|
|
1260
|
+
if (unexpected) {
|
|
1261
|
+
mcpLog(`${appendMcpStderrTail(`[mcp-client] MCP server "${name}" closed unexpectedly`, stderrTail())}\n`);
|
|
1262
|
+
}
|
|
1263
|
+
if (typeof priorClose === 'function') priorClose.apply(client, args);
|
|
1264
|
+
};
|
|
1265
|
+
client.onerror = (error, ...rest) => {
|
|
1266
|
+
liveness.lastError = scrubMcpConnectionMessage(error instanceof Error ? error.message : String(error), cfg);
|
|
1267
|
+
if (typeof priorError === 'function') priorError.call(client, error, ...rest);
|
|
1268
|
+
};
|
|
1269
|
+
return liveness;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/** The SDK client for one server, advertising the project root when known.
|
|
1273
|
+
* A config marked `_mixdogRootsFollowScope` advertises its scope's current
|
|
1274
|
+
* root (setMcpScopeRoot) and is told when it changes. */
|
|
1275
|
+
function createMcpClient(sdk, name, cfg, scopeId = DEFAULT_MCP_SCOPE_ID) {
|
|
1276
|
+
const followScope = cfg?._mixdogRootsFollowScope === true;
|
|
1277
|
+
const fixedRoot = String(cfg?._mixdogProjectRoot || cfg?.cwd || '').trim();
|
|
1278
|
+
const currentRoot = () => (followScope ? _scopeRoots.get(normalizeMcpScopeId(scopeId)) || fixedRoot : fixedRoot);
|
|
1047
1279
|
const client = new sdk.Client(
|
|
1048
1280
|
{ name: `mixdog-agent/${name}`, version: '1.0.0' },
|
|
1049
|
-
{
|
|
1281
|
+
{
|
|
1282
|
+
capabilities: followScope || fixedRoot ? { roots: { listChanged: followScope } } : {},
|
|
1283
|
+
jsonSchemaValidator: createLazyOutputValidator(sdk.AjvJsonSchemaValidator),
|
|
1284
|
+
}
|
|
1050
1285
|
);
|
|
1051
|
-
if (
|
|
1286
|
+
if ((followScope || fixedRoot) && sdk.ListRootsRequestSchema) {
|
|
1052
1287
|
client.setRequestHandler(sdk.ListRootsRequestSchema, async () => {
|
|
1053
|
-
const
|
|
1288
|
+
const root = currentRoot();
|
|
1289
|
+
if (!root) return { roots: [] };
|
|
1290
|
+
const rootPath = resolve(root);
|
|
1054
1291
|
return {
|
|
1055
1292
|
roots: [
|
|
1056
1293
|
{
|
|
@@ -1064,23 +1301,85 @@ function createMcpClient(sdk, name, cfg) {
|
|
|
1064
1301
|
return client;
|
|
1065
1302
|
}
|
|
1066
1303
|
|
|
1067
|
-
/**
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1304
|
+
/**
|
|
1305
|
+
* Set the root advertised by `scopeId`'s scope-following servers (plugin
|
|
1306
|
+
* servers, whose roots track the session cwd) and notify the connected ones
|
|
1307
|
+
* when it changed. No reconnect: the connection and its tools stay as they are.
|
|
1308
|
+
*/
|
|
1309
|
+
export function setMcpScopeRoot(scopeId, root) {
|
|
1310
|
+
const normalized = normalizeMcpScopeId(scopeId);
|
|
1311
|
+
const next = String(root || '').trim();
|
|
1312
|
+
if ((_scopeRoots.get(normalized) || '') === next) return;
|
|
1313
|
+
if (next) _scopeRoots.set(normalized, next);
|
|
1314
|
+
else _scopeRoots.delete(normalized);
|
|
1315
|
+
for (const [, server] of scopedServerEntries(normalized)) {
|
|
1316
|
+
if (server.cfg?._mixdogRootsFollowScope !== true) continue;
|
|
1317
|
+
Promise.resolve()
|
|
1318
|
+
.then(() => server.client?.sendRootsListChanged?.())
|
|
1319
|
+
.catch(() => {});
|
|
1320
|
+
}
|
|
1076
1321
|
}
|
|
1077
1322
|
|
|
1078
|
-
|
|
1323
|
+
/** The connected server and tool definition a prefixed tool name refers to.
|
|
1324
|
+
* The name's server part finds it directly; a name shortened to fit provider
|
|
1325
|
+
* limits is found by scanning this scope's servers. A known server without
|
|
1326
|
+
* the tool still resolves (definition null) so the call reaches the server. */
|
|
1327
|
+
function resolveMcpToolTarget(scopeId, name) {
|
|
1328
|
+
const match = MCP_TOOL_NAME_RE.exec(String(name || ''));
|
|
1329
|
+
if (!match) return null;
|
|
1330
|
+
const direct = scopedServer(scopeId, match[1]);
|
|
1331
|
+
const directTool = direct?.tools?.find((tool) => tool.name === name);
|
|
1332
|
+
if (directTool) return { server: direct, definition: directTool };
|
|
1333
|
+
for (const [, server] of scopedServerEntries(scopeId)) {
|
|
1334
|
+
const definition = server.tools?.find((tool) => tool.name === name);
|
|
1335
|
+
if (definition) return { server, definition };
|
|
1336
|
+
}
|
|
1337
|
+
return direct ? { server: direct, definition: null } : null;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/** Protocol tools under the `mcp__<server>__` prefix plus the feature tools.
|
|
1341
|
+
* Names that would exceed provider limits are shortened deterministically
|
|
1342
|
+
* (`providerSafeMcpToolName`); a tool whose name was shortened carries the
|
|
1343
|
+
* server's own tool name as `mcpToolName` and its server as `mcpServerName`.
|
|
1344
|
+
* A plugin-declared server's tools carry its owner as `mcpPluginId`, so
|
|
1345
|
+
* scope filtering never has to infer ownership from the server name. */
|
|
1346
|
+
function mcpServerTools(name, capabilities, rawTools, cfg) {
|
|
1347
|
+
const protocolTools = (rawTools || []).map((t) => {
|
|
1348
|
+
const safeName = providerSafeMcpToolName(name, t.name);
|
|
1349
|
+
return {
|
|
1350
|
+
name: safeName,
|
|
1351
|
+
...(safeName === `mcp__${name}__${t.name}` ? {} : { mcpToolName: t.name, mcpServerName: name }),
|
|
1352
|
+
description: t.description || '',
|
|
1353
|
+
inputSchema: t.inputSchema || { type: 'object', properties: {} },
|
|
1354
|
+
...(t.annotations && typeof t.annotations === 'object' ? { annotations: t.annotations } : {}),
|
|
1355
|
+
};
|
|
1356
|
+
});
|
|
1357
|
+
const tools = [...protocolTools, ...mcpFeatureTools(name, capabilities, protocolTools)];
|
|
1358
|
+
const pluginId = String(cfg?._mixdogPlugin?.id || '').trim();
|
|
1359
|
+
return pluginId ? tools.map((tool) => ({ ...tool, mcpPluginId: pluginId })) : tools;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// tools/list_changed refreshes run one at a time per server connection, so an
|
|
1363
|
+
// older listing can never land after a newer one.
|
|
1364
|
+
function refreshServerTools(registryKey, client, name, cfg, capabilities) {
|
|
1079
1365
|
const live = servers.get(registryKey);
|
|
1080
|
-
if (!live || live.client !== client) return;
|
|
1366
|
+
if (!live || live.client !== client) return Promise.resolve();
|
|
1367
|
+
live.refreshTail = (live.refreshTail || Promise.resolve()).then(() =>
|
|
1368
|
+
runToolRefresh(registryKey, live, name, cfg, capabilities)
|
|
1369
|
+
);
|
|
1370
|
+
return live.refreshTail;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
async function runToolRefresh(registryKey, live, name, cfg, capabilities) {
|
|
1374
|
+
if (servers.get(registryKey) !== live) return;
|
|
1081
1375
|
try {
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1376
|
+
const { sdkTimeoutMs } = resolveMcpRequestTimeouts(cfg);
|
|
1377
|
+
const refreshed = await listAllMcpTools(live.client, { timeout: sdkTimeoutMs });
|
|
1378
|
+
if (!refreshed || !Array.isArray(refreshed.tools)) {
|
|
1379
|
+
throw new Error('ListTools returned invalid shape: missing or non-array tools field');
|
|
1380
|
+
}
|
|
1381
|
+
if (servers.get(registryKey) !== live) return;
|
|
1382
|
+
live.tools = mcpServerTools(name, capabilities, refreshed.tools, cfg);
|
|
1084
1383
|
_invalidateMcpToolFieldMemo();
|
|
1085
1384
|
} catch (error) {
|
|
1086
1385
|
mcpLog(
|
|
@@ -1093,22 +1392,33 @@ async function refreshServerTools(registryKey, client, name, cfg, capabilities)
|
|
|
1093
1392
|
export function _registerMcpServerForTest(scopeId, name, rawTools = [], options = {}) {
|
|
1094
1393
|
const normalizedScopeId = normalizeMcpScopeId(scopeId);
|
|
1095
1394
|
const registryKey = mcpServerRegistryKey(normalizedScopeId, name);
|
|
1096
|
-
const tools = rawTools.map((tool) =>
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1395
|
+
const tools = rawTools.map((tool) => {
|
|
1396
|
+
const prefixed = String(tool?.name || '').startsWith('mcp__');
|
|
1397
|
+
const leaf = String(tool?.name || '');
|
|
1398
|
+
const safeName = prefixed ? leaf : providerSafeMcpToolName(name, leaf);
|
|
1399
|
+
const shortened = !prefixed && safeName !== `mcp__${name}__${leaf}`;
|
|
1400
|
+
return {
|
|
1401
|
+
...tool,
|
|
1402
|
+
name: safeName,
|
|
1403
|
+
...(shortened ? { mcpToolName: leaf, mcpServerName: name } : {}),
|
|
1404
|
+
inputSchema: tool?.inputSchema || { type: 'object', properties: {} },
|
|
1405
|
+
};
|
|
1406
|
+
});
|
|
1103
1407
|
const capabilities = options.capabilities || {};
|
|
1408
|
+
const pluginId = String(options.cfg?._mixdogPlugin?.id || '').trim();
|
|
1104
1409
|
const server = {
|
|
1105
1410
|
scopeId: normalizedScopeId,
|
|
1106
1411
|
registryKey,
|
|
1107
1412
|
name,
|
|
1108
|
-
tools: [...tools, ...mcpFeatureTools(name, capabilities, tools)]
|
|
1413
|
+
tools: [...tools, ...mcpFeatureTools(name, capabilities, tools)].map((tool) =>
|
|
1414
|
+
pluginId ? { ...tool, mcpPluginId: pluginId } : tool
|
|
1415
|
+
),
|
|
1416
|
+
warnings: options.warnings || [],
|
|
1109
1417
|
cfg: options.cfg || {},
|
|
1110
1418
|
instructions: options.instructions || '',
|
|
1111
1419
|
capabilities,
|
|
1420
|
+
liveness: { alive: true, closing: false, closedAt: 0, lastError: '' },
|
|
1421
|
+
stderrTail: typeof options.stderrTail === 'function' ? options.stderrTail : () => '',
|
|
1112
1422
|
transport: null,
|
|
1113
1423
|
client: {
|
|
1114
1424
|
callTool:
|