mixdog 1.2.1 → 1.2.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/package.json +1 -1
- package/scripts/gate-local.mjs +19 -3
- package/scripts/model-catalog-audit.mjs +1 -15
- package/src/agents/reviewer/AGENT.md +3 -3
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +1 -0
- package/src/repl/slash-commands.mjs +16 -1
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +29 -4
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +10 -0
- package/src/runtime/agent/orchestrator/mcp/client-config.mjs +36 -7
- package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +49 -10
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +40 -3
- package/src/runtime/agent/orchestrator/mcp/client.mjs +230 -96
- package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +103 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +150 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +111 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-probe.mjs +87 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +95 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-request.mjs +122 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +9 -0
- package/src/runtime/agent/orchestrator/mcp/oauth.mjs +229 -0
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +188 -29
- package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +94 -31
- package/src/runtime/agent/orchestrator/providers/anthropic-error-contract.mjs +96 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +39 -18
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +55 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +25 -21
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-midstream-recovery.mjs +18 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +13 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-midstream.mjs +10 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-recovery.mjs +23 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +46 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +14 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +73 -35
- package/src/runtime/agent/orchestrator/providers/anthropic-overload-fallback.mjs +69 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +34 -7
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +30 -9
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +30 -14
- package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-retry-policy.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +82 -9
- package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +38 -5
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/cursor-wire-guards.mjs +97 -2
- package/src/runtime/agent/orchestrator/providers/cursor-wire-stream-bridge.mjs +102 -23
- package/src/runtime/agent/orchestrator/providers/cursor-wire-transport.mjs +103 -33
- package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +29 -6
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +18 -1
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +77 -17
- package/src/runtime/agent/orchestrator/providers/gemini-cache-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +29 -8
- package/src/runtime/agent/orchestrator/providers/gemini-error-contract.mjs +191 -0
- package/src/runtime/agent/orchestrator/providers/gemini-family.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/gemini-history-signatures.mjs +9 -4
- package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +180 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +21 -82
- package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -12
- package/src/runtime/agent/orchestrator/providers/gemini-thinking.mjs +21 -20
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +56 -23
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +68 -19
- package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +4 -9
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +4 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-result.mjs +22 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-error-contract.mjs +50 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-response-normalization.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +16 -8
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses-state.mjs +3 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +94 -13
- package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +193 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +66 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +6 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +4 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +23 -14
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +17 -3
- package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +42 -25
- package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +274 -0
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-request.mjs +72 -32
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/terminal-frames.mjs +18 -11
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-outcome.mjs +1 -2
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-stamps.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-state.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +1 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-dispatch.mjs +34 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +5 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +22 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +5 -0
- package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +5 -9
- package/src/runtime/agent/orchestrator/providers/openai-retry-policy.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-handshake.mjs +16 -8
- package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/attempt-context.mjs +1 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/policy.mjs +25 -14
- package/src/runtime/agent/orchestrator/providers/openai-ws-terminal.mjs +18 -11
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +31 -20
- package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +581 -0
- package/src/runtime/agent/orchestrator/providers/retry-backoff.mjs +46 -7
- package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +118 -219
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +148 -45
- package/src/runtime/agent/orchestrator/providers/retry-midstream.mjs +81 -16
- package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/goal-reminder.mjs +0 -18
- package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +57 -28
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +19 -16
- package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -97
- package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +102 -18
- package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +30 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +41 -21
- package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +5 -10
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +18 -7
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +155 -29
- package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +6 -3
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +16 -0
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +22 -10
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +20 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +12 -15
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
- package/src/runtime/memory/lib/pg/process.mjs +34 -24
- package/src/runtime/memory/lib/pg/supervisor.mjs +16 -13
- package/src/runtime/office/README.md +1 -1
- package/src/runtime/office/shared/values.mjs +0 -5
- package/src/runtime/shared/background-tasks.mjs +92 -14
- package/src/runtime/shared/err-text.mjs +25 -0
- package/src/runtime/shared/error-presentation.mjs +159 -19
- package/src/runtime/shared/llm/anthropic-betas.mjs +4 -3
- package/src/runtime/shared/llm/anthropic-effort.mjs +123 -134
- package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +50 -14
- package/src/runtime/shared/llm/model-catalog.mjs +53 -40
- package/src/runtime/shared/llm/openai-daybreak.mjs +4 -16
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +11 -0
- package/src/runtime/shared/llm/provider-model-identities.mjs +13 -4
- package/src/runtime/shared/llm/usage-rollup.mjs +0 -1
- package/src/runtime/shared/model-display.mjs +13 -0
- package/src/runtime/shared/quota-resume-store.mjs +11 -0
- package/src/runtime/shared/session-baseline-store.mjs +960 -0
- package/src/runtime/shared/statusline/statusline-format.mjs +4 -4
- package/src/runtime/shared/task-notification-envelope.mjs +2 -2
- package/src/runtime/shared/turn-changes.mjs +447 -219
- package/src/runtime/shared/turn-snapshot-store.mjs +182 -1
- package/src/runtime/shared/turn-snapshot.mjs +170 -48
- package/src/runtime/shared/turn-worktree-snapshot.mjs +135 -46
- package/src/runtime/shared/worktrees/contract.mjs +0 -24
- package/src/runtime/shared/worktrees/test-support.mjs +19 -4
- package/src/session-runtime/boot/apis.mjs +6 -0
- package/src/session-runtime/boot/infrastructure.mjs +6 -0
- package/src/session-runtime/lifecycle/session-catalog.mjs +2 -0
- package/src/session-runtime/lifecycle/teardown.mjs +1 -1
- package/src/session-runtime/mcp-glue/connect.mjs +29 -9
- package/src/session-runtime/mcp-glue/server-catalog.mjs +20 -3
- package/src/session-runtime/mcp-glue.mjs +3 -0
- package/src/session-runtime/mcp-oauth-login.mjs +174 -0
- package/src/session-runtime/model-route/model-switch-target.mjs +34 -0
- package/src/session-runtime/model-route/set-route.mjs +83 -15
- package/src/session-runtime/model-route/web-search-route.mjs +1 -1
- package/src/session-runtime/native-web-search/routes.mjs +22 -3
- package/src/session-runtime/provider-models/model-meta.mjs +12 -3
- package/src/session-runtime/quick-model-rows/web-search-rows.mjs +13 -8
- package/src/session-runtime/quick-web-search-models.mjs +13 -38
- package/src/session-runtime/resource-extension-sync.mjs +4 -0
- package/src/session-runtime/resource-mcp-api.mjs +98 -2
- package/src/session-runtime/runtime-review-api.mjs +13 -0
- package/src/session-runtime/services/agent-tool/job-task-reconcile.mjs +13 -1
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +7 -2
- package/src/session-runtime/services/agent-tool/send-flow.mjs +22 -4
- package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +2 -0
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +22 -5
- package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +8 -1
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +2 -2
- package/src/session-runtime/services/agent-tool/worker-rows.mjs +17 -0
- package/src/session-runtime/settings-compaction-api.mjs +1 -0
- package/src/standalone/daemon-drain-policy.mjs +8 -0
- package/src/standalone/daemon-telemetry.mjs +55 -0
- package/src/standalone/daemon.mjs +3 -7
- package/src/standalone/session-protocol.mjs +7 -0
- package/src/standalone/session-service/agent-tree/agent-cancel.mjs +33 -2
- package/src/standalone/session-service/agent-tree/agent-turns.mjs +26 -10
- package/src/standalone/session-service/agent-tree.mjs +12 -0
- package/src/standalone/session-service/quota-resume.mjs +8 -0
- package/src/standalone/session-service/retention/busy.mjs +15 -1
- package/src/standalone/session-service-api.mjs +5 -0
- package/src/standalone/session-service.mjs +1 -0
- package/src/tui/app/app-view.jsx +1 -0
- package/src/tui/app/create-app-pickers.mjs +4 -0
- package/src/tui/app/extension-pickers/mcp-login.mjs +107 -0
- package/src/tui/app/extension-pickers/mcp-servers-picker.mjs +2 -1
- package/src/tui/app/extension-pickers.mjs +2 -1
- package/src/tui/app/model-options.mjs +11 -16
- package/src/tui/app/slash-commands.mjs +7 -2
- package/src/tui/app/slash-dispatch/panel-commands.mjs +16 -1
- package/src/tui/components/StatusLine.jsx +2 -0
- package/src/tui/components/status-line/local-line.mjs +5 -2
- package/src/tui/components/status-line/use-statusline-text.mjs +7 -1
- package/src/tui/dist/index.mjs +816 -43
- package/src/tui/session/quota-resume.mjs +46 -14
- package/src/tui/session/session-api/commands.mjs +33 -24
- package/src/tui/session/session-api/extensions.mjs +22 -0
- package/src/tui/session/session-api/integrations.mjs +7 -0
- package/src/tui/session/session-api/lifecycle.mjs +5 -1
- package/src/tui/session/session-api/routes.mjs +71 -7
- package/src/tui/session/session-api/settings.mjs +5 -14
- package/src/tui/session/session-flow/drain.mjs +7 -1
- package/src/tui/session/session-flow/failed-turn-rewind.mjs +10 -4
- package/src/tui/session/turn.mjs +38 -15
- package/src/tui/statusline-ansi-bridge.mjs +1 -1
- package/src/ui/statusline-agents.mjs +15 -4
- package/src/ui/statusline.mjs +7 -3
- package/src/ui/streaming-markdown-heal.mjs +34 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +14 -4
- package/src/runtime/memory/lib/memory-session-merge.mjs +0 -32
- package/src/runtime/shared/llm/usage-session-history.mjs +0 -266
|
@@ -24,13 +24,22 @@ import { currentToolExecutionOwner } from '../../../shared/tool-execution-owner.
|
|
|
24
24
|
import { positiveInt } from '../../../shared/numbers.mjs';
|
|
25
25
|
import {
|
|
26
26
|
providerSafeMcpToolName,
|
|
27
|
+
resolveMcpOAuthUrl,
|
|
27
28
|
resolveMcpRequestTimeouts,
|
|
28
29
|
resolveMcpTransportKind,
|
|
29
30
|
scrubMcpConnectionMessage,
|
|
31
|
+
scrubMcpSecrets,
|
|
30
32
|
} from './client-config.mjs';
|
|
31
33
|
import { createLazyOutputValidator } from './lazy-output-validator.mjs';
|
|
32
|
-
import { appendMcpStderrTail, createMcpTransport } from './client-transport.mjs';
|
|
33
|
-
import { listAllMcpTools, runBoundedHandshake } from './client-handshake.mjs';
|
|
34
|
+
import { appendMcpStderrTail, createMcpTransport, hasConfiguredMcpAuth } from './client-transport.mjs';
|
|
35
|
+
import { listAllMcpTools, MCP_CONNECT_CANCELLED, runBoundedHandshake } from './client-handshake.mjs';
|
|
36
|
+
import {
|
|
37
|
+
clearMcpOAuth,
|
|
38
|
+
hasStoredMcpOAuthTokens,
|
|
39
|
+
isMcpOAuthReauthenticationRequired,
|
|
40
|
+
MCP_OAUTH_CALLBACK_NOT_PENDING,
|
|
41
|
+
} from './oauth.mjs';
|
|
42
|
+
export { probeMcpServerAuth } from './oauth-probe.mjs';
|
|
34
43
|
|
|
35
44
|
// Config interpretation (transport kind, URL policy, env expansion, headers,
|
|
36
45
|
// timeouts, log scrubbing) lives in client-config.mjs; re-exported so every
|
|
@@ -103,6 +112,20 @@ function mcpLog(line) {
|
|
|
103
112
|
if (process.env.MIXDOG_QUIET_MCP_LOG) return;
|
|
104
113
|
process.stderr.write(line);
|
|
105
114
|
}
|
|
115
|
+
// OAuth tokens belong to the live connection, not to its config: a server may
|
|
116
|
+
// echo the request (Authorization included) into an error body or result.
|
|
117
|
+
function oauthSecrets(...transports) {
|
|
118
|
+
return transports.flatMap((transport) => transport?.oauth?.secrets() || []);
|
|
119
|
+
}
|
|
120
|
+
function scrubMcpOAuthError(error, cfg, secrets) {
|
|
121
|
+
if (secrets.length === 0) return error;
|
|
122
|
+
if (!(error instanceof Error)) return new Error(scrubMcpConnectionMessage(String(error), cfg, secrets));
|
|
123
|
+
error.message = scrubMcpConnectionMessage(error.message, cfg, secrets);
|
|
124
|
+
if (typeof error.stack === 'string') error.stack = scrubMcpConnectionMessage(error.stack, cfg, secrets);
|
|
125
|
+
// A JSON-RPC error carries the server's own payload (McpError.data).
|
|
126
|
+
if (error.data !== undefined) error.data = scrubMcpSecrets(error.data, secrets);
|
|
127
|
+
return error;
|
|
128
|
+
}
|
|
106
129
|
|
|
107
130
|
async function loadMcpSdk() {
|
|
108
131
|
mcpSdkPromise ??= Promise.all([
|
|
@@ -148,14 +171,16 @@ export async function connectMcpServers(config, options = {}) {
|
|
|
148
171
|
});
|
|
149
172
|
// Connect all servers in PARALLEL: a slow/hung server (bounded by its
|
|
150
173
|
// per-server startup timeout) must never delay the others' handshakes.
|
|
151
|
-
const settled = await Promise.allSettled(
|
|
174
|
+
const settled = await Promise.allSettled(
|
|
175
|
+
entries.map(([name, cfg]) => connectServer(name, cfg, scopeId, genAtStart, options))
|
|
176
|
+
);
|
|
152
177
|
settled.forEach((res, i) => {
|
|
153
178
|
if (res.status !== 'rejected') return;
|
|
154
179
|
const [name, cfg] = entries[i];
|
|
155
180
|
const rawMessage = res.reason instanceof Error ? res.reason.message : String(res.reason);
|
|
156
181
|
const msg = scrubMcpConnectionMessage(rawMessage, cfg);
|
|
157
182
|
mcpLog(`[mcp-client] Failed to connect "${name}": ${msg}\n`);
|
|
158
|
-
failures.push({ name, msg });
|
|
183
|
+
failures.push({ name, msg, ...(res.reason?.authRequired ? { authRequired: true } : {}) });
|
|
159
184
|
});
|
|
160
185
|
if (failures.length > 0) {
|
|
161
186
|
const detail = failures.map((f) => `${f.name}: ${f.msg}`).join('; ');
|
|
@@ -330,7 +355,7 @@ export function getMcpServerInstructionsMap(scopeId = DEFAULT_MCP_SCOPE_ID) {
|
|
|
330
355
|
// shutdown generation captured BEFORE the caller queued/closed anything: a
|
|
331
356
|
// disconnectAll() landing during the close (or the connect) bumps it, and the
|
|
332
357
|
// replacement must then never register (no resurrection after shutdown).
|
|
333
|
-
async function replaceServerConnection(scopeId, name, current, cfg, genAtStart) {
|
|
358
|
+
async function replaceServerConnection(scopeId, name, current, cfg, genAtStart, options = {}) {
|
|
334
359
|
const registryKey = mcpServerRegistryKey(scopeId, name);
|
|
335
360
|
if (current) {
|
|
336
361
|
await _closeServer(current);
|
|
@@ -343,10 +368,12 @@ async function replaceServerConnection(scopeId, name, current, cfg, genAtStart)
|
|
|
343
368
|
try {
|
|
344
369
|
// connectServer re-checks the same generation after loading the SDK and
|
|
345
370
|
// again right before registering, so a shutdown mid-handshake also aborts.
|
|
346
|
-
await connectServer(name, cfg, scopeId, genAtStart);
|
|
371
|
+
await connectServer(name, cfg, scopeId, genAtStart, options);
|
|
347
372
|
} catch (reason) {
|
|
348
373
|
const rawMessage = reason instanceof Error ? reason.message : String(reason);
|
|
349
|
-
|
|
374
|
+
const error = new Error(scrubMcpConnectionMessage(rawMessage, cfg));
|
|
375
|
+
if (reason?.authRequired) error.authRequired = true;
|
|
376
|
+
throw error;
|
|
350
377
|
}
|
|
351
378
|
}
|
|
352
379
|
|
|
@@ -356,6 +383,7 @@ async function replaceServerConnection(scopeId, name, current, cfg, genAtStart)
|
|
|
356
383
|
// connect per server is ever running (no doubly registered transports).
|
|
357
384
|
async function runExclusiveServerOp(registryKey, task) {
|
|
358
385
|
for (let pending = reconnects.wait(registryKey); pending; pending = reconnects.wait(registryKey)) {
|
|
386
|
+
// Only waiting for the reconnect to settle; its failure belongs to its caller.
|
|
359
387
|
await pending.catch(() => {});
|
|
360
388
|
}
|
|
361
389
|
return reconnects.run(registryKey, task);
|
|
@@ -396,7 +424,7 @@ async function reconnectMcpServer(scopeId, serverName, failedServer) {
|
|
|
396
424
|
* rejects (credential-scrubbed message) when the connect fails, leaving the
|
|
397
425
|
* server unregistered.
|
|
398
426
|
*/
|
|
399
|
-
export async function reconnectSingleMcpServer(scopeId, name, cfg = undefined) {
|
|
427
|
+
export async function reconnectSingleMcpServer(scopeId, name, cfg = undefined, options = {}) {
|
|
400
428
|
const normalized = normalizeMcpScopeId(scopeId);
|
|
401
429
|
_knownMcpScopes.add(normalized);
|
|
402
430
|
const registryKey = mcpServerRegistryKey(normalized, name);
|
|
@@ -415,7 +443,7 @@ export async function reconnectSingleMcpServer(scopeId, name, cfg = undefined) {
|
|
|
415
443
|
closeCallAdmission(normalized, name, `MCP ${name} disabled`);
|
|
416
444
|
return false;
|
|
417
445
|
}
|
|
418
|
-
await replaceServerConnection(normalized, name, current, nextCfg, genAtStart);
|
|
446
|
+
await replaceServerConnection(normalized, name, current, nextCfg, genAtStart, options);
|
|
419
447
|
return true;
|
|
420
448
|
});
|
|
421
449
|
}
|
|
@@ -438,96 +466,101 @@ export async function executeMcpTool(name, args, options = {}) {
|
|
|
438
466
|
definition?.mcpOperation
|
|
439
467
|
? _callMcpFeatureWithTimeout(target, definition.mcpOperation, args, callSignal)
|
|
440
468
|
: _callToolWithTimeout(target, toolName, args, callSignal);
|
|
469
|
+
let retryServer = null;
|
|
470
|
+
const secrets = () => oauthSecrets(server.transport, retryServer?.transport);
|
|
441
471
|
const gate = callAdmissionFor(server);
|
|
442
|
-
return gate
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
throw mcpAbortError(
|
|
449
|
-
callSignal,
|
|
450
|
-
`MCP tool call aborted before dispatch (server="${serverName}", tool="${toolName}")`
|
|
451
|
-
);
|
|
452
|
-
}
|
|
453
|
-
let result;
|
|
454
|
-
try {
|
|
455
|
-
result = await dispatch(server);
|
|
456
|
-
} catch (firstErr) {
|
|
457
|
-
const firstMsg = firstErr instanceof Error ? firstErr.message : String(firstErr);
|
|
458
|
-
if (isMcpCallAbortError(firstErr) || callSignal?.aborted) {
|
|
459
|
-
// A cancelled call is not a transport failure: reconnecting and
|
|
460
|
-
// replaying it would duplicate the side effect the caller just
|
|
461
|
-
// walked away from.
|
|
462
|
-
mcpLog(`[mcp-client] Tool call aborted by caller for "${serverName}/${toolName}".\n`);
|
|
463
|
-
throw firstErr;
|
|
464
|
-
}
|
|
465
|
-
if (isMcpToolCallTimeoutError(firstErr)) {
|
|
466
|
-
mcpLog(`[mcp-client] Tool call timed out; skipping reconnect retry for "${serverName}/${toolName}".\n`);
|
|
467
|
-
throw firstErr;
|
|
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?.() : '';
|
|
479
|
-
mcpLog(`[mcp-client] Tool call failed, attempting shared reconnect...\n`);
|
|
480
|
-
let retryServer;
|
|
481
|
-
try {
|
|
482
|
-
// The reconnect is SHARED (singleflight): the race only stops
|
|
483
|
-
// THIS caller from waiting on it — the reconnect keeps running
|
|
484
|
-
// for its other waiters. Without this, an abort mid-reconnect
|
|
485
|
-
// could not settle the call and the admission slot stayed held
|
|
486
|
-
// until the reconnect finished, or forever if it hung.
|
|
487
|
-
retryServer = await raceMcpAbort(
|
|
488
|
-
reconnectMcpServer(scopeId, serverName, server),
|
|
489
|
-
callSignal,
|
|
490
|
-
`MCP tool call aborted during reconnect (server="${serverName}", tool="${toolName}")`
|
|
491
|
-
);
|
|
492
|
-
} catch (reconnectErr) {
|
|
493
|
-
if (isMcpCallAbortError(reconnectErr) || callSignal?.aborted) throw reconnectErr;
|
|
494
|
-
const reconnectMsg = reconnectErr instanceof Error ? reconnectErr.message : String(reconnectErr);
|
|
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
|
-
);
|
|
506
|
-
}
|
|
507
|
-
// A reconnect that outlived the caller's abort must not dispatch the
|
|
508
|
-
// retry: that would run the side effect after cancellation.
|
|
472
|
+
return gate
|
|
473
|
+
.run(
|
|
474
|
+
options?.ownerKey || currentToolExecutionOwner(),
|
|
475
|
+
async () => {
|
|
476
|
+
// The gate stops forwarding the caller's abort once this task is
|
|
477
|
+
// admitted, so the task itself observes the signal for its whole run.
|
|
509
478
|
if (callSignal?.aborted) {
|
|
510
479
|
throw mcpAbortError(
|
|
511
480
|
callSignal,
|
|
512
|
-
`MCP tool call aborted
|
|
481
|
+
`MCP tool call aborted before dispatch (server="${serverName}", tool="${toolName}")`
|
|
513
482
|
);
|
|
514
483
|
}
|
|
484
|
+
let result;
|
|
515
485
|
try {
|
|
516
|
-
result = await dispatch(
|
|
517
|
-
} catch (
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
486
|
+
result = await dispatch(server);
|
|
487
|
+
} catch (firstErr) {
|
|
488
|
+
const firstMsg = firstErr instanceof Error ? firstErr.message : String(firstErr);
|
|
489
|
+
if (isMcpCallAbortError(firstErr) || callSignal?.aborted) {
|
|
490
|
+
// A cancelled call is not a transport failure: reconnecting and
|
|
491
|
+
// replaying it would duplicate the side effect the caller just
|
|
492
|
+
// walked away from.
|
|
493
|
+
mcpLog(`[mcp-client] Tool call aborted by caller for "${serverName}/${toolName}".\n`);
|
|
494
|
+
throw firstErr;
|
|
495
|
+
}
|
|
496
|
+
if (isMcpToolCallTimeoutError(firstErr)) {
|
|
497
|
+
mcpLog(`[mcp-client] Tool call timed out; skipping reconnect retry for "${serverName}/${toolName}".\n`);
|
|
498
|
+
throw firstErr;
|
|
499
|
+
}
|
|
500
|
+
const serverDead = server.liveness?.alive === false;
|
|
501
|
+
if (!serverDead && isMcpServerResponseError(firstErr)) {
|
|
502
|
+
// The server answered (JSON-RPC error): it is alive — nothing to
|
|
503
|
+
// reconnect, and the call already ran its course.
|
|
504
|
+
throw firstErr;
|
|
505
|
+
}
|
|
506
|
+
// Only a failure that proves the request never left this process
|
|
507
|
+
// may be replayed; anything else may already have run server-side.
|
|
508
|
+
const undelivered = isMcpUndeliveredError(firstErr);
|
|
509
|
+
const deadTail = serverDead ? server.stderrTail?.() : '';
|
|
510
|
+
mcpLog(`[mcp-client] Tool call failed, attempting shared reconnect...\n`);
|
|
511
|
+
try {
|
|
512
|
+
// The reconnect is SHARED (singleflight): the race only stops
|
|
513
|
+
// THIS caller from waiting on it — the reconnect keeps running
|
|
514
|
+
// for its other waiters. Without this, an abort mid-reconnect
|
|
515
|
+
// could not settle the call and the admission slot stayed held
|
|
516
|
+
// until the reconnect finished, or forever if it hung.
|
|
517
|
+
retryServer = await raceMcpAbort(
|
|
518
|
+
reconnectMcpServer(scopeId, serverName, server),
|
|
519
|
+
callSignal,
|
|
520
|
+
`MCP tool call aborted during reconnect (server="${serverName}", tool="${toolName}")`
|
|
521
|
+
);
|
|
522
|
+
} catch (reconnectErr) {
|
|
523
|
+
if (isMcpCallAbortError(reconnectErr) || callSignal?.aborted) throw reconnectErr;
|
|
524
|
+
const reconnectMsg = reconnectErr instanceof Error ? reconnectErr.message : String(reconnectErr);
|
|
525
|
+
throw new Error(
|
|
526
|
+
appendMcpStderrTail(`Tool call failed: ${firstMsg}; reconnect also failed: ${reconnectMsg}`, deadTail)
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
if (!undelivered) {
|
|
530
|
+
throw new Error(
|
|
531
|
+
appendMcpStderrTail(
|
|
532
|
+
`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`,
|
|
533
|
+
deadTail
|
|
534
|
+
)
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
// A reconnect that outlived the caller's abort must not dispatch the
|
|
538
|
+
// retry: that would run the side effect after cancellation.
|
|
539
|
+
if (callSignal?.aborted) {
|
|
540
|
+
throw mcpAbortError(
|
|
541
|
+
callSignal,
|
|
542
|
+
`MCP tool call aborted after reconnect (server="${serverName}", tool="${toolName}")`
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
try {
|
|
546
|
+
result = await dispatch(retryServer);
|
|
547
|
+
} catch (retryErr) {
|
|
548
|
+
if (isMcpCallAbortError(retryErr) || callSignal?.aborted) throw retryErr;
|
|
549
|
+
const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
550
|
+
throw new Error(`Tool call failed: ${firstMsg}; retry after reconnect also failed: ${retryMsg}`);
|
|
551
|
+
}
|
|
521
552
|
}
|
|
553
|
+
const normalized = normalizeToolEnvelope(await normalizeMcpToolResult(scrubMcpSecrets(result, secrets())));
|
|
554
|
+
const text = capMcpResult(normalized.result);
|
|
555
|
+
return normalized.explicitSuccess ? makeToolEnvelope(text, [], { explicitSuccess: true }) : text;
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
signal: callSignal,
|
|
522
559
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
}
|
|
527
|
-
{
|
|
528
|
-
signal: callSignal,
|
|
529
|
-
}
|
|
530
|
-
);
|
|
560
|
+
)
|
|
561
|
+
.catch((error) => {
|
|
562
|
+
throw scrubMcpOAuthError(error, server.cfg, secrets());
|
|
563
|
+
});
|
|
531
564
|
}
|
|
532
565
|
|
|
533
566
|
const MCP_FEATURE_REQUESTS = {
|
|
@@ -903,6 +936,7 @@ export function raceMcpAbort(promise, signal, message) {
|
|
|
903
936
|
if (!signal) return promise;
|
|
904
937
|
allowConcurrentAbortListeners(signal);
|
|
905
938
|
if (signal.aborted) {
|
|
939
|
+
// The abort wins; the abandoned promise's late rejection must not go unhandled.
|
|
906
940
|
void Promise.resolve(promise).catch(() => {});
|
|
907
941
|
return Promise.reject(mcpAbortError(signal, message));
|
|
908
942
|
}
|
|
@@ -1090,6 +1124,7 @@ export async function disconnectAll(options = undefined) {
|
|
|
1090
1124
|
/* ignore */
|
|
1091
1125
|
}
|
|
1092
1126
|
try {
|
|
1127
|
+
entry.transport.oauth?.close();
|
|
1093
1128
|
void entry.client.close().catch(() => {
|
|
1094
1129
|
/* ignore */
|
|
1095
1130
|
});
|
|
@@ -1118,6 +1153,9 @@ export async function disconnectAll(options = undefined) {
|
|
|
1118
1153
|
export async function disconnectMcpServer(name, options = {}) {
|
|
1119
1154
|
const scopeId = normalizeMcpScopeId(options);
|
|
1120
1155
|
const registryKey = mcpServerRegistryKey(scopeId, name);
|
|
1156
|
+
// An exclusive reconnect may currently be waiting for human authorization.
|
|
1157
|
+
// Cancel before queueing the disconnect behind that operation.
|
|
1158
|
+
cancelMcpLogin(name, options);
|
|
1121
1159
|
return runExclusiveServerOp(registryKey, async () => {
|
|
1122
1160
|
const server = servers.get(registryKey);
|
|
1123
1161
|
if (!server) return false;
|
|
@@ -1132,6 +1170,73 @@ export async function disconnectMcpServer(name, options = {}) {
|
|
|
1132
1170
|
return true;
|
|
1133
1171
|
});
|
|
1134
1172
|
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Cancel the interactive OAuth login of a connect of `name` still in its
|
|
1175
|
+
* handshake; that connect then fails. Callers that queue behind the connect
|
|
1176
|
+
* (disable, remove) call this first so they never wait out the login.
|
|
1177
|
+
*/
|
|
1178
|
+
export function cancelMcpLogin(name, options = {}) {
|
|
1179
|
+
const scopeId = normalizeMcpScopeId(options);
|
|
1180
|
+
for (const entry of _pendingConnects) {
|
|
1181
|
+
if (entry.scopeId === scopeId && entry.name === name) entry.transport.oauth?.close();
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Answer the interactive OAuth login of a connect of `name` still in its
|
|
1186
|
+
* handshake with the redirect URL pasted from the browser (remote / headless
|
|
1187
|
+
* sign-in); that connect then proceeds as after the loopback callback. Throws
|
|
1188
|
+
* a fixed message, never the pasted URL, for an invalid paste (the login stays
|
|
1189
|
+
* pending) or when no login is waiting.
|
|
1190
|
+
*/
|
|
1191
|
+
export function completeMcpLogin(name, callbackUrl, options = {}) {
|
|
1192
|
+
const scopeId = normalizeMcpScopeId(options);
|
|
1193
|
+
for (const entry of _pendingConnects) {
|
|
1194
|
+
if (entry.scopeId === scopeId && entry.name === name && entry.transport.oauth?.loginStarted) {
|
|
1195
|
+
entry.transport.oauth.completeCallback(callbackUrl);
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
throw new Error(MCP_OAUTH_CALLBACK_NOT_PENDING);
|
|
1200
|
+
}
|
|
1201
|
+
/** Whether a connect of `name` still in its handshake is waiting for an
|
|
1202
|
+
* interactive (browser) OAuth login. */
|
|
1203
|
+
export function isMcpLoginPending(name, options = {}) {
|
|
1204
|
+
const scopeId = normalizeMcpScopeId(options);
|
|
1205
|
+
for (const entry of _pendingConnects) {
|
|
1206
|
+
if (entry.scopeId === scopeId && entry.name === name && entry.transport.oauth?.loginStarted) return true;
|
|
1207
|
+
}
|
|
1208
|
+
return false;
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Stored OAuth sign-in state of an HTTP/SSE server without configured
|
|
1212
|
+
* authentication: `{ signedIn }` (tokens stored for its exact URL), or null
|
|
1213
|
+
* for servers that never use MCP OAuth.
|
|
1214
|
+
*/
|
|
1215
|
+
export function getMcpServerOAuthState(cfg) {
|
|
1216
|
+
let url;
|
|
1217
|
+
try {
|
|
1218
|
+
url = resolveMcpOAuthUrl(cfg);
|
|
1219
|
+
} catch {
|
|
1220
|
+
return null;
|
|
1221
|
+
}
|
|
1222
|
+
if (!url || hasConfiguredMcpAuth(cfg)) return null;
|
|
1223
|
+
return { signedIn: hasStoredMcpOAuthTokens(url) };
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Log out of an HTTP/SSE server URL: delete the OAuth tokens and client
|
|
1227
|
+
* registration stored for it, shared by every server configured with that
|
|
1228
|
+
* exact URL, and drop them from live connections.
|
|
1229
|
+
*/
|
|
1230
|
+
export async function clearMcpServerOAuth(cfg) {
|
|
1231
|
+
let url;
|
|
1232
|
+
try {
|
|
1233
|
+
url = resolveMcpOAuthUrl(cfg);
|
|
1234
|
+
} catch {
|
|
1235
|
+
// A config without a valid remote URL never connected, so never logged in.
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
if (url) await clearMcpOAuth({ url });
|
|
1239
|
+
}
|
|
1135
1240
|
/**
|
|
1136
1241
|
* Close a single server: for stdio transports first shut down the full child
|
|
1137
1242
|
* process tree (close stdin -> grace -> tree kill) so wrapper chains such as
|
|
@@ -1143,6 +1248,7 @@ async function _closeServer(server) {
|
|
|
1143
1248
|
// A deliberate close is not a crash: suppress the unexpected-close report.
|
|
1144
1249
|
if (server?.liveness) server.liveness.closing = true;
|
|
1145
1250
|
const transport = server?.transport;
|
|
1251
|
+
transport?.oauth?.close();
|
|
1146
1252
|
// Live stdio transports expose the spawned ChildProcess on _process.
|
|
1147
1253
|
if (transport?._process) {
|
|
1148
1254
|
try {
|
|
@@ -1165,7 +1271,8 @@ async function connectServer(
|
|
|
1165
1271
|
name,
|
|
1166
1272
|
cfg,
|
|
1167
1273
|
scopeId = DEFAULT_MCP_SCOPE_ID,
|
|
1168
|
-
genAtStart = currentConnectAbortGeneration(scopeId)
|
|
1274
|
+
genAtStart = currentConnectAbortGeneration(scopeId),
|
|
1275
|
+
options = {}
|
|
1169
1276
|
) {
|
|
1170
1277
|
scopeId = normalizeMcpScopeId(scopeId);
|
|
1171
1278
|
_knownMcpScopes.add(scopeId);
|
|
@@ -1182,8 +1289,14 @@ async function connectServer(
|
|
|
1182
1289
|
sdk,
|
|
1183
1290
|
log: mcpLog,
|
|
1184
1291
|
strictEnv: cfg?._mixdogStrictEnv === true,
|
|
1292
|
+
// Only an explicit sign-in (beginMcpOAuthLogin) is interactive; it may
|
|
1293
|
+
// supply its own browser launcher to capture the authorization URL.
|
|
1294
|
+
oauthOptions: {
|
|
1295
|
+
interactive: options.interactive === true,
|
|
1296
|
+
...(typeof options.openBrowser === 'function' ? { openBrowser: options.openBrowser } : {}),
|
|
1297
|
+
},
|
|
1185
1298
|
});
|
|
1186
|
-
const liveness = trackMcpLiveness(client, name, cfg, stderrTail);
|
|
1299
|
+
const liveness = trackMcpLiveness(client, name, cfg, stderrTail, transport);
|
|
1187
1300
|
const pending = { scopeId, name, client, transport };
|
|
1188
1301
|
_pendingConnects.add(pending);
|
|
1189
1302
|
try {
|
|
@@ -1237,6 +1350,22 @@ async function connectServer(
|
|
|
1237
1350
|
}
|
|
1238
1351
|
_invalidateMcpToolFieldMemo();
|
|
1239
1352
|
mcpLog(`[mcp] connected: ${tools.length} tools — ${tools.map((t) => t.name).join(', ')}\n`);
|
|
1353
|
+
} catch (error) {
|
|
1354
|
+
await _closeServer({ client, transport, liveness });
|
|
1355
|
+
const scrubbed = scrubMcpOAuthError(error, cfg, oauthSecrets(transport));
|
|
1356
|
+
// A server that only needs an OAuth sign-in is not a broken server: the
|
|
1357
|
+
// sign-in was required, or an explicit one did not complete (cancelled,
|
|
1358
|
+
// denied, timed out or a failed exchange after it started, or cancelled
|
|
1359
|
+
// before it could). A failure after the sign-in obtained tokens is real.
|
|
1360
|
+
const signInIncomplete =
|
|
1361
|
+
transport.oauth &&
|
|
1362
|
+
(isMcpOAuthReauthenticationRequired(error) ||
|
|
1363
|
+
(transport.oauth.loginStarted && !transport.oauth.loginCompleted) ||
|
|
1364
|
+
(transport.oauth.interactive && error?.code === MCP_CONNECT_CANCELLED));
|
|
1365
|
+
if (signInIncomplete && scrubbed && typeof scrubbed === 'object') {
|
|
1366
|
+
scrubbed.authRequired = true;
|
|
1367
|
+
}
|
|
1368
|
+
throw scrubbed;
|
|
1240
1369
|
} finally {
|
|
1241
1370
|
_pendingConnects.delete(pending);
|
|
1242
1371
|
}
|
|
@@ -1249,7 +1378,7 @@ async function connectServer(
|
|
|
1249
1378
|
* records the last error (parse/send hiccups do not mean the server died).
|
|
1250
1379
|
* Handlers already installed on the client are chained, not replaced.
|
|
1251
1380
|
*/
|
|
1252
|
-
function trackMcpLiveness(client, name, cfg, stderrTail) {
|
|
1381
|
+
function trackMcpLiveness(client, name, cfg, stderrTail, transport) {
|
|
1253
1382
|
const liveness = { alive: true, closing: false, closedAt: 0, lastError: '' };
|
|
1254
1383
|
const priorClose = client.onclose;
|
|
1255
1384
|
const priorError = client.onerror;
|
|
@@ -1263,7 +1392,11 @@ function trackMcpLiveness(client, name, cfg, stderrTail) {
|
|
|
1263
1392
|
if (typeof priorClose === 'function') priorClose.apply(client, args);
|
|
1264
1393
|
};
|
|
1265
1394
|
client.onerror = (error, ...rest) => {
|
|
1266
|
-
liveness.lastError = scrubMcpConnectionMessage(
|
|
1395
|
+
liveness.lastError = scrubMcpConnectionMessage(
|
|
1396
|
+
error instanceof Error ? error.message : String(error),
|
|
1397
|
+
cfg,
|
|
1398
|
+
oauthSecrets(transport)
|
|
1399
|
+
);
|
|
1267
1400
|
if (typeof priorError === 'function') priorError.call(client, error, ...rest);
|
|
1268
1401
|
};
|
|
1269
1402
|
return liveness;
|
|
@@ -1316,6 +1449,7 @@ export function setMcpScopeRoot(scopeId, root) {
|
|
|
1316
1449
|
if (server.cfg?._mixdogRootsFollowScope !== true) continue;
|
|
1317
1450
|
Promise.resolve()
|
|
1318
1451
|
.then(() => server.client?.sendRootsListChanged?.())
|
|
1452
|
+
// A server that rejects the roots notification keeps its previous roots.
|
|
1319
1453
|
.catch(() => {});
|
|
1320
1454
|
}
|
|
1321
1455
|
}
|
|
@@ -1383,7 +1517,7 @@ async function runToolRefresh(registryKey, live, name, cfg, capabilities) {
|
|
|
1383
1517
|
_invalidateMcpToolFieldMemo();
|
|
1384
1518
|
} catch (error) {
|
|
1385
1519
|
mcpLog(
|
|
1386
|
-
`[mcp-client] Failed to refresh tools for "${name}": ${scrubMcpConnectionMessage(error?.message || error, cfg)}\n`
|
|
1520
|
+
`[mcp-client] Failed to refresh tools for "${name}": ${scrubMcpConnectionMessage(error?.message || error, cfg, oauthSecrets(live.transport))}\n`
|
|
1387
1521
|
);
|
|
1388
1522
|
}
|
|
1389
1523
|
}
|
|
@@ -1419,7 +1553,7 @@ export function _registerMcpServerForTest(scopeId, name, rawTools = [], options
|
|
|
1419
1553
|
capabilities,
|
|
1420
1554
|
liveness: { alive: true, closing: false, closedAt: 0, lastError: '' },
|
|
1421
1555
|
stderrTail: typeof options.stderrTail === 'function' ? options.stderrTail : () => '',
|
|
1422
|
-
transport: null,
|
|
1556
|
+
transport: options.transport || null,
|
|
1423
1557
|
client: {
|
|
1424
1558
|
callTool:
|
|
1425
1559
|
typeof options.callTool === 'function'
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { OAUTH_PAGE_CONTENT_TYPE, oauthErrorHtml, oauthSuccessHtml } from '../providers/lib/oauth-page.mjs';
|
|
3
|
+
|
|
4
|
+
/** Upper bound of a pasted redirect URL (`completeMcpLogin`). */
|
|
5
|
+
export const MCP_OAUTH_CALLBACK_MAX_BYTES = 64 * 1024;
|
|
6
|
+
// Fixed messages: a pasted URL carries the authorization code and is never
|
|
7
|
+
// echoed into an error, a log or a status.
|
|
8
|
+
const INVALID_CALLBACK_PASTE =
|
|
9
|
+
'The pasted sign-in URL is invalid. Paste the full address the browser was redirected to after signing in.';
|
|
10
|
+
export const MCP_OAUTH_CALLBACK_NOT_PENDING = 'MCP OAuth sign-in is not waiting for a callback';
|
|
11
|
+
const sameState = (supplied, expected) => {
|
|
12
|
+
const a = Buffer.from(supplied || '');
|
|
13
|
+
const b = Buffer.from(expected || '');
|
|
14
|
+
return b.length > 0 && a.length === b.length && timingSafeEqual(a, b);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The outcome of a redirect URL pasted from the browser for a pending login
|
|
19
|
+
* whose redirect URI is `redirectUrl` and state `state`: `{ code }`, or
|
|
20
|
+
* `{ denied: true }` for an authorization error. Throws a fixed message (never
|
|
21
|
+
* the pasted text) for anything that is not exactly that login's callback.
|
|
22
|
+
*/
|
|
23
|
+
export function parsePastedMcpOAuthCallback(pasted, { redirectUrl, state }) {
|
|
24
|
+
const invalid = () => new Error(INVALID_CALLBACK_PASTE);
|
|
25
|
+
if (typeof pasted !== 'string' || Buffer.byteLength(pasted) > MCP_OAUTH_CALLBACK_MAX_BYTES) throw invalid();
|
|
26
|
+
const raw = pasted.trim();
|
|
27
|
+
let target;
|
|
28
|
+
let expected;
|
|
29
|
+
try {
|
|
30
|
+
target = new URL(raw);
|
|
31
|
+
expected = new URL(redirectUrl);
|
|
32
|
+
} catch {
|
|
33
|
+
throw invalid();
|
|
34
|
+
}
|
|
35
|
+
if (
|
|
36
|
+
raw.includes('#') ||
|
|
37
|
+
target.username ||
|
|
38
|
+
target.password ||
|
|
39
|
+
target.origin !== expected.origin ||
|
|
40
|
+
target.pathname !== expected.pathname
|
|
41
|
+
)
|
|
42
|
+
throw invalid();
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
for (const [key, value] of target.searchParams) {
|
|
45
|
+
if (seen.has(key)) throw invalid();
|
|
46
|
+
seen.add(key);
|
|
47
|
+
// The redirect URI's own parameters come back unchanged; any others an
|
|
48
|
+
// authorization server adds (e.g. Google's scope/authuser/prompt) are
|
|
49
|
+
// ignored, as in Codex's parse_callback_url.
|
|
50
|
+
if (expected.searchParams.has(key) && expected.searchParams.get(key) !== value) throw invalid();
|
|
51
|
+
}
|
|
52
|
+
for (const key of expected.searchParams.keys()) if (!seen.has(key)) throw invalid();
|
|
53
|
+
if (!sameState(target.searchParams.get('state'), state)) throw invalid();
|
|
54
|
+
if (seen.has('code') === seen.has('error')) throw invalid();
|
|
55
|
+
if (seen.has('error')) return { denied: true };
|
|
56
|
+
const code = target.searchParams.get('code');
|
|
57
|
+
if (!code) throw invalid();
|
|
58
|
+
return { code };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Answers a request to the loopback redirect URI: only a GET of the exact
|
|
62
|
+
// callback path and host, carrying exactly the pending state (compared in
|
|
63
|
+
// constant time) and either one code or an error, finishes the login.
|
|
64
|
+
export function answerOAuthCallback(req, res, { redirectUrl, state, finish, serverName }) {
|
|
65
|
+
const page = (status, html) => {
|
|
66
|
+
res.writeHead(status, {
|
|
67
|
+
'Content-Type': OAUTH_PAGE_CONTENT_TYPE,
|
|
68
|
+
'Cache-Control': 'no-store',
|
|
69
|
+
'Referrer-Policy': 'no-referrer',
|
|
70
|
+
});
|
|
71
|
+
res.end(html);
|
|
72
|
+
};
|
|
73
|
+
const invalid = () =>
|
|
74
|
+
page(400, oauthErrorHtml('This sign-in link is invalid or has expired.', 'Start the sign-in again from Mixdog.'));
|
|
75
|
+
let target;
|
|
76
|
+
try {
|
|
77
|
+
target = new URL(req.url || '/', redirectUrl);
|
|
78
|
+
} catch {
|
|
79
|
+
invalid();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// `finish` is absent once the login was answered (e.g. by a pasted URL).
|
|
83
|
+
const valid =
|
|
84
|
+
typeof finish === 'function' &&
|
|
85
|
+
req.method === 'GET' &&
|
|
86
|
+
req.headers.host === new URL(redirectUrl).host &&
|
|
87
|
+
target.pathname === '/oauth/callback' &&
|
|
88
|
+
target.searchParams.getAll('state').length === 1 &&
|
|
89
|
+
sameState(target.searchParams.get('state'), state);
|
|
90
|
+
if (!valid || (!target.searchParams.has('error') && target.searchParams.getAll('code').length !== 1)) {
|
|
91
|
+
invalid();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const server = serverName || 'the MCP server';
|
|
95
|
+
if (target.searchParams.has('error')) {
|
|
96
|
+
// The authorization server's error text is never reflected.
|
|
97
|
+
page(400, oauthErrorHtml(`Sign-in to ${server} was not completed.`, 'Start the sign-in again from Mixdog.'));
|
|
98
|
+
finish(undefined);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
page(200, oauthSuccessHtml(server));
|
|
102
|
+
finish(target.searchParams.get('code'));
|
|
103
|
+
}
|