Repository navigation
Remove the probe-era auth-challenge workaround and detect the SDK's typed 401/403 #1807
Description
Follow-up from #1805 / PR #1806.
Remove the client-side workaround that compensates for an SDK probe-classification gap. The gap is closed — the SDK fix shipped in @modelcontextprotocol/client@2.0.0, which is in the tree as of #1989. This is ordinary local cleanup, ready to pick up.
Background — why the workaround exists
Era auto/modern sends the SDK's server/discover negotiation probe before initialize, so authorization first surfaces at the probe. On the direct transport path (CLI/TUI, and any path with no stored tokens and therefore no authProvider), a 401 reached the SDK as a raw SdkHttpError. The old classifyHttpError only looked for a JSON-RPC error body — it ignored the HTTP status — so it verdicted "not a modern server". In pin mode (protocolEra: "modern") that verdict was rethrown with the 401 discarded entirely: no status, not even a cause.
#1806 worked around it by enabling interceptAuthChallenges for the probing eras even with no stored tokens, so the 401 became a typed AuthChallengeError that survives the probe as data.cause.
What the SDK now does
typescript-sdk#2564 rewrote the classification, covering both of the original asks:
- 401/403 are auth outcomes, never era evidence.
classifyHttpErrorgets explicit rows ahead of the JSON-RPC body parse: a 401/403 rejects as a typedSdkHttpErrorwith codeClientHttpAuthentication/ClientHttpForbidden. The codes are deliberately notEraNegotiationFailed, so an auth wall cannot enter era-recovery flows keyed on that code. - The status is no longer dropped. The error carries
status,statusText, and the responsetextindata. 5xx is split out as well (asEraNegotiationFailed), leaving the legacy-fallback set equal to the spec-licensed one — the 4xx a deployed 2025 server answers a request it does not recognize. - Auth errors propagate unchanged. A new internal auth-seam stamp (
Symbol.for('mcp.authSeamEscape')) marks errors escaping the transport's auth boundaries, and the probe routes stamped errors toauth-requiredwith the original object intact — identity,instanceof, andcausechain preserved.
The work
-
Delete the
|| this.probesProtocolEra()clause incore/mcp/inspectorClient.ts, sointerceptAuthChallengesis enabled only when there is actually anauthProvider. Trim the long comment block above it down to what still applies. -
Teach
isUnauthorizedError(core/auth/utils.ts) to recognize the new error. This is the part that is easy to miss, and it is load-bearing:SdkHttpErrorcarries the status aterr.data.status, noterr.status.err.codeisSdkErrorCode.ClientHttpAuthentication, not401.- The message reads
Version negotiation failed: the server requires authorization (HTTP 401), which the existing\bfailed\b[^\n]*\(401\)pattern does not match —(HTTP 401)is not(401). - There is no
causechain to walk.
So with the intercept removed and the detector untouched, a probe 401 would fail to start OAuth recovery. Both changes land together or neither does.
-
Keep
findNestedAuthError()incore/auth/challenge.ts— it is the permanent fix and is deliberately not gated on the SDK error code or message, so it keeps working across SDK rewording.
Bonus: an accepted side effect goes away
Turning the intercept on with no stored tokens meant parseAuthChallengeFromResponse treated 403 as a challenge too, so a probe answered 403 for a non-auth reason (a gateway rejecting the unknown server/discover method, say) started OAuth discovery instead of letting auto fall back to legacy. That was documented as a known, accepted cost of the workaround. Removing the clause removes it.
Acceptance criteria
- The
|| this.probesProtocolEra()clause is gone;interceptAuthChallengesfollowsauthProvideralone. -
isUnauthorizedErrorrecognizesSdkHttpErrorwithdata.statusof 401/403, with tests covering the real shape the SDK produces (status, code, and message all as emitted — not a hand-rolled stand-in). - Connect to an OAuth-protected server on era
autoandmodern, with no stored tokens, on CLI/TUI (direct transport) and web: OAuth recovery starts in every combination. - A probe answered 403 for a non-auth reason falls back to legacy under
autorather than starting OAuth discovery. -
npm run cipasses.
Activity
cliffhall commentedon Jul 27, 2026
Filed upstream: modelcontextprotocol/typescript-sdk#2561 — "server/discover probe classifier discards HTTP 401/403, and pin mode drops the cause".
Covers both asks from this issue, with the beta.5 source excerpts for
classifyHttpErrorand the pin-mode throw site, plus a note on thenormalizeReply→classifyNetworkErrorpath (typed auth errors land atdata.cause, notcause, sinceSdkError's third parameter isdata).Once ask (1) lands upstream, the
probesProtocolEra()/interceptAuthChallengesworkaround incore/mcp/inspectorClient.tscan be narrowed or removed. ThefindNestedAuthError()unwrap incore/auth/challenge.tsis not gated on the SDK's error code or message, so it stays correct either way and can be left in place.fix(core): start OAuth on a modern/auto-era connect (#1805) (#1809)
[-]Upstream SDK: probe classifier should treat 401/403 as auth-required, and pin mode must not drop the status[/-][+][Blocked upstream] SDK probe classifier should treat 401/403 as auth-required, and pin mode must not drop the status[/+]cliffhall commentedon Aug 1, 2026
Triage: Priority = Low
Scored 5/16 with the priority rubric added to
AGENTS.mdin #1891. This card is already approved and sitting in Todo — only Priority was set here, its Status is unchanged.Related issues. These look connected and may be worth reading together:
[Blocked upstream]itemScores are a starting point, not a verdict — the rubric is meant to be overruled when it's plainly wrong, provided the reason is written down.
cliffhall commentedon Aug 12, 2026
Unblocked upstream. The SDK fix landed as typescript-sdk#2564 —
classifyHttpErrornow gets explicit rows ahead of the JSON-RPC body parse, rejecting a probe 401/403 as a typedSdkHttpError(ClientHttpAuthentication/ClientHttpForbidden) carrying the status, reason phrase, and response text, instead of falling into the conservative legacy fallback. 5xx is split out too (asEraNegotiationFailed), so the remaining legacy set is exactly the spec-licensed one.It ships in 2.0.0, which #1988 / #1989 brings in. That PR deliberately does not touch this — the
directAuthRecoveryclause still setsinterceptAuthChallenges, so the challenge is typed before the classifier sees it and connect behavior is unchanged by the bump. This issue keeps ownership of removing the|| this.probesProtocolEra()workaround, as its in-code comment instructs.One thing to account for when doing it:
SdkHttpErrorcarries the status aterr.data.status, noterr.status, and its message readsVersion negotiation failed: the server requires authorization (HTTP 401).isUnauthorizedErrorincore/auth/utils.tscheckserr.status/err.code === 401and matches\bfailed\b[^\n]*\(401\)—(HTTP 401)does not match that pattern, and there is nocausechain to walk. So dropping the intercept without also teaching the detector aboutdata.statuswould leave a 401 probe failing to start OAuth recovery.[-][Blocked upstream] SDK probe classifier should treat 401/403 as auth-required, and pin mode must not drop the status[/-][+]SDK probe classifier should treat 401/403 as auth-required, and pin mode must not drop the status[/+][-]SDK probe classifier should treat 401/403 as auth-required, and pin mode must not drop the status[/-][+]Remove the probe-era auth-challenge workaround and detect the SDK's typed 401/403[/+]chore: upgrade the MCP TypeScript SDK from 2.0.0-beta.5 to 2.0.0