inspector/specification/v2_auth_sdk_consolidation.md
cliffhall 3e76498366 chore: upgrade the MCP TypeScript SDK from 2.0.0-beta.5 to 2.0.0
The SDK shipped 2.0.0 final on 2026-07-27. Shipping the Inspector against a
prerelease of its core protocol library is a supply-chain and reproducibility
liability we no longer have a reason to carry: a beta is eligible for unpublish
and deprecation in a way a stable release is not.

Bumped in the root package.json only — client, core, server, server-legacy.
Node resolution walks up, so the root install already serves every client; a
per-client declaration would install a second copy that drifts from it (#1970).
Verified after install: one copy of each at 2.0.0, no clients/*/node_modules/
@modelcontextprotocol/* at all. ext-apps is a separate package on its own ^1.7.4
line and is untouched.

The beta.5 → 2.0.0 delta is seven commits. Reviewed each for call-site impact:

  - core/src/schemas.ts (+16/-16) is doc-comment only — every hunk rewrites a
    spec URL from a commit-pinned GitHub blob to the published 2026-07-28 page.
    No schema surface change, so nothing in core/mcp or core/json is affected.
  - client 2.0.0 wants zod ^4.2.0; we are on ^4.4.3, so no zod move and no risk
    of the #1896 dual-copy tsc heap blowup. verify:dep-lockstep stays green and
    the lockfile delta is exactly the four packages with no transitive churn.
  - fix(validators) honors declared draft-07/06 JSON Schema dialects instead of
    rejecting them, which can only widen the set of tool schemas we accept.
  - The SSE keep-alive and legacyWrap changes are server-side, exercised by the
    test servers rather than by client code.

The one change with real behavioral reach is #2564: the negotiation probe now
classifies HTTP 401/403 as an auth failure instead of legacy-era evidence. That
is the upstream fix #1807 is blocked on, and it is left for that issue — the
directAuthRecovery clause in inspectorClient.ts still intercepts the challenge
before the classifier sees it, so this bump does not change connect behavior.
#1807 can now delete the workaround and lean on the SDK verdict, but doing it
here would smuggle an auth change into a dependency bump.

Also corrected the two auth as-built specs, which still claimed beta.4 —
already stale before this change. v2_new_spec_impact.md is deliberately left
alone: it is a dated pre-upgrade analysis, and editing its premises would
rewrite the record rather than update it.

npm run ci passes end to end: validate, the per-file >=90 coverage gate,
verify:build-gate, all six smokes (including web:app driving connect -> open
app -> data-app-status="ready"), and 472 Storybook tests.

Closes #1988

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDdo1rNRVQnVRRdsqSrvG3
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-08-12 08:34:08 -04:00

12 KiB
Raw Permalink Blame History

Inspector V2 Auth — SDK consolidation (as-built)

Brief | V1 Problems | V2 Scope | V2 Tech Stack | V2 UX | V2 Auth | V2 New Spec Impact

Overview | EMA / XAA | Hardening | Mid-session | Smoke testing | SDK consolidation

Record of how Inspector uses @modelcontextprotocol/client 2.0.0 for authorization after the v2 SDK upgrade: what we moved onto the SDK, what we left Inspector-owned and why, and which small SDK API gaps would let us delete more local wire later.

Related as-built specs: Hardening, EMA, Mid-session.


Summary

Inspector already delegated connect-time standard OAuth to SDK auth() (via a thin mcpAuth() wrapper) as part of the v2 upgrade and hardening work. A follow-up pass looked for remaining protocol code that still duplicated the SDK.

Changed:

Change Detail
EMA leg 2 (ID-JAG mint) exchangeIdJag → SDK discoverAndRequestJwtAuthGrant
Scope union helpers scopes.ts re-exports SDK computeScopeUnion / isStrictScopeSuperset

Not changed (host integration or incomplete SDK helpers): remoting recovery, durable OAuthStorage, navigation/callbacks, EMA leg 1 + leg 3, AuthChallenge pipeline, mcpAuth(), step-up UX, CrossAppAccessProvider.

Net effect: ~30 lines of duplicate protocol logic removed; no files deleted (tokenEndpoint.ts remains for leg 3).


Constraint: two transport models

Web:  browser owns OAuth UX + storage API
      Hono owns MCP transport with a frozen/mutable token stub
      401/403 → auth_challenge envelope → browser handleAuthChallenge → POST /api/mcp/auth-state

TUI/CLI: in-process StreamableHTTP/SSE + live OAuthClientProvider
         silent SDK retry where policy allows; Inspector owns interactive confirm + EMA

The SDK’s in-process 401/403 retry assumes the live provider and redirect owner share a process. On web they do not — interactive OAuth cannot run on the Hono stub, and recovery must hot-swap tokens on the existing remote session. That architecture is intentional and permanent; consolidation only targets protocol helpers that work on both paths.


Ownership (as-built)

Concern Owner Notes
Standard OAuth flow SDK auth() Forwarded by core/auth/mcpAuth.ts
OAuthClientProvider bridge Inspector BaseOAuthClientProvider Storage + navigation
Issuer-keyed OAuthStorage Inspector File / remote / session adapters; SEP-2352 byIssuer
Discovery / CIMD / refresh / start+exchange SDK Called from Inspector host code
Scope union / strict superset SDK (re-export) core/auth/scopes.ts
Grant persistence helpers Inspector resolvePersistedScopeAfterGrant, resolveEffectiveGrantedScope
EMA leg 1 (IdP OIDC) Inspector + SDK primitives idpOidc.ts
EMA leg 2 (ID-JAG) SDK Wrapped by exchangeIdJag
EMA leg 3 (resource token) Inspector redeemIdJagForAccessToken + tokenEndpoint.ts
EMA transport adapter Inspector EmaTransportOAuthProvider
Mid-session orchestration Inspector OAuthManager.handleAuthChallenge()
WWW-Authenticate → AuthChallenge Inspector challenge.ts (string + remoting wire)
Web remoting auth Inspector Stub provider, intercept fetch, auth-state push
TUI/CLI loopback callback Inspector 127.0.0.1:6276
Step-up / OAuth UX copy Inspector Modal, Auth tab, CLI confirm

All three clients share core/auth + OAuthManager. Per-client code is navigation, storage adapter choice, and confirmation UX only.


What we changed

EMA leg 2 → SDK discoverAndRequestJwtAuthGrant

core/auth/ema/wire.ts exchangeIdJag wraps the SDK helper. emaFlow.mintEmaResourceTokens always passes a real resource (resourceUrl ?? resourceMetadata.resource). SDK errors are mapped to existing Inspector prefixes for tests/UX.

Accepted tradeoff: the SDK helper always posts client_id / client_secret in the body (client_secret_post). That matches our declared method and EMA mocks; a Basic-only IdP for ID-JAG would need a local fallback or an SDK authMethod option (see wishlist).

Scope helpers → SDK re-exports

core/auth/scopes.ts re-exports computeScopeUnion and isStrictScopeSuperset (semantics matched existing tests). Local RFC 6749 persistence helpers stay — the SDK does not export equivalents, and OAuthManager depends on them.


What we decided not to change (and why)

EMA leg 3 — keep local redeemIdJagForAccessToken

SDK exchangeJwtAuthGrant only sends grant_type + assertion. It has no resource or scope parameters. Inspector’s leg 3 (and wire.test.ts) set both on the JWT-bearer body. Adopting the helper today would drop wire fields. Tracked as a wishlist gap, not a permanent architectural split.

CrossAppAccessProvider — do not adopt

In-memory tokens, IdP assertion callback only (no IdP OIDC leg 1 / session cache), no durable storage, aimed at direct auth() + transport. Inspector needs shared oauth.json, IdP redirect/loopback, remoting token stub, and step-up UX. Wrapping it would be larger than EmaTransportOAuthProvider without covering web remoting. Not a wishlist item — that would mean reshaping the SDK around Inspector’s host model.

WWW-Authenticate — keep Inspector parseWwwAuthenticateBearer

SDK extractWWWAuthenticateParams takes a Response only. Remoting/parseAuthChallengeFromError work from header strings. The SDK parser also mishandles Bearer + extra whitespace and scope="". Using it on the Response path only would create two parsers. Keep the local string parser + AuthChallenge builder.

Web remoting recovery — keep Inspector protocol

Do not run interactive SDK auth() on Hono; do not replace POST /api/mcp/auth-state with in-process transport retry. See Mid-session.

mcpAuth() — keep the thin wrapper

Sole production boundary over SDK auth() for standard OAuth. Pins McpAuthOptions, guards forceReauthorization + authorizationCode, provides the vi.mock seam for OAuthManager tests, and a stable @inspector/core import. Inlining would save almost nothing and cost that discipline. handleAuthChallenge() stays for the same reason: no SDK orchestrator covers EMA + remoting + step-up confirm across clients.

M2M / withOAuth — unused

No product feature needs ClientCredentialsProvider, PrivateKeyJwtProvider, or withOAuth today.


SDK adoption wishlist

Only gaps where the SDK’s own helpers are incomplete or buggy, such that a normal fix would let Inspector call them without asking the SDK to adopt Inspector’s remoting/storage shape.

Gap Fix in SDK Then Inspector can…
exchangeJwtAuthGrant omits optional resource / scope on the JWT-bearer body (leg-2 helpers and CrossAppAccessProvider.prepareTokenRequest already deal in resource/scope) Add optional resource? / scope? and set them when present Replace redeemIdJagForAccessToken; likely delete tokenEndpoint.ts
Leg-2 helpers hardcode body client_secret_post Optional authMethod?: ClientAuthMethod Pass through when an IdP is Basic-only
extractWWWAuthenticateParams is Response-only; brittle on whitespace / empty quoted scope String overload (or shared header parser) with those edge cases fixed Optionally thin-wrap in parseWwwAuthenticateBearer if behavior matches challenge.test.ts

Out of wishlist (would require the SDK to mold itself to Inspector): durable/OAuthStorage-backed CrossAppAccessProvider, remoting-aware interactive auth on a node stub, replacing auth-state hot-swap, exporting host-only scope persistence policy.

When a wishlist row ships: bump the client package, swap behind existing tests, update this section and v2_auth_ema.md.


Code map

Role Path
SDK auth() forwarder core/auth/mcpAuth.ts
Provider / storage / challenges / scopes core/auth/providers.ts, oauth-storage.ts, challenge.ts, scopes.ts
EMA core/auth/ema/* (wire.ts leg 2 = SDK; leg 3 local)
Orchestration core/mcp/oauthManager.ts
Remoting tokenAuthProvider.ts, authChallengeFetch.ts, remoteClientTransport.ts
Client UX web App.tsx / step-up modal; TUI tuiOAuth.ts; CLI cliOAuth.ts; node loopback callback