All of the mid-session and step-up auth work

This commit is contained in:
Bob Dickinson 2026-07-02 16:35:31 -07:00
commit 562bdd00f1
4 changed files with 11437 additions and 1212 deletions

View file

@ -365,6 +365,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 +464,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 +509,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

@ -25,10 +25,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 +54,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 +72,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 +86,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 +99,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 +156,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 +175,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 +195,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

File diff suppressed because it is too large Load diff

View file

@ -475,7 +475,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 +483,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 +527,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 +840,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