Repository navigation
Remove the probe-era auth-challenge workaround and detect the SDK's typed 401/403 #1807
Description
Activity
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.- added a commit that references this issue
on Jul 27, 2026 - changed the title
[-]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[/+]on Jul 27, 2026 - removed a sub-issue
on Jul 28, 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.Axis Score Reasoning Severity / impact 2/5 A real misclassification — a 401/403 at the discover probe should read as auth-required — but the Inspector already ships a working client-side compensation in PR #1806, so users aren't currently affected. Urgency / staleness 2/5 Blocked on typescript-sdk#2561; no in-repo work is possible until that lands, and the compensation holds in the meantime. Signal bonuses +1 milestone (v2.2.0) (+1) Total 5 Lands in the Low band (5 or below). Related issues. These look connected and may be worth reading together:
- Remove ext-apps SDK-v1 shims when the package ships a v2 peer (ext-apps#702) #1745 — the sibling
[Blocked upstream]item - OIDC Auto-Discovery login not working in v2 web ui #1849 — an OIDC discovery hang — adjacent auth-probe territory worth checking against once the SDK change lands
Scores 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.
- Remove ext-apps SDK-v1 shims when the package ships a v2 peer (ext-apps#702) #1745 — the sibling
- addedauthIssues and PRs related to authorizationIssues and PRs related to authorizationbugSomething isn't workingSomething isn't working
on Aug 5, 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.- changed the title
[-][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[/+]on Aug 12, 2026 - changed the title
[-]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[/+]on Aug 12, 2026 - added a commit that references this issue
on Aug 12, 2026 - linked a pull request that will close this issuechore: upgrade the MCP TypeScript SDK from 2.0.0-beta.5 to 2.0.0 #1989
on Aug 16, 2026
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/modernsends the SDK'sserver/discovernegotiation probe beforeinitialize, so authorization first surfaces at the probe. On the direct transport path (CLI/TUI, and any path with no stored tokens and therefore noauthProvider), a 401 reached the SDK as a rawSdkHttpError. The oldclassifyHttpErroronly 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 acause.#1806 worked around it by enabling
interceptAuthChallengesfor the probing eras even with no stored tokens, so the 401 became a typedAuthChallengeErrorthat survives the probe asdata.cause.What the SDK now does
typescript-sdk#2564 rewrote the classification, covering both of the original asks:
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.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.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.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).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
parseAuthChallengeFromResponsetreated 403 as a challenge too, so a probe answered 403 for a non-auth reason (a gateway rejecting the unknownserver/discovermethod, say) started OAuth discovery instead of lettingautofall back to legacy. That was documented as a known, accepted cost of the workaround. Removing the clause removes it.Acceptance criteria
|| 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).autoandmodern, with no stored tokens, on CLI/TUI (direct transport) and web: OAuth recovery starts in every combination.autorather than starting OAuth discovery.npm run cipasses.