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

136 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Inspector V2 Auth — SDK consolidation (as-built)
### [Brief](README.md) | [V1 Problems](v1_problems.md) | [V2 Scope](v2_scope.md) | [V2 Tech Stack](v2_web_client.md) | [V2 UX](v2_ux.md) | [V2 Auth](v2_auth.md) | [V2 New Spec Impact](v2_new_spec_impact.md)
#### [Overview](v2_auth.md) | [EMA / XAA](v2_auth_ema.md) | [Hardening](v2_auth_hardening.md) | [Mid-session](v2_auth_mid_session.md) | [Smoke testing](v2_auth_smoke_testing.md) | 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](v2_auth_hardening.md), [EMA](v2_auth_ema.md), [Mid-session](v2_auth_mid_session.md).
---
## 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
```text
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](v2_auth_mid_session.md).
### `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](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 |