Skip to content

OAuth never starts on a modern/auto-era connect: the era-negotiation probe hides the auth challenge #1805

Description

@rinormaloku

Summary

Connecting to an OAuth-protected HTTP server with Protocol Era = Auto or Modern never starts the authorization flow. The same server authorizes fine with Protocol Era = Legacy. The user sees a red toast instead of the OAuth redirect:

Failed to connect to "<server>": Version negotiation probe failed: Interactive auth recovery required

Root cause

Era auto/modern makes the SDK send a server/discover negotiation probe before anything else. Its classifier (probeClassifier.ts in @modelcontextprotocol/client) reports whatever the transport threw as SdkError(ERA_NEGOTIATION_FAILED), keeping the original error at data.cause. That hides both connect-time auth signals this codebase keys off:

  • Remote path (web). The backend always runs interceptAuthChallenges: true (core/mcp/remote/node/server.ts), so a 401 becomes AuthChallengeErrorauth_challengeRemoteClientTransport rejects with AuthRecoveryRequiredError (carrying the authorization URL). Wrapped in SdkError, it no longer matches err instanceof AuthRecoveryRequiredError at clients/web/src/App.tsx:2654, and isUnauthorizedError() at :2670 can't see it either (it has no status/code). Also flips connection status to error at core/mcp/inspectorClient.ts:1548, which isConnectAuthRecoveryError exists to prevent.

  • Direct path (CLI/TUI). No stored tokens means no authProvider, so no interception, so the SDK surfaces the raw 401 as SdkHttpError. The probe classifier ignores the HTTP status (it only looks for a JSON-RPC error body), verdicts "legacy", and in pin mode rethrows as ERA_NEGOTIATION_FAILED with the 401 discarded entirely — no status, not even a cause:

    SdkError ERA_NEGOTIATION_FAILED — "the server did not offer pinned protocol
    version 2026-07-28 via server/discover (no fallback in pin mode)"
    

Reproduced against a real dual-era OAuth server (401 + WWW-Authenticate: Bearer resource_metadata=… on both initialize and server/discover):

era error out of client.connect() recovery matches?
legacy AuthRecoveryRequiredError ✅ redirect
auto SdkError / ERA_NEGOTIATION_FAILED ❌ generic toast
modern (pin) SdkError / ERA_NEGOTIATION_FAILED ❌ generic toast

core/auth/utils.ts:177-181 already anticipates this wrapper shape, but only walks the chain for UnauthorizedError — not for the two typed errors the Inspector's own paths actually produce.

Not related to the 2026-07-28 authorization SEPs (SEP-2468 iss, SEP-837 application_type, SEP-2352 issuer binding, DCR deprecated in favour of CIMD). Era-legacy authorization against the same server works; only the probe wrapper is at fault.

Fix

  1. core/auth/challenge.ts: add a cause-chain walker that returns a nested AuthRecoveryRequiredError / AuthChallengeError hidden inside a wrapper error.
  2. core/mcp/inspectorClient.ts: unwrap the client.connect() rejection with it, before withDirectAuthRecovery and the outer isConnectAuthRecoveryError check see it. Fixes web, CLI, and TUI in one place — no client-side changes needed.
  3. core/mcp/inspectorClient.ts: on the direct path, enable interceptAuthChallenges for era auto/modern even with no stored tokens, so the 401 becomes a typed AuthChallengeError that survives the probe as data.cause instead of being silently reclassified as "not a modern server".

Workaround until then

Connect once with Protocol Era = Legacy, complete OAuth, then switch to Modern and reconnect — with tokens stored the probe carries the Bearer and server/discover succeeds.

Upstream

Worth reporting separately: classifyHttpError should treat HTTP 401/403 on the probe as auth-required rather than as absence of modern evidence, and pin mode should not drop the status.

Metadata

Metadata

Assignees

Labels

v2Issues and PRs for v2

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions