@vellumai/assistant 0.11.3 → 0.11.4-staging.2
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/ARCHITECTURE.md +11 -6
- package/docs/architecture/memory.md +11 -0
- package/docs/architecture/turn-actor.md +70 -0
- package/docs/flux-turn-detection-spike.md +243 -0
- package/docs/stt-provider-onboarding.md +3 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/openapi.yaml +139 -37
- package/package.json +1 -1
- package/scripts/voice-ttft-spike.ts +3 -3
- package/src/__tests__/app-compiler.test.ts +38 -3
- package/src/__tests__/attachments-store.test.ts +21 -12
- package/src/__tests__/byok-default-profile-ensure.test.ts +17 -0
- package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
- package/src/__tests__/call-site-routing-provider.test.ts +1 -1
- package/src/__tests__/channel-availability-routes.test.ts +14 -1
- package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
- package/src/__tests__/channel-delivery-store.test.ts +14 -14
- package/src/__tests__/config-loader-backfill.test.ts +3 -3
- package/src/__tests__/config-schema.test.ts +25 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop.test.ts +28 -20
- package/src/__tests__/conversation-attention-store.test.ts +63 -0
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
- package/src/__tests__/conversation-fork-crud.test.ts +69 -0
- package/src/__tests__/conversation-fork-referential.test.ts +67 -0
- package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
- package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
- package/src/__tests__/conversation-queue.test.ts +177 -4
- package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
- package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
- package/src/__tests__/credential-prompt-route.test.ts +7 -10
- package/src/__tests__/custom-profile-ensure.test.ts +5 -1
- package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
- package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
- package/src/__tests__/document-append-idempotency.test.ts +233 -0
- package/src/__tests__/edit-propagation.test.ts +0 -7
- package/src/__tests__/helpers/mock-actor-context.ts +49 -0
- package/src/__tests__/helpers/mock-conversation.ts +13 -1
- package/src/__tests__/injector-chain.test.ts +63 -41
- package/src/__tests__/injector-disk-pressure.test.ts +11 -23
- package/src/__tests__/llm-context-resolution.test.ts +73 -1
- package/src/__tests__/llm-schema.test.ts +5 -2
- package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
- package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
- package/src/__tests__/mtime-cache.test.ts +1 -1
- package/src/__tests__/non-member-access-request.test.ts +0 -20
- package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
- package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
- package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
- package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
- package/src/__tests__/scoped-approval-grants.test.ts +11 -6
- package/src/__tests__/secret-ingress-channel.test.ts +0 -1
- package/src/__tests__/skills.test.ts +32 -0
- package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
- package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
- package/src/__tests__/turn-events-store.test.ts +43 -0
- package/src/__tests__/ui-shape-teaching.test.ts +33 -0
- package/src/__tests__/ui-voice-picker-surface.test.ts +128 -0
- package/src/__tests__/user-plugin-loader.test.ts +1 -1
- package/src/__tests__/visible-app-context.test.ts +16 -9
- package/src/__tests__/voice-config-update.test.ts +40 -0
- package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
- package/src/__tests__/worker-plugin-surface.test.ts +77 -0
- package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
- package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
- package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
- package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
- package/src/__tests__/workspace-migration-146-repair-retired-fireworks-deepseek-flash-model-id.test.ts +235 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
- package/src/acp/__tests__/auth-required.test.ts +161 -0
- package/src/acp/acp-claude-oauth.ts +19 -2
- package/src/acp/agent-process.test.ts +100 -0
- package/src/acp/agent-process.ts +29 -26
- package/src/acp/auth-required.ts +102 -0
- package/src/acp/session-manager.test.ts +119 -0
- package/src/acp/session-manager.ts +68 -2
- package/src/api/events/acp-auth-required.ts +55 -0
- package/src/api/index.ts +7 -0
- package/src/api/surfaces.ts +7 -3
- package/src/apps/app-store.ts +3 -0
- package/src/bundler/package-resolver.ts +2 -30
- package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
- package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
- package/src/calls/call-controller.ts +19 -3
- package/src/calls/call-setup-flow.ts +0 -1
- package/src/calls/media-stream-stt-session.ts +15 -0
- package/src/calls/voice-session-bridge.ts +71 -16
- package/src/calls/voice-triage-escalate.ts +104 -2
- package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
- package/src/channels/config.ts +13 -0
- package/src/channels/plugin-channel-declarations.ts +108 -0
- package/src/channels/types.ts +30 -0
- package/src/cli/AGENTS.md +5 -2
- package/src/cli/commands/credentials.help.ts +2 -2
- package/src/cli/commands/inference-providers.ts +1 -1
- package/src/cli/commands/mcp.help.ts +13 -4
- package/src/cli/commands/mcp.ts +9 -0
- package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
- package/src/cli/commands/memory/index.help.ts +43 -1
- package/src/cli/commands/memory/memory-v3.ts +64 -0
- package/src/cli/commands/stt.help.ts +27 -2
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/upgrade-plugin.ts +42 -0
- package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
- package/src/config/__tests__/default-provider.test.ts +6 -1
- package/src/config/__tests__/profile-materialization.test.ts +75 -19
- package/src/config/bundled-skills/acp/SKILL.md +6 -7
- package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
- package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.test.ts +65 -0
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +7 -1
- package/src/config/bundled-skills/settings/tools/shared.ts +16 -0
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
- package/src/config/call-site-defaults.ts +4 -5
- package/src/config/default-profile-catalog.ts +83 -12
- package/src/config/default-profile-names.ts +4 -1
- package/src/config/default-provider-resolution.ts +4 -0
- package/src/config/llm-context-resolution.ts +11 -3
- package/src/config/llm-resolver.ts +28 -1
- package/src/config/profile-materialization.ts +70 -22
- package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
- package/src/config/schemas/call-site-catalog.ts +4 -4
- package/src/config/schemas/live-voice.ts +57 -23
- package/src/config/schemas/llm.ts +59 -32
- package/src/config/schemas/mcp.ts +23 -0
- package/src/config/schemas/plugin-updates.ts +6 -2
- package/src/config/schemas/stt.ts +1 -0
- package/src/context/outbound-sanitize.ts +96 -1
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
- package/src/daemon/conversation-agent-loop.ts +17 -6
- package/src/daemon/conversation-messaging.ts +5 -1
- package/src/daemon/conversation-notifiers.ts +9 -1
- package/src/daemon/conversation-process.ts +36 -6
- package/src/daemon/conversation-runtime-assembly.ts +3 -4
- package/src/daemon/conversation-surfaces.ts +27 -5
- package/src/daemon/conversation-tool-setup.ts +1 -2
- package/src/daemon/conversation.ts +48 -0
- package/src/daemon/interactive-turn-sender.ts +59 -0
- package/src/daemon/mcp-reload-service.ts +36 -6
- package/src/daemon/process-message.ts +24 -24
- package/src/daemon/providers-setup.ts +6 -3
- package/src/daemon/trust-context-types.ts +29 -0
- package/src/daemon/wake-conversation-ops.ts +3 -2
- package/src/documents/document-store.ts +138 -5
- package/src/hooks/hook-loader.ts +3 -3
- package/src/hooks/registry.ts +50 -6
- package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
- package/src/inbound/oauth-callback-url.ts +61 -0
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
- package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
- package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
- package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
- package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
- package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
- package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
- package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
- package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
- package/src/live-voice/live-voice-archive.ts +2 -0
- package/src/live-voice/live-voice-metrics.ts +57 -32
- package/src/live-voice/live-voice-photo.ts +1 -2
- package/src/live-voice/live-voice-session.ts +535 -314
- package/src/live-voice/progress-narration.ts +277 -0
- package/src/live-voice/protocol.ts +21 -1
- package/src/mcp/__tests__/effective-config.test.ts +238 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
- package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
- package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
- package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
- package/src/mcp/client.ts +16 -11
- package/src/mcp/effective-config.ts +113 -0
- package/src/mcp/manager.ts +11 -6
- package/src/mcp/mcp-auth-orchestrator.ts +13 -22
- package/src/mcp/mcp-oauth-provider.ts +205 -240
- package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
- package/src/monitoring/plugin-auto-update.ts +128 -24
- package/src/notifications/signal.ts +1 -0
- package/src/permissions/confirmation-guardian-request.test.ts +15 -11
- package/src/permissions/confirmation-guardian-request.ts +2 -2
- package/src/permissions/question-guardian-request.test.ts +14 -6
- package/src/permissions/question-guardian-request.ts +1 -2
- package/src/persistence/attachments-store.ts +8 -1
- package/src/persistence/bookmark-crud.ts +3 -7
- package/src/persistence/conversation-attention-store.ts +16 -45
- package/src/persistence/conversation-crud.ts +33 -4
- package/src/persistence/conversation-lineage.ts +9 -0
- package/src/persistence/conversation-queries.ts +108 -41
- package/src/persistence/delivery-crud.ts +38 -29
- package/src/persistence/external-conversation-store.ts +32 -4
- package/src/persistence/llm-request-log-store.ts +4 -10
- package/src/persistence/llm-usage-store.ts +8 -3
- package/src/persistence/message-reads.test.ts +197 -0
- package/src/persistence/message-reads.ts +211 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
- package/src/persistence/real-user-turn-filter.ts +27 -3
- package/src/persistence/steps.ts +9 -0
- package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
- package/src/plugin-api/conversation-turn.ts +168 -5
- package/src/plugin-api/index.ts +21 -5
- package/src/plugin-api/vision-support.test.ts +1 -1
- package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
- package/src/plugins/defaults/memory/AGENTS.md +4 -0
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
- package/src/plugins/defaults/memory/buffer-format.ts +165 -0
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
- package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
- package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
- package/src/plugins/defaults/memory/indexer.ts +3 -1
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
- package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
- package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
- package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
- package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
- package/src/plugins/defaults/memory/v3/prune.ts +2 -0
- package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
- package/src/plugins/defaults/memory/worker.ts +6 -3
- package/src/plugins/external-plugin-loader.ts +47 -0
- package/src/plugins/mcp-servers.ts +361 -0
- package/src/plugins/mtime-cache.ts +23 -49
- package/src/plugins/worker-plugin-surface.ts +33 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
- package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
- package/src/providers/__tests__/retry-callsite.test.ts +5 -2
- package/src/providers/call-site-routing.ts +30 -3
- package/src/providers/connection-resolution.ts +194 -11
- package/src/providers/inference/auth.ts +6 -0
- package/src/providers/inference/connection-availability.ts +24 -2
- package/src/providers/inference/connections.ts +2 -0
- package/src/providers/model-catalog.ts +3 -3
- package/src/providers/model-intents.ts +28 -8
- package/src/providers/openai/chat-completions-provider.ts +5 -6
- package/src/providers/openai/codex-models.ts +2 -1
- package/src/providers/provider-send-message.ts +32 -3
- package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
- package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
- package/src/providers/speech-to-text/provider-catalog.ts +99 -8
- package/src/providers/speech-to-text/resolve.ts +25 -2
- package/src/routes/worker.ts +17 -5
- package/src/runtime/access-request-helper.ts +9 -12
- package/src/runtime/agent-wake.ts +3 -3
- package/src/runtime/pre-first-message-gate.ts +4 -0
- package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
- package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
- package/src/runtime/routes/channel-availability-routes.ts +32 -14
- package/src/runtime/routes/channel-route-shared.ts +0 -6
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
- package/src/runtime/routes/conversation-list-routes.ts +112 -1
- package/src/runtime/routes/conversation-query-routes.ts +40 -27
- package/src/runtime/routes/credential-prompt-routes.ts +4 -7
- package/src/runtime/routes/default-provider-routes.ts +15 -0
- package/src/runtime/routes/inbound-message-handler.ts +17 -41
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
- package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
- package/src/runtime/routes/inference-profiles-routes.ts +20 -11
- package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
- package/src/runtime/routes/log-export-routes.ts +3 -0
- package/src/runtime/routes/mcp-auth-routes.ts +148 -57
- package/src/runtime/routes/plugins-routes.ts +21 -3
- package/src/runtime/routes/stt-routes.ts +31 -25
- package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
- package/src/runtime/routes/user-route-dispatcher.ts +39 -14
- package/src/runtime/routes/user-route-import.ts +108 -0
- package/src/schedule/worker.ts +6 -0
- package/src/security/oauth2.ts +6 -22
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
- package/src/stt/__tests__/types.test.ts +94 -0
- package/src/stt/daemon-batch-transcriber.ts +10 -0
- package/src/stt/stt-stream-session.ts +8 -4
- package/src/stt/types.ts +103 -0
- package/src/subagent/manager.ts +1 -3
- package/src/subagent/types.ts +7 -6
- package/src/tools/acp/spawn.test.ts +97 -0
- package/src/tools/acp/spawn.ts +32 -0
- package/src/tools/document/document-tool.ts +12 -3
- package/src/tools/registry.ts +2 -1
- package/src/tools/ui-surface/surface-shape-docs.ts +11 -0
- package/src/tools/workflows/run-workflow.ts +1 -2
- package/src/tts/__tests__/reasoning-tag-filter.test.ts +78 -0
- package/src/tts/reasoning-tag-filter.ts +89 -0
- package/src/util/think-tag-stream.ts +95 -0
- package/src/workspace/byok-default-profile-ensure.ts +76 -24
- package/src/workspace/custom-profile-ensure.ts +4 -24
- package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
- package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
- package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
- package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
- package/src/workspace/migrations/146-repair-retired-fireworks-deepseek-flash-model-id.ts +195 -0
- package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +10 -0
- package/src/workspace/provider-commit-message-generator.ts +7 -5
- package/src/live-voice/__tests__/front-decision.test.ts +0 -645
- package/src/live-voice/front-decision.ts +0 -476
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reuse and invalidation of the stored dynamic client registration.
|
|
3
|
+
*
|
|
4
|
+
* Dynamic registration writes a record on the authorization server, so
|
|
5
|
+
* registering per attempt leaves records behind that nobody cleans up. The
|
|
6
|
+
* stored registration is therefore reused, and only withheld when it
|
|
7
|
+
* provably no longer applies. Both halves matter: reusing one bound to a
|
|
8
|
+
* stale redirect URI produces authorization requests the server rejects,
|
|
9
|
+
* and re-registering unnecessarily is the accumulation this exists to stop.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
13
|
+
|
|
14
|
+
const CALLBACK_URL =
|
|
15
|
+
"https://platform.example/v1/gateway/callbacks/abc/webhooks/oauth/callback/";
|
|
16
|
+
|
|
17
|
+
let resolvedCallbackUrl = CALLBACK_URL;
|
|
18
|
+
|
|
19
|
+
mock.module("../../inbound/platform-callback-registration.js", () => ({
|
|
20
|
+
resolveCallbackUrl: async () => resolvedCallbackUrl,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
mock.module("../../inbound/public-ingress-urls.js", () => ({
|
|
24
|
+
getOAuthCallbackUrl: () => resolvedCallbackUrl,
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
mock.module("../../config/loader.js", () => ({
|
|
28
|
+
loadConfig: () => ({}),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
/** In-memory stand-in for the credential store. */
|
|
32
|
+
const store = new Map<string, string>();
|
|
33
|
+
|
|
34
|
+
mock.module("../../security/secure-keys.js", () => ({
|
|
35
|
+
getSecureKeyAsync: async (k: string) => store.get(k) ?? null,
|
|
36
|
+
setSecureKeyAsync: async (k: string, v: string) => {
|
|
37
|
+
store.set(k, v);
|
|
38
|
+
return true;
|
|
39
|
+
},
|
|
40
|
+
deleteSecureKeyAsync: async (k: string) =>
|
|
41
|
+
store.delete(k) ? "deleted" : "not-found",
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
mock.module("../../daemon/identity-helpers.js", () => ({
|
|
45
|
+
getAssistantName: () => "Jarvis",
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
mock.module("../../config/env.js", () => ({
|
|
49
|
+
getPlatformAssistantId: () => "019d6d4f-6dbd-779f-91d3-cb273b9429a5",
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
const { McpOAuthProvider } = await import("../mcp-oauth-provider.js");
|
|
53
|
+
|
|
54
|
+
const REGISTRATION = { client_id: "generated-client-id" };
|
|
55
|
+
|
|
56
|
+
function newProvider() {
|
|
57
|
+
return new McpOAuthProvider(
|
|
58
|
+
"unabyss",
|
|
59
|
+
"https://mcp.unabyss.com",
|
|
60
|
+
/* interactive */ false,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Seed discovery state the way the SDK would after running discovery. */
|
|
65
|
+
async function seedDiscovery(
|
|
66
|
+
provider: InstanceType<typeof McpOAuthProvider>,
|
|
67
|
+
issuer: string,
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
await provider.saveDiscoveryState({
|
|
70
|
+
authorizationServerUrl: issuer,
|
|
71
|
+
authorizationServerMetadata: { issuer },
|
|
72
|
+
} as never);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Run one registration the way a completed flow would. */
|
|
76
|
+
async function register(issuer = "https://mcp.unabyss.com"): Promise<void> {
|
|
77
|
+
const provider = newProvider();
|
|
78
|
+
await provider.startCallbackServer();
|
|
79
|
+
await seedDiscovery(provider, issuer);
|
|
80
|
+
await provider.saveClientInformation(REGISTRATION as never);
|
|
81
|
+
provider.stopCallbackServer();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
store.clear();
|
|
86
|
+
resolvedCallbackUrl = CALLBACK_URL;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("McpOAuthProvider client registration reuse", () => {
|
|
90
|
+
test("a stored registration is reused on the next flow", async () => {
|
|
91
|
+
await register();
|
|
92
|
+
|
|
93
|
+
const next = newProvider();
|
|
94
|
+
await next.startCallbackServer();
|
|
95
|
+
await seedDiscovery(next, "https://mcp.unabyss.com");
|
|
96
|
+
|
|
97
|
+
// Returning the stored value is what keeps the server from accruing a
|
|
98
|
+
// fresh client record on every `assistant mcp auth`.
|
|
99
|
+
expect(await next.clientInformation()).toEqual(REGISTRATION as never);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("no stored registration means the SDK registers", async () => {
|
|
103
|
+
const provider = newProvider();
|
|
104
|
+
await provider.startCallbackServer();
|
|
105
|
+
expect(await provider.clientInformation()).toBeUndefined();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("a changed redirect URI withholds the registration", async () => {
|
|
109
|
+
await register();
|
|
110
|
+
|
|
111
|
+
// The assistant moved, so its callback URL moved with it. The stored
|
|
112
|
+
// client is registered for the old one and the server would reject it.
|
|
113
|
+
resolvedCallbackUrl = "https://other.example/webhooks/oauth/callback";
|
|
114
|
+
const next = newProvider();
|
|
115
|
+
await next.startCallbackServer();
|
|
116
|
+
await seedDiscovery(next, "https://mcp.unabyss.com");
|
|
117
|
+
|
|
118
|
+
expect(await next.clientInformation()).toBeUndefined();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("a changed authorization server withholds the registration", async () => {
|
|
122
|
+
await register("https://auth.unabyss.com");
|
|
123
|
+
|
|
124
|
+
const next = newProvider();
|
|
125
|
+
await next.startCallbackServer();
|
|
126
|
+
await seedDiscovery(next, "https://different-auth.example");
|
|
127
|
+
|
|
128
|
+
expect(await next.clientInformation()).toBeUndefined();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("a silent reconnect reuses the registration without a redirect URI", async () => {
|
|
132
|
+
await register();
|
|
133
|
+
|
|
134
|
+
// `McpClient.connect` never prepares a callback, so `_redirectUrl` is
|
|
135
|
+
// unset. Treating that as a mismatch would turn every background
|
|
136
|
+
// reconnect into a registration.
|
|
137
|
+
const silent = newProvider();
|
|
138
|
+
await seedDiscovery(silent, "https://mcp.unabyss.com");
|
|
139
|
+
|
|
140
|
+
expect(await silent.clientInformation()).toEqual(REGISTRATION as never);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("a registration stored before bindings existed is kept", async () => {
|
|
144
|
+
// Upgrading must not invalidate every existing registration at once.
|
|
145
|
+
store.set(
|
|
146
|
+
"mcp:unabyss:client_info",
|
|
147
|
+
JSON.stringify({ client_id: "legacy" }),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const provider = newProvider();
|
|
151
|
+
await provider.startCallbackServer();
|
|
152
|
+
await seedDiscovery(provider, "https://mcp.unabyss.com");
|
|
153
|
+
|
|
154
|
+
expect(await provider.clientInformation()).toEqual({
|
|
155
|
+
client_id: "legacy",
|
|
156
|
+
} as never);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("invalidating the client drops its binding too", async () => {
|
|
160
|
+
await register();
|
|
161
|
+
expect(store.has("mcp:unabyss:client_binding")).toBe(true);
|
|
162
|
+
|
|
163
|
+
const provider = newProvider();
|
|
164
|
+
await provider.invalidateCredentials("client");
|
|
165
|
+
|
|
166
|
+
expect(store.has("mcp:unabyss:client_info")).toBe(false);
|
|
167
|
+
// A surviving binding would let the next registration inherit the
|
|
168
|
+
// previous one's issuer and redirect URI.
|
|
169
|
+
expect(store.has("mcp:unabyss:client_binding")).toBe(false);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("McpOAuthProvider client metadata", () => {
|
|
174
|
+
test("registers under the assistant's own name", async () => {
|
|
175
|
+
const provider = newProvider();
|
|
176
|
+
expect(provider.clientMetadata.client_name).toEqual("Jarvis");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("carries the assistant id as software_id", async () => {
|
|
180
|
+
const provider = newProvider();
|
|
181
|
+
expect(provider.clientMetadata.software_id).toEqual(
|
|
182
|
+
"019d6d4f-6dbd-779f-91d3-cb273b9429a5",
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("presents an anonymously fetchable logo", async () => {
|
|
187
|
+
// The consent screen loads this without credentials, so it cannot be
|
|
188
|
+
// the assistant's own avatar, which is served behind authentication.
|
|
189
|
+
const provider = newProvider();
|
|
190
|
+
expect(provider.clientMetadata.logo_uri).toEqual(
|
|
191
|
+
"https://www.vellum.ai/favicon.svg",
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("registers the resolved callback URL as the redirect URI", async () => {
|
|
196
|
+
const provider = newProvider();
|
|
197
|
+
await provider.startCallbackServer();
|
|
198
|
+
expect(provider.clientMetadata.redirect_uris).toEqual([CALLBACK_URL]);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -16,22 +16,24 @@ mock.module("../../inbound/public-ingress-urls.js", () => ({
|
|
|
16
16
|
|
|
17
17
|
mock.module("../../config/loader.js", () => ({
|
|
18
18
|
loadConfig: () => ({}),
|
|
19
|
+
// `clientMetadata` reads the assistant's name, which pulls the persona
|
|
20
|
+
// resolver into the provider's import graph, and that reads config.
|
|
21
|
+
getConfig: () => ({}),
|
|
19
22
|
}));
|
|
20
23
|
|
|
21
24
|
// ── Import SUT after mocks ────────────────────────────────────────────────────
|
|
22
25
|
|
|
23
26
|
const { McpOAuthProvider } = await import("../mcp-oauth-provider.js");
|
|
24
27
|
|
|
25
|
-
function
|
|
28
|
+
function newProvider() {
|
|
26
29
|
return new McpOAuthProvider(
|
|
27
30
|
"comms",
|
|
28
31
|
"https://comms.example/mcp",
|
|
29
32
|
/* interactive */ false,
|
|
30
|
-
"gateway",
|
|
31
33
|
);
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
describe("McpOAuthProvider
|
|
36
|
+
describe("McpOAuthProvider callback", () => {
|
|
35
37
|
test("stopCallbackServer() before a consumer attaches does not emit an unhandled rejection", async () => {
|
|
36
38
|
// stopCallbackServer() rejects the deferred code promise. When no consumer
|
|
37
39
|
// is attached, that rejection must stay observed so it does not surface as
|
|
@@ -40,7 +42,7 @@ describe("McpOAuthProvider gateway callback", () => {
|
|
|
40
42
|
const onUnhandled = (reason: unknown) => unhandled.push(reason);
|
|
41
43
|
process.on("unhandledRejection", onUnhandled);
|
|
42
44
|
try {
|
|
43
|
-
const provider =
|
|
45
|
+
const provider = newProvider();
|
|
44
46
|
|
|
45
47
|
// Intentionally do NOT consume the returned codePromise — this mirrors the
|
|
46
48
|
// early-exit paths where connect() throws before the OAuth tail is wired up.
|
|
@@ -53,7 +55,7 @@ describe("McpOAuthProvider gateway callback", () => {
|
|
|
53
55
|
const cancelRejections = unhandled.filter(
|
|
54
56
|
(r) =>
|
|
55
57
|
r instanceof Error &&
|
|
56
|
-
r.message.includes("MCP OAuth
|
|
58
|
+
r.message.includes("MCP OAuth callback cancelled"),
|
|
57
59
|
);
|
|
58
60
|
expect(cancelRejections).toHaveLength(0);
|
|
59
61
|
} finally {
|
|
@@ -62,14 +64,12 @@ describe("McpOAuthProvider gateway callback", () => {
|
|
|
62
64
|
});
|
|
63
65
|
|
|
64
66
|
test("stopCallbackServer() still rejects a consumer that is awaiting the code", async () => {
|
|
65
|
-
const provider =
|
|
67
|
+
const provider = newProvider();
|
|
66
68
|
await provider.startCallbackServer();
|
|
67
69
|
|
|
68
70
|
const codePromise = provider.waitForCode();
|
|
69
71
|
provider.stopCallbackServer();
|
|
70
72
|
|
|
71
|
-
await expect(codePromise).rejects.toThrow(
|
|
72
|
-
"MCP OAuth gateway callback cancelled",
|
|
73
|
-
);
|
|
73
|
+
await expect(codePromise).rejects.toThrow("MCP OAuth callback cancelled");
|
|
74
74
|
});
|
|
75
75
|
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A plugin-declared server must connect without resolving
|
|
3
|
+
* `mcp:<serverId>:*` from the workspace credential store.
|
|
4
|
+
*
|
|
5
|
+
* A plugin controls both its server key and its URL, so a stored
|
|
6
|
+
* credential whose id happens to match would otherwise be sent to an
|
|
7
|
+
* endpoint the plugin chose. The listing route avoids this by never
|
|
8
|
+
* probing a plugin server; the connect path avoids it by reading the
|
|
9
|
+
* server's own `source`, so the rule cannot be lost by a caller that
|
|
10
|
+
* forgets to pass something.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { beforeEach, describe, expect, jest, mock, test } from "bun:test";
|
|
14
|
+
|
|
15
|
+
const getSecureKeyAsync = jest.fn(async (_key: string) => "stored-oauth-token");
|
|
16
|
+
const getMcpHeaders = jest.fn(async (_serverId: string) => ({
|
|
17
|
+
Authorization: "Bearer workspace-secret",
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
mock.module("../../security/secure-keys.js", () => ({
|
|
21
|
+
getSecureKeyAsync,
|
|
22
|
+
setSecureKeyAsync: jest.fn(async () => true),
|
|
23
|
+
deleteSecureKeyAsync: jest.fn(async () => "deleted"),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
mock.module("../mcp-header-store.js", () => ({
|
|
27
|
+
getMcpHeaders,
|
|
28
|
+
setMcpHeaders: jest.fn(async () => {}),
|
|
29
|
+
deleteMcpHeaders: jest.fn(async () => true),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
// `McpOAuthProvider` is left real: its constructor only assigns fields, and
|
|
33
|
+
// mocking the module here would bleed into the sibling provider tests.
|
|
34
|
+
const { McpServerManager } = await import("../manager.js");
|
|
35
|
+
|
|
36
|
+
/** Refused immediately, so `connect` fails after the credential lookups. */
|
|
37
|
+
const UNREACHABLE = "http://127.0.0.1:1/mcp";
|
|
38
|
+
|
|
39
|
+
function httpServer(source: "workspace" | "plugin") {
|
|
40
|
+
return {
|
|
41
|
+
transport: { type: "streamable-http" as const, url: UNREACHABLE },
|
|
42
|
+
enabled: true,
|
|
43
|
+
defaultRiskLevel: "low" as const,
|
|
44
|
+
maxTools: 20,
|
|
45
|
+
source,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe("plugin-declared MCP servers", () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
getSecureKeyAsync.mockClear();
|
|
52
|
+
getMcpHeaders.mockClear();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("connect without resolving workspace credentials", async () => {
|
|
56
|
+
const manager = new McpServerManager();
|
|
57
|
+
|
|
58
|
+
await manager.start({
|
|
59
|
+
servers: { unabyss: httpServer("plugin") },
|
|
60
|
+
globalMaxTools: 50,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(getMcpHeaders).not.toHaveBeenCalled();
|
|
64
|
+
expect(getSecureKeyAsync).not.toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("workspace servers keep resolving theirs", async () => {
|
|
68
|
+
const manager = new McpServerManager();
|
|
69
|
+
|
|
70
|
+
await manager.start({
|
|
71
|
+
servers: { "from-workspace": httpServer("workspace") },
|
|
72
|
+
globalMaxTools: 50,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(getMcpHeaders).toHaveBeenCalledWith("from-workspace");
|
|
76
|
+
expect(getSecureKeyAsync).toHaveBeenCalledWith("mcp:from-workspace:tokens");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("isolation is per-server, not per-start", async () => {
|
|
80
|
+
// Both kinds start together in one call — the plugin one must not
|
|
81
|
+
// widen credential access for the workspace one, or vice versa.
|
|
82
|
+
const manager = new McpServerManager();
|
|
83
|
+
|
|
84
|
+
await manager.start({
|
|
85
|
+
servers: {
|
|
86
|
+
unabyss: httpServer("plugin"),
|
|
87
|
+
"from-workspace": httpServer("workspace"),
|
|
88
|
+
},
|
|
89
|
+
globalMaxTools: 50,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const lookedUpIds = getMcpHeaders.mock.calls.map(([id]) => id);
|
|
93
|
+
expect(lookedUpIds).toEqual(["from-workspace"]);
|
|
94
|
+
});
|
|
95
|
+
});
|
package/src/mcp/client.ts
CHANGED
|
@@ -5,8 +5,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
|
|
5
5
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
6
6
|
import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import type { McpTransport } from "../config/schemas/mcp.js";
|
|
8
|
+
import type { McpServerSource, McpTransport } from "../config/schemas/mcp.js";
|
|
10
9
|
import { getSecureKeyAsync } from "../security/secure-keys.js";
|
|
11
10
|
import { getLogger } from "../util/logger.js";
|
|
12
11
|
import { getMcpHeaders } from "./mcp-header-store.js";
|
|
@@ -55,6 +54,12 @@ export interface McpCallResult {
|
|
|
55
54
|
|
|
56
55
|
export class McpClient {
|
|
57
56
|
readonly serverId: string;
|
|
57
|
+
/**
|
|
58
|
+
* Where this server was declared. Only a `workspace` server resolves
|
|
59
|
+
* `mcp:<serverId>:tokens` / `mcp:<serverId>:headers` from the credential
|
|
60
|
+
* store — see {@link McpServerSource} for why a plugin server must not.
|
|
61
|
+
*/
|
|
62
|
+
readonly source: McpServerSource;
|
|
58
63
|
private client: Client;
|
|
59
64
|
private transport:
|
|
60
65
|
| StdioClientTransport
|
|
@@ -74,8 +79,9 @@ export class McpClient {
|
|
|
74
79
|
return this.connected;
|
|
75
80
|
}
|
|
76
81
|
|
|
77
|
-
constructor(serverId: string) {
|
|
82
|
+
constructor(serverId: string, source: McpServerSource = "workspace") {
|
|
78
83
|
this.serverId = serverId;
|
|
84
|
+
this.source = source;
|
|
79
85
|
this.client = new Client({
|
|
80
86
|
name: "vellum-assistant",
|
|
81
87
|
version: "1.0.0",
|
|
@@ -100,22 +106,21 @@ export class McpClient {
|
|
|
100
106
|
const isHttpTransport =
|
|
101
107
|
transportConfig.type === "sse" ||
|
|
102
108
|
transportConfig.type === "streamable-http";
|
|
109
|
+
const usesStoredCredentials = this.source === "workspace";
|
|
103
110
|
|
|
104
|
-
// For HTTP transports, only attach an OAuth provider if cached tokens
|
|
105
|
-
// This avoids triggering client registration
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
if (isHttpTransport) {
|
|
111
|
+
// For HTTP transports, only attach an OAuth provider if cached tokens
|
|
112
|
+
// exist. This avoids triggering client registration during daemon
|
|
113
|
+
// startup. If no tokens, try without auth: if the server requires it,
|
|
114
|
+
// skip silently.
|
|
115
|
+
if (isHttpTransport && usesStoredCredentials) {
|
|
109
116
|
const cachedTokens = await getSecureKeyAsync(
|
|
110
117
|
`mcp:${this.serverId}:tokens`,
|
|
111
118
|
);
|
|
112
119
|
if (cachedTokens) {
|
|
113
|
-
const callbackTransport = getIsPlatform() ? "gateway" : "loopback";
|
|
114
120
|
this.oauthProvider = new McpOAuthProvider(
|
|
115
121
|
this.serverId,
|
|
116
122
|
transportConfig.url,
|
|
117
123
|
/* interactive */ false,
|
|
118
|
-
callbackTransport,
|
|
119
124
|
);
|
|
120
125
|
}
|
|
121
126
|
}
|
|
@@ -123,7 +128,7 @@ export class McpClient {
|
|
|
123
128
|
// Resolve static auth headers from credential store, falling back to
|
|
124
129
|
// any legacy headers in the transport config for backward compatibility.
|
|
125
130
|
let effectiveConfig = transportConfig;
|
|
126
|
-
if (isHttpTransport) {
|
|
131
|
+
if (isHttpTransport && usesStoredCredentials) {
|
|
127
132
|
const storedHeaders = await getMcpHeaders(this.serverId);
|
|
128
133
|
if (storedHeaders) {
|
|
129
134
|
effectiveConfig = {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MCP configuration the daemon actually runs.
|
|
3
|
+
*
|
|
4
|
+
* Two sources contribute servers: the workspace `config.json`, which the
|
|
5
|
+
* user owns, and each installed plugin's `mcp.json`, which its author
|
|
6
|
+
* owns. Both must reach the MCP manager, so every consumer that starts
|
|
7
|
+
* servers builds its config through here rather than reading
|
|
8
|
+
* `config.mcp` directly — a path that only reads the workspace half
|
|
9
|
+
* leaves a plugin's tools silently missing.
|
|
10
|
+
*
|
|
11
|
+
* Every server comes out attributed with its {@link McpServerSource}, so
|
|
12
|
+
* the one place origin still matters downstream — a plugin server must
|
|
13
|
+
* never resolve `mcp:<serverId>:*` from the credential store — reads it
|
|
14
|
+
* off the server rather than from a caller-supplied list.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
type McpConfig,
|
|
19
|
+
McpConfigSchema,
|
|
20
|
+
type ResolvedMcpConfig,
|
|
21
|
+
type ResolvedMcpServerConfig,
|
|
22
|
+
} from "../config/schemas/mcp.js";
|
|
23
|
+
import { readPluginMcpServers } from "../plugins/mcp-servers.js";
|
|
24
|
+
import { getLogger } from "../util/logger.js";
|
|
25
|
+
|
|
26
|
+
const log = getLogger("mcp-effective-config");
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Fingerprint of the plugin-declared servers in the config last built here.
|
|
30
|
+
* Null until the first build, which happens during daemon startup.
|
|
31
|
+
*/
|
|
32
|
+
let lastBuiltPluginFingerprint: string | null = null;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Merge the workspace MCP config with the servers plugins declare.
|
|
36
|
+
*
|
|
37
|
+
* A workspace entry outranks a plugin's declaration of the same id: the
|
|
38
|
+
* user's own configuration is the more specific statement, and it is the
|
|
39
|
+
* one they can edit. `internal_mcp_list` applies the same precedence, so
|
|
40
|
+
* what the listing shows is what the daemon runs.
|
|
41
|
+
*/
|
|
42
|
+
export function buildEffectiveMcpConfig(
|
|
43
|
+
workspaceConfig?: McpConfig,
|
|
44
|
+
): ResolvedMcpConfig {
|
|
45
|
+
// An absent `mcp` key still has to yield the schema's own defaults
|
|
46
|
+
// (`globalMaxTools`), since plugin servers alone are enough to need them.
|
|
47
|
+
const base = workspaceConfig ?? McpConfigSchema.parse({});
|
|
48
|
+
const servers: Record<string, ResolvedMcpServerConfig> = {};
|
|
49
|
+
for (const [id, config] of Object.entries(base.servers)) {
|
|
50
|
+
servers[id] = { ...config, source: "workspace" };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const { servers: pluginServers, issues } = readPluginMcpServers();
|
|
54
|
+
for (const issue of issues) {
|
|
55
|
+
log.warn(
|
|
56
|
+
{
|
|
57
|
+
plugin: issue.pluginName,
|
|
58
|
+
...(issue.serverKey && { serverKey: issue.serverKey }),
|
|
59
|
+
},
|
|
60
|
+
`Plugin MCP declaration problem: ${issue.message}`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const server of pluginServers) {
|
|
65
|
+
if (Object.hasOwn(servers, server.id)) {
|
|
66
|
+
log.warn(
|
|
67
|
+
{ plugin: server.pluginName, serverId: server.id },
|
|
68
|
+
"Plugin MCP server shadowed by a workspace server of the same id; skipping",
|
|
69
|
+
);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
servers[server.id] = server.config;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
lastBuiltPluginFingerprint = fingerprintPluginServers();
|
|
76
|
+
|
|
77
|
+
return { ...base, servers };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Whether the plugin-declared servers on disk differ from the ones in the
|
|
82
|
+
* config last built by {@link buildEffectiveMcpConfig}.
|
|
83
|
+
*
|
|
84
|
+
* The plugin source reconcile asks this after an install / uninstall /
|
|
85
|
+
* upgrade / enable / disable, so an MCP reload happens exactly when a
|
|
86
|
+
* plugin actually changed the server set — not on every unrelated plugin
|
|
87
|
+
* edit, which would tear down healthy workspace connections for nothing.
|
|
88
|
+
*
|
|
89
|
+
* True before the first build, since nothing has been applied yet.
|
|
90
|
+
*/
|
|
91
|
+
export function pluginMcpServersChangedSinceLastBuild(): boolean {
|
|
92
|
+
return fingerprintPluginServers() !== lastBuiltPluginFingerprint;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Stable digest of every plugin-declared server: its id and the config it
|
|
97
|
+
* resolves to. Covers a server appearing, disappearing, or changing its
|
|
98
|
+
* transport — a disabled or uninstalled plugin contributes nothing, so it
|
|
99
|
+
* drops out of the digest the same way a deleted `mcp.json` entry does.
|
|
100
|
+
*/
|
|
101
|
+
function fingerprintPluginServers(): string {
|
|
102
|
+
const { servers } = readPluginMcpServers();
|
|
103
|
+
return JSON.stringify(
|
|
104
|
+
[...servers]
|
|
105
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
106
|
+
.map((s) => [s.id, s.config]),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Test seam: forget the last built config, as on a fresh daemon. */
|
|
111
|
+
export function resetEffectiveMcpConfigForTests(): void {
|
|
112
|
+
lastBuiltPluginFingerprint = null;
|
|
113
|
+
}
|
package/src/mcp/manager.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ResolvedMcpConfig,
|
|
3
|
+
ResolvedMcpServerConfig,
|
|
4
|
+
} from "../config/schemas/mcp.js";
|
|
2
5
|
import { getLogger } from "../util/logger.js";
|
|
3
6
|
import { McpClient, type McpToolInfo } from "./client.js";
|
|
4
7
|
|
|
@@ -6,15 +9,15 @@ const log = getLogger("mcp-manager");
|
|
|
6
9
|
|
|
7
10
|
export interface McpServerToolInfo {
|
|
8
11
|
serverId: string;
|
|
9
|
-
serverConfig:
|
|
12
|
+
serverConfig: ResolvedMcpServerConfig;
|
|
10
13
|
tools: McpToolInfo[];
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
export class McpServerManager {
|
|
14
17
|
private clients = new Map<string, McpClient>();
|
|
15
|
-
private serverConfigs = new Map<string,
|
|
18
|
+
private serverConfigs = new Map<string, ResolvedMcpServerConfig>();
|
|
16
19
|
|
|
17
|
-
async start(config:
|
|
20
|
+
async start(config: ResolvedMcpConfig): Promise<McpServerToolInfo[]> {
|
|
18
21
|
const results: McpServerToolInfo[] = [];
|
|
19
22
|
|
|
20
23
|
console.log(
|
|
@@ -40,7 +43,9 @@ export class McpServerManager {
|
|
|
40
43
|
"HTTP transport — OAuth provider will be available if server requires authentication",
|
|
41
44
|
);
|
|
42
45
|
}
|
|
43
|
-
|
|
46
|
+
// The server's own origin decides whether it may resolve
|
|
47
|
+
// `mcp:<serverId>:*` from the credential store.
|
|
48
|
+
const client = new McpClient(serverId, serverConfig.source);
|
|
44
49
|
await client.connect(serverConfig.transport);
|
|
45
50
|
|
|
46
51
|
if (!client.isConnected) {
|
|
@@ -142,7 +147,7 @@ export class McpServerManager {
|
|
|
142
147
|
|
|
143
148
|
private filterTools(
|
|
144
149
|
tools: McpToolInfo[],
|
|
145
|
-
config:
|
|
150
|
+
config: ResolvedMcpServerConfig,
|
|
146
151
|
): McpToolInfo[] {
|
|
147
152
|
let filtered = tools;
|
|
148
153
|
|
|
@@ -13,7 +13,6 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
|
13
13
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
14
14
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
15
15
|
|
|
16
|
-
import { getIsContainerized } from "../config/env-registry.js";
|
|
17
16
|
import { reloadMcpServers } from "../daemon/mcp-reload-service.js";
|
|
18
17
|
import { getLogger } from "../util/logger.js";
|
|
19
18
|
import {
|
|
@@ -22,10 +21,7 @@ import {
|
|
|
22
21
|
setMcpAuthPending,
|
|
23
22
|
} from "./mcp-auth-state.js";
|
|
24
23
|
import { getMcpHeaders } from "./mcp-header-store.js";
|
|
25
|
-
import {
|
|
26
|
-
type McpOAuthCallbackTransport,
|
|
27
|
-
McpOAuthProvider,
|
|
28
|
-
} from "./mcp-oauth-provider.js";
|
|
24
|
+
import { McpOAuthProvider } from "./mcp-oauth-provider.js";
|
|
29
25
|
|
|
30
26
|
const log = getLogger("mcp-auth-orchestrator");
|
|
31
27
|
|
|
@@ -53,20 +49,11 @@ export async function orchestrateMcpOAuthConnect(args: {
|
|
|
53
49
|
}): Promise<OrchestrateMcpOAuthConnectResult> {
|
|
54
50
|
const { serverId, transport } = args;
|
|
55
51
|
|
|
56
|
-
// Containerized deployments (platform-managed AND self-hosted Docker) must
|
|
57
|
-
// use the gateway transport: the browser is outside the daemon container,
|
|
58
|
-
// so a loopback callback inside the container is unreachable. Bare-metal
|
|
59
|
-
// daemons can use loopback as before.
|
|
60
|
-
const callbackTransport: McpOAuthCallbackTransport = getIsContainerized()
|
|
61
|
-
? "gateway"
|
|
62
|
-
: "loopback";
|
|
63
|
-
|
|
64
52
|
let capturedAuthUrl: string | undefined;
|
|
65
53
|
const provider = new McpOAuthProvider(
|
|
66
54
|
serverId,
|
|
67
55
|
transport.url,
|
|
68
56
|
/* interactive */ false,
|
|
69
|
-
callbackTransport,
|
|
70
57
|
{
|
|
71
58
|
onAuthorizationUrl: (url) => {
|
|
72
59
|
capturedAuthUrl = url;
|
|
@@ -74,8 +61,11 @@ export async function orchestrateMcpOAuthConnect(args: {
|
|
|
74
61
|
},
|
|
75
62
|
);
|
|
76
63
|
|
|
77
|
-
//
|
|
78
|
-
|
|
64
|
+
// Re-discover, but keep the client registration. Discovery is a read and
|
|
65
|
+
// costs a request; registration is a write on the authorization server,
|
|
66
|
+
// and remaking it per attempt leaves a record behind every time. The
|
|
67
|
+
// provider drops a stored registration on its own when the redirect URI
|
|
68
|
+
// or the authorization server it was made against has changed.
|
|
79
69
|
await provider.invalidateCredentials("discovery");
|
|
80
70
|
|
|
81
71
|
// Register the pending callback in the daemon heap
|
|
@@ -146,10 +136,9 @@ export async function orchestrateMcpOAuthConnect(args: {
|
|
|
146
136
|
// so the daemon can reconnect on the next MCP reload without re-connecting here.
|
|
147
137
|
void (async () => {
|
|
148
138
|
try {
|
|
149
|
-
// Apply an explicit timeout: the
|
|
150
|
-
//
|
|
151
|
-
// the
|
|
152
|
-
// forever if the user never completes the OAuth handshake.
|
|
139
|
+
// Apply an explicit timeout: the deferred code promise relies on the
|
|
140
|
+
// caller for time-boxing. Without this race, a tail leaks forever if
|
|
141
|
+
// the user never completes the OAuth handshake.
|
|
153
142
|
const code = await Promise.race([
|
|
154
143
|
codePromise,
|
|
155
144
|
new Promise<never>((_, reject) => {
|
|
@@ -215,6 +204,8 @@ export async function orchestrateMcpOAuthConnect(args: {
|
|
|
215
204
|
return { auth_url: capturedAuthUrl };
|
|
216
205
|
}
|
|
217
206
|
|
|
218
|
-
//
|
|
219
|
-
//
|
|
207
|
+
// How long a user has to complete the browser handshake before the tail
|
|
208
|
+
// gives up. Shorter than the callback registry's own 5-minute TTL, so the
|
|
209
|
+
// failure surfaces here with a clear message rather than as a registry
|
|
210
|
+
// expiry the caller cannot attribute.
|
|
220
211
|
const CALLBACK_TIMEOUT_MS = 2 * 60 * 1000;
|