Merge remote-tracking branch 'origin/v2/main' into 1579-wave-1

# Conflicts:
#	AGENTS.md
#	clients/cli/README.md
#	clients/web/src/App.test.tsx
#	clients/web/src/App.tsx
#	clients/web/src/test/core/auth/utils.test.ts
#	core/mcp/node/transport.ts
#	test-servers/src/composable-test-server.ts
This commit is contained in:
cliffhall 2026-07-03 17:39:07 -04:00
commit cfbdfdf2bd
23 changed files with 15921 additions and 1202 deletions

View file

@ -1,6 +1,6 @@
# Inspector V2 Brief
### Brief | [V1 Problems](v1_problems.md) | [V2 Scope](v2_scope.md) | [V2 Tech Stack](v2_web_client.md) | [V2 UX](v2_ux.md)
### Brief | [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)
## Table of Contents
* [Motivation and Context](#motivation-and-context)

View file

@ -1,6 +1,6 @@
# Inspector V1 Problems
### [Brief](README.md) | V1 Problems | [V2 Scope](v2_scope.md) | [V2 Tech Stack](v2_web_client.md) | [V2 UX](v2_ux.md)
### [Brief](README.md) | V1 Problems | [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)
## Table of Contents

26
specification/v2_auth.md Normal file
View file

@ -0,0 +1,26 @@
# Inspector V2 Authorization - Specification
### [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 New Spec Impact](v2_new_spec_impact.md)
#### Overview | [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)
Inspector v2's authorization support spans OAuth 2.1 connect-time flows, Enterprise-Managed Authorization (EMA / XAA), mid-session credential recovery, and the hardening required by the MCP `2026-07-28` specification. This page is the landing point for that work; each area has its own spec below.
Inspector already ships connect-time OAuth and EMA infrastructure (`core/auth/`, `core/mcp/oauthManager.ts`, per-server OAuth fields in `~/.mcp-inspector/mcp.json` — see [Servers file](v2_servers_file.md)). The specs here cover extending that foundation across web, TUI, and CLI.
## Authorization specs
### [EMA / XAA](v2_auth_ema.md)
Support for [Enterprise-Managed Authorization](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization) (EMA, also referred to as XAA / ID-JAG). EMA extends the OAuth flow so an enterprise IdP (OIDC) can authenticate the client once; any MCP resource authorization server configured to trust that IdP is then accessible with minimal or no user prompting. Covers the web connect flow (implemented in `core/auth/ema/`), install-level `client.json` persistence, and wiring into `InspectorClient`.
### [Hardening](v2_auth_hardening.md)
Aligns Inspector with the six authorization SEPs in the MCP `2026-07-28` release candidate — all OAuth client requirements (`iss` validation per RFC 9207, `application_type` in DCR, credentials bound to AS issuer, and more). Strategy: upgrade to the v2 TypeScript SDK (`@modelcontextprotocol/client`) and wire the gaps rather than reimplementing SDK OAuth logic. Covers connect-time and storage hardening that mid-session builds on.
### [Mid-session](v2_auth_mid_session.md)
Detecting when in-flight MCP traffic needs new or elevated credentials, responding with the correct OAuth or EMA flow, and restoring the connection — across web, TUI, and CLI. Generalizes beyond expired access tokens to **step-up authorization** (a `403` with `insufficient_scope` and per-operation scopes, per SEP-2350). Addresses the web client's token-snapshot constraint, where MCP runs on the Hono backend and cannot complete interactive OAuth on its own.
### [Smoke testing](v2_auth_smoke_testing.md)
Manual smoke procedures for exercising Inspector OAuth against **hosted** MCP servers, complementing the automated integration coverage. Records known-good real endpoints, which client-ID mechanism each server supports, and how to configure Inspector (web, TUI, or CLI) via install-level `client.json`.

View file

@ -1,6 +1,7 @@
# Inspector V2 — Enterprise-Managed Authorization (EMA / XAA)
# Inspector V2 Auth — Enterprise-Managed Authorization (EMA / XAA)
### [Brief](README.md) | [V2 Scope](v2_scope.md) | [Servers file](v2_servers_file.md) | [Auth hardening](v2_auth_hardening.md) | [Mid-session auth](v2_auth_mid_session.md) | [Smoke testing](v2_auth_smoke_testing.md)
### [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 New Spec Impact](v2_new_spec_impact.md)
#### [Overview](v2_auth.md) | EMA / XAA | [Hardening](v2_auth_hardening.md) | [Mid-session](v2_auth_mid_session.md) | [Smoke testing](v2_auth_smoke_testing.md)
Tracks [#1509](https://github.com/modelcontextprotocol/inspector/issues/1509).
@ -365,6 +366,22 @@ Leg 1 reuses the existing OAuth redirect/callback/PKCE machinery but **targets t
**401 re-auth:** on EMA connections, re-run legs 2–3 (and leg 1 only if the cached ID Token is missing or expired). Do not fall back to standard resource authorization-code OAuth. Mid-session detection, step-up scopes, and web remote propagation are specified in **[Mid-session auth](v2_auth_mid_session.md)**.
### EMA step-up (web confirmation)
When an EMA server returns **403** + `insufficient_scope`, `OAuthManager.handleAuthChallenge()` returns **`step_up_confirm`** until the web user clicks **Authorize** in `StepUpAuthModal`. Only then does Inspector call `trySilentEmaAuth()` (legs 2–3 re-mint with SEP-2350 union scopes) or start IdP leg 1 if the organization session is missing.
**Why confirm on web?** EMA can often upgrade scopes silently when the IdP session is still valid. Inspector deliberately shows the additional scopes first — same pattern as standard OAuth step-up — because it is a **testing and exploration** client: operators should see permission elevation while validating MCP servers, not discover it only after the fact.
**Web UX after Authorize:**
| Outcome | User sees |
| ------- | --------- |
| `satisfied` (silent re-mint) | Blue in-progress toast → green “Organization permissions were updated” (+ “Retry your action” when step-up was triggered by a tool/prompt/resource/app) |
| `interactive` (IdP redirect) | Pre-redirect toast → full-page IdP callback → same resume snapshot behavior as other EMA flows |
| `failed` | Red toast with error detail; triggering panel shows failure |
TUI/CLI still use their existing confirm prompts (Auth tab **A**/**C**, CLI **y/N**) before silent re-mint — no web modal.
**EMA resource token tagging + sign-out:** per-server `oauth.enterpriseManaged` in `mcp.json` is **config** (routing); the OAuth store does not read the catalog on sign-out. Instead, when EMA legs 2–3 persist a resource access token, the store tags that entry (`ServerOAuthState.enterpriseManaged: true` via `SaveTokensOptions`). Sign-out uses that tag to find and clear EMA resource state inside the same `OAuthStorage` blob without the client enumerating MCP servers. This avoids clearing standard OAuth servers and avoids clearing EMA catalog entries that were never connected.
## Inspector mapping
@ -448,7 +465,7 @@ Design decisions for EMA are complete. Remaining work is the phased plan and che
#### Phase 4 — Other clients (after web works)
13. ~~Wire TUI/CLI to load `client.json` and pass `enterpriseManagedAuth` into `InspectorClient`.~~ **Done** — `loadRunnerClientConfig` + `buildRunnerClientAuthOptions` in TUI/CLI entrypoints. EMA sign-out (`clearEmaIdpSession`) is already client-agnostic in `core/` — Phase 4 clients can call it without catalog enumeration once Client Settings / logout UX lands.
14. ~~TUI interactive EMA + standard OAuth connect~~ **Done (TUI)** — 401 → `authenticate()` → `OAuthCallbackServer` on `6276` → reconnect; Auth tab OAuth snapshot + **S** clear (disconnects when connected). **Still open:** dedicated Client Settings UX in terminal, CLI local callback server for interactive login, EMA-specific terminal UX refinements.
14. ~~TUI interactive EMA + standard OAuth connect~~ **Done (TUI)** — 401 → `authenticate()` → `OAuthCallbackServer` on `6276` → reconnect; Auth tab OAuth snapshot + **S** clear (disconnects when connected). **Still open:** dedicated Client Settings UX in terminal, [CLI interactive OAuth](v2_auth_mid_session.md#tui-and-cli-implementation), EMA-specific terminal UX refinements.
## Implementation checklist
@ -493,7 +510,7 @@ Design decisions for EMA are complete. Remaining work is the phased plan and che
- [x] Wire TUI/CLI to load client config → `InspectorClientOptions.enterpriseManagedAuth` (+ CIMD / per-server OAuth via `buildRunnerClientAuthOptions`)
- [x] TUI interactive EMA + standard OAuth connect (401 → authenticate → callback → reconnect; Auth tab; keychain secret rehydration via `loadServerEntries`)
- [ ] TUI/CLI polish: Client Settings dialog, CLI interactive callback server, terminal EMA UX refinements
- [ ] TUI/CLI polish: Client Settings dialog, [CLI interactive OAuth](v2_auth_mid_session.md#tui-and-cli-implementation), terminal EMA UX refinements
### Later

View file

@ -1,6 +1,7 @@
# Inspector V2 — Authorization hardening (MCP 2026-07-28)
# Inspector V2 Auth — Authorization hardening (MCP 2026-07-28)
### [Brief](README.md) | [V2 Scope](v2_scope.md) | [Mid-session auth](v2_auth_mid_session.md) | [EMA / XAA](v2_auth_ema.md) | [Smoke testing](v2_auth_smoke_testing.md)
### [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 New Spec Impact](v2_new_spec_impact.md)
#### [Overview](v2_auth.md) | [EMA / XAA](v2_auth_ema.md) | Hardening | [Mid-session](v2_auth_mid_session.md) | [Smoke testing](v2_auth_smoke_testing.md)
Align Inspector v2 with the **authorization hardening** in the upcoming MCP **`2026-07-28`** specification release — six SEPs that tighten OAuth/OIDC client behavior for MCP's single-client, many-server deployment pattern.
@ -25,10 +26,11 @@ The [2026-07-28 release candidate](https://blog.modelcontextprotocol.io/posts/20
| Concern | Mid-session spec | This doc |
| ------- | ---------------- | -------- |
| **SEP-2350** scope union on 403 step-up | `handleAuthChallenge()` owns union + UX for runtime challenges | Connect-time step-up inherits SDK union after v2 upgrade; mid-session still uses `saveScope(authorizationScopes)` in the challenge handler |
| **SEP-2350** scope union on 403 step-up | `handleAuthChallenge()` + `mcpAuth({ forceReauthorization })` for **web remote**, EMA, and **interactive** defer; union persisted on `completeOAuthFlow` success | **Direct streamable HTTP silent step-up:** v2 SDK transport ([#2265](https://github.com/modelcontextprotocol/typescript-sdk/pull/2265)). **Do not** duplicate with client-side fetch intercept or `AuthRecoveryTransport` on TUI/CLI |
| **SEP-2207** refresh / `offline_access` | EMA legs 2–3 re-mint; standard OAuth silent refresh | Connect-time scope selection and DCR metadata |
| **SEP-2468** `iss` validation | Applies to any interactive re-auth redirect (401 mid-session) | Connect-time callback wiring is the first place to land `iss` passthrough |
| **SEP-2352** issuer-bound credentials | Re-register after AS migration may surface as mid-session failure | Storage and `invalidateCredentials` must be correct before mid-session recovery can succeed |
| **TUI/CLI mid-session** | [TUI and CLI implementation](v2_auth_mid_session.md#tui-and-cli-implementation): provider + UX; silent retry via v2 SDK | Provider v2 hooks (`invalidateCredentials`, issuer-keyed storage, `application_type`) |
Implement mid-session auth now. Land auth hardening on the v2 SDK upgrade path without blocking mid-session work — use compatible storage and callback shapes so both tracks merge cleanly.
@ -53,8 +55,17 @@ Inspector today uses **`@modelcontextprotocol/sdk` ^1.29.0** (v1 monolith). Auth
Connect-time standard OAuth EMA Web remote backend
─────────────────────────── ─── ──────────────────
SDK auth() + BaseOAuthClientProvider Local wire (ema/) Frozen token stub
OAuthStorage keyed by serverUrl IdP OIDC in idpOidc.ts No interactive OAuth
completeOAuthFlow(code) only No SDK CrossAppAccess Mid-session → browser
OAuthStorage keyed by serverUrl IdP OIDC in idpOidc.ts No interactive OAuth on node
completeOAuthFlow(code, iss?) v2 CrossAppAccess optional Mid-session → browser
(evaluate vs ema/) fetch intercept (permanent)
RemoteClientTransport (permanent)
TUI / CLI direct (not web remote):
──────────────────────────────────
StreamableHTTPClientTransport + live OAuthClientProvider in-process
v2 SDK: silent 401/403 retry + unionScopes on streamable HTTP (#2265)
Inspector: handleAuthChallenge for EMA + interactive defer; TUI/CLI UX only
Do NOT: fetch intercept or AuthRecoveryTransport on client SDK transport
```
### What the v2 SDK provides (after upgrade)
@ -62,7 +73,7 @@ completeOAuthFlow(code) only No SDK CrossAppAccess Mid-session
| SEP | SDK status (Jun 2026) | Inspector gets |
| --- | --------------------- | -------------- |
| SEP-2207 | Implemented on v2 `main` ([#2199](https://github.com/modelcontextprotocol/typescript-sdk/issues/2199)) | `offline_access` augmentation in authorize scope for standard OAuth via `auth()` |
| SEP-2350 | Open PR [#2265](https://github.com/modelcontextprotocol/typescript-sdk/pull/2265) | `unionScopes()` + 403 retry uses union on streamable HTTP direct transport |
| SEP-2350 | Open PR [#2265](https://github.com/modelcontextprotocol/typescript-sdk/pull/2265) | `unionScopes()` + 403 retry uses union on **streamable HTTP direct transport**; SSE unchanged (401 only) |
| SEP-2352 | Open PR [#2271](https://github.com/modelcontextprotocol/typescript-sdk/pull/2271) | AS migration detection → `invalidateCredentials` → re-DCR |
| SEP-2468 | Open PR [#2272](https://github.com/modelcontextprotocol/typescript-sdk/pull/2272) | `iss` validation in code-exchange path when host passes `iss` |
| SEP-837 | Open [#2198](https://github.com/modelcontextprotocol/typescript-sdk/issues/2198) | `application_type` in DCR request body |
@ -76,7 +87,7 @@ completeOAuthFlow(code) only No SDK CrossAppAccess Mid-session
| SEP-837 | Ensure web vs TUI/CLI set correct client type (`web` / `native`) in provider metadata or environment | SDK may infer from redirect URI; Inspector must not mis-declare cross-environment |
| SEP-2352 | Key `OAuthStorage` credentials by AS `issuer` (not only `serverUrl`); implement `invalidateCredentials` on `BaseOAuthClientProvider`; migrate shared `oauth.json` | Storage is Inspector-owned; SDK calls provider hooks |
| SEP-2207 | EMA leg 1 already requests `openid offline_access` (`IDP_OIDC_SCOPES`); verify standard OAuth path after upgrade | EMA bypasses SDK authorize for leg 1 |
| SEP-2350 | Mid-session union in `handleAuthChallenge()` + `saveScope(authorizationScopes)`; web/EMA paths bypass SDK transport retry | See [Mid-session auth](v2_auth_mid_session.md) |
| SEP-2350 | **Web remote:** mid-session union in `handleAuthChallenge()` (bypasses SDK — proxy architecture). **TUI/CLI direct silent step-up:** v2 SDK transport after #2265. **Interactive defer** (step-up modal / Auth tab confirm): Inspector `setSuppressAuthorizationNavigation` + `mcpAuth({ forceReauthorization })` | See [Mid-session auth § Core API](v2_auth_mid_session.md#core-api--handleauthchallenge) and [TUI and CLI implementation](v2_auth_mid_session.md#tui-and-cli-implementation) |
| SEP-2351 | Verify discovery after upgrade; no duplicate discovery logic in Inspector | Uses SDK `discoverOAuthProtectedResourceMetadata` today |
**Do not duplicate** SDK `auth.ts` logic (RFC 9207 decision table, AS migration edge cases, scope union helpers). Upgrade and wire.
@ -89,15 +100,29 @@ completeOAuthFlow(code) only No SDK CrossAppAccess Mid-session
2. **Wire** callback parameters, storage, and client-type metadata in Inspector.
3. **Extend** EMA and mid-session paths locally where the SDK has no EMA/XAA API.
4. **Upgrade** from `@modelcontextprotocol/sdk` v1 to v2 packages as a gated step — not a silent dependency bump.
5. **Do not unify transport layers** between web remote and TUI/CLI — web keeps `RemoteClientTransport` + node fetch intercept; TUI/CLI rely on v2 SDK transport for silent direct retry.
### Transport ownership (v2 upgrade)
| Path | Silent 401/403 + RPC retry | Interactive OAuth | Inspector integration |
| ---- | -------------------------- | ----------------- | --------------------- |
| **Web remote** | `RemoteClientTransport.postSend()` + `pushAuthState` (unchanged) | Browser redirect + resume snapshot | Node fetch intercept only (frozen stub) |
| **TUI/CLI direct (streamable HTTP)** | **v2 SDK transport** (`token()` + retry; #2265 union on 403) | Provider `redirectToAuthorization` + host callback | `handleAuthChallenge()` for EMA + defer-navigate; catch `UnauthorizedError` / `SdkHttpError` |
| **TUI/CLI direct (SSE)** | v2 SDK 401 retry only | Same as streamable | No 403 step-up on SSE |
**v2 SDK hooks (direct):** `AuthProvider` with `token()` + optional `onUnauthorized()`; full `OAuthClientProvider` adapted internally. Inspector extends `BaseOAuthClientProvider` toward the v2 contract — **not** a parallel fetch intercept on the client transport.
**Delete on upgrade:** `core/auth/mcpAuth.ts` body → re-export SDK `auth`; delete `core/auth/scopes.ts` → import `unionScopes` from SDK. **Keep:** `handleAuthChallenge()`, web `RemoteClientTransport`, node `createAuthChallengeInterceptFetch`.
### Order of work
| Step | When | Work |
| ---- | ---- | ---- |
| 1 | **Now (parallel with mid-session auth)** | Compatible prep: callback type includes optional `iss`; storage schema reserves `authorizationServerIssuer`; mid-session `saveScope(union)` per [Mid-session auth](v2_auth_mid_session.md) |
| 1 | **Now (parallel with [TUI/CLI mid-session](v2_auth_mid_session.md#tui-and-cli-implementation))** | Compatible prep: callback `iss`; storage schema reserves `authorizationServerIssuer`; provider v2-shaped hooks; direct mid-session e2e baseline; **no** client-side fetch intercept / `AuthRecoveryTransport` |
| 2 | **When v2 SDK auth PRs merge** | Upgrade to `@modelcontextprotocol/client`; run existing OAuth integration tests |
| 3 | **Immediately after upgrade** | SEP-2468 callback passthrough; SEP-2352 storage + `invalidateCredentials`; SEP-837 client type per environment |
| 4 | **Verify** | Smoke scenarios in [v2_auth_smoke_testing.md](v2_auth_smoke_testing.md); add hardening-specific cases |
| 3 | **Immediately after upgrade** | SEP-2468 callback passthrough; SEP-2352 storage + `invalidateCredentials`; SEP-837 `application_type` per environment; wire provider `onUnauthorized` → `handleAuthChallenge` where interactive defer needed |
| 4 | **TUI/CLI Phase C** | Interactive mid-session UX (Auth tab / callback server) — can overlap step 3 |
| 5 | **Verify** | Smoke scenarios in [v2_auth_smoke_testing.md](v2_auth_smoke_testing.md); direct mid-session e2e green on v2 |
Land order in the SDK (maintainer plan): **SEP-2350 → SEP-2352 → SEP-2468**, with **SEP-837** in the same auth-release series.
@ -132,9 +157,12 @@ Land order in the SDK (maintainer plan): **SEP-2350 → SEP-2352 → SEP-2468**,
#### SEP-2350 — step-up scope accumulation
- **Connect-time (direct transport):** v2 SDK transport 403 retry after [#2265](https://github.com/modelcontextprotocol/typescript-sdk/pull/2265).
- **Mid-session:** `handleAuthChallenge()` computes `authorizationScopes = union(previous, required)`; `saveScope` after success. Standard OAuth step-up = interactive; EMA step-up = silent legs 2–3 when IdP session valid.
- **401 re-login:** Replace scope set (not union) — mid-session and connect-time.
- **Web remote (mid-session):** `handleAuthChallenge()` union + `pushAuthState` — SDK transport not involved (proxy).
- **TUI/CLI direct (silent, streamable HTTP):** v2 SDK transport 403 retry with `unionScopes()` after [#2265](https://github.com/modelcontextprotocol/typescript-sdk/pull/2265).
- **Interactive step-up (all clients):** `handleAuthChallenge()` + `mcpAuth({ forceReauthorization })`; `setSuppressAuthorizationNavigation` until user confirms (web modal / TUI Auth tab); `saveScope` on `completeOAuthFlow` success.
- **EMA step-up:** silent legs 2–3 when IdP session valid — Inspector `handleAuthChallenge()`, not SDK transport alone.
- **401 re-login:** Replace scope set (not union) — all clients.
- **SSE:** 401 only; no 403 step-up in v1 or v2 SDK.
#### SEP-2351 — discovery suffix
@ -148,6 +176,8 @@ Land order in the SDK (maintainer plan): **SEP-2350 → SEP-2352 → SEP-2468**,
- **Client credentials grant** ([#1225](https://github.com/modelcontextprotocol/inspector/issues/1225)) — separate track.
- Full MCP **2026-07-28 stateless transport** migration (separate from auth hardening).
- Server-side scope-challenge middleware in Inspector test servers beyond what [Mid-session auth](v2_auth_mid_session.md) requires for 403 fixtures.
- **`AuthRecoveryTransport`** or client-side fetch intercept on TUI/CLI SDK transport (defer silent retry to v2 SDK; web node intercept stays).
- Unifying web remote and TUI/CLI at the transport wrapper layer.
## Testing
@ -166,12 +196,15 @@ Document manual scenarios in [v2_auth_smoke_testing.md](v2_auth_smoke_testing.md
| Area | Files |
| ---- | ----- |
| Upgrade | Root and client `package.json`; import paths v1 → v2 |
| Upgrade | Root and client `package.json`; import paths v1 → `@modelcontextprotocol/client` |
| Callback | `clients/web/src/App.tsx`, `core/auth/node/oauth-callback-server.ts`, `core/mcp/oauthManager.ts`, `core/mcp/inspectorClient.ts` |
| Provider | `core/auth/providers.ts`, `core/auth/browser/providers.ts` |
| Provider | `core/auth/providers.ts`, `core/auth/browser/providers.ts` — v2 hooks: `invalidateCredentials`, issuer-keyed storage, `application_type` |
| Storage | `core/auth/store.ts`, `core/auth/oauth-storage.ts`, `core/mcp/remote/node/remoteOAuthStorage.ts` (if landed) |
| Mid-session overlap | `core/auth/challenge.ts`, `core/mcp/oauthManager.ts` (`handleAuthChallenge`, `saveScope`) — see [Mid-session auth](v2_auth_mid_session.md) |
| Tests | `clients/web/src/test/core/auth/`, `clients/web/src/test/integration/mcp/inspectorClient-oauth*.test.ts` |
| Web remote (unchanged on v2) | `core/mcp/remote/remoteClientTransport.ts`, `core/mcp/node/authChallengeFetch.ts`, `core/mcp/remote/node/server.ts` |
| TUI/CLI | [Mid-session § TUI and CLI](v2_auth_mid_session.md#tui-and-cli-implementation): `clients/tui/src/App.tsx`; `clients/cli/src/cliOAuth.ts`; provider wiring in `core/mcp/inspectorClient.ts` |
| Mid-session overlap | `core/auth/challenge.ts`, `core/mcp/oauthManager.ts` (`handleAuthChallenge`, `saveScope`) |
| Delete on upgrade | `core/auth/mcpAuth.ts` (re-export SDK), `core/auth/scopes.ts` (use SDK `unionScopes`) |
| Tests | `inspectorClient-oauth*.test.ts`, `inspectorClient-oauth-direct-mid-session-e2e.test.ts` (planned) |
| Smoke doc | `specification/v2_auth_smoke_testing.md` |
## Related specs

View file

@ -1,502 +1,560 @@
# Inspector V2 — Mid-session authorization
# Inspector V2 Auth — Mid-session authorization
### [Brief](README.md) | [V2 Scope](v2_scope.md) | [Auth hardening](v2_auth_hardening.md) | [EMA / XAA](v2_auth_ema.md) | [Smoke testing](v2_auth_smoke_testing.md)
### [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 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 | [Smoke testing](v2_auth_smoke_testing.md)
Design for **mid-session authorization** in Inspector: detecting when MCP traffic needs new or elevated credentials, responding with the correct OAuth or EMA flow, and restoring the connection — across web, TUI, and CLI.
Design and implementation reference for **mid-session authorization** in Inspector: detecting when MCP traffic needs new or elevated credentials, running the correct OAuth or EMA recovery flow, and restoring the session — across **web**, **TUI**, and **CLI**.
This spec generalizes beyond **expired access tokens** to include **step-up authorization** (e.g. a tool call returns **403** with `error="insufficient_scope"` and the scopes required for that operation — see [SEP-2350](#sep-2350-step-up-authorization) below).
For hands-on verification, see [v2_auth_smoke_testing.md §5](v2_auth_smoke_testing.md#5-mid-session-auth--step-up--manual-validation).
---
## Summary
Inspector v2 already supports **connect-time** OAuth and EMA:
Inspector already supports **connect-time** OAuth and EMA: the first `connect()` that hits **401** triggers `authenticate()` / browser redirect / loopback callback, and tokens land in per-client storage.
- `InspectorClient.authenticate()` / `completeOAuthFlow()` ([V2 Scope](v2_scope.md))
- Connect-time 401 handling in web `App.tsx` and TUI Auth tab hints
- EMA legs 2–3 refresh via `EmaTransportOAuthProvider` when a **live** `OAuthClientProvider` is on the transport ([EMA spec](v2_auth_ema.md))
**Mid-session authorization** covers everything **after** that: a token expires, is revoked, or lacks scopes for a specific operation while the user is working. The server signals this with HTTP **401** (bad/missing token) or **403** + `insufficient_scope` (valid token, not enough scope — **step-up**). Inspector normalizes those signals into an **`AuthChallenge`**, runs **`handleAuthChallenge()`**, and either refreshes silently or starts interactive OAuth.
**Gap:** Authorization can fail whenever the MCP server rejects the credentials in use — **during** `connect()` (including reconnect with a stored token snapshot), **after** a successful connect (expired or revoked tokens), or on **insufficient scope** for a specific request. The web client compounds this: MCP runs on the Hono backend with a **token snapshot** at connect time (`createTokenAuthProvider`), so the backend cannot complete interactive OAuth or reliable silent refresh on its own.
| Client | Transport model | Auth runs where | Recovery highlights |
| ------ | --------------- | --------------- | ------------------- |
| **Web** | Browser → Hono **remote** backend → MCP server | Browser (`OAuthManager`) | `POST /api/mcp/auth-state` hot-swap; full-page OAuth + **resume snapshot** |
| **TUI / CLI** | **Direct** SDK transport in-process | Node (`OAuthManager`) | Loopback callback on `127.0.0.1:6276`; user retries action after interactive auth |
This spec defines:
All three clients share **`core/auth/challenge.ts`** and **`OAuthManager.handleAuthChallenge()`**. UX and wire protocol differ by client.
1. A normalized **`AuthChallenge`** model (what went wrong + what is required).
2. A single core entry point **`handleAuthChallenge()`** (how Inspector responds).
3. **Remote event propagation** so the browser can run auth and reconnect.
4. **Phased delivery** (recovery → step-up → client parity → RPC replay) — see [Architecture](#architecture).
---
## Architecture
## Background
### Code layout
### Connect-time vs mid-session
- Challenge types live in `core/auth/challenge.ts`.
- `OAuthManager.handleAuthChallenge()` implements the handler; `InspectorClient` exposes a delegating wrapper.
- Web orchestration starts in `App.tsx`; extract `clients/web/src/utils/authChallengeFlow.ts` if wiring exceeds ~50 lines.
| | Connect-time | Mid-session |
| --- | --- | --- |
| **When** | First connect with no valid tokens, or reconnect before a remote session exists | Any MCP RPC **after** credentials were supplied |
| **Typical signal** | `connect()` throws **401** | MCP HTTP **401/403** on an in-flight request |
| **Web handler** | `App.tsx` → `authenticate()` → redirect | `handleAuthChallenge()` → `auth-state` push and/or redirect |
| **Core handler** | Same OAuth primitives | `OAuthManager.handleAuthChallenge()` |
### Web: detection and wire protocol
Both paths use the same token storage, refresh, and authorization-code exchange; only **detection** and **session update** mechanics differ.
- **Backend detection:** fetch wrapper on the transport passed to `createTransportNode` — intercept the MCP HTTP `Response` before the SDK consumes it, parse `WWW-Authenticate`, emit an SSE `auth_challenge` event. The frozen `createTokenAuthProvider` stub cannot complete interactive OAuth; do not rely on stub `auth()`.
- **SSE event type:** dedicated `auth_challenge` (not `transport_error`).
- **Browser dispatch:** `InspectorClient` `authChallenge` typed event.
- **HTTP status helpers:** `isAuthChallengeError()` for mid-session 401 and 403; `isUnauthorizedError()` remains for connect-time 401 only.
- **Post-recovery:** `disconnect()` → `connect()` to re-snapshot tokens to the backend. No token-push API in v1.
- **Deduplication:** in-memory per session, keyed by `reason` + sorted `requiredScopes`; suppress duplicates until satisfied or scopes change.
- **Multi-tab:** duplicate modals are acceptable until Phase 4 `RemoteOAuthStorage`; then `navigator.locks.request()` single-flight per server URL inside `handleAuthChallenge()`.
### Step-up authorization (SEP-2350)
### TUI / CLI: detection
**Step-up** is OAuth for “I have a token, but not permission for *this* operation.” MCP servers usually express it as:
- Same `handleAuthChallenge()` entry via a transport fetch wrapper, before the SDK auth retry path.
- Intercept 401 and 403 on streamable HTTP; run union scopes in `handleAuthChallenge()` for step-up. Do not rely on the SDK built-in 403 retry (challenge-only scope, no SEP-2350 union).
- Legacy SSE transport: 401 only (no 403 step-up in SDK).
- Replace TUI `show401AuthHint` with the `authChallenge` event (Phase 4).
- Phase A: rely on SDK in-flight retry where applicable. Phase C adds explicit pending-RPC replay.
```http
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope", scope="weather:read"
```
The SDK (`@modelcontextprotocol/sdk` 1.29.0) auto-retries 401/403 on streamable HTTP `send()` but does not union scopes for step-up — Inspector owns SEP-2350 union in `handleAuthChallenge()`.
By contrast, **401** means the token is missing, invalid, or expired — fix by refresh or full re-login.
### Scope and EMA
**[SEP-2350](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2350)** (in the MCP 2026-07-28 authorization hardening) requires clients to **accumulate scopes** on step-up:
- **Previously requested scopes:** `OAuthStorage.scope` / `saveScope()` per server.
- **After successful authorize:** `saveScope(authorizationScopes)`; step-up uses union; 401 re-login replaces scope.
- **EMA mint scopes on 401 refresh:** challenge `scope` from `WWW-Authenticate` if present, else configured `oauth.scope`, else PRM `scopes_supported` (`resolveEmaScopes` order).
- **EMA step-up (valid IdP session):** silent legs 2–3 re-mint with `authorizationScopes` (union). Same toast as 401 refresh. No modal, no resource-AS redirect. Resource MCP scopes are on leg 2/3 token requests, not leg 1 (`openid offline_access` only).
| Situation | Client scope behavior |
| --------- | --------------------- |
| **403** step-up | **Union** previously requested scopes with scopes from the challenge — do not drop scopes needed for other tools |
| **401** re-login | **Replace** scope set (user may down-scope at the AS) |
### RPC retry
Inspector persists granted scopes in `OAuthStorage.scope` (`saveScope()`): when the authorization server returns a `scope` parameter on the token response, that value is **authoritative** (RFC 6749 §5.1 — including when the grant is a subset of what was requested). When `scope` is **omitted** on a successful response, granted scope is assumed to equal what was requested for that exchange. Storage must never claim scopes the access token was not granted.
- After every successful recovery (401 refresh, EMA re-mint, step-up), **retry the failed MCP request**. Phases 1–3 may ship without auto-retry; Phase 5 queues `AuthChallenge.context.pendingRequest` and replays once after `satisfied` + reconnect (bounded).
For step-up, `handleAuthChallenge()` computes `authorizationScopes` as the union of stored grant + challenge scopes for the **authorization request**; `saveScope()` runs only after a successful grant, using the rules above — not the pre-redirect requested union when the AS returned an explicit smaller `scope`.
### UX
**UX consequence:** standard-OAuth and **web EMA** step-up need **user-visible consent** before proceeding (web modal, TUI Auth tab confirm, CLI **y/N**). On the web client, EMA `insufficient_scope` shows the same **`StepUpAuthModal`** pattern as standard OAuth, with organization/IdP copy; only after **Authorize** does Inspector run silent re-mint or start an IdP redirect. TUI/CLI may still re-mint silently after their own confirm prompt — see [EMA step-up (web)](v2_auth_ema.md#ema-step-up-web-confirmation).
| Situation | Behavior |
| --------- | -------- |
| Silent recovery (refresh / EMA re-mint / EMA step-up) | Brief toast: “Refreshing authorization…” |
| **401** — interactive re-auth required | Toast “Session expired, re-authenticating…” → auto-start redirect (same as connect-time). No confirm modal. |
| **403** step-up — standard OAuth | Blocking modal: scopes, tool context, **Authorize** / **Cancel** |
| **Cancel** on standard-OAuth step-up modal | Stay connected. Failed tool shows error. Other scoped operations may still work. Do not disconnect. |
| **401** — user aborts IdP redirect or callback fails | Stay connected (degraded). Persistent re-auth banner. Auth-gated calls fail until recovery. Do not auto-disconnect. |
| Hard failure (`kind: "failed"`) | Persistent error toast |
**Rationale (Inspector as a testing tool):** EMA can often satisfy scope upgrades without a visible IdP prompt when the organization session is still valid. That is convenient in production clients, but Inspector deliberately surfaces the requested scopes first so operators can see *what* permission elevation is happening while exploring MCP servers — the same visibility standard OAuth step-up already provides on web.
TUI: standard-OAuth step-up uses Auth tab message + Cancel semantics. EMA step-up is silent. CLI mirrors when OAuth is wired (Phase 4).
Normative background: [MCP authorization — Step-Up Authorization Flow](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#step-up-authorization-flow), [Runtime Insufficient Scope Errors](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#runtime-insufficient-scope-errors), [RFC 6750 §3.1](https://datatracker.ietf.org/doc/html/rfc6750#section-3.1).
Connection Info showing effective vs pending scopes is out of scope for v1.
### When silent recovery fails
Silent path = refresh token grant (standard OAuth) or EMA legs 2–3 re-mint (valid IdP session). Falls through to interactive when silent cannot succeed:
| Protocol | Silent fails when |
| -------- | ----------------- |
| Standard OAuth | No `refresh_token`; refresh token expired/revoked; AS rejects refresh; no tokens in storage |
| EMA | No IdP session; legs 2–3 mint error (bad resource client creds, AS/network errors) |
| Step-up (403) | Standard OAuth: interactive consent (modal + resource-AS redirect). EMA (valid IdP session): silent legs 2–3 re-mint — same as 401 refresh |
| Web | Silent runs in the browser after SSE `auth_challenge`; the backend cannot refresh frozen tokens |
### Test infrastructure
Extend `test-server-oauth.ts` with scope-check middleware that returns **403** + `insufficient_scope` for scoped tool routes.
## Normative references
- [MCP authorization (2025-11-25)](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization) — current stable; includes [Step-Up Authorization Flow](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#step-up-authorization-flow) and [Runtime Insufficient Scope Errors](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#runtime-insufficient-scope-errors)
- [MCP authorization (draft — 2026-07-28 RC target)](https://modelcontextprotocol.io/specification/draft/basic/authorization) — upcoming auth hardening; [release candidate announcement](https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/) lists six authorization SEPs including **SEP-2350**
- **[SEP-2350](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2350)** — *Clarify client-side scope accumulation in step-up authorization* ([issue #2349](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/2349)); merged into the draft spec's step-up flow
- [EMA extension](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization) — see [v2_auth_ema.md](v2_auth_ema.md)
- OAuth 2.0 Bearer Token Usage (`WWW-Authenticate`, `insufficient_scope`) — [RFC 6750 §3.1](https://datatracker.ietf.org/doc/html/rfc6750#section-3.1)
- `@modelcontextprotocol/sdk` **1.29.0** (pinned in repo) — `StreamableHTTPClientTransport` invokes SDK `auth()` on **401** and **403 `insufficient_scope`** during `send()` when an `authProvider` is attached; legacy `SSEClientTransport` handles **401 only** on `send()` (no 403 step-up)
## Goals
### Phase A — Mid-session token recovery (implement first)
- **One core path** for all clients: parse or receive a challenge → `handleAuthChallenge()` → updated tokens or interactive auth.
- **Web remote architecture:** backend detects and **emits** challenges; browser **handles** auth (never runs OAuth redirects on the server).
- Support **token refresh** (silent when possible) for **401 / invalid or expired tokens** at runtime.
- **EMA-aware:** re-run legs 2–3 when the resource token expires; leg 1 only when IdP session is missing or expired ([EMA 401 rules](v2_auth_ema.md)).
- Preserve existing connect-time OAuth behavior — no regressions.
### Phase B — MCP step-up authorization (after Phase A)
- Implement **[SEP-2350](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2350)** client-side scope accumulation when servers emit runtime **`403 insufficient_scope`** challenges per the [draft Step-Up Authorization Flow](https://modelcontextprotocol.io/specification/draft/basic/authorization#step-up-authorization-flow).
- Align with the upcoming **2026-07-28** MCP authorization revision (Inspector targets the draft semantics even while pinned to SDK/spec `2025-11-25` today).
### Phase C — Pending RPC retry (after Phase A recovery works)
- After any successful auth recovery (401 refresh, EMA re-mint, step-up), **automatically retry the MCP request that failed**.
- Phases 1–3 may ship without auto-retry; Phase 5 adds queued replay of `AuthChallenge.context.pendingRequest`.
## Non-goals
- v1 / v1.5 backport (v2 only).
- **Client credentials grant** ([#1225](https://github.com/modelcontextprotocol/inspector/issues/1225)) — separate track.
- **SAML** EMA leg 1 — out of scope per EMA spec.
- **IdP RP-initiated logout (end-session)** — local sign-out only today; see EMA spec §Future.
- Defining MCP server or authorization-server wire formats — Inspector consumes whatever the SDK and HTTP responses expose; extensibility hooks documented below.
---
## Terminology
| Term | Meaning |
| ---- | ------- |
| **Auth challenge** | Structured description of why authorization failed and what is required to proceed. Not raw HTTP. |
| **`handleAuthChallenge()`** | Core orchestrator: given a challenge, attempt silent satisfaction, else start interactive auth. |
| **Connect-time auth** | First authorization during `InspectorClient.connect()` when **no** token snapshot is sent (web: `App.tsx` → `authenticate()` on plain 401). Already implemented for that path. |
| **Mid-session auth** | Any authorization failure **after** tokens were supplied to the transport — including reconnect with a stored snapshot, post-connect RPCs, expiry, revocation, and step-up scopes. **This spec.** |
| **Step-up auth** | MCP [Step-Up Authorization Flow](https://modelcontextprotocol.io/specification/draft/basic/authorization#step-up-authorization-flow): token is valid but **insufficient scope** for the current operation. Runtime signal is typically **HTTP 403** + `WWW-Authenticate: Bearer error="insufficient_scope"`. Governed by **[SEP-2350](#sep-2350-step-up-authorization)**. |
| **Recover / refresh** | Informal shorthand for satisfying a **`token_expired`** (or similar) challenge without user interaction when refresh or EMA re-mint succeeds. Prefer **`handleAuthChallenge`** in API names. |
| **Token snapshot** | Web-only: OAuth tokens copied into `POST /api/mcp/connect` and frozen in `createTokenAuthProvider` on the backend. |
| **App tab** | An Inspector **view** inside one browser window: Tools, Resources, Prompts, etc. (`activeTab` in `App.tsx`). Not a browser tab. |
| **Browser tab** | A top-level browsing context (window/tab) running the Inspector SPA. Each has its own `InspectorClient`, remote session, and OAuth context. |
| **Auth challenge** | Normalized `{ reason, requiredScopes?, … }` — not raw HTTP. Built by `parseAuthChallengeFromResponse()` or received on the remote wire. |
| **`handleAuthChallenge()`** | Core orchestrator: silent refresh / EMA re-mint, or start interactive OAuth. Returns `satisfied`, `step_up_confirm` (EMA/web deferral), `interactive`, or `failed`. |
| **`RemoteAuthState`** | Payload for `POST /api/mcp/auth-state`: fresh `oauthTokens` (+ optional `oauthClient`) pushed to the node backend without tearing down the MCP transport. |
| **Command-scoped recovery** | Failure during an active user action (tool call, connect button). May **retry the same JSON-RPC** once after silent recovery. |
| **Ambient recovery** | Failure with no correlated in-flight command (idle SSE). Prepares the session for the **next** user action — no auto-retry of a specific RPC. |
| **OAuth resume snapshot** | `sessionStorage` blob written before a **full-page** OAuth redirect so the web app can restore server, app tab, and form state after callback. |
## Current architecture (why mid-session fails on web)
---
## Architecture
### Two transport models
Inspector implements mid-session auth twice, by design — not as a shared transport wrapper.
```text
TUI / CLI Web
───────── ───
InspectorClient InspectorClient (browser)
└─ live OAuthClientProvider └─ live OAuthClientProvider
└─ MCP SDK transport └─ RemoteClientTransport.start()
└─ 401 → provider.auth() └─ snapshots tokens once
└─ Hono backend
└─ createTokenAuthProvider (frozen)
└─ MCP SDK transport
└─ 401 → stub cannot refresh/redirect
Web remote (browser proxy):
Browser RemoteClientTransport
→ POST /api/mcp/send
→ Hono RemoteSession + fetch intercept (frozen stub OAuth provider on node)
→ upstream MCP server
Recovery: handleAuthChallenge() in browser → POST /api/mcp/auth-state → optional send retry
TUI / CLI direct:
InspectorClient + live OAuthClientProvider on SDK StreamableHTTPClientTransport
Recovery: handleAuthChallenge() in-process → token swap / reconnect
Interactive: runRunnerInteractiveOAuth() + loopback callback (127.0.0.1:6276)
```
**TUI/CLI:** OAuth authority and MCP transport live in the same process. The SDK can call `tokens()`, refresh, or fire `oauthAuthorizationRequired`.
**Why not unify?** The web node backend uses a **stub** provider (no redirects on the server). TUI/CLI hold a **live** provider. The v2 TypeScript SDK will own **silent** 401/403 retry on direct streamable HTTP ([#2265](https://github.com/modelcontextprotocol/typescript-sdk/pull/2265)); Inspector keeps **`handleAuthChallenge()`**, EMA paths, and **interactive deferral** (modal / confirm / snapshot). See [v2_auth_hardening.md](v2_auth_hardening.md).
**Web:** OAuth authority is in the browser; MCP HTTP is on the backend. Only **connect-time** 401 is wired today when **no** token snapshot is sent (`App.tsx` → `authenticate()`). When the browser **does** send a snapshot (reconnect with stored tokens, or post-OAuth `connect()` where the server still rejects the token), failures on `/api/mcp/send` — **including `initialize` during `connect()`** — do not trigger browser re-auth. The MCP SDK invokes `auth()` on the frozen `createTokenAuthProvider` stub; recovery fails and often surfaces as **HTTP 500** (e.g. SDK error *"OAuth client information must be saveable for dynamic registration"*) instead of **401**, because the stub cannot persist DCR results or run browser redirects.
**Explicit non-goals for TUI/CLI:** client-side fetch intercept on the SDK transport, `AuthRecoveryTransport`, or mirroring `RemoteClientTransport` on direct paths.
### Known failure: reconnect with stored tokens (pre–Phase 2)
### Recovery shapes
This is the same root cause as mid-session tool-call failures; only the triggering RPC differs (`initialize` during `connect()` vs e.g. `tools/list` after connected).
| Shape | When | MCP retry | Page unload |
| ----- | ---- | --------- | ----------- |
| **Silent in-process** | Refresh, EMA re-mint after user confirmed step-up (web modal **Authorize**), TUI/CLI confirm | **Yes** (command-scoped, once) | No |
| **In-app step-up confirm** | Web EMA `insufficient_scope` before re-mint / IdP | **No** — modal only; retry after success toast | No |
| **Interactive full-page** | Standard-OAuth step-up, 401 re-login, EMA IdP leg 1 (after web confirm when applicable) | **No** — user retries action after callback | Yes (web) |
| Situation | Today (web remote) | After Phase 2 |
| --------- | ------------------ | ------------- |
| No tokens in storage; user clicks **Connect** | Remote 401 → browser `authenticate()` → OAuth → connect | Unchanged (connect-time path) |
| Stored tokens present (expired, revoked, wrong registration, or server-invalidated); user clicks **Connect** | Token snapshot sent → MCP 401 → stub `auth()` → **500** / opaque SDK error; user sees **Failed to connect**, not re-auth | Fetch wrapper emits **`auth_challenge`** → browser `handleAuthChallenge()` → refresh or interactive re-auth → reconnect |
| Connected; token becomes invalid; user calls a tool | Same stub failure or opaque error on `/api/mcp/send` | Same **`auth_challenge`** → recovery → reconnect |
```text
Browser InspectorClient Hono RemoteSession MCP server
───────────────────────── ────────────────── ──────────
RemoteClientTransport StreamableHTTPClientTransport
OAuthClientProvider (live) createRemoteAuthProvider (mutable)
│ │
POST /api/mcp/send ───────────────────────►│ same transport ───────────────►│
│◄─ { ok: false, auth_challenge }──────│ │
handleAuthChallenge() │
POST /api/mcp/auth-state { authState } ───►│ setAuthState() → new Bearer │
POST /api/mcp/send (retry) ─────────────────►│ same mcp-session-id ────────►│
```
**Workaround until Phase 2:** **Clear stored OAuth state** for the server (Server Settings or Connection Info), then **Connect** again to take the no-snapshot connect-time path. Do **not** rely on proactive JWT `exp` checks at connect as a substitute for Phase 2 — they only cover clock-expired JWTs, not server-rejected tokens, opaque access tokens, or registration mismatches; challenge detection from the MCP HTTP response remains the source of truth (see [Parsing](#parsing-best-effort-extensible)).
**Why auth-state push?** An earlier approach disconnected and reconnected the remote session on every recovery, which broke upstream MCP session continuity. Hot-swapping credentials on the existing transport matches direct streamable-http behavior.
## SEP-2350 — step-up authorization
### Code map
[SEP-2350](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2350) clarifies how MCP clients should behave during **step-up authorization** — when a server rejects a request because the access token lacks scopes needed for **that specific operation**.
| Area | Primary files |
| ---- | ------------- |
| Challenge types & parsing | `core/auth/challenge.ts`, `core/auth/oauthUx.ts` |
| Handler & storage | `core/mcp/oauthManager.ts`, `core/auth/mcpAuth.ts`, `core/auth/scopes.ts` |
| EMA | `core/auth/ema/emaFlow.ts`, `core/auth/ema/resourceContext.ts` — see [v2_auth_ema.md](v2_auth_ema.md) |
| Web remote backend | `core/mcp/remote/node/server.ts`, `remote-session.ts`, `core/mcp/node/authChallengeFetch.ts` |
| Web remote client | `core/mcp/remote/remoteClientTransport.ts`, `core/mcp/inspectorClient.ts` |
| Web app | `clients/web/src/App.tsx`, `utils/oauthResume.ts`, `utils/pendingReauth.ts`, `utils/browserTabVisibility.ts`, `components/groups/StepUpAuthModal/` |
| TUI | `clients/tui/src/App.tsx`, `utils/tuiOAuth.ts` |
| CLI | `clients/cli/src/cliOAuth.ts` |
| Runner OAuth (TUI/CLI) | `core/auth/node/runner-interactive-oauth.ts`, `oauth-callback-server.ts` |
| Step-up test fixture | `test-servers/configs/oauth-step-up-demo.json`, `test-servers/src/test-server-oauth.ts` |
**Inspector implements SEP-2350 in Phase B** (after basic mid-session 401 / token-refresh handling). Until then, step-up challenges may surface as opaque tool-call failures.
### What the upcoming MCP spec requires
From the [draft authorization spec](https://modelcontextprotocol.io/specification/draft/basic/authorization) (incorporating SEP-2350):
| Situation | HTTP status | `WWW-Authenticate` | Client behavior |
| --------- | ----------- | ------------------ | --------------- |
| No token / invalid / expired token | **401** | `scope` may guide initial selection | Refresh if possible; else full (re-)authorization. **Replace** scope set on full re-login (down-scoping opportunity). |
| Valid token, insufficient scope (runtime) | **403** | `error="insufficient_scope"`, `scope="…"` per [RFC 6750 §3.1](https://datatracker.ietf.org/doc/html/rfc6750#section-3.1) | **Step-up flow** — see below. |
**Server posture (SEP-2350):** servers emit scopes needed for the **current operation only**, not the union of everything the client was ever granted. Servers remain stateless regarding client scope history.
**Client posture (SEP-2350 — Step-Up Authorization Flow step 2):**
1. Parse `WWW-Authenticate` from the 403 (or AS error) response.
2. Compute **`requiredScopes = union(previouslyRequestedScopes, challengeScopes)`** — union of the client's previously **requested** scope set and the scopes from the current challenge. Do **not** replace the prior set with the challenge scopes alone (that would drop permissions needed for other tools).
3. Initiate (re-)authorization with the union scope set.
4. Retry the original MCP request with the new token (bounded retries).
Reference implementation discussion: [python-sdk PR #2676](https://github.com/modelcontextprotocol/python-sdk/pull/2676) (403 → union; 401 → replace).
### Inspector mapping for SEP-2350
- Persist **`previouslyRequestedScopes`** per server in `OAuthStorage.scope` via `saveScope()`.
- `parseAuthChallengeFromResponse()` maps **403 + `insufficient_scope`** → `AuthChallenge { reason: "insufficient_scope", requiredScopes }`.
- `handleAuthChallenge()` for **standard OAuth**: build authorize URL with **union scopes** (interactive). For **EMA** (valid IdP session): silent legs 2–3 re-mint with **`authorizationScopes`** — resource scopes are on the leg 2/3 token requests, not leg 1 OIDC scopes.
- UX: **standard OAuth** step-up — modal (“**Tool X** needs additional permissions…”). **EMA** step-up — silent toast only (valid IdP session); mint failure surfaces error toast.
---
## Auth challenge model
### Type shape (`core/auth/challenge.ts`)
### `AuthChallenge` (`core/auth/challenge.ts`)
```typescript
/** Why authorization failed for this MCP interaction. */
export type AuthChallengeReason =
| "unauthorized" // Generic 401 — details unknown
| "token_expired" // Access token no longer accepted
| "insufficient_scope" // Step-up: more scopes required
| "invalid_token"; // Malformed or wrong audience/resource
| "unauthorized"
| "token_expired"
| "insufficient_scope"
| "invalid_token";
/** Normalized challenge for handleAuthChallenge(). */
export interface AuthChallenge {
reason: AuthChallengeReason;
/** Scopes from the current challenge (step-up). Per RFC 6750 §3.1 / MCP Runtime Insufficient Scope Errors — scopes needed for this operation. */
requiredScopes?: string[];
/**
* For step-up (SEP-2350): union of previously requested scopes and requiredScopes.
* Set by handleAuthChallenge before re-authorization; not sent on the wire.
*/
authorizationScopes?: string[];
/** Resource indicator / MCP resource URL when known (EMA RFC 8707). */
requiredScopes?: string[]; // From WWW-Authenticate scope= (this operation)
authorizationScopes?: string[]; // SEP-2350 union — set before re-auth, not on wire
resource?: string;
/** Resource authorization server audience when known. */
audience?: string;
/** Optional human-readable detail from server or SDK (for UI, not parsing). */
message?: string;
/** MCP method / tool name that triggered the challenge (for UX: “authorizing for tool X”). */
context?: {
method?: string;
toolName?: string;
/** Phase C: JSON-RPC request to replay after successful recovery. */
pendingRequest?: import("@modelcontextprotocol/sdk/types.js").JSONRPCMessage;
};
/** Opaque raw hints for logging and forward-compatible parsers. */
raw?: {
httpStatus?: number;
wwwAuthenticate?: string;
};
context?: { method?: string; toolName?: string };
raw?: { httpStatus?: number; wwwAuthenticate?: string };
}
```
### Parsing (best effort, extensible)
### Parsing
Challenge construction is **layered** — do not message-guess. Parse at the point of failure: when the MCP transport returns **401/403**, when `transport.send()` throws, or when `onerror` fires with an auth status code.
Parsing is **layered** at the point of failure — not by guessing from error messages:
1. **SDK / transport error** — preserve HTTP status / `code` (existing pattern in `core/mcp/remote/node/server.ts`; web uses `isAuthChallengeError()` for mid-session detection). Treat **401** and **403** separately per MCP [Error Handling](https://modelcontextprotocol.io/specification/draft/basic/authorization#error-handling) (401 = invalid/missing token; 403 = insufficient scope).
2. **`WWW-Authenticate` Bearer** — parse `error="insufficient_scope"`, `scope="…"`, `error="invalid_token"`, `resource_metadata="…"`, etc. from the **HTTP response headers on the failing MCP request**. See [Runtime Insufficient Scope Errors](https://modelcontextprotocol.io/specification/draft/basic/authorization#runtime-insufficient-scope-errors).
3. **Future MCP extensions** — challenge payloads attached to JSON-RPC errors; map into the same struct without changing `handleAuthChallenge()`'s signature.
1. HTTP **401/403** on the MCP response (fetch intercept or transport error).
2. **`WWW-Authenticate: Bearer`** — `error`, `scope`, `error_description` (quoted and unquoted RFC 6750 forms).
3. Embedded `authChallenge` on SDK/transport errors.
When parsing fails, use `reason: "unauthorized"` and still allow interactive re-auth.
Mapping highlights:
### Challenge vs connect-time 401
- **403** + `insufficient_scope` → `reason: "insufficient_scope"`.
- **401** + `invalid_token` → `invalid_token` or `token_expired` as appropriate.
- **401** without Bearer error → `token_expired` (coarse default for silent refresh / reauth UX).
- Parse failure → `unauthorized` (still allows interactive re-auth).
| | Connect-time (no snapshot) | Runtime / reconnect (snapshot sent) |
| --- | --- | --- |
| **When** | First connect with no stored tokens; `initialize` gets 401 before any bearer token was sent to the backend | Reconnect with stored tokens, or any MCP request after a token snapshot was frozen on the backend — **including `initialize` during `connect()`** |
| **Detection** | `connect()` throws **401** to the browser | MCP HTTP **401/403** on backend transport → **`auth_challenge`** (Phase 2); today often **500** from stub `auth()` |
| **Handler** | `authenticate()` (today) | `handleAuthChallenge()` (this spec) |
| **Web follow-up** | Redirect or silent connect | Recover tokens in browser → **disconnect + connect** to re-snapshot → Phase C replays pending RPC |
`isAuthChallengeError()` treats mid-session failures only when auth markers are present (challenge object, `WWW-Authenticate`, or `AuthChallengeError`) — not bare HTTP status alone.
Both paths may call the same underlying OAuth/EMA primitives (`authenticate()`, refresh, `completeOAuthFlow()`); only **detection** and **re-snapshot reconnect** differ. Phase 2 unifies recovery for the snapshot path; it does **not** replace the no-snapshot connect-time path.
Connect-time **401** before tokens exist still uses `isUnauthorizedError()` and `authenticate()` — separate from mid-session detection.
---
## Core API — `handleAuthChallenge()`
**Location:** `OAuthManager.handleAuthChallenge()`; `InspectorClient` exposes a delegating wrapper.
**Location:** `OAuthManager.handleAuthChallenge()`; `InspectorClient.handleAuthChallenge()` delegates.
```typescript
export type AuthChallengeOutcome =
| { kind: "satisfied" } // New tokens in OAuthStorage; caller may reconnect transport
| { kind: "interactive"; authorizationUrl: URL }
| { kind: "satisfied" }
| { kind: "interactive"; authorizationUrl: URL; challenge: AuthChallenge }
| { kind: "failed"; error: Error };
/** Satisfy an auth challenge when possible. */
async handleAuthChallenge(challenge: AuthChallenge): Promise<AuthChallengeOutcome>;
```
### Strategy by protocol and reason
### `checkAuthChallengeSatisfied(challenge)`
#### Standard OAuth (`protocol: "standard"`)
Read-only check against **current storage** (and token expiry helpers). Used before starting visible OAuth — especially when a background browser tab regains focus and another tab may have already re-authenticated. Does **not** call the authorization server.
| Reason | Silent path | Interactive path |
| ------ | ----------- | ---------------- |
| `token_expired`, `invalid_token`, `unauthorized` | SDK refresh via stored `refresh_token` when AS supports it | New authorization code flow (`authenticate()`) |
| `insufficient_scope` | Not applicable — need new consent | **[SEP-2350](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2350):** authorization URL with **`authorizationScopes`** = union(previously requested, `requiredScopes`) — not replace |
- **`token_expired` / `unauthorized`:** valid non-expired access token in storage.
- **`insufficient_scope`:** stored/token scope is a superset of required / union scopes. Empty scope on an `insufficient_scope` challenge returns **false** (do not short-circuit).
Uses existing `BaseOAuthClientProvider`, storage, and `authenticate()` / `completeOAuthFlow()`.
### Strategy by protocol
#### EMA (`protocol: "ema"`)
#### Standard OAuth
Per [EMA 401 rules](v2_auth_ema.md): **do not** fall back to standard resource-OAuth redirect.
| Reason | Silent | Interactive |
| ------ | ------ | ----------- |
| `token_expired`, `invalid_token`, `unauthorized` | Refresh via `refresh_token` when supported | Authorization code flow (`authenticate()`) |
| `insufficient_scope` | N/A | Authorize with **`authorizationScopes`** = union(previous, challenge) via `mcpAuth({ forceReauthorization: true })`; navigation **deferred** until UI confirms (web modal / TUI Auth / CLI prompt) |
| Reason | Silent path | Interactive path |
| ------ | ----------- | ---------------- |
| `token_expired`, `unauthorized` (resource token) | `refreshEmaResourceTokens()` / legs 2–3; scopes: challenge `WWW-Authenticate` scope → configured `oauth.scope` → PRM `scopes_supported` | — |
| IdP session missing / expired | — | Leg 1 IdP OIDC redirect (`startEmaIdpAuthorization`) then legs 2–3 |
| `insufficient_scope` | **Silent:** re-mint legs 2–3 with **`authorizationScopes`** (union) | **Leg 1** IdP redirect when IdP session invalid — separate from step-up UX; then legs 2–3 with union scopes |
Union scope is held in `pendingAuthorizationScope` until `completeOAuthFlow()` succeeds; cleared on failure. On success, `saveScope()` stores the AS-granted scope (explicit `scope` on the token response, or the requested union when `scope` is omitted per RFC 6749 §5.1).
Uses `EmaTransportOAuthProvider`, `emaFlow.ts`, and `resourceContext.ts` (extend scope resolution to prefer challenge scopes when present).
#### EMA
### After `kind: "satisfied"`
Per [v2_auth_ema.md](v2_auth_ema.md): **no** fallback to standard resource-OAuth redirect.
| Client | Action |
| ------ | ------ |
| **TUI / CLI** | Live provider on transport; SDK may retry in flight. Phase C: replay `context.pendingRequest` if set. |
| **Web** | **`disconnect()` → `connect()`** to re-snapshot tokens. Phase C: replay `context.pendingRequest` after reconnect. |
| Reason | Silent | Interactive |
| ------ | ------ | ----------- |
| Resource token expired / unauthorized | Legs 2–3 re-mint | — |
| IdP session missing | — | Leg 1 IdP OIDC redirect, then legs 2–3 |
| `insufficient_scope` | After user confirms (web modal / TUI / CLI), re-mint legs 2–3 with union scopes when IdP session valid | IdP redirect if IdP session invalid |
Until Phase C, the failed tool call may still require a manual retry after recovery.
On **web**, `handleAuthChallenge()` returns **`step_up_confirm`** for EMA `insufficient_scope` until the user clicks **Authorize** in `StepUpAuthModal`; it does **not** call `trySilentEmaAuth()` before that. After confirm, silent re-mint runs in-process when possible; otherwise IdP redirect + callback (same as before).
### After `kind: "interactive"`
### Outcomes — what callers do
Same as connect-time today:
| Outcome | Web remote | TUI / CLI direct |
| ------- | ---------- | ---------------- |
| **`satisfied`** | `pushAuthState()`; command-scoped: **retry send once** | Reconnect or SDK retry (v2); command wrapper may retry RPC |
| **`step_up_confirm`** | Throw `AuthRecoveryRequiredError` (`emaStepUpConfirm`) → `StepUpAuthModal` (EMA copy) | N/A (web-only deferral) |
| **`interactive`** | Throw `AuthRecoveryRequiredError` (enriched challenge) → App shows modal or redirect + snapshot | `AuthRecoveryRequiredError` or `authChallengeInteractive` → callback server flow |
- **Web (401):** toast → auto-redirect → `/oauth/callback` → `completeOAuthFlow()` → reconnect.
- **Web (403, standard OAuth):** modal → on Authorize, stash pending server id → redirect → `/oauth/callback` → `completeOAuthFlow()` → reconnect.
- **EMA (IdP session missing or expired):** leg 1 IdP redirect → callback → legs 2–3 → reconnect.
- **TUI / CLI:** `oauthAuthorizationRequired` → browser → callback → `completeOAuthFlow()` → reconnect if needed.
**InspectorClient events (direct transport):** `authChallengeAmbient` (idle SSE / ambient recovery), `authChallengeCommand` (command-scoped direct recovery — no ambient toast), `authChallengeInteractive`, `authChallengeRecovered`.
| **`failed`** | Toast / banner; stay connected (degraded) | Error message; stay connected (TUI) or exit non-zero (CLI one-shot) |
### After `kind: "failed"` or user **Cancel** (standard-OAuth step-up modal only)
---
Do **not** disconnect the MCP session for recoverable challenges.
## Web implementation
| Reason | Cancel / failed outcome |
| ------ | ------------------------ |
| `insufficient_scope` (standard OAuth, user Cancelled) | Stay connected; failed tool shows error; other scoped operations may still work |
| `insufficient_scope` (EMA, silent path) | No Cancel — auto re-mint; on mint failure → `kind: "failed"` toast, stay connected (degraded) |
| `token_expired` / `unauthorized` | Stay connected (**degraded**); banner to re-authenticate; auth-gated calls fail until recovery |
### Detection and delivery
## Remote wire protocol (web)
**Backend** (`core/mcp/remote/node/`):
Backend **reports** challenges; browser **handles** them.
- **Auth-challenge intercept fetch** on the MCP HTTP transport: on **401/403**, parse headers, throw `AuthChallengeError` **before** the stub provider invokes SDK `auth()`.
- **`createRemoteAuthProvider`**: mutable credentials; `RemoteSession.setAuthState()` updates the upstream Bearer without new `connect()`.
### Detection
**Dual delivery — one channel per incident:**
Auth challenges are detected when MCP traffic fails — on the HTTP response from the MCP server, in `transport.send()` error handling, or in transport `onerror`. The backend emits a structured event; the browser runs `handleAuthChallenge()`.
| Path | Trigger | Wire | Client behavior |
| ---- | ------- | ---- | --------------- |
| **Command-scoped** | Active `POST /api/mcp/send` | HTTP **200** `{ ok: false, kind: "auth_challenge", authChallenge }` | `handleAuthChallenge()` → `pushAuthState()` → **retry same JSON-RPC once** |
| **Ambient** | Transport error while idle | SSE `auth_challenge` | `handleAmbientAuthChallenge()` → push auth state; **no RPC retry** |
```text
MCP SDK transport (RemoteSession on Hono backend)
└─ HTTP 401/403 or SDK auth failure on send / stream
└─ parseAuthChallengeFromResponse() at this hook
└─ RemoteSession.pushEvent({ type: "auth_challenge", data })
└─ SSE → browser RemoteClientTransport
└─ InspectorClient → handleAuthChallenge()
```
`authReturnedViaHttp` prevents duplicating a command-scoped challenge on SSE.
#### Web — detection (`core/mcp/remote/node/`)
Inspector API **4xx** are reserved for malformed requests / missing session — not for upstream MCP token expiry.
Inside an active `RemoteSession`, when MCP traffic fails with an auth error:
- **Fetch wrapper on the backend transport** — wrap the fetch passed to `createTransportNode`. Intercept the MCP HTTP `Response` **before** the SDK consumes it. On **401** or **403**, parse `WWW-Authenticate`, emit `auth_challenge`, and do not let the SDK call `auth()` on the frozen `createTokenAuthProvider` stub.
- **`/api/mcp/send`** — extend to preserve **403** and map stub-auth failures to structured errors (today: **401** only; stub failures often return **500**).
- **Transport `onerror`** — secondary path when the SDK reports auth-related failures without a parseable response (preserve status/code; do not collapse to generic 500).
Parse `WWW-Authenticate` from the response headers on the failing request.
Do **not** confuse MCP server OAuth with Inspector launcher auth (`x-mcp-remote-auth` on requests to the Hono API — that is session auth to the remote backend, not MCP server OAuth).
#### TUI / CLI — detection (direct transport)
Same **`handleAuthChallenge()`** entry via **transport fetch wrapper** (before SDK auth retry):
- Intercept **401** and **403** on streamable HTTP; run `handleAuthChallenge()` with SEP-2350 union scopes for step-up. Do **not** rely on SDK built-in 403 retry alone.
- Legacy **SSE** transport: **401** only (no 403 step-up in SDK).
- Dispatch **`authChallenge`** on `InspectorClient` (Phase 4 replaces TUI `show401AuthHint`).
- **`oauthAuthorizationRequired`** fires when `handleAuthChallenge()` returns `interactive`.
### SSE event
Extend `RemoteEvent` in `core/mcp/remote/types.ts`:
### `POST /api/mcp/auth-state`
```typescript
export interface RemoteAuthChallengeEvent {
type: "auth_challenge";
data: AuthChallenge & {
/** Server catalog id — browser resolves InspectorClient instance. */
serverId?: string;
};
interface RemoteSetAuthStateRequest {
sessionId: string;
authState: RemoteAuthState;
}
interface RemoteAuthState {
oauthTokens?: { access_token; token_type; refresh_token?; scope?; … };
oauthClient?: { client_id; client_secret? }; // reserved for future server-side refresh
}
```
**Rules:**
Called by `RemoteClientTransport.pushAuthState()` after browser-side recovery. Seeding uses the same shape on `POST /api/mcp/connect`.
- Emit **once per recoverable 401/403 auth challenge** (dedupe per [Architecture §Web: detection and wire protocol](#web-detection-and-wire-protocol)).
- Do **not** mark transport dead for recoverable auth challenges unless the SDK closed the connection.
- Include `requiredScopes` when parsed from `WWW-Authenticate`.
- Attach **`context.pendingRequest`** when the failing RPC is known (Phase C).
### App orchestration (`clients/web/src/App.tsx`)
### Browser handling
**Command-scoped paths** (tool, prompt, resource, app) share `handleCommandScopedAuthRecovery()`:
1. `RemoteClientTransport` receives `auth_challenge` on SSE.
2. `InspectorClient` dispatches **`authChallenge`**.
3. App calls `handleAuthChallenge(challenge)` (via `authChallengeFlow.ts` once extracted).
4. On `satisfied` or post-callback success: reconnect active server; Phase C replays pending RPC.
5. UX per [Architecture §UX](#ux).
- Standard-OAuth **or EMA** step-up → `StepUpAuthModal` (defer redirect / re-mint until **Authorize**).
- 401 / EMA IdP (non-step-up) → `prepareOAuthRedirect()` (auto-redirect + snapshot).
- Background browser tab hidden → defer to `pendingReauth`; resume on `visibilitychange` with `checkAuthChallengeSatisfied` first.
**Ambient path:** listens for `authChallengeInteractive` on `InspectorClient` (from SSE when silent recovery cannot complete).
**Disconnect** clears `pendingStepUp`, `pendingReauth`, and `reAuthBanner`.
### Step-up confirmation modal
Shown for **`insufficient_scope`** on **standard OAuth** and **EMA (web)** when recovery will **redirect** to an AS or **re-mint** organization permissions.
- Copy from `core/auth/oauthUx.ts` (`stepUpConfirmMessage`, `stepUpFollowUpMessage`, `stepUpModalTitle`). EMA uses organization / IdP language; standard OAuth uses resource-AS redirect language.
- Lists **`requiredScopes`** (additional scopes only — not the full SEP-2350 union).
- **Authorize (standard OAuth):** write [OAuth resume snapshot](#oauth-resume-snapshot), pre-redirect toast, `beginInteractiveAuthorization()`.
- **Authorize (EMA):** in-progress toast → `handleAuthChallenge(..., { confirmedStepUp: true })` → on `satisfied`, push auth state + success toast (retry hint when command-scoped); on `interactive`, same snapshot + IdP redirect as other EMA flows; on `failed`, error toast.
- **Cancel:** scoped by `StepUpSource` (tool / prompt / resource / app / ambient) — only the triggering panel shows error; session stays connected.
Not shown for: token refresh, 401 re-login, connect-time OAuth.
**Rationale:** Inspector is primarily a **testing and exploration** client. Surfacing step-up scopes before silent EMA re-mint makes permission elevation visible during manual validation (same UX bar as standard OAuth step-up on web). Production MCP clients may skip this confirm when silent re-mint is acceptable.
### OAuth resume snapshot
Full-page redirect (`window.location.href`) destroys in-memory React state. Before navigate, `prepareOAuthRedirect()` writes:
```typescript
interface OAuthResumeSnapshot {
version: 1;
serverId: string;
activeTab: string; // App tab id ("Tools", …)
authKind: "step_up" | "reauth";
tabUi: Partial<Record<InspectorTabId, unknown>>; // lifted *UiState shells
remoteSessionId?: string;
authChallenge?: AuthChallenge; // step-up: verify scopes after callback
}
```
Key: `mcp-inspector:oauth-resume` in `sessionStorage` (`clients/web/src/utils/oauthResume.ts`).
**Callback flow** (`InspectorClient.resumeAfterOAuth()`):
1. `completeOAuthFlow(code)`.
2. **Consume** snapshot from `sessionStorage` (read + clear — one-shot).
3. `setupClientForServer(serverId)`.
4. If `remoteSessionId` still valid: `attachToSession()` + `pushAuthState()`; else `connect()` (skipped if already connected).
5. Restore `tabUi` and `activeTab` **immediately after consume** (before async token work finishes); clear in-flight result panels. Never re-applied on later reconnect.
6. Step-up: `checkAuthChallengeSatisfied(authChallenge)` — warning toast if scopes still insufficient.
7. Success toast: step-up vs reauth copy; **user manually retries** the action (no auto-replay).
Snapshots **per app tab UI**, not message logs, tool results, or network bodies. Each snapshot is **one-shot**: written immediately before a full-page OAuth redirect, **consumed** (read + cleared from `sessionStorage`) when the `/oauth/callback` handler runs, and UI restored once at that moment. **Explicit user disconnect** clears any pending snapshot and resets `activeTab` to Servers so a later manual reconnect does not pop back to the OAuth-restored tab; transport/client teardown during connect setup does **not** clear the snapshot (so connect-time OAuth can still match the server on callback). A later manual reconnect does not read or apply a consumed snapshot.
### Multiple browser tabs
Each browser tab has its own `RemoteSession` and (today) `BrowserOAuthStorage` in `sessionStorage`. SSE `auth_challenge` is scoped to that session.
When a tab is **hidden**, **interactive** OAuth must not steal focus:
1. Set **`pendingReauth`** (in-memory) instead of modal/redirect.
2. On **`visibilitychange` → visible`:** `checkAuthChallengeSatisfied()` first; if still needed, run visible flow.
Command-scoped recovery (user clicked Run in the foreground tab) is **not** deferred.
Future: shared `RemoteOAuthStorage` → `oauth.json` may use optional `navigator.locks` around silent refresh only — see [v2_auth_ema.md §Shared storage](v2_auth_ema.md).
### Web UX reference
| Situation | Behavior |
| --------- | -------- |
| Silent refresh / EMA re-mint (after confirm) | Toast “Refreshing authorization…” or EMA in-progress toast — no modal |
| **401** interactive | Toast “Session expired…” → auto-redirect; no confirm modal |
| **403** standard-OAuth step-up | Modal → optional pre-redirect toast → redirect → callback restore |
| **403** EMA step-up (web) | Modal (organization copy) → in-progress toast → silent re-mint or IdP redirect → success toast |
| EMA IdP leg 1 (401 / expired IdP) | Toast “Re-authenticating…” → auto-redirect |
| Step-up **Cancel** | Connected; failed action shows error |
| OAuth abort / callback failure | **ReAuthBanner**; Re-authenticate uses in-session `authenticate()` when already connected (no disconnect cycle) |
| Step-up callback, scopes still insufficient | Warning toast — not green success |
| `insufficient_scope` recovery failure (non-banner reasons) | Yellow toast — not ReAuthBanner |
| Concurrent step-up while modal open | Yellow toast — complete or cancel current step-up first |
---
## TUI and CLI implementation
### Principles
- **Live provider** on the SDK transport — same process as MCP.
- **`InspectorClient.withDirectAuthRecovery()`** wraps RPCs: silent `handleAuthChallenge()` + reconnect; **`AuthRecoveryRequiredError`** for interactive.
- **`directAuthRecovery: true`** enables `interceptAuthChallenges` on `createTransportNode` until v2 SDK transport owns silent retry.
- **Interactive OAuth** uses shared **`runRunnerInteractiveOAuth()`** (`core/auth/node/runner-interactive-oauth.ts`): loopback server, browser redirect, `completeOAuthFlow()`, optional post-step-up scope check, **15-minute callback timeout** (configurable).
Default callback: `http://127.0.0.1:6276/oauth/callback` (`--callback-url` / `MCP_OAUTH_CALLBACK_URL`). Only one TUI/CLI listener on that port at a time.
CLI never spawns TUI/web for auth — completes locally or fails.
### TUI UX (`clients/tui/src/App.tsx`)
| Situation | Behavior |
| --------- | -------- |
| Silent recovery | Auth tab: “Refreshing authorization…” |
| **401** | Auto `runOAuthAuthentication()` (same as connect-time) |
| **403** standard-OAuth step-up | Switch to Auth tab; **A** authorize / **C** cancel; browser → callback |
| **403** EMA | Silent re-mint |
| Step-up on connected server | `presentStepUpForServer()` selects server + opens Auth tab |
| Reauth for affected server | `handleAuthRecoveryRequired()` switches server when needed |
| Clear OAuth | Auth tab **S** |
### CLI UX (`clients/cli/src/cliOAuth.ts`)
| Situation | Behavior |
| --------- | -------- |
| Connect-time 401 | `connectInspectorWithOAuth()` → authorize URL on stdout → callback |
| Mid-session interactive | `handleCliAuthRecoveryRequired()` / `withCliAuthRecoveryRetry()` (**one retry**) |
| **403** standard-OAuth step-up | stderr: scope message + `Proceed with step-up authorization? [y/N]` |
| Decline step-up | Exit non-zero |
| Insufficient scope after OAuth | stderr message from `stepUpInsufficientScopeMessage()` |
### SSE limitation
Legacy **SSE** transport: **401 only** on mid-session `send()` (no 403 step-up). Step-up testing targets **streamable HTTP**.
---
## Client matrix
| Concern | Web | TUI | CLI |
| ------- | --- | --- | --- |
| Challenge detection | SSE `auth_challenge` from `RemoteSession` | `InspectorClient` auth hook on live transport / provider | Same as TUI when OAuth wired |
| Auth execution | Browser `OAuthManager` | Node `OAuthManager` | Node (when implemented) |
| OAuth storage today | `BrowserOAuthStorage` (sessionStorage) | `NodeOAuthStorage` (file) | None |
| OAuth storage target | `RemoteOAuthStorage` → shared `oauth.json` ([EMA spec §Shared storage](v2_auth_ema.md)) | File | File |
| Post-success | Remote reconnect (+ Phase C RPC replay) | Reconnect / SDK retry (+ Phase C) | Same as TUI when OAuth wired |
| Step-up UX | Modal (standard OAuth); silent (EMA) | Same | Same as TUI when OAuth wired |
| EMA IdP config | Client Settings | `client.json` (Phase 4) | `client.json` (Phase 4) |
| Challenge detection | Inline send + SSE ambient | SDK transport + intercept | Same as TUI |
| Auth execution | Browser `OAuthManager` | Node `OAuthManager` | Node `OAuthManager` |
| OAuth storage | `BrowserOAuthStorage` (sessionStorage) | `NodeOAuthStorage` (file) | Same file as TUI |
| Silent recovery | `auth-state` push + send retry | Reconnect / SDK retry (v2) | One-shot RPC retry |
| Interactive recovery | Modal + full-page redirect + snapshot | Auth tab + callback | stderr **y/N** + callback |
| Step-up confirm | `StepUpAuthModal` | Auth tab **A** / **C** | **y/N** |
| EMA step-up | Web modal + confirm (then silent or IdP) | Confirm on Auth tab, then silent | **y/N**, then silent |
| Multiple browser tabs | Independent sessions; background defer | N/A | N/A |
## Relationship to other specs
---
| Doc | Relationship |
| --- | ------------ |
| [v2_auth_ema.md](v2_auth_ema.md) | EMA legs 2–3 re-mint on resource-token challenges; scope resolution; no resource-OAuth fallback |
| [v2_auth_smoke_testing.md](v2_auth_smoke_testing.md) | Manual smokes after implementation; add mid-session / step-up scenarios |
| [v2_storage.md](v2_storage.md) | Shared `oauth.json` via `RemoteOAuthStorage` |
| [v2_scope.md](v2_scope.md) | Mid-session authorization extends “OAuth Handling” |
| [v2_auth_hardening.md](v2_auth_hardening.md) | Connect-time SEPs (2468, 837, 2352, 2207, 2351); v2 SDK upgrade path; overlaps SEP-2350 scope union |
## Test infrastructure
## Phased implementation
Step-up integration tests and manual smokes use the **composable OAuth server** (`test-servers/build/server-composable.js`) with per-capability scope requirements enforced in HTTP middleware (`test-server-oauth.ts`).
Phases 1–2 deliver **Phase A** (token recovery). Phase 3 delivers **Phase B** (SEP-2350 step-up). Phase 4 is client parity and shared storage. Phase 5 delivers **Phase C** (pending RPC replay).
### How scopes work at two levels
### Phase 1 — Foundation (core + types)
| Level | Config field | Role |
| ----- | ------------ | ---- |
| **Authorization server** | `oauth.scopesSupported` | Advertises every scope the AS **may grant** (PRM / AS metadata). Inspector uses this for connect-time consent and step-up union. Must include all scopes referenced by `requiredScopes` below. |
| **Per capability** | `requiredScopes` on a tool, resource, or prompt preset ref | Scopes the **access token must already include** before that RPC succeeds. Missing scope → **403** + `WWW-Authenticate: Bearer error="insufficient_scope", scope="…"`. Omitted → only global bearer validity (401 if no/invalid token). |
- [ ] Add `AuthChallenge`, `AuthChallengeReason`, `AuthChallengeOutcome` in `core/auth/challenge.ts`
- [ ] Add `parseAuthChallengeFromResponse(...)` — **401 and 403**, `WWW-Authenticate`, SDK error
- [ ] Add `isAuthChallengeError()` in web utils
- [ ] Implement `OAuthManager.handleAuthChallenge()` for **standard OAuth** (`token_expired` / generic 401 → refresh or interactive)
- [ ] Unit tests for parser and standard-OAuth branches
**Connect-time vs step-up:** Inspector catalog OAuth scopes control what the user grants on **first connect**. The composable server may accept that token for some operations but reject others until step-up adds more scopes.
### Phase 2 — Web remote propagation (401 / token recovery)
Example flow with the sample below:
- [ ] Backend fetch wrapper: detect MCP **401/403** before frozen stub `auth()`; emit SSE **`auth_challenge`** (applies to **`/api/mcp/send` and failures during connect handshake**, e.g. `initialize`)
- [ ] Extend `/api/mcp/send` and **connect-time** transport failures for **403** and stub-auth error mapping (never surface raw SDK *saveable for dynamic registration* as an opaque **500** to the browser)
- [ ] Browser: `RemoteClientTransport` → `InspectorClient` **`authChallenge`** event → `handleAuthChallenge()`
- [ ] On satisfaction: disconnect + reconnect; wire 401 auto-redirect; standard-OAuth step-up modal
- [ ] Integration test (mid-session): invalidate access token **after** connect → challenge → reconnect → `tools/list` succeeds (manual tool retry until Phase 5)
- [ ] Integration test (reconnect): complete OAuth, invalidate access token (or use expired JWT fixture), **disconnect** → **`connect()`** → challenge → recovery → connected (must **not** throw *saveable for dynamic registration*)
- [ ] Integration test (silent refresh, web remote): static client + `refresh_token`, invalidate access token only → challenge → silent refresh → reconnect → success (mirror local `inspectorClient-oauth-e2e` refresh test via `createRemoteTransport`)
1. Catalog entry: `"oauth": { "scopes": "mcp tools:read" }` — user connects successfully.
2. `tools/call echo` — no `requiredScopes` → succeeds.
3. `tools/call get_temp` — needs `weather:read` → **403 insufficient_scope** → Inspector step-up with SEP-2350 union (`mcp`, `tools:read`, `weather:read`).
4. After step-up, `get_temp` succeeds; `resources/read` on `file:///secret.txt` still needs another step-up for `secrets:read`.
### Phase 3 — SEP-2350 step-up + EMA scope challenges (Phase B)
Step-up is enforced on **use** (`tools/call`, `resources/read`, `prompts/get`), not on discovery (`tools/list`, `resources/list`, `prompts/list`).
- [ ] Parse **403 `insufficient_scope`**; scope union via `saveScope(authorizationScopes)`
- [ ] EMA 403: silent legs 2–3 with union scopes (valid IdP session); leg 1 only when IdP session invalid
- [ ] Extend **`test-server-oauth.ts`** with **403** + `insufficient_scope` fixture
- [ ] Integration test: 403 step-up → union re-auth → tool succeeds (manual retry until Phase 5)
- [ ] Backend: **`auth_challenge`** for **403** (included in Phase 2 wrapper; verify step-up path)
### Sample composable config
### Phase 4 — Client parity + storage
Illustrates server-level `scopesSupported` plus tool-, resource-, and prompt-level `requiredScopes`. The checked-in smoke fixture [`test-servers/configs/oauth-step-up-demo.json`](../test-servers/configs/oauth-step-up-demo.json) is a **minimal** subset (tools only: `echo` + `get_temp`).
- [ ] TUI: fetch wrapper + `authChallenge` event (replace `show401AuthHint`)
- [ ] CLI: wire `environment.oauth`; same handler
- [ ] Web: `RemoteOAuthStorage` (shared `oauth.json`) + `navigator.locks` single-flight
- [ ] Multi-tab dedupe once shared storage lands
```json
{
"serverInfo": { "name": "step-up-demo", "version": "1.0.0" },
"transport": { "type": "streamable-http", "port": 8081 },
"oauth": {
"enabled": true,
"mode": "combined",
"requireAuth": true,
"scopesSupported": ["mcp", "tools:read", "weather:read", "secrets:read", "admin:write"],
"supportDCR": true,
"supportRefreshTokens": true
},
"tools": [
{ "preset": "echo" },
{ "preset": "get_temp", "requiredScopes": ["weather:read"] },
{ "preset": "add", "requiredScopes": ["admin:write"] }
],
"resources": [
{
"preset": "static_text",
"params": { "uri": "file:///secret.txt", "name": "secret" },
"requiredScopes": ["secrets:read"]
}
],
"prompts": [
{ "preset": "simple_prompt", "requiredScopes": ["weather:read"] }
]
}
```
### Phase 5 — Pending RPC replay (Phase C)
**Matching Inspector catalog entry** (connect with subset scopes so step-up is exercisable):
- [ ] Attach failing JSON-RPC to `AuthChallenge.context.pendingRequest` at detection
- [ ] After `satisfied` + reconnect (web) or satisfied on live transport (TUI/CLI): **replay once** (bounded)
- [ ] Integration tests: 401 refresh, EMA re-mint, and 403 step-up all replay the original tool call
- [ ] On replay failure: surface tool error; do not loop
```json
"oauth-step-up-demo": {
"type": "streamable-http",
"url": "http://127.0.0.1:8081/mcp",
"oauth": { "scopes": "mcp tools:read" }
}
```
## Testing
**Run the server:**
| Layer | What to prove |
| ----- | ------------- |
| Unit | Challenge parsing; scope merge; EMA scope preference over config |
| Integration (local AS) | Expired token → silent refresh → success (TUI direct transport) |
| Integration (web remote, mid-session) | Invalidate token after connect → SSE `auth_challenge` → reconnect → `tools/list` |
| Integration (web remote, reconnect) | Invalidate/expired token before `connect()` with stored snapshot → challenge → recovery → connected (no stub DCR **500**) |
| Integration (web remote, refresh) | Invalidate access token only; `refresh_token` present → silent refresh → reconnect |
| Integration (Phase C replay) | 401 / EMA / 403 recovery → original tool call replays automatically |
| Integration (SEP-2350 step-up) | MCP server returns **403** `insufficient_scope` → union re-auth → retried tool call |
| EMA | Invalidate resource JWT only; legs 2–3 re-run; IdP session still valid |
| Manual | Document in [v2_auth_smoke_testing.md](v2_auth_smoke_testing.md) §Mid-session auth |
```bash
cd clients/web && npm run test-servers:build
node ../../test-servers/build/server-composable.js \
--config ../../test-servers/configs/oauth-step-up-demo.json
```
## File touch list (expected)
### Enforcement (HTTP middleware)
| Area | Files |
| ---- | ----- |
| Types | `core/auth/challenge.ts` |
| Handler | `core/mcp/oauthManager.ts`, `core/auth/ema/emaFlow.ts`, `core/auth/ema/resourceContext.ts` |
| Remote | `core/mcp/remote/types.ts`, `core/mcp/remote/node/remote-session.ts`, `core/mcp/remote/node/server.ts`, `core/mcp/remote/remoteClientTransport.ts`, transport fetch wrapper in `core/mcp/node/transport.ts` |
| Web app | `clients/web/src/App.tsx`, `clients/web/src/utils/authChallengeFlow.ts`, `clients/web/src/utils/oauthFlow.ts` (`isAuthChallengeError`) |
| TUI | `clients/tui/src/App.tsx` |
| Test server | `test-servers/src/test-server-oauth.ts` |
| Tests | `clients/web/src/test/integration/mcp/inspectorClient-oauth-e2e.test.ts`, new remote auth-challenge + Phase C replay tests |
On each MCP request, after bearer validation:
1. Parse JSON-RPC `method` and `params`.
2. Look up `requiredScopes` from the registry built at startup:
| MCP method | Registry key |
| ---------- | ------------ |
| `tools/call` | tool `name` (`params.name`) |
| `resources/read` | resource `uri` (`params.uri`) |
| `prompts/get` | prompt `name` (`params.name`) |
| `resources/templates/read` | template name or URI from `params` |
3. Compare granted scopes on the access token (stored at token issue time in combined mode) against `requiredScopes`.
4. If any required scope is missing, respond **403** with:
```http
WWW-Authenticate: Bearer error="insufficient_scope", scope="weather:read"
```
(`scope=` lists the missing scope(s); body is a JSON-RPC error envelope.)
`requiredScopes` on preset refs is merged in `resolve-config.ts`; no application-code routes — config drives behavior.
### Verification
**Automated (high level):** challenge parsing, scope union, `checkAuthChallengeSatisfied`, `oauthResume`, `runRunnerInteractiveOAuth`; integration suites `inspectorClient-oauth-remote-mid-session-e2e.test.ts`, `inspectorClient-oauth-direct-mid-session-e2e.test.ts`, CLI `oauth-interactive.test.ts` / `cliOAuth.test.ts`.
**Manual:** [v2_auth_smoke_testing.md §5](v2_auth_smoke_testing.md#5-mid-session-auth--step-up--manual-validation) — required gate **W1 + W5–W7**, **T1–T2 + T4**, **C1–C2**.
---
## Related specifications
| Document | Relationship |
| -------- | ------------ |
| [v2_auth_hardening.md](v2_auth_hardening.md) | Connect-time SEPs; v2 SDK upgrade; direct transport silent retry delegation |
| [v2_auth_ema.md](v2_auth_ema.md) | EMA legs 2–3 re-mint; scope resolution; no resource-OAuth fallback |
| [v2_auth_smoke_testing.md](v2_auth_smoke_testing.md) | Manual OAuth and mid-session validation procedures |
| [v2_storage.md](v2_storage.md) | Target: shared `oauth.json` via `RemoteOAuthStorage` on web |
---
## Future work
- **Web default `RemoteOAuthStorage`** — shared `oauth.json` with TUI/CLI; optional `navigator.locks` for silent refresh single-flight across browser tabs.
- **v2 SDK transport upgrade** — delegate direct streamable HTTP silent 401/403 + SEP-2350 union to SDK; remove `mcpAuth` / client intercept shims where redundant.
- **Server-side token refresh** on the node using `RemoteAuthState.oauthClient` + `refresh_token` (browser owns refresh today).
- **Connection Info** — display effective vs pending scopes.
- **Composable `oauth.operations`** map for method-wide scope defaults in test fixtures.
- **Popup OAuth window** as an alternative to full-page resume (not implemented).
---
## Normative references
- [MCP authorization (2025-11-25)](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization)
- [MCP authorization (draft — 2026-07-28 RC)](https://modelcontextprotocol.io/specification/draft/basic/authorization)
- [SEP-2350 — client-side scope accumulation in step-up](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2350)
- [EMA extension](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization)
- [RFC 6750 §3.1](https://datatracker.ietf.org/doc/html/rfc6750#section-3.1) — Bearer `insufficient_scope`

View file

@ -1,6 +1,7 @@
# Inspector V2 — OAuth smoke testing (real servers)
# Inspector V2 Auth — OAuth smoke testing (real servers)
### [Brief](README.md) | [V2 Scope](v2_scope.md) | [Servers file](v2_servers_file.md) | [Auth hardening](v2_auth_hardening.md) | [Mid-session auth](v2_auth_mid_session.md) | [EMA / XAA](v2_auth_ema.md)
### [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 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
Manual smoke procedures for exercising Inspector OAuth against **hosted** MCP servers. Complements automated coverage in `clients/web/src/test/integration/mcp/inspectorClient-oauth-e2e.test.ts`, which uses the in-repo `TestServerHttp` (`createOAuthTestServerConfig`).
@ -475,7 +476,7 @@ Same two phases (prefer **`stytch-mcp-demo`** URL):
### Procedure (CLI)
Reuse tokens from a prior web/TUI session, or run after interactive auth in TUI:
Interactive OAuth uses the loopback callback server (same as TUI). First-time connect or mid-session step-up:
```bash
mcp-inspector --cli --catalog configs/mcp.json --server stytch-mcp-demo \
@ -483,7 +484,11 @@ mcp-inspector --cli --catalog configs/mcp.json --server stytch-mcp-demo \
--method tools/list
```
Interactive OAuth on CLI prints the authorize URL to stdout (`ConsoleNavigation`); prefer TUI for first-time login smokes.
**Expect:** stdout prints `Please navigate to: …` → open in browser → redirect to `http://127.0.0.1:6276/oauth/callback` → stderr `Authorization complete.` → JSON on stdout.
Reuse tokens from a prior web/TUI/CLI session when `~/.mcp-inspector/storage/oauth.json` already has valid tokens for that server (passive file sharing).
Mid-session / step-up manual validation: [§5](v2_auth_smoke_testing.md#5-mid-session-auth--step-up--manual-validation) (web **W1 + W5–W7**, TUI **T1–T2 + T4**, CLI **C1–C2** required; **W8–W11** recommended after auth recovery changes).
### Alternative: DCR on Stytch (no metadata hosting)
@ -523,6 +528,293 @@ Register **`http://127.0.0.1:6276/oauth/callback`** on the xaa.dev IdP before le
---
## 5. Mid-session auth + step-up — manual validation
**Purpose:** Manual checklist for mid-session OAuth recovery across **web**, **TUI**, and **CLI** after changes to auth / step-up UX. Design: [v2_auth_mid_session.md](v2_auth_mid_session.md).
**Fixture:** local composable server `test-servers/configs/oauth-step-up-demo.json` (`echo` unscoped, `get_temp` requires `weather:read`). Catalog scope **`mcp tools:read` only** (omit `weather:read`) so `get_temp` triggers step-up.
### Required vs optional
Run **required** smokes before release or after any auth UX change. **Optional** smokes extend coverage; run when time allows or when touching the listed area.
| ID | Client | When | Required? |
| -- | ------ | ---- | --------- |
| **W1** | Web | Step-up modal + OAuth resume | **Yes** |
| W2–W4 | Web | Connect-time, silent refresh, multi-tab | Optional |
| **W5–W11** | Web | P0/P1/P2 code-review UX (below) | **Recommended** after auth recovery changes |
| **T1, T2, T4** | TUI | Connect, step-up, clear OAuth | **Yes** |
| T3, T5–T6 | TUI | Modal path, tab/server selection | Optional |
| **C1, C2** | CLI | Connect + step-up **y/N** | **Yes** |
| C3 | CLI | Built launcher subprocess | Optional |
**Minimum release gate:** **W1** + **W5–W7** (web), **T1–T2** + **T4** (TUI), **C1–C2** (CLI).
### What CI covers vs what you verify manually
| Capability | Web | TUI | CLI | Automated in CI? |
| ---------- | --- | --- | --- | ---------------- |
| Core `handleAuthChallenge()` / token exchange | Remote transport | Direct transport | Direct (`cliOAuth.ts`) | Yes — `inspectorClient-oauth-*-mid-session-e2e.test.ts`, `oauth-interactive.test.ts` |
| Silent mid-session refresh (valid refresh token) | Yes | Yes (reconnect) | N/A (one-shot) | Yes — remote e2e |
| Connect-time 401 → interactive OAuth | Yes | Yes | Yes | Partial — core/CLI integration; **not** full UI/binary |
| Step-up confirm before second OAuth | Modal | Auth tab **A** / **C** | stderr **y/N** | Partial — unit/modal tests; **not** real browser + Ink/terminal |
| OAuth resume after full-page redirect | Yes (`6274`) | N/A (loopback) | N/A (loopback) | Partial — `oauthResume` unit tests; **not** browser tab restore |
| Tab + form restore after step-up | Yes | N/A | N/A | **Manual only** |
| Network log survives redirect | Yes | N/A | N/A | **Manual only** |
| Multi–browser-tab defer (background tab) | Yes | N/A | N/A | **Manual only** |
| Tool / prompt / resource modal → auth recovery | Yes | Yes | CLI one-shot RPC only | Partial — component unit tests |
| Loopback callback server (`6276`) | N/A | Yes | Yes | Partial — integration tests auto-complete authorize URL |
| Step-up **decline** (stay connected / exit cleanly) | Modal Cancel | Auth **C** | **N** | **Manual only** |
| ReAuthBanner reconnect without disconnect | Yes | N/A | N/A | **Manual only** (W7) |
| Abandoned step-up / reauth snapshot → banner | Yes | N/A | N/A | **Manual only** (W6) |
| Step-up pre-redirect toast | Yes | N/A | N/A | **Manual only** (W5) |
| Background-tab defer + resume on visibility | Yes | N/A | N/A | **Manual only** (W8) |
| Concurrent step-up → warning toast | Yes | TUI overwrite message | N/A | **Manual only** (W9 / T5) |
| Partial consent after step-up → warning toast | Yes | Yes | Yes (stderr message) | Partial — unit copy only (W10) |
| Prompt / resource / app command-scoped recovery | Yes | Yes | N/A (tools only in CLI) | Partial — no full UI e2e (W11) |
| Built launcher → CLI binary subprocess | N/A | N/A | Yes | `scripts/smoke-cli.mjs` — **no OAuth** |
Run this section when touching mid-session auth UX. CI proves protocol/core paths; **you** prove each client’s interactive UX with a real browser (and terminal for CLI **y/N**).
### Shared setup
1. **Composable OAuth server** (terminal A):
```bash
cd clients/web && npm run test-servers:build
node ../../test-servers/build/server-composable.js \
--config ../../test-servers/configs/oauth-step-up-demo.json
```
Confirm stderr: `Composable server listening at http://127.0.0.1:8081/mcp`.
2. **Catalog entry** — add to `~/.mcp-inspector/mcp.json` (all three clients):
```json
"oauth-step-up-demo": {
"type": "streamable-http",
"url": "http://127.0.0.1:8081/mcp",
"oauth": { "scopes": "mcp tools:read" }
}
```
3. **Clean OAuth state** before each client’s run:
- **Web:** Server Settings → OAuth → **Clear stored OAuth state** (or Connection Info → **Clear and disconnect**)
- **TUI / CLI:** Auth tab → **S** (TUI), or remove the server key from `~/.mcp-inspector/storage/oauth.json` / use a fresh `HOME` (CLI)
4. **Ports:** composable server **8081**; web callback **6274**; TUI/CLI callback **6276** (only one TUI/CLI listener at a time).
---
### Web — manual validation
**Start web** (terminal B): `npm run web:dev -- --catalog configs/mcp.json` (or your `mcp.json` with the entry above).
#### W1 — Step-up modal + OAuth resume (**required**)
Primary path; CI does **not** exercise modal UX or post-redirect tab restore.
1. Connect to **oauth-step-up-demo** → browser OAuth → **connected**.
2. **Tools** → **echo** → **Run** → success.
3. **Tools** → **get_temp** (city `NYC`, units `C`) → **Run**.
4. **Expect:** **“Additional permissions required”** modal (scopes include `weather:read`); **no** immediate redirect.
5. **Cancel** → modal closes, error shown, still **connected** → **echo** still works.
6. **Run get_temp** again → **Authorize** → redirect to AS on `:8081` → approve → `http://localhost:6274/oauth/callback` → return to app.
7. **Expect:** **Tools** tab, **get_temp** form restored, toast **“Step-up authorization succeeded. Retry your action.”**, network log from pre-redirect still visible.
8. **Run get_temp** → temperature result.
#### W2 — Connect-time OAuth with no prior session (optional)
1. Clear OAuth state, disconnect.
2. Connect → full-page OAuth → callback → toast **“Authentication succeeded. Retry your action.”**
#### W3 — Silent mid-session refresh (optional)
1. Connect, run **echo** successfully.
2. Invalidate access token only (devtools → `sessionStorage` → `mcp-inspector-oauth`), keep refresh token.
3. **Run echo** again → success **without** leaving the page.
> CI: `inspectorClient-oauth-remote-mid-session-e2e.test.ts`. Skip W3 if W1 passed and time is short.
#### W4 — Multi–browser-tab defer (optional, manual only)
1. Tab A: connect, start step-up on **get_temp**, **do not** click Authorize yet.
2. Tab B: same server, run **echo** → should **not** redirect Tab B to OAuth while Tab A’s modal is open.
3. Tab A: **Authorize** → complete flow.
#### W5 — Step-up pre-redirect toast (**recommended**, MR-219)
1. Complete W1 steps 1–3 to open the step-up modal.
2. Click **Authorize**.
3. **Expect:** blue toast **“Step-up authorization for …”** / **“Redirecting to authorize additional permissions…”** appears **before** the browser leaves the page.
4. Complete OAuth → W1 steps 7–8.
#### W6 — Abandoned step-up → re-auth banner (**recommended**, MR-111)
1. Complete W1 steps 1–3; click **Authorize** and reach the AS consent page on `:8081`.
2. **Do not** approve — navigate back to `http://localhost:6274/` (or close the OAuth tab and open the inspector URL without callback query params).
3. **Expect:** persistent red **Re-authentication required** banner mentioning step-up was not completed.
4. Dismiss or use **Re-authenticate** (see W7).
#### W7 — ReAuthBanner reconnect without disconnect (**recommended**, MR-218)
**Prerequisite:** connected session + re-auth banner visible (from W6, OAuth callback error, or invalidate tokens and trigger a `token_expired` / `unauthorized` recovery that shows the banner — not an `insufficient_scope` toast).
1. Confirm Connection toggle still shows **connected** (session not torn down).
2. Click banner **Re-authenticate**.
3. **Expect:** **no** disconnect/reconnect cycle; browser OAuth opens (or silent restore if tokens still valid).
4. After successful OAuth, banner clears; **echo** still works without manual reconnect.
#### W8 — Background-tab defer + resume (**recommended**, MR-104)
1. Connect; open step-up modal on **get_temp** (W1 steps 1–3) but **do not** click Authorize yet.
2. Switch away from the inspector tab (another browser tab or app) so the page is **hidden** (`document.visibilityState === "hidden"` — DevTools → **Rendering** → *Emulate page visibility hidden* also works).
3. Return to the inspector tab (or disable emulation).
4. **Expect:** deferred recovery runs — step-up modal or OAuth flow resumes without losing the connected session.
5. Complete step-up; run **get_temp** successfully.
#### W9 — Concurrent step-up blocked (**recommended**, MR-110)
1. Connect; open step-up modal on **get_temp**; leave it open.
2. Trigger a second step-up need (e.g. open step-up again via a second **Run** on **get_temp**, or a second scoped action if the fixture supports it).
3. **Expect:** yellow toast **“Step-up authorization in progress…”**; first modal remains; no second redirect.
#### W10 — Partial consent after step-up (**optional**)
1. Complete step-up **Authorize** flow but on the AS consent screen **deny** or omit `weather:read` if the AS offers granular scopes.
2. **Expect:** return to app with **warning** toast about permissions not granted — **not** the green **“Step-up authorization succeeded…”** toast.
3. **Run get_temp** again → step-up modal reappears.
#### W11 — Prompt / resource / app recovery (**optional**, MR-003)
The default `oauth-step-up-demo.json` fixture is **tools-only**. To smoke command-scoped recovery on other surfaces, temporarily add a scoped prompt or resource to the composable config (see [v2_auth_mid_session.md § Test infrastructure](v2_auth_mid_session.md#test-infrastructure)) and restart the server.
1. Connect with `mcp tools:read` only.
2. **Prompts** → run a scoped **Get** (or **Resources** → **Read**, or **Apps** → open) that returns `403 insufficient_scope`.
3. **Expect:** same step-up modal / redirect / resume behavior as W1 — not an unhandled error. Cancel should only affect that panel’s in-flight state (MR-106).
---
### TUI — manual validation
**Start TUI:** `mcp-inspector --tui --catalog ~/.mcp-inspector/mcp.json` (or `--config`). No web dev required.
#### T1 — Connect-time OAuth on 401 (**required**)
CI does **not** run Ink + real browser callback.
1. Clear OAuth state (**Auth** tab → **S**, or fresh `oauth.json`).
2. Select **oauth-step-up-demo** → **Connect** (`c`).
3. **Expect:** Auth tab shows authenticating; terminal/OS opens authorize URL (or log shows URL); browser completes redirect to `http://127.0.0.1:6276/oauth/callback`.
4. **Expect:** connected; **Tools** tab lists **echo** and **get_temp**.
#### T2 — Step-up confirm on Auth tab (**required**)
Requires connected session with **`mcp tools:read` only**. If connect already granted `weather:read`, clear state and retry, or skip to T3 only if `get_temp` succeeds without step-up.
1. **Tools** tab → **get_temp** → run with city/units.
2. **Expect:** focus moves to **Auth** tab; message that extra scopes are needed; prompts **A** (authorize) / **C** (cancel).
3. Press **C** → message “Authorization cancelled.” → still connected → **echo** still works.
4. Run **get_temp** again → press **A** → browser OAuth → callback on **6276**.
5. **Expect:** Auth message **“Step-up authorization succeeded. Retry your action.”**
6. Run **get_temp** again → success.
#### T3 — Tool test modal auth recovery (optional)
1. From **Tools**, open test modal for **get_temp**, trigger step-up as in T2.
2. **Expect:** same Auth tab step-up flow (not a separate modal).
#### T4 — Clear OAuth state (**required**, quick)
1. While connected, **Auth** tab → **S**.
2. **Expect:** tokens cleared, disconnected if was connected.
#### T5 — Step-up while on another tab (**optional**, MR-004)
1. Connect (T1); stay on **Tools** tab (not Auth).
2. Run **get_temp** → step-up needed.
3. **Expect:** focus moves to **Auth** tab; **oauth-step-up-demo** remains selected; step-up prompt visible (T2 step 2).
#### T6 — Concurrent step-up overwrite message (**optional**, MR-110)
1. Connect; open step-up prompt on Auth tab (T2 step 2); leave it open.
2. Trigger another step-up (run **get_temp** again).
3. **Expect:** Auth tab message about an existing step-up prompt; no silent loss of the first prompt.
> CI: `inspectorClient-oauth-direct-mid-session-e2e.test.ts` (core client, no Ink). TUI component tests: `App.test.tsx`, `tuiOAuth.test.ts`.
---
### CLI — manual validation
Uses loopback callback **`http://127.0.0.1:6276/oauth/callback`** (override with `--callback-url`). **Do not** run TUI and CLI OAuth smokes simultaneously on 6276.
#### C1 — Connect-time OAuth + `tools/list` (**required**)
CI auto-completes authorize URL; **you** use a real browser.
1. Shared setup steps 1–3 (web dev **not** required).
2. ```bash
mcp-inspector --cli --catalog ~/.mcp-inspector/mcp.json --server oauth-step-up-demo \
--method tools/list
```
3. **Expect stdout:** `Please navigate to: http://127.0.0.1:8081/oauth/authorize?...`
4. Open URL → approve → redirect to **6276** → “OAuth complete”.
5. **Expect stderr:** `Authorization complete.`
6. **Expect stdout:** JSON with `echo` and `get_temp`.
#### C2 — Step-up **y/N** + `tools/call get_temp` (**required**)
Session from C1 must have **`mcp tools:read` only**. If step-up does not prompt, clear OAuth state and retry C1.
1. ```bash
mcp-inspector --cli --catalog ~/.mcp-inspector/mcp.json --server oauth-step-up-demo \
--method tools/call --tool-name get_temp --tool-arg city=NYC --tool-arg 'units="C"'
```
2. **Expect stderr:** scope message + `Proceed with step-up authorization? [y/N]`.
3. Type **`n`** → exit ≠ 0, “Step-up authorization declined.”
4. Re-run; type **`y`** → authorize URL on stdout → browser → **6276** callback.
5. **Expect stderr:** `Authorization complete. Retrying…` then stdout JSON with temperature.
#### C3 — Subprocess binary path (optional)
`scripts/smoke-cli.mjs` (`npm run smoke:cli`) exercises the **built launcher + binary** for catalog/config/`tools/list` — **not** OAuth. Optional sanity check after CLI auth changes; C1/C2 remain the OAuth manual gate.
> CI: `clients/cli/__tests__/oauth-interactive.test.ts`, `cliOAuth.test.ts` (in-process `cliOAuth.ts`, not subprocess).
---
### Manual sign-off checklist
| # | Check | Web | TUI | CLI |
| - | ----- | --- | --- | --- |
| 1 | Connect-time OAuth with empty storage | W1 prep / W2 | T1 | C1 |
| 2 | Unscoped tool works after connect | W1 step 2 | T1 | C1 output |
| 3 | Step-up prompt before second OAuth | W1 step 4 | T2 step 2 | C2 step 2 |
| 4 | Decline step-up, stay usable | W1 step 5 | T2 step 3 | C2 step 3 |
| 5 | Accept step-up + complete browser OAuth | W1 step 6–8 | T2 step 4–6 | C2 step 4–5 |
| 6 | Clear OAuth state | Settings | T4 | delete `oauth.json` / fresh HOME |
| 7 | Step-up pre-redirect toast | W5 | — | — |
| 8 | Abandoned step-up → banner | W6 | — | — |
| 9 | ReAuthBanner reconnect (no disconnect) | W7 | — | — |
| 10 | Background-tab defer + resume | W8 | — | — |
| 11 | Concurrent step-up warning | W9 | T6 | — |
| 12 | Partial consent warning toast | W10 | T2 (if AS allows) | C2 (if AS allows) |
| 13 | Prompt / resource / app recovery | W11 | T3 | — |
### Troubleshooting
| Symptom | Likely cause |
| ------- | ------------- |
| Redirect with no modal / no step-up prompt | Scopes already include `weather:read`; clear OAuth state and ensure catalog has `"scopes": "mcp tools:read"` only |
| Web callback “could not be matched” | Missing `mcp-inspector:oauth-resume` in `sessionStorage` — use modal **Authorize**, not manual URL |
| TUI/CLI `EADDRINUSE` on 6276 | Another TUI/CLI (or stale process) holds callback port |
| CLI step-up never prompts | Connect-time recovery unioned scopes — clear tokens and retry C1 |
| `get_temp` fails after web step-up without re-run | Expected — click **Run** again (no auto-replay after full-page OAuth) |
| ReAuthBanner **Re-authenticate** disconnects session | Should **not** happen when already connected — see W7 |
| Step-up shows toast instead of modal | `insufficient_scope` recovery failures use a toast, not ReAuthBanner (MR-206) |
| No pre-redirect toast on step-up Authorize | See W5 — blue toast expected before redirect (MR-219) |
| Port conflict on 8081 | Change `transport.port` in config and catalog URL |
---
## What to verify (all smokes)
| Check | Where |
@ -549,14 +841,17 @@ Register **`http://127.0.0.1:6276/oauth/callback`** on the xaa.dev IdP before le
| Static | GitHub MCP + your OAuth App | `test-static-client` / `test-static-secret` on TestServerHttp |
| CIMD | **Stytch demo MCP** (`stytch-as-demo.val.run`) + [MCPJam metadata URL](#cimd-credentials-for-smoke-mcpjam) (or local composable) | `createClientMetadataServer()` in e2e |
| EMA | xaa.dev staging | `inspectorClient-ema-e2e.test.ts` + mocks |
| Mid-session / step-up (web remote) | §5 **W1** + **W5–W7** (required gate); W2–W4, W8–W11 optional | `inspectorClient-oauth-remote-mid-session-e2e.test.ts`; web unit (`oauthResume`, `StepUpAuthModal`) |
| Mid-session / step-up (TUI direct) | §5 **T1–T2, T4** (required); T3, T5–T6 optional | `inspectorClient-oauth-direct-mid-session-e2e.test.ts` (core); `App.test.tsx`, `tuiOAuth.test.ts` (no Ink+browser) |
| Mid-session / step-up (CLI direct) | §5 **C1–C2** (required) | `clients/cli/__tests__/oauth-interactive.test.ts`, `cliOAuth.test.ts` (in-process; not subprocess) |
## Known gaps (Inspector)
See **[Mid-session authorization](v2_auth_mid_session.md)** for the design to address mid-session 401, token refresh, and step-up scope challenges (including web remote reconnect).
**Mid-session auth** is implemented for web (remote transport), TUI, and CLI — see [Mid-session authorization](v2_auth_mid_session.md). **Remaining:** shared `RemoteOAuthStorage` on web, optional idle SSE E2E, v2 SDK transport upgrade for direct silent retry.
See **[Auth hardening (MCP 2026-07-28)](v2_auth_hardening.md)** for connect-time OAuth hardening (SEP-2468, SEP-837, SEP-2352, SEP-2207, SEP-2350, SEP-2351) and the v2 SDK upgrade strategy.
- **CLI interactive OAuth:** no local callback server yet — reuse tokens from web/TUI or complete auth in TUI first; CLI prints authorize URLs to stdout when a new login is required.
- **Mid-session auth:** see [§5 manual validation](v2_auth_smoke_testing.md#5-mid-session-auth--step-up--manual-validation) — CI covers core protocol; **W1 + W5–W7 / T1–T2 + T4 / C1–C2** are the required manual gate per client; **W8–W11, T3, T5–T6, C3** extend P0/P1/P2 UX coverage.
- **Client credentials grant:** not implemented ([#1225](https://github.com/modelcontextprotocol/inspector/issues/1225)).
## References

View file

@ -1,6 +1,6 @@
# Inspector V2 Tech Stack - Storage - Catalog and Launch Configuration
### [Brief](README.md) | [V1 Problems](v1_problems.md) | [V2 Scope](v2_scope.md) | V2 Tech Stack | [V2 UX](v2_ux.md)
### [Brief](README.md) | [V1 Problems](v1_problems.md) | [V2 Scope](v2_scope.md) | V2 Tech Stack | [V2 UX](v2_ux.md) | [V2 Auth](v2_auth.md) | [V2 New Spec Impact](v2_new_spec_impact.md)
#### [Web Client](v2_web_client.md) | [CLI, TUI, Launcher](v2_cli_tui_launcher.md) | [Server](v2_server.md) | [Storage](v2_storage.md)

View file

@ -1,6 +1,6 @@
# Inspector V2 Tech Stack - CLI, TUI, and Launcher
### [Brief](README.md) | [V1 Problems](v1_problems.md) | [V2 Scope](v2_scope.md) | V2 Tech Stack | [V2 UX](v2_ux.md)
### [Brief](README.md) | [V1 Problems](v1_problems.md) | [V2 Scope](v2_scope.md) | V2 Tech Stack | [V2 UX](v2_ux.md) | [V2 Auth](v2_auth.md) | [V2 New Spec Impact](v2_new_spec_impact.md)
#### [Web Client](v2_web_client.md) | CLI, TUI, Launcher | [Server](v2_server.md) | [Storage](v2_storage.md)

View file

@ -0,0 +1,375 @@
# Inspector V2 - MCP Spec 2026-07-28: SEP Review and Inspector V2 Impact
### [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
<!-- TOC -->
* [1. Executive summary](#1-executive-summary)
* [2. Scope note: milestone vs. release](#2-scope-note-milestone-vs-release)
* [3. High-level overview of the milestone SEPs](#3-high-level-overview-of-the-milestone-seps)
* [4. Functional area: transport, HTTP, and observability](#4-functional-area-transport-http-and-observability)
* [4.1 SEP-2243 — HTTP Header Standardization (final title: "HTTP Header Standardization for Streamable HTTP Transport")](#41-sep-2243--http-header-standardization-final-title-http-header-standardization-for-streamable-http-transport)
* [4.2 SEP-2260 — Server requests must be associated with a client request](#42-sep-2260--server-requests-must-be-associated-with-a-client-request)
* [4.3 SEP-414 — OpenTelemetry trace context in `_meta`](#43-sep-414--opentelemetry-trace-context-in-_meta)
* [5. Functional area: authorization](#5-functional-area-authorization)
* [5.1 SEP-837 — `application_type` in Dynamic Client Registration](#51-sep-837--application_type-in-dynamic-client-registration)
* [5.2 SEP-2350 — Client-side scope accumulation in step-up authorization](#52-sep-2350--client-side-scope-accumulation-in-step-up-authorization)
* [5.3 SEP-2351 — RFC 8414 well-known suffix declaration](#53-sep-2351--rfc-8414-well-known-suffix-declaration)
* [5.4 SEP-2352 — Authorization server binding and migration](#54-sep-2352--authorization-server-binding-and-migration)
* [6. Functional area: extensibility, UI, and governance](#6-functional-area-extensibility-ui-and-governance)
* [6.1 SEP-2133 — Extensions framework](#61-sep-2133--extensions-framework)
* [6.2 SEP-1865 — MCP Apps (`io.modelcontextprotocol/ui`)](#62-sep-1865--mcp-apps-iomodelcontextprotocolui)
* [6.3 SEP-1730 — SDK tiers](#63-sep-1730--sdk-tiers)
* [7. How the over-the-wire conversation changes](#7-how-the-over-the-wire-conversation-changes)
* [7.1 Connection establishment](#71-connection-establishment)
* [7.2 Responses and request-scoped streaming](#72-responses-and-request-scoped-streaming)
* [7.3 Server→client interactions: MRTR replaces server-initiated requests](#73-serverclient-interactions-mrtr-replaces-server-initiated-requests)
* [7.4 Push notifications: `subscriptions/listen` replaces the GET stream](#74-push-notifications-subscriptionslisten-replaces-the-get-stream)
* [7.5 Cancellation, resumability, keepalive](#75-cancellation-resumability-keepalive)
* [7.6 Logging](#76-logging)
* [7.7 Errors and version negotiation](#77-errors-and-version-negotiation)
* [7.8 Backward compatibility: the "era" model](#78-backward-compatibility-the-era-model)
* [8. TypeScript SDK v2: what it handles vs. what bubbles up](#8-typescript-sdk-v2-what-it-handles-vs-what-bubbles-up)
* [8.1 Structural/API migration (independent of the new spec)](#81-structuralapi-migration-independent-of-the-new-spec)
* [8.2 Handled automatically by SDK v2](#82-handled-automatically-by-sdk-v2)
* [8.3 Bubbles up to the application](#83-bubbles-up-to-the-application)
* [9. Impact on MCP Inspector V2 (`v2/main`)](#9-impact-on-mcp-inspector-v2-v2main)
* [9.1 Connection model and state management](#91-connection-model-and-state-management)
* [9.2 History and Network tabs (the Inspector's core value)](#92-history-and-network-tabs-the-inspectors-core-value)
* [9.3 Feature tabs](#93-feature-tabs)
* [9.4 Auth and the OAuth store](#94-auth-and-the-oauth-store)
* [9.5 Suggested sequencing](#95-suggested-sequencing)
* [10. Reference: new error codes](#10-reference-new-error-codes)
* [11. Sources](#11-sources)
<!-- TOC -->
---
## 1. Executive summary
The upcoming release (protocol version **`2026-07-28`**, successor to `2025-11-25`) is the largest revision in MCP's history. The milestone's closed SEPs fall into four functional areas — transport/HTTP, authorization, extensibility/UI, and ecosystem governance — but the milestone list alone understates the change: the final draft spec also incorporates several other merged SEPs (**SEP-2567 sessionless**, **SEP-2575 stateless**, **SEP-2322 MRTR**, **SEP-2663 tasks-as-extension**) that dominate the over-the-wire differences and interact directly with the milestone items. Both are covered here, clearly attributed.
The headline for the Inspector:
1. **The HTTP transport is redesigned.** POST-only, no `initialize` handshake, no sessions, no GET stream, no SSE resumability. Every request is self-describing via `_meta`; server→client requests (sampling/elicitation/roots) are replaced by an in-band `input_required` retry pattern (MRTR); push notifications move to a `subscriptions/listen` stream; new `Mcp-Method`/`Mcp-Name` headers are mandatory (SEP-2243).
2. **SDK v2 absorbs most of the mechanics** — era negotiation, `_meta` envelopes, header mirroring, MRTR auto-fulfilment, listen streams — behind the existing handler APIs. But it defaults to **legacy** behavior, and its docs explicitly warn that *debugging tools should not default to auto-negotiation*. Era selection, per-request log levels, response caching, pagination, tasks, and a new error taxonomy all bubble up to Inspector UX and state.
3. **Auth changes are storage-schema changes.** Credentials must be keyed by `(server, issuer)`, DCR must send `application_type`, scope step-up accumulation is a client responsibility. SDK v2 implements the flows but the Inspector's persisted OAuth store and Network-tab visualizations must follow.
4. **Extensions become a first-class concept** (`capabilities.extensions` on both sides), MCP Apps is the flagship extension (already implemented in Inspector v2 via `ext-apps`), and **tasks moves out of core into an extension with a redesigned polling model** — and SDK v2 removed its built-in tasks support entirely.
---
## 2. Scope note: milestone vs. release
The milestone was 41% complete at review time (10 closed, 14 open). The 10 closed items are covered in §3–§6. However, the published **draft spec changelog** already lists as *major changes* several SEPs merged outside this closed list — SEP-2567 (remove sessions), SEP-2575 (stateless + `server/discover` + `subscriptions/listen`), SEP-2322 (MRTR + `resultType`), and SEP-2663 (tasks extension). Since the Inspector must implement the *composite* protocol, §7 (wire changes) and §9 (Inspector impact) describe the full 2026-07-28 picture. Caveat: items could still shift before the July 28 release; the RC window is the time to re-verify.
A reading caveat that applies throughout: **the merged SEP documents themselves contain stale examples** (e.g., SEP-2243's doc still shows `Mcp-Session-Id` and `initialize`, and error code `-32001` before renumbering to `-32020`). The authoritative integration is the draft spec pages at `modelcontextprotocol.io/specification/draft`, not the SEP files.
---
## 3. High-level overview of the milestone SEPs
| SEP | Title | Area | One-liner |
|---|---|---|---|
| [2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243) | HTTP Header Standardization | Transport | Mandatory `Mcp-Method`/`Mcp-Name` headers mirroring the JSON-RPC body; opt-in `x-mcp-header` param mirroring; `-32020 HeaderMismatch` |
| [2260](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2260) | Server requests tied to client requests | Transport | Sampling/elicitation/roots MUST be associated with an in-flight client request; no standalone server→client requests. Doctrinal precursor to MRTR |
| [414](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/414) | OTel trace context conventions | Observability | `traceparent`/`tracestate`/`baggage` reserved as bare `_meta` keys (W3C formats) |
| [837](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/837) | Client type in DCR | Auth | Clients MUST send `application_type` (`"native"`/`"web"`) during Dynamic Client Registration |
| [2350](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2350) | Scope accumulation in step-up auth | Auth | Servers challenge per-operation; **clients** union previously requested scopes with challenged scopes before re-authorizing |
| [2351](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2351) | RFC 8414 well-known suffix | Auth | MCP formally declares the default `oauth-authorization-server` suffix; no MCP-specific suffix. Confirmatory, no wire change |
| [2352](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2352) | AS binding and migration | Auth | Credentials MUST be keyed by AS `issuer`; on AS change, re-register (DCR) or error (pre-registered); CIMD IDs are portable |
| [2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133) | Extensions framework | Extensibility | `capabilities.extensions` map on both sides; governance for official (`ext-*`) / experimental / third-party extensions |
| [1865](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1865) | MCP Apps | Extensibility/UI | Extension `io.modelcontextprotocol/ui`: predeclared `ui://` HTML resources, sandboxed-iframe rendering, JSON-RPC-over-postMessage host bridge |
| [1730](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1777) | SDK tiers | Governance | Three-tier SDK classification driven by conformance testing and issue-triage SLAs. No wire impact |
Also landed in the draft (not in the closed milestone list, but essential context): **SEP-2567** (sessionless), **SEP-2575** (stateless, `server/discover`, `subscriptions/listen`, removal of `ping`/`logging/setLevel`/`notifications/roots/list_changed`), **SEP-2322** (MRTR, required `resultType`), **SEP-2663** (tasks redesigned as extension `io.modelcontextprotocol/tasks`), **SEP-2549** (list-response caching hints), **SEP-2577** (deprecation annotations for sampling/roots/logging), **SEP-991** (CIMD; DCR deprecated in its favor), **SEP-2596** (feature lifecycle; HTTP+SSE formally Deprecated).
---
## 4. Functional area: transport, HTTP, and observability
### 4.1 SEP-2243 — HTTP Header Standardization (final title: "HTTP Header Standardization for Streamable HTTP Transport")
The problem: intermediaries (gateways, load balancers, WAFs, rate limiters) could not route or police MCP traffic without parsing JSON bodies. The fix: mirror key body fields into HTTP headers on every Streamable HTTP POST.
Standard headers, **required for compliance**:
- `MCP-Protocol-Version` — must match `_meta["io.modelcontextprotocol/protocolVersion"]` in the body.
- `Mcp-Method` — mirrors the JSON-RPC `method`, on every request.
- `Mcp-Name` — mirrors `params.name`/`params.uri` on `tools/call`, `resources/read`, `prompts/get`.
Opt-in custom headers: a server may annotate tool `inputSchema` properties with `x-mcp-header: "{Name}"`, and conforming clients **MUST** then mirror those argument values into `Mcp-Param-{Name}` headers. Constraints on annotatable properties are strict (primitive types only — string/integer/boolean, no `number`; statically reachable via plain `properties` chains — no `$ref`, `oneOf`, `items`, conditionals). **A violating annotation invalidates the whole tool definition, and clients on Streamable HTTP MUST exclude that tool from `tools/list` results** (a new client-side conformance duty). Values that aren't header-safe ASCII must be encoded with a Base64 sentinel format: `=?base64?{b64}?=`.
Validation: any server processing the body MUST verify header/body consistency and reject mismatches with HTTP `400` + JSON-RPC error **`-32020 HeaderMismatch`** (renumbered from the SEP's draft `-32001`; `-32020`–`-32099` is now reserved for spec-defined errors). Intermediaries forward unknown `Mcp-Param-*` headers untouched.
Client duties beyond sending headers: build `Mcp-Param-*` from the most recently fetched `inputSchema`; on rejection without a schema, re-fetch `tools/list` and retry; handle `413`/`431` gracefully; treat header names case-insensitively but values case-sensitively.
### 4.2 SEP-2260 — Server requests must be associated with a client request
Upgrades the 2025-era "SHOULD relate to the originating request" to **MUST**: `sampling/createMessage`, `elicitation/create`, and `roots/list` may only be sent in association with an in-flight client request, never standalone on an independent stream (`ping` excepted). Clients receiving an orphaned server request SHOULD respond `-32602`.
Strategically this SEP is the bridge that made MRTR (SEP-2322) and GET-stream removal possible. In the final 2026-07-28 protocol its constraint is fully absorbed: **servers cannot send any JSON-RPC requests to clients at all.** Server→client interactions arrive as data inside the client's own response (`InputRequiredResult`), and the client answers by retrying the original request. Against a modern server, SEP-2260 requires no dedicated code; against 2025-era servers it lets a client drop out-of-band request handling from the standalone GET stream.
One operational consequence survives into both eras: transports must tolerate **unbounded human-in-the-loop delays** on the originating request (servers keep streams alive with SSE keepalive comments; clients must not apply short timeouts to requests that may elicit).
### 4.3 SEP-414 — OpenTelemetry trace context in `_meta`
A small, documentation-only standardization: the bare keys `traceparent`, `tracestate`, and `baggage` are **reserved** in `_meta` (an explicit exception to the reverse-DNS prefix rule) and, when present, MUST follow W3C Trace Context / Baggage formats. This codifies what instrumented SDKs already emit and keeps interop with the OTel semantic conventions for MCP. There is no requirement for clients to send or servers to process them.
```json
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{
"name":"get_weather","arguments":{"location":"NYC"},
"_meta":{"traceparent":"00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01"}}}
```
The carrier is the JSON-RPC body (transport-independent — survives stdio). Note for a tool that displays `_meta` verbatim: `baggage` can carry arbitrary correlation key-values; treat as potentially sensitive.
---
## 5. Functional area: authorization
All four auth SEPs are clarifications/hardening of the OAuth 2.1 framework — no new endpoints or protocol machinery — but two of them (837, 2352) impose hard requirements on client behavior and persisted state. Note also that the draft restructures the authorization spec into four pages and **deprecates DCR in favor of CIMD** (SEP-991, separate PR), so the DCR requirements below live under a deprecated-but-supported mechanism.
### 5.1 SEP-837 — `application_type` in Dynamic Client Registration
OIDC-flavored authorization servers default omitted `application_type` to `"web"`, which forbids loopback redirect URIs — breaking DCR for desktop/CLI/localhost clients. Now: clients **MUST** send an appropriate `application_type` in the DCR request. Native apps (desktop, mobile, CLI, *locally hosted web apps accessed via localhost* — i.e., the Inspector) SHOULD use `"native"`; remote browser-based apps SHOULD use `"web"`. Clients MUST handle registration rejections caused by redirect-URI constraints, SHOULD surface a meaningful error, and MAY retry with adjusted parameters.
### 5.2 SEP-2350 — Client-side scope accumulation in step-up authorization
Resolves an ambiguity: the old spec's `insufficient_scope` example implied servers echo accumulated scopes; RFC 6750 says the `scope` attribute describes the *current* resource. The resolution: **servers stay stateless and challenge per-operation; clients own accumulation.**
- The 403 challenge now carries only the scopes for the failing operation (spec example changed from `scope="files:read files:write user:profile"` to `scope="files:write"`).
- On step-up, the client **computes the union** of its previously requested scope set and the challenged scopes, and re-authorizes with that union. A naive client that requests only the challenged scope will receive a token that silently *lost* its earlier grants.
- Clients MUST NOT assume any set relationship between challenged scopes and `scopes_supported`, and need not deduplicate hierarchical scopes.
Practical requirement: persist the requested scope set per `(server, AS)` pair and union on every challenge.
### 5.3 SEP-2351 — RFC 8414 well-known suffix declaration
Purely declaratory: MCP uses the default `oauth-authorization-server` suffix and **does not define an application-specific suffix**. The existing priority-ordered discovery probing (path-inserted `oauth-authorization-server`, then `openid-configuration` variants) is unchanged. A client that already implements the probing correctly is compliant; the only rule is don't invent an MCP-specific suffix.
### 5.4 SEP-2352 — Authorization server binding and migration
Closes a credential-confusion hole when a server's Protected Resource Metadata changes or lists multiple ASes:
- Persisted client credentials (pre-registered or DCR-obtained) **MUST be keyed by the issuing AS's `issuer` identifier**.
- Multiple ASes in PRM are fully independent; separate registration state per AS, and credentials MUST NOT be reused across ASes.
- On AS change (detected via updated PRM): DCR clients **MUST re-register** with the new AS; pre-registered clients SHOULD surface an error rather than send mismatched credentials; **CIMD client IDs are explicitly portable** (no re-registration).
This is a storage-schema change for any client that caches OAuth state: `(MCP server, AS issuer) → {client_id, tokens, scopes}` instead of `MCP server → client_id`.
The four interact: 2352's re-registration goes through DCR where 837's `application_type` applies; 2350's accumulated scope set is part of the per-issuer state 2352 mandates; 2351's declared suffix governs the AS-metadata fetch used in 2352's issuer comparison.
---
## 6. Functional area: extensibility, UI, and governance
### 6.1 SEP-2133 — Extensions framework
Exactly one core schema change: an optional `extensions` map on both `ClientCapabilities` and `ServerCapabilities`, keyed by `{vendor-prefix}/{extension-name}` (e.g. `io.modelcontextprotocol/ui`), each value being an extension-defined settings object (`{}` = supported, no settings).
```json
"capabilities": {
"extensions": {
"io.modelcontextprotocol/ui": { "mimeTypes": ["text/html;profile=mcp-app"] }
}
}
```
The rest is governance: official extensions live in `ext-*` repos with delegated maintainers and independent versioning, created via a new Extensions Track SEP type (reference implementation required before review); experimental extensions incubate in `experimental-ext-*` repos; breaking changes require a new identifier. Graceful degradation is normative, SDK extension support must be opt-in/disabled by default, and extension support is **not** required for conformance or SDK tiering. Official extensions now include Apps (`io.modelcontextprotocol/ui`), Tasks (`io.modelcontextprotocol/tasks`), and the `ext-auth` pair (OAuth client credentials, Enterprise-Managed Authorization).
### 6.2 SEP-1865 — MCP Apps (`io.modelcontextprotocol/ui`)
The flagship extension: servers deliver interactive HTML UIs that hosts render inline. Mechanics:
- **Declaration:** UI templates are *predeclared resources* under a `ui://` URI scheme with MIME type `text/html;profile=mcp-app`, fetched via ordinary `resources/read`. Tools link to them via `_meta.ui.resourceUri`; resources can carry `_meta.ui.csp` (origin allowlist over a deny-by-default CSP) and `_meta.ui.permissions` (camera/mic etc.). Predeclaration enables prefetching at `tools/list` time and security review.
- **Negotiation:** via the SEP-2133 `extensions` map, with the client declaring supported `mimeTypes`. Servers SHOULD fall back to text-only tools for non-supporting clients.
- **Runtime:** mandatory sandboxed iframe; iframe↔host communication is a JSON-RPC dialect of MCP over `postMessage`: `ui/initialize` handshake, host→app notifications (`ui/notifications/tool-input`, `tool-input-partial`, `tool-result`, `tool-cancelled`, `host-context-changed`, `size-changed`, `resource-teardown`), and app-initiated `tools/call` proxied — and policed/consent-gated — by the host, plus `ui/message`, `ui/open-link`, `ui/request-display-mode`.
- **No new methods on the client↔server channel.** All new methods live on the postMessage bridge. The normative spec versions independently in `modelcontextprotocol/ext-apps` (`2026-01-26`); `@modelcontextprotocol/ext-apps` ships the host-side `AppBridge`.
### 6.3 SEP-1730 — SDK tiers
Governance only, zero wire impact. Three tiers: Tier 1 (100% conformance via `modelcontextprotocol/conformance`, new spec features within each release's RC window, 2-business-day triage, 7-day P0 fixes), Tier 2 (≥80%, slower SLAs), Tier 3 (experimental). Standardized issue-label taxonomy feeds automated tier metrics; automatic relegation triggers exist. Extensions and experimental features are excluded from conformance scoring. Relevance to the Inspector: it's a signal on SDK dependency choice (the TypeScript SDK targets Tier 1, meaning day-one 2026-07-28 support), and the conformance suite itself is useful tooling for an inspector-class project.
---
## 7. How the over-the-wire conversation changes
This section describes the composite 2026-07-28 Streamable HTTP transport (SEP-2243 + 2567 + 2575 + 2322 + 2260), before vs. after. Verified against the draft spec pages and changelog.
### 7.1 Connection establishment
**Before (2025-11-25):** POST `initialize` → server responds with capabilities + optional `Mcp-Session-Id` header → POST `notifications/initialized` → all subsequent requests carry `Mcp-Session-Id` and `MCP-Protocol-Version` headers. Optional GET opens a standalone SSE stream for server-initiated messages. DELETE terminates the session.
**After (2026-07-28):** there is no handshake and no session. The client either calls `server/discover` (which servers MUST implement) to learn versions/capabilities/identity up front, or just sends its first real request. Every request and notification is its own POST and is self-describing:
```http
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_weather
Mcp-Param-Region: us-west1
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{
"name":"get_weather",
"arguments":{"region":"us-west1","location":"NYC"},
"_meta":{
"io.modelcontextprotocol/protocolVersion":"2026-07-28",
"io.modelcontextprotocol/clientInfo":{"name":"mcp-inspector","version":"2.0.0"},
"io.modelcontextprotocol/clientCapabilities":{"elicitation":{"form":{},"url":{}}}
}}}
```
GET and DELETE are gone (modern servers return 405). `Mcp-Session-Id` is gone (modern servers ignore it). One JSON-RPC message per POST; clients never POST JSON-RPC responses; no batching.
### 7.2 Responses and request-scoped streaming
Unchanged in shape: the server answers each request with either `application/json` (single object) or a `text/event-stream` **scoped to that request** — but the stream may now carry only *notifications related to that request* (`notifications/progress`, `notifications/message`) followed by the final response. Servers MUST NOT send JSON-RPC requests on any stream.
### 7.3 Server→client interactions: MRTR replaces server-initiated requests
**Before:** the server sent `elicitation/create` / `sampling/createMessage` / `roots/list` as JSON-RPC *requests* (on the POST's SSE stream, or pre-SEP-2260 even on the GET stream); the client POSTed the response back, correlated by JSON-RPC id.
**After:** all results carry a required **`resultType`** (`"complete"` | `"input_required"`; results from older servers lacking it are treated as complete). When a server needs input, it *returns*:
```json
{"jsonrpc":"2.0","id":1,"result":{
"resultType":"input_required",
"inputRequests":{"1":{"method":"elicitation/create","params":{...}}},
"requestState":"opaque-server-token"}}
```
The client gathers the answers locally and **retries the original request with a new id**, attaching `inputResponses` and echoing `requestState`. There is no server→client JSON-RPC request framing left on the wire.
### 7.4 Push notifications: `subscriptions/listen` replaces the GET stream
The client POSTs `subscriptions/listen` with a filter (`toolsListChanged`, `promptsListChanged`, `resourcesListChanged`, `resourceSubscriptions: [uris]`); the response is a long-lived SSE stream that begins with `notifications/subscriptions/acknowledged` and thereafter carries only opted-in notification types, each tagged `_meta["io.modelcontextprotocol/subscriptionId"]`. This also replaces `resources/subscribe`/`unsubscribe`. Request-scoped notifications never appear on this stream.
### 7.5 Cancellation, resumability, keepalive
Closing a request's SSE stream **is** cancellation (`notifications/cancelled` is stdio-only now). SSE resumability is removed — no `Last-Event-ID`, no event IDs; a broken stream loses the in-flight request and the client MUST re-issue it with a new id. Servers keep long streams alive with SSE comment lines; `ping` is removed from the modern protocol.
### 7.6 Logging
`logging/setLevel` and its session-scoped level are gone. The client opts into logs **per request** via `_meta["io.modelcontextprotocol/logLevel"]`; servers MUST NOT emit `notifications/message` for requests that omitted it.
### 7.7 Errors and version negotiation
New spec-defined JSON-RPC error codes, all surfaced as HTTP 400 with a JSON-RPC body: **`-32020 HeaderMismatch`**, **`-32021 MissingRequiredClientCapability`**, **`-32022 UnsupportedProtocolVersion`** (the error body lists `supported` versions so the client can retry). Unknown methods return HTTP **404** with JSON-RPC `-32601` — the JSON-RPC body is what distinguishes a modern server from a legacy HTTP+SSE server's bare 404.
### 7.8 Backward compatibility: the "era" model
The spec defines two eras: **modern** (≥2026-07-28, per-request metadata) and **legacy** (≤2025-11-25, initialize handshake). A dual-era client sends a modern request first; on 400 it inspects the body — a recognized modern JSON-RPC error means "modern server, fix and retry"; an empty/unrecognized body means legacy → fall back to `initialize`, and possibly further to the now-formally-Deprecated 2024-11-05 HTTP+SSE transport (GET, look for the `endpoint` event). Era determination is per-origin and SHOULD be cached.
---
## 8. TypeScript SDK v2: what it handles vs. what bubbles up
The Inspector v2 currently depends on `@modelcontextprotocol/sdk@1.29.0`. SDK v2 (`main`, `2.0.0-beta.2`) targets the 2026-07-28 spec, with stable release planned alongside the spec on July 28; v1.x remains supported ≥6 months after. There is also a `v1.x-2026-07-28` backport branch worth watching as a possible lower-effort path.
### 8.1 Structural/API migration (independent of the new spec)
- **Package split:** `@modelcontextprotocol/sdk` → `@modelcontextprotocol/client` (Client, transports, OAuth), `@modelcontextprotocol/core` (public Zod schemas), plus server packages. stdio transport moves to subpath `@modelcontextprotocol/client/stdio` (root barrel is browser-safe — convenient for the Inspector's web/Node split). `WebSocketClientTransport` removed. A codemod exists: `npx @modelcontextprotocol/codemod@beta v1-to-v2 .`
- **Handlers take method strings, not Zod schemas:** `client.setRequestHandler('elicitation/create', ...)`. Result schemas are dropped from `request()`/`callTool()` for spec methods (SDK enforces them); custom/raw passthrough requests need an explicit schema — an Inspector-style "send raw request" feature should pass `ResultSchema` from core.
- **Zod 4 required** (`^4.2.0`); zod 3 unsupported.
- **Error taxonomy rename:** `McpError` → `ProtocolError`; new `SdkError` with string codes (`RequestTimeout`, `ConnectionClosed`, `EraNegotiationFailed`, `MethodNotSupportedByProtocolVersion`, `InputRequiredRoundsExceeded`, ...); `StreamableHTTPError` → `SdkHttpError` (status on `.status`). Unknown-tool calls now *reject* with `-32602` instead of resolving `isError: true`.
- **Elicitation `mode` discriminant:** branch on `params.mode === 'url'` vs. everything-else-is-form; `ElicitResult.content` values strictly `string | number | boolean | string[]`.
- `Protocol` base class no longer exported; `client.fallbackRequestHandler` is the hook for catching arbitrary inbound requests (relevant for a debugging UI).
### 8.2 Handled automatically by SDK v2
- **Era/version negotiation:** `versionNegotiation: { mode: 'auto' }` probes `server/discover` and falls back to `initialize`; `{ pin: '2026-07-28' }` forces modern. **Default is `'legacy'`** — nothing 2026 goes on the wire unless opted in. State accessors: `getProtocolEra()`, `getNegotiatedProtocolVersion()`, `getDiscoverResult()` (persistable; feed back as `connect(transport, { prior })` for a zero-round-trip reconnect).
- **`_meta` envelope** attached to every modern request; `resultType` consumed internally.
- **SEP-2243 headers:** `Mcp-Method`/`Mcp-Name`/`MCP-Protocol-Version` emitted; `x-mcp-header` args auto-mirrored into `Mcp-Param-*` (skipped in browsers due to CORS — note for the Inspector's browser-side transport: header mirroring only happens on the Node/proxy path); `-32020` recovery retry built in.
- **MRTR auto-fulfilment:** on modern connections, `input_required` results are fulfilled through the *same* `setRequestHandler` handlers for elicitation/sampling/roots, and the original request is retried automatically (default 10 rounds; opt out with `inputRequired: { autoFulfill: false }` or drive manually with `withInputRequired()`). Existing sampling/elicitation UI handlers work on both eras unchanged.
- **Notifications:** `listChanged` options transparently open/manage a `subscriptions/listen` stream on modern; explicit `client.listen(filter)` returns an `McpSubscription`.
- **Cancellation** switches to stream-abort on modern; calling code unchanged.
- **Auth:** RFC 9207 `iss` validation in `finishAuth(URLSearchParams)`, issuer stamping (SEP-2352), HTTPS token-endpoint enforcement, scope step-up union (SEP-2350, `onInsufficientScope: 'reauthorize' | 'throw'`), DCR `application_type` defaults (SEP-837), CIMD support (DCR marked deprecated).
### 8.3 Bubbles up to the application
- **Negotiation mode is a product decision.** The SDK docs explicitly warn that **debugging tools should not default to `'auto'`** (the probe stalls on silent stdio legacy servers and pollutes recorded transcripts with an extra round trip). The Inspector should expose era/version selection as per-server configuration.
- **List verbs auto-aggregate all pages** (up to `listMaxPages`, default 64) and return `nextCursor: undefined`. **A pagination-debugging UI must drop to raw `client.request({method:'tools/list', ...})` to observe individual pages.**
- **List verbs no longer throw on missing capability** (return empty instead) unless `enforceStrictCapabilities: true` — an Inspector probably wants strictness on, or should annotate the difference.
- **Response caching (SEP-2549):** on 2026 connections, list verbs honor server `ttlMs`/`cacheScope` and may answer *without a round trip*. A debugging tool should default to `cacheMode: 'bypass'`/`'refresh'` or display cache provenance.
- **Per-request log level:** the SDK does **not** auto-attach `logLevel`; server logs are silently absent until the client stamps the `_meta` key per request. An Inspector that displays server logs must manage this itself on modern connections.
- **Tasks: removed from the SDK.** The experimental tasks layer (polling helpers, `callToolStream`, `TaskStore`) is deleted. Task wire types remain importable as deprecated for 2025-11-25 interop, but `tasks/*` calls require the explicit-schema raw-request form. On a 2026 connection, tasks live in the redesigned `io.modelcontextprotocol/tasks` extension (SEP-2663: polling via `tasks/get`, new `tasks/update`, `tasks/list` removed, unsolicited task handles allowed) — with **no SDK support**; the Inspector implements it or waits for an ext package.
- **Extensions:** capability plumbing only — declare via constructor capabilities, read via `getServerCapabilities()?.extensions`; extension methods are custom methods with your own schemas. **MCP Apps has no support in this SDK** (it stays in `@modelcontextprotocol/ext-apps`, which Inspector v2 already uses).
- **OAuth UX details:** under probing negotiation, a connect-time 401 surfaces wrapped as `SdkError(EraNegotiationFailed)` with the `UnauthorizedError` at `error.data.cause` — the Inspector's 401→authorize→`finishAuth`→reconnect flow must unwrap it. `finishAuth` now takes the full callback `URLSearchParams`.
- **Transcript-classification caveat:** the modern probe itself carries the `_meta` envelope *before* era is known — "saw an envelope" ≠ "modern negotiated" when labeling captured traffic in History.
---
## 9. Impact on MCP Inspector V2 (`v2/main`)
Grounded in the current branch architecture: `InspectorClient` (core/mcp/inspectorClient.ts) wrapping SDK 1.29 with typed events → state stores → React hooks; browser transport remoted through the Hono backend (`core/mcp/remote/`); zustand-persisted OAuth store; capability-gated tabs.
### 9.1 Connection model and state management
This is the deepest change. Today `InspectorClient` and the UI assume: connect ⇒ `initialize` exchange ⇒ negotiated capabilities + optional session id ⇒ session-scoped everything. On modern servers none of that exists.
- **Era becomes first-class connection state.** Add per-server config (auto / legacy / pin-2026, defaulting *not* to auto per SDK guidance) and expose `getProtocolEra()` / `getNegotiatedProtocolVersion()` through the event target and stores. Most downstream UI (which tabs show, which affordances render, how messages are interpreted) should gate on era, not just capabilities.
- **`ConnectionInfoModal` needs an era-aware redesign:** for modern servers there is no initialize result or session id to display — show the `server/discover` result, the pinned/negotiated version, and "sessionless" explicitly. Consider persisting `getDiscoverResult()` in the server catalog and passing it as `prior` for instant reconnects.
- **`getServerType` (`core/mcp/config.ts`) hard-codes `stdio | sse | streamable-http`.** Transport type and protocol era are now orthogonal dimensions; the config model and the remote-transport protocol (`/api/mcp/connect`) must carry both. The backend's `RemoteSession` also currently assumes a persistent transport per session — still true mechanically, but "connected" no longer implies any server-side session, which affects reconnect/disconnect semantics and what `/api/mcp/disconnect` means for a modern server (nothing to DELETE).
- **Capability gating changes source:** tabs are currently gated on the `initialize` result. On modern connections capabilities come from `server/discover` (or lazily from error responses). Also new gates: `-32021 MissingRequiredClientCapability` handling, and the `extensions` maps on both sides.
### 9.2 History and Network tabs (the Inspector's core value)
- **New message vocabulary to render and correlate:** `_meta` envelopes on every request, `resultType` on every result, `input_required` results, retried requests with **new ids** linked by `requestState`, `server/discover`, `subscriptions/listen` + `notifications/subscriptions/acknowledged` + `subscriptionId` tagging. The History view's request/response pairing logic must learn that one logical operation can span multiple JSON-RPC ids (MRTR retries) — probably the single most valuable new visualization: group an MRTR conversation (original call → input requests → user answers → retry → final result) as one expandable unit.
- **If MRTR auto-fulfilment is left on**, the SDK hides the retry loop; the Inspector should either drive MRTR manually (`autoFulfill: false` + `withInputRequired()`) to keep its pending-request UX and full visibility, or capture the auto-fulfilled rounds via the transport-level message log. The existing `PendingClientRequests`/inline panels survive either way since handlers are era-agnostic, but the *semantics* shown to the user ("server sent a request" vs. "server returned input_required; response goes back as a retry") should be accurate per era.
- **Network tab:** display and validate the new headers (`Mcp-Method`, `Mcp-Name`, `Mcp-Param-*`, sentinel-encoded values), show `-32020/-32021/-32022` failures distinctly, and reflect that browser-path requests won't carry `Mcp-Param-*` (mirroring happens on the Node side — since Inspector's web client proxies through the backend, mirroring should work, but verify in the remote-transport layer). Cancellation now appears as connection abort, not a `notifications/cancelled` frame.
- **Raw-request passthrough** must supply explicit result schemas (`ResultSchema`) under SDK v2, and is also the only way to exercise page-by-page pagination and cache-bypass behaviors worth surfacing as Inspector features ("fetch single page", "bypass cache", "refresh").
### 9.3 Feature tabs
- **Tools:** parse/validate `x-mcp-header` annotations and *visibly flag tools excluded for invalid annotations* (the spec makes exclusion mandatory — a debugging tool should show *why* a tool vanished). Show which args will be mirrored to headers. Unknown-tool calls now reject with `-32602` rather than returning `isError` results — adjust result rendering.
- **Tasks tab: substantial rework.** The current implementation targets the 2025-11-25 experimental core tasks (including receiver tasks and `tasks/list`). Under SEP-2663 tasks are an extension (`io.modelcontextprotocol/tasks`) with polling `tasks/get`, new `tasks/update`, no `tasks/list`, no blocking `tasks/result`, and unsolicited task handles. SDK v2 gives no help. Plan: keep the current tab for legacy-era servers, build an extension-aware implementation (raw requests + explicit schemas) for modern ones, and gate on the extension being negotiated.
- **Resources:** `resources/subscribe`/`unsubscribe` and `ResourceSubscriptionsState` become legacy-only; on modern, subscriptions are entries in the `subscriptions/listen` filter — the state store should model "one listen stream + filter set + acknowledged state + reconnect-by-re-listen" (no resumability).
- **Logs:** `logging/setLevel` and the `LoggingScreen`'s level selector are legacy-only. On modern, add a per-request (or global stamp-every-request) log-level control and make clear that logs arrive on the originating request's stream. `ping` UI, if any, is legacy-only.
- **Apps:** already ahead of the curve via `ext-apps`. Work is alignment: advertise `io.modelcontextprotocol/ui` with `mimeTypes` through the now-formalized `capabilities.extensions` (the EMA advertisement in `inspectorClient.ts` is the existing precedent to generalize), and track the ext-apps `2026-01-26` spec/`AppBridge` version.
- **Extensions UI (new, small but high-leverage):** display the server's `extensions` map in Connection Info, and let users toggle which extensions the Inspector *advertises* — servers legitimately change tool registration based on client-declared extensions, so this is a real debugging knob.
### 9.4 Auth and the OAuth store
- **Re-key the zustand OAuth store** (`core/auth/store.ts`) from per-server to per-`(server, issuer)`: stamp `issuer` on stored `clientInformation`/`tokens`, detect issuer changes against freshly fetched PRM/AS metadata on every flow, invalidate + re-register (DCR) / error (static) / continue (CIMD) accordingly. SDK v2 handles the flow logic but expects providers to round-trip issuer-stamped objects and optionally implement `discoveryState()`/`saveDiscoveryState()`.
- **Persist the requested-scope set** per (server, issuer) and show the step-up union computation in the auth visualizations — this is exactly the kind of thing Inspector users will want to see when debugging 403 loops. Surface the `onInsufficientScope` policy as a setting.
- **DCR panel:** show the `application_type` sent (Inspector = `"native"`), render registration rejections meaningfully, and reflect DCR's deprecated-in-favor-of-CIMD status (Inspector v2 already supports CIMD registration kind — good position).
- **401 handling under negotiation:** unwrap `EraNegotiationFailed` → `UnauthorizedError` in `oauthManager.ts`; switch to `finishAuth(callbackParams)` whole-params form (RFC 9207 iss validation happens inside; mismatches throw `IssuerMismatchError` — a new error state to display).
- **Network tab auth capture** continues to work (discovery/DCR/token traffic through `/api/fetch`), but the masked-secret-field list and the flow-step model (`OAuthStep`) should gain the new steps/errors (issuer comparison, step-up union, CIMD fetch).
### 9.5 Suggested sequencing
1. **SDK v2 migration mechanics** (packages, codemod, Zod 4, handler signatures, error taxonomy) with `versionNegotiation: 'legacy'` — behavior-neutral, unblocks everything else. Evaluate the `v1.x-2026-07-28` branch as a hedge if v2-stable slips past your release window.
2. **Auth store re-keying + SEP-837/2350/2352 UX** — independent of transport work, applies to both eras, and the SDK betas already enforce the flows.
3. **Era-aware connection model** (config, remote-transport protocol, Connection Info, capability gating) with explicit era selection UI.
4. **History/Network upgrades** for the new vocabulary (MRTR grouping, listen streams, Mcp-* headers, new error codes).
5. **Tab-by-tab era forks:** logging, resources subscriptions, tasks-as-extension, x-mcp-header tooling, extensions toggle UI.
---
## 10. Reference: new error codes
| Code | Name | HTTP | Source |
|---|---|---|---|
| -32020 | HeaderMismatch | 400 | SEP-2243 (renumbered from draft -32001) |
| -32021 | MissingRequiredClientCapability | 400 | SEP-2575 |
| -32022 | UnsupportedProtocolVersion (body lists `supported`) | 400 | SEP-2575 |
| -32601 | Method not found (era/method mismatch) | **404** | draft transport spec |
| -32602 | Invalid params — incl. orphaned server request (legacy), unknown tool (SDK v2) | — | SEP-2260 / SDK v2 |
`-32020`–`-32099` is now reserved for spec-defined errors; `-32000`–`-32019` remains implementation-defined.
---
## 11. Sources
- Milestone: https://github.com/modelcontextprotocol/modelcontextprotocol/milestone/4?closed=1
- Draft spec + changelog: https://modelcontextprotocol.io/specification/draft (esp. `/basic/transports/streamable-http`, `/basic/authorization/*`, `/basic/patterns/mrtr`, `/basic/patterns/subscriptions`, `/changelog`)
- SEP documents (final): https://modelcontextprotocol.io/seps/2243-http-standardization, `/seps/2575-stateless-mcp`, `/seps/1865-mcp-apps-interactive-user-interfaces-for-mcp`, `/seps/2133-extensions`, `/seps/1730-sdks-tiering-system`
- Extensions & Apps: https://modelcontextprotocol.io/extensions/overview, https://github.com/modelcontextprotocol/ext-apps (spec `2026-01-26`)
- SDK tiers / conformance: https://modelcontextprotocol.io/community/sdk-tiers, https://github.com/modelcontextprotocol/conformance
- TypeScript SDK v2 (`main`, cloned at commit `7635115`, packages `2.0.0-beta.2`): `docs/migration/upgrade-to-v2.md`, `docs/migration/support-2026-07-28.md`, `docs/protocol-versions.md`, `docs/clients/*`
- Inspector `v2/main` (cloned at commit `066676a`): `AGENTS.md`, `core/mcp/inspectorClient.ts`, `core/mcp/remote/`, `core/auth/`, `clients/web/src/`
- Release blog posts: https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/, `/posts/sdk-betas-2026-07-28/`
*Verification note: transport and protocol claims were cross-checked three ways — the draft spec pages, the draft changelog, and the SDK v2 source/docs (independently cloned). SEP-level normative wording was diffed against the 2025-11-25 spec where relevant. GitHub API access was intermittently unavailable during research, so PR-level diffs for the four auth SEPs were reconstructed from the published draft vs. 2025-11-25 text; attribution of individual sentences to specific PRs is high-confidence but not diff-verified. Re-verify against the final spec when it ships July 28.*

View file

@ -1,6 +1,6 @@
# Inspector V2 Scope
### [Brief](README.md) | [V1 Problems](v1_problems.md) | V2 Scope | [V2 Tech Stack](v2_web_client.md) | [V2 UX](v2_ux.md)
### [Brief](README.md) | [V1 Problems](v1_problems.md) | V2 Scope | [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)
## Table of Contents

View file

@ -1,7 +1,7 @@
# Inspector V2 UX - Visual Reference
### [Brief](README.md) | [V1 Problems](v1_problems.md) | [V2 Scope](v2_scope.md) | [V2 Tech Stack](v2_web_client.md) | V2 UX
#### [Overview](v2_ux.md) | [Features](v2_ux_features.md) | [Handlers](v2_ux_handlers.md) | Screenshots | [Components](v2_ux_components.md)
### [Brief](README.md) | [V1 Problems](v1_problems.md) | [V2 Scope](v2_scope.md) | [V2 Tech Stack](v2_web_client.md) | V2 UX | [V2 Auth](v2_auth.md) | [V2 New Spec Impact](v2_new_spec_impact.md)
#### [Overview](v2_ux.md) | [Features](v2_ux_features.md) | [Handlers](v2_ux_handlers.md) | Screenshots | [Components](v2_ux_components.md) | [Interfaces](v2_ux_interfaces.md)
---

View file

@ -1,6 +1,6 @@
# Inspector V2 Tech Stack - Server
### [Brief](README.md) | [V1 Problems](v1_problems.md) | [V2 Scope](v2_scope.md) | V2 Tech Stack | [V2 UX](v2_ux.md)
### [Brief](README.md) | [V1 Problems](v1_problems.md) | [V2 Scope](v2_scope.md) | V2 Tech Stack | [V2 UX](v2_ux.md) | [V2 Auth](v2_auth.md) | [V2 New Spec Impact](v2_new_spec_impact.md)
#### [Web Client](v2_web_client.md) | [CLI, TUI, Launcher](v2_cli_tui_launcher.md) | Server | [Storage](v2_storage.md)

View file

@ -1,6 +1,6 @@
# Inspector V2 Tech Stack - Storage - Server List File
### Brief | [V1 Problems](v1_problems.md) | [V2 Scope](v2_scope.md) | V2 Tech Stack | [V2 UX](v2_ux.md)
### [Brief](README.md) | [V1 Problems](v1_problems.md) | [V2 Scope](v2_scope.md) | V2 Tech Stack | [V2 UX](v2_ux.md) | [V2 Auth](v2_auth.md) | [V2 New Spec Impact](v2_new_spec_impact.md)
#### [Web Client](v2_web_client.md) | [CLI, TUI, Launcher](v2_cli_tui_launcher.md) | [Server](v2_server.md) | Storage
##### [Overview](v2_storage.md) | Server List File

View file

@ -1,6 +1,6 @@
# Inspector V2 Tech Stack - Storage Specification
### Brief | [V1 Problems](v1_problems.md) | [V2 Scope](v2_scope.md) | V2 Tech Stack | [V2 UX](v2_ux.md)
### [Brief](README.md) | [V1 Problems](v1_problems.md) | [V2 Scope](v2_scope.md) | V2 Tech Stack | [V2 UX](v2_ux.md) | [V2 Auth](v2_auth.md) | [V2 New Spec Impact](v2_new_spec_impact.md)
#### [Web Client](v2_web_client.md) | [CLI, TUI, Launcher](v2_cli_tui_launcher.md) | [Server](v2_server.md) | Storage
##### Overview | [Server List File](v2_servers_file.md)

View file

@ -1,6 +1,6 @@
# Inspector V2 UX - Specification
### [Brief](README.md) | [V1 Problems](v1_problems.md) | [V2 Scope](v2_scope.md) | [V2 Tech Stack](v2_web_client.md) | V2 UX
### [Brief](README.md) | [V1 Problems](v1_problems.md) | [V2 Scope](v2_scope.md) | [V2 Tech Stack](v2_web_client.md) | V2 UX | [V2 Auth](v2_auth.md) | [V2 New Spec Impact](v2_new_spec_impact.md)
#### Overview | [Features](v2_ux_features.md) | [Handlers](v2_ux_handlers.md) | [Screenshots](v2_screenshots.md) | [Components](v2_ux_components.md) | [Interfaces](v2_ux_interfaces.md)
## Table of Contents

View file

@ -1,6 +1,6 @@
# Inspector V2 UX - Component Development
### [Brief](README.md) | [V1 Problems](v1_problems.md) | [V2 Scope](v2_scope.md) | [V2 Tech Stack](v2_web_client.md) | V2 UX
### [Brief](README.md) | [V1 Problems](v1_problems.md) | [V2 Scope](v2_scope.md) | [V2 Tech Stack](v2_web_client.md) | V2 UX | [V2 Auth](v2_auth.md) | [V2 New Spec Impact](v2_new_spec_impact.md)
#### [Overview](v2_ux.md) | [Features](v2_ux_features.md) | [Handlers](v2_ux_handlers.md) | [Screenshots](v2_screenshots.md) | Components | [Interfaces](v2_ux_interfaces.md)
We are developing React components with Mantine, Zustand, and Storybook using a pattern called "Presentational Components" that emphasizes separation of concerns and testability.

View file

@ -1,6 +1,6 @@
# Inspector V2 UX - Feature Screens
### [Brief](README.md) | [V1 Problems](v1_problems.md) | [V2 Scope](v2_scope.md) | [V2 Tech Stack](v2_web_client.md) | V2 UX
### [Brief](README.md) | [V1 Problems](v1_problems.md) | [V2 Scope](v2_scope.md) | [V2 Tech Stack](v2_web_client.md) | V2 UX | [V2 Auth](v2_auth.md) | [V2 New Spec Impact](v2_new_spec_impact.md)
#### [Overview](v2_ux.md) | Features | [Handlers](v2_ux_handlers.md) | [Screenshots](v2_screenshots.md) | [Components](v2_ux_components.md) | [Interfaces](v2_ux_interfaces.md)
---

View file

@ -1,6 +1,6 @@
# Inspector V2 UX - Handlers & Patterns
### [Brief](README.md) | [V1 Problems](v1_problems.md) | [V2 Scope](v2_scope.md) | [V2 Tech Stack](v2_web_client.md) | V2 UX
### [Brief](README.md) | [V1 Problems](v1_problems.md) | [V2 Scope](v2_scope.md) | [V2 Tech Stack](v2_web_client.md) | V2 UX | [V2 Auth](v2_auth.md) | [V2 New Spec Impact](v2_new_spec_impact.md)
#### [Overview](v2_ux.md) | [Features](v2_ux_features.md) | Handlers | [Screenshots](v2_screenshots.md) | [Components](v2_ux_components.md) | [Interfaces](v2_ux_interfaces.md)
---

View file

@ -1,6 +1,6 @@
# Inspector V2 UX - Component Interfaces
### [Brief](README.md) | [V1 Problems](v1_problems.md) | [V2 Scope](v2_scope.md) | [V2 Tech Stack](v2_web_client.md) | V2 UX
### [Brief](README.md) | [V1 Problems](v1_problems.md) | [V2 Scope](v2_scope.md) | [V2 Tech Stack](v2_web_client.md) | V2 UX | [V2 Auth](v2_auth.md) | [V2 New Spec Impact](v2_new_spec_impact.md)
#### [Overview](v2_ux.md) | [Features](v2_ux_features.md) | [Handlers](v2_ux_handlers.md) | [Screenshots](v2_screenshots.md) | [Components](v2_ux_components.md) | Interfaces
## Summary

Some files were not shown because too many files have changed in this diff Show more