inspector/specification/v2_auth_smoke_testing.md
cliffhall d7f90144c6 docs: make the local CIMD smoke deterministic, and fix two HTTPS-only claims
Copilot review round 4, four findings, all correct.

The CIMD-off / CIMD-on pair the smoke guide documents is not a control without
clearing OAuth state first: tokens and the registered client persist in
oauth.json independently of the install-wide CIMD toggle, and the Inspector
reuses valid stored tokens before prompting — so a leftover grant from an
earlier CIMD-on run makes the CIMD-off phase connect and the negative control
silently proves nothing. Both the spec's pair and the walkthrough in
docs/test-servers.md now say to clear state (or isolate MCP_STORAGE_DIR) first.

The composable server announces its MCP endpoint, not the metadata document —
`server-composable.ts` prints a URL ending in `/mcp`. The guide called the
announced URL a metadata URL; it now says to take the origin from that line and
append `/client-metadata.json`.

Two general statements elsewhere in the same file still defined CIMD as always
HTTPS (the glossary row, and the Stytch section's opening), so a reader
following them would have rejected the loopback fixture the same file now
documents. Both note the exemption; the Stytch examples stay HTTPS, which they
are.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GfHQ8vMm5NBgzbUJ6UwxmB
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-11 03:32:02 -04:00

68 KiB
Raw Permalink Blame History

Inspector V2 Auth — OAuth smoke testing (real servers)

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

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

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).

This document does not replace CI. It records known-good real endpoints, which client-ID mechanism each server supports, and how to configure Inspector (web, TUI, or CLI).

Install-level client config (TUI / CLI)

EMA and CIMD credentials live in ~/.mcp-inspector/storage/client.json (same file the web Client Settings dialog writes). TUI and CLI load it automatically at startup:

Flag / env Default
--client-config <path> ~/.mcp-inspector/storage/client.json
MCP_CLIENT_CONFIG_PATH Same default when --client-config is omitted

For repo smoke fixtures, point at the checked-in template:

--client-config configs/client.json

CLI flags (--client-metadata-url, --client-id, --client-secret) override values from client.json when present. Per-server OAuth fields in mcp.json still apply for static/DCR/EMA resource credentials.

OAuth callback URL (TUI/CLI only):

Flag / env Default
--callback-url <url> http://127.0.0.1:6276/oauth/callback
MCP_OAUTH_CALLBACK_URL Same default when --callback-url is omitted

Web uses http://localhost:6274/oauth/callback on the main app server — not these runner settings.

Terminology

Term Meaning in this doc
Static / preregistered client You supply oauthClientId and optionally oauthClientSecret in Server Settings. Inspector skips DCR and uses your credentials.
DCR Dynamic Client Registration (RFC 7591). Inspector registers at the AS registration_endpoint on first connect; no client id in config.
CIMD Client ID Metadata Document (SEP-991). Inspector uses an HTTPS metadata URL as client_id when the AS advertises client_id_metadata_document_supported — or a plain http:// one on localhost / 127.0.0.1 / [::1], the loopback exemption added in #2305 for local fixtures.
Client credentials grant OAuth 2.0 machine-to-machine grant_type=client_credentials. Not what we mean by “static client credentials” here. Inspector does not implement this grant yet (#1225).

Prerequisites

  1. Inspector web, TUI, or CLI with OAuth environment wired (environment.oauth — web always; TUI/CLI for HTTP/SSE servers).
  2. Redirect URI must match what the authorization server expects:
    • Web (dev): http://localhost:6274/oauth/callback (default CLIENT_PORT; see clients/web/server/web-server-config.ts).
    • Web (prod launcher): {origin}/oauth/callback (same host/port as the Hono server).
    • TUI / CLI: loopback callback from createOAuthCallbackServer() (TUI) — default http://127.0.0.1:6276/oauth/callback (port 6276 ≈ T9 “MCPO”, MCP OAuth; separate from web 6274). Override with --callback-url or MCP_OAUTH_CALLBACK_URL. Use http://127.0.0.1:0/oauth/callback for an OS-assigned ephemeral port when the AS registers redirect URIs dynamically (DCR).
  3. Catalog entry in ~/.mcp-inspector/mcp.json (or ad-hoc connect) with type: "streamable-http" and the server URL.
  4. Keychain-backed secrets (TUI/CLI): when OAuth client secrets or stdio env values were saved via web Server Settings, they live in the OS keychain — not in mcp.json. TUI and CLI merge keychain values on catalog load (same effective config as web GET /api/servers; see Servers file §Secret storage). For static/EMA smokes you can also pass --client-secret (CLI/TUI), use a local hand-edited mcp.json with plaintext secrets (dev only — never commit), or save secrets once via web on the same machine before running the TUI/CLI.

Example catalog shape (HTTP server)

{
  "mcpServers": {
    "mcp-example-everything": {
      "type": "streamable-http",
      "url": "https://example-server.modelcontextprotocol.io/mcp",
    },
    "github-mcp": {
      "type": "streamable-http",
      "url": "https://api.githubcopilot.com/mcp/",
    },
    "stytch-mcp-demo": {
      "type": "streamable-http",
      "url": "https://stytch-as-demo.val.run/mcp",
    },
    "stytch-mcp": {
      "type": "streamable-http",
      "url": "https://mcp.stytch.dev/mcp",
    },
  },
}

Per-server OAuth fields live on the same entry (lifted to InspectorServerSettings in memory). On disk (#1358 flat shape):

"oauth": {
  "clientId": "<from GitHub OAuth App>",
  "clientSecret": "<from GitHub OAuth App>",
  "scopes": "repo read:user"
}

Smoke matrix (real servers)

Server URL Mechanism to smoke Credentials in repo?
MCP Example “Everything” (hosted) https://example-server.modelcontextprotocol.io/mcp DCR No — register at connect time
GitHub MCP (remote) https://api.githubcopilot.com/mcp/ Static OAuth App (or PAT header bypass) No — you create a GitHub OAuth App
Stytch MCP demo (hosted) https://stytch-as-demo.val.run/mcp CIMD (also DCR) No — use MCPJam CIMD (default for smoke)
Stytch MCP (management API) https://mcp.stytch.dev/mcp CIMD (also DCR) No — same MCPJam CIMD URL; real Stytch login at stytch.com
Composable test server (local) http://127.0.0.1:<port>/mcp Static, DCR, CIMD (all) Fake ids in e2e tests only
xaa.dev EMA Local resource + auth.resource.xaa.dev EMA (not standard OAuth ladder) Registered on xaa.dev — see EMA staging

1. MCP Example “Everything” server (DCR)

Hosted reference server implementing the full MCP feature surface (tools, resources, prompts, sampling, elicitation). Source: modelcontextprotocol/example-remote-server. Public deployment:

Field Value
MCP URL https://example-server.modelcontextprotocol.io/mcp
Resource identifier https://example-server.modelcontextprotocol.io/
Authorization server Same host (combined resource + AS)
Protected resource metadata https://example-server.modelcontextprotocol.io/.well-known/oauth-protected-resource
AS metadata https://example-server.modelcontextprotocol.io/.well-known/oauth-authorization-server

Verified AS capabilities (June 2026):

  • registration_endpoint: present → DCR supported
  • client_id_metadata_document_supported: not advertised → CIMD not available on this host
  • token_endpoint_auth_methods_supported: ["none"] → public client after DCR
  • code_challenge_methods_supported: ["S256"] → PKCE required

Procedure (web)

  1. Add catalog entry (no oauth block needed for DCR).
  2. Start Inspector web (mcp-inspector --web --dev or launcher).
  3. Connect → expect 401, then OAuth redirect (or use Connection flow that calls authenticate() on 401).
  4. Complete browser authorization on the example AS.
  5. Pass: tools/list succeeds; Connection Info shows authorized + dynamic client id from DCR.

Procedure (TUI)

  1. mcp-inspector --tui with catalog containing the server.
  2. Press C to connect — OAuth starts automatically when the server returns 401.
  3. Complete browser flow against callback server (http://127.0.0.1:6276/oauth/callback by default).
  4. Pass: connect succeeds without a second C; Auth tab shows authorized state.

Notes

  • First connect performs discovery + DCR + authorization code + PKCE. Tokens persist in ~/.mcp-inspector/storage/oauth.json (CLI/TUI direct file, web via RemoteOAuthStorage → same file on the local backend).
  • Reconnect should reuse stored DCR client_id unless storage was cleared.

2. GitHub MCP server (static / preregistered OAuth App)

Remote GitHub MCP is the usual choice for static client credential smoke testing against a production authorization server that does not expose DCR to arbitrary MCP clients.

Field Value
MCP URL https://api.githubcopilot.com/mcp/
Resource identifier https://api.githubcopilot.com/mcp
Protected resource metadata https://api.githubcopilot.com/.well-known/oauth-protected-resource/mcp
Authorization server https://github.com/login/oauth
Upstream docs github/github-mcp-server — remote-server.md

Verified PRM (June 2026): authorization_servers: ["https://github.com/login/oauth"]. Scopes advertised include repo, read:org, read:user, user:email, gist, workflow, etc.

Why GitHub is the static-credentials smoke server

  • GitHub’s OAuth platform expects a pre-registered OAuth App (or GitHub App) with a fixed callback URL. There is no open registration_endpoint for Inspector-style DCR.
  • VS Code, Cursor, and other hosts register their own GitHub OAuth applications; Inspector does not ship shared production client id/secret pairs.
  • This matches the “user knows static credentials are required” path: set oauthClientId / oauthClientSecret in Server Settings before or after the first 401.

Credentials to use (you create these)

There are no shared Inspector test credentials in this repository. Create a GitHub OAuth App under your account or org:

  1. Open GitHub Developer settings → OAuth Apps → New OAuth App.

  2. Suggested values for local web smoke:

    Field Value
    Application name MCP Inspector (local dev)
    Homepage URL http://localhost:6274
    Authorization callback URL http://localhost:6274/oauth/callback

    For TUI/CLI, register http://127.0.0.1:6276/oauth/callback (default) on the OAuth app, or override with --callback-url / MCP_OAUTH_CALLBACK_URL.

  3. After creation, copy Client ID and generate a Client secret.

  4. In Inspector Server Settings → OAuth for the GitHub MCP entry:

    • Client ID → oauth.clientId on disk
    • Client secret → oauth.clientSecret (stored via keychain on web backend when saved through UI)
    • Scopes → space-separated subset of PRM scopes, e.g. read:user repo (match what you need for tools/list; tighter scopes reduce consent friction)

Example mcp.json fragment:

"github-mcp": {
  "type": "streamable-http",
  "url": "https://api.githubcopilot.com/mcp/",
  "oauth": {
    "clientId": "Ov23liXXXXXXXXXXXX",
    "clientSecret": "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "scopes": "read:user repo"
  }
}

Never commit real secrets. Use local catalog only.

Procedure (web)

Run both phases in order. Clear OAuth state between phases: Server Settings → OAuth → Clear stored OAuth state, or (while connected) Connection Info → OAuth Details → Clear and disconnect.

Phase 1 — static credentials off (expect failure)

  1. Add the GitHub MCP catalog entry with no Server Settings OAuth fields (no oauth block on disk).
  2. Connect → 401 → authenticate → complete or attempt GitHub login.
  3. Pass (negative control): connect does not reach an authorized MCP session. Typical outcomes: token exchange or registration error, or 401 persists after callback — GitHub has no DCR registration_endpoint, so Inspector cannot obtain a client id without your OAuth App credentials.

Phase 2 — static credentials on (expect success)

  1. Create the GitHub OAuth App and fill Server Settings → OAuth (Client ID, Client secret, scopes) as above.
  2. Clear OAuth state for this server again (Server Settings or Connection Info as above).
  3. Connect → 401 → authenticate → GitHub login/consent for your OAuth App.
  4. Pass: connected; tools/list returns GitHub tools; Connection Info shows Client registration = Static (preregistered) and your OAuth App client id.

Procedure (TUI)

Same two phases:

  1. Off: catalog entry without oauth block → connect, A to authenticate → expect failure (no DCR on GitHub).
  2. On: add oauth block or --client-id / --client-secret → clear stored OAuth tokens for the server → connect → expect success as in Phase 2 (web).

Alternative: Personal Access Token (non-OAuth smoke)

GitHub documents PAT auth with a static header (bypasses OAuth flow entirely). Useful for MCP tool smoke, not for testing Inspector OAuth:

"github-mcp-pat": {
  "type": "streamable-http",
  "url": "https://api.githubcopilot.com/mcp/",
  "headers": {
    "Authorization": "Bearer <GITHUB_PAT>"
  }
}

See GitHub MCP README — Using a GitHub PAT.

GitHub org / enterprise

  • Org may block OAuth Apps or MCP policies — see policies-and-governance.md.
  • GitHub Enterprise Cloud (ghe.com): use https://copilot-api.octocorp.ghe.com/mcp and enterprise OAuth settings (README).

3. Stytch MCP server (CIMD)

Stytch advertises first-class CIMD support for MCP. Inspector smoke testing uses two hosted Stytch MCP targets:

Target When to use
stytch-mcp-demo — https://stytch-as-demo.val.run/mcp Default for local CIMD smoke. Test Stytch project; authorize at the demo app (/oauth/authorize) with email OTP / test login — not the stytch.com dashboard.
stytch-mcp — https://mcp.stytch.dev/mcp Production-style smoke. Stytch’s hosted Management API MCP; authorize at https://stytch.com/oauth/authorize with a real Stytch workspace account (often social SSO).

3a. Stytch demo MCP (preferred for dev)

Field Value
MCP URL https://stytch-as-demo.val.run/mcp
Resource identifier https://stytch-as-demo.val.run/mcp
Protected resource metadata https://stytch-as-demo.val.run/.well-known/oauth-protected-resource/mcp
Authorization server (issuer) https://industrious-dress-4239.customers.stytch.dev (resolve from PRM — may change)
AS metadata https://industrious-dress-4239.customers.stytch.dev/.well-known/oauth-authorization-server
Token endpoint https://industrious-dress-4239.customers.stytch.dev/v1/oauth2/token
DCR registration endpoint https://industrious-dress-4239.customers.stytch.dev/v1/oauth2/register
Authorization UI https://stytch-as-demo.val.run/oauth/authorize (demo-hosted; test login — not stytch.com)

Verified discovery (June 2026): PRM at the URL above currently returns:

{
  "resource": "https://stytch-as-demo.val.run/mcp",
  "authorization_servers": [
    "https://industrious-dress-4239.customers.stytch.dev"
  ],
  "scopes_supported": ["openid", "email"]
}

AS metadata from that issuer currently includes:

  • issuer: https://industrious-dress-4239.customers.stytch.dev
  • client_id_metadata_document_supported: true → CIMD
  • registration_endpoint: …/v1/oauth2/register → DCR also works on the same server
  • token_endpoint: …/v1/oauth2/token
  • authorization_endpoint: https://stytch-as-demo.val.run/oauth/authorize (not stytch.com)
  • code_challenge_methods_supported: ["S256"]
  • token_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post", "none"]

Use this server when validating Inspector CIMD/DCR against a real Connected Apps AS without needing a Stytch Management workspace login.

Typical network sequence (CIMD, web):

  1. POST https://stytch-as-demo.val.run/mcp → 401 (no token)
  2. GET …/.well-known/oauth-protected-resource/mcp → PRM
  3. GET …/.well-known/oauth-authorization-server → AS metadata (on the issuer host from PRM)
  4. Browser redirect to https://stytch-as-demo.val.run/oauth/authorize?client_id=… (not logged as an Inspector fetch)
  5. Callback to http://localhost:6274/oauth/callback
  6. POST …/v1/oauth2/token → 200 (see Verifying CIMD for the request body)
  7. POST https://stytch-as-demo.val.run/mcp → 200

CIMD does not call POST …/v1/oauth2/register — Inspector stores the metadata URL as client_id locally before auth(). Stytch fetches the metadata document server-side during authorize; that fetch does not appear in Inspector’s network log.

3b. Stytch Management MCP (production-style)

Field Value
MCP URL https://mcp.stytch.dev/mcp
Resource identifier https://mcp.stytch.dev
Protected resource metadata https://mcp.stytch.dev/.well-known/oauth-protected-resource
Product docs Stytch MCP Server, mcp.stytch.dev

Verified discovery (June 2026): PRM at the URL above currently returns:

{
  "resource": "https://mcp.stytch.dev",
  "authorization_servers": [
    "https://rustic-kilogram-6347.customers.stytch.com"
  ],
  "scopes_supported": [
    "openid",
    "email",
    "profile",
    "admin:projects",
    "manage:api_keys",
    "manage:api_keys:test",
    "manage:project_settings",
    "manage:project_data"
  ]
}

AS metadata from that issuer includes:

  • client_id_metadata_document_supported: true → CIMD
  • registration_endpoint: present → DCR also works on the same server
  • authorization_endpoint: https://stytch.com/oauth/authorize (Stytch workspace login)
  • code_challenge_methods_supported: ["S256"]

Resolve token_endpoint and registration_endpoint fresh from AS metadata on the issuer host (rustic-kilogram-6347.customers.stytch.com at time of writing). Issuer hostnames are project-specific and may change — always follow PRM → AS discovery rather than hardcoding.

Use this server when you need to smoke CIMD against Stytch’s live Management API MCP and can sign in with a real Stytch account.

CIMD credentials for smoke (MCPJam)

Inspector Stytch CIMD smoke uses MCPJam’s public Client ID Metadata Document — no hosting or OAuth App setup required.

CIMD does not use oauth.clientId / oauth.clientSecret in mcp.json. Configure install-wide in Client Settings (web), client.json (TUI/CLI default path or --client-config), or --client-metadata-url (CLI/TUI override).

Metadata URL (use this in Client Settings)

https://www.mcpjam.com/.well-known/oauth/client-metadata.json

Live document (June 2026; fetch fresh if debugging redirect mismatches):

{
  "client_id": "https://www.mcpjam.com/.well-known/oauth/client-metadata.json",
  "client_name": "MCPJam",
  "client_uri": "https://www.mcpjam.com",
  "logo_uri": "https://www.mcpjam.com/mcp_jam_2row.png",
  "redirect_uris": [
    "mcpjam://oauth/callback",
    "mcpjam://authkit/callback",
    "http://127.0.0.1:6274/oauth/callback",
    "http://localhost:6274/oauth/callback",
    "…"
  ],
  "grant_types": [
    "authorization_code",
    "refresh_token",
    "urn:ietf:params:oauth:grant-type:device_code"
  ],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "application_type": "native"
}

The full redirect_uris list also includes other localhost ports and MCPJam app URLs — see the live document.

Inspector configuration

Surface Setting
Web Client Settings → Client ID Metadata Document → paste the MCPJam URL above → enable Use Client ID Metadata Document
TUI --client-metadata-url https://www.mcpjam.com/.well-known/oauth/client-metadata.json (or enable CIMD in client.json / --client-config)
CLI Same flags; reuses tokens from ~/.mcp-inspector/storage/oauth.json when already authorized via web/TUI

Why MCPJam’s document works with Inspector

CIMD treats the metadata HTTPS URL as the OAuth client_id (loopback http:// is the one exemption — see § local fallback; everything in this Stytch section is HTTPS). On authorize, Stytch (or any CIMD-capable AS) fetches that JSON and checks:

  1. The document’s client_id field matches the URL.
  2. The redirect_uri on the request appears exactly in redirect_uris.

Inspector web dev uses http://localhost:6274/oauth/callback by default (CLIENT_PORT=6274). MCPJam’s published metadata includes that URI (and 127.0.0.1:6274), so a smoke run succeeds without Inspector hosting its own CIMD file.

Implications for smoke (expected, not bugs):

  • Connection Info → Client ID shows the MCPJam metadata URL, not “MCP Inspector”.
  • Consent UI may show “MCPJam” name/logo from the document — you authorized MCPJam’s published OAuth identity, not a separate Inspector-specific registration.
  • No POST …/v1/oauth2/register — Inspector stores the metadata URL locally; Stytch validates by fetching MCPJam’s HTTPS document server-side.
  • Access token JWT still carries Stytch’s internal connected-app-test-… id; see Verifying CIMD.

This is appropriate for protocol smoke only. MCPJam’s doc is a shared dev identity with permissive localhost redirects; production Inspector deployments should use an Inspector-owned metadata URL (future — see below).

Reference only (won’t work for Inspector callback): client.dev/oauth/metadata.json is Stytch’s public CIMD demo document; its redirect_uris point at client.dev, not localhost.

Or use DCR against the same Stytch server with CIMD disabled (see Alternative: DCR on Stytch).

Inspector-owned CIMD (future)

Hosting a dedicated https://…/inspector/oauth-client.json with Inspector branding and a minimal redirect_uris list (only Inspector callbacks) is the right model for non-smoke use. Not required for Stytch smoke today; document shape and Stytch requirements will be added here when we ship a hosted metadata URL.

Verifying CIMD vs DCR (network and tokens)

Stytch normalizes both CIMD and DCR into internal Connected App records. The access token JWT looks the same either way — client_id in the decoded payload is Stytch’s internal id (e.g. connected-app-test-0bb7b586-…), not your metadata URL. Do not use the JWT alone to distinguish CIMD from DCR.

Check CIMD DCR
Connection Info → Client ID MCPJam metadata URL Opaque connected-app-test-…
Connection Info → Client registration Client ID Metadata (CIMD) Dynamic (DCR)
POST …/v1/oauth2/token body → client_id Metadata URL connected-app-test-…
Authorize URL → client_id param (browser) Metadata URL connected-app-test-…
POST …/v1/oauth2/register (after cleared OAuth state) Absent Present on first connect
Decoded access token JWT → client_id Internal Stytch id (same shape as DCR) Internal Stytch id

Definitive smoke signal: expand the POST …/v1/oauth2/token row in the network log (auth category). The form body must include the MCPJam metadata URL as client_id. Verified example against stytch-mcp-demo (June 2026):

grant_type=authorization_code
code=<authorization_code>
code_verifier=<pkce_verifier>
redirect_uri=http://localhost:6274/oauth/callback
resource=https://stytch-as-demo.val.run/mcp
client_id=https://www.mcpjam.com/.well-known/oauth/client-metadata.json

Example decoded access token payload from the same flow (Stytch internal id — expected, not a CIMD failure):

{
  "aud": ["project-test-d06972e8-6af2-4952-bcb0-44d795ec5d6f"],
  "client_id": "connected-app-test-0bb7b586-e16a-43f6-b2b4-6511a146cd1d",
  "iss": "https://industrious-dress-4239.customers.stytch.dev",
  "scope": "openid email",
  "sub": "user-test-5ccf82bc-485d-4919-9e11-40084b02dc28"
}

Procedure (web)

Use stytch-mcp-demo unless you explicitly need the Management API host. Configure the MCPJam CIMD URL, then run both phases. Clear OAuth state between phases: Server Settings → OAuth → Clear stored OAuth state, or (while connected) Connection Info → OAuth Details → Clear and disconnect.

Stytch note: Both hosts advertise DCR. With CIMD disabled, Inspector may still authorize via dynamic registration. Phase 1 therefore checks that CIMD was not used (client id ≠ MCPJam metadata URL), not necessarily that connect hard-fails. For a strict fail-without-CIMD smoke, use the local composable server with supportCIMD: true and supportDCR: false.

Phase 1 — CIMD off (expect failure or non-CIMD client id)

  1. Client Settings → Client ID Metadata Document: paste the MCPJam URL but leave Use Client ID Metadata Document unchecked (URL stays saved for Phase 2).
  2. Add Stytch demo MCP (https://stytch-as-demo.val.run/mcp) with no per-server OAuth fields.
  3. Connect → 401 → authenticate.
  4. Pass (negative control): either connect does not reach an authorized session (e.g. you blocked DCR in your Stytch workspace), or Connection Info shows a dynamic DCR client id (not the MCPJam metadata URL). If you already see the MCPJam URL as client id, CIMD was still active — clear OAuth storage and confirm the checkbox is off.

Phase 2 — CIMD on (expect success)

  1. Enable Use Client ID Metadata Document (MCPJam URL from above).
  2. Clear OAuth state for the Stytch server again.
  3. Connect → 401 → authenticate → demo login/consent at stytch-as-demo.val.run (or stytch.com if using Management MCP).
  4. Pass: connected; tools/list succeeds; Connection Info shows client id = MCPJam metadata URL and Client registration = Client ID Metadata (CIMD); POST …/v1/oauth2/token request body shows the same URL as client_id (see Verifying CIMD).

Procedure (TUI)

Same two phases (prefer stytch-mcp-demo URL):

  1. Off: launch without CIMD enabled in client.json and without --client-metadata-url → connect (C) → complete browser auth if prompted → expect failure or DCR client id as in Phase 1 (web).
  2. On:
    mcp-inspector --tui --catalog configs/mcp.json \
      --client-metadata-url https://www.mcpjam.com/.well-known/oauth/client-metadata.json
    
    Or enable CIMD in client.json and pass --client-config configs/client.json (or rely on the default install path after saving via web Client Settings). Clear OAuth tokens → connect (C) → expect success with client id = metadata URL.

Procedure (CLI)

Interactive OAuth uses the loopback callback server (same as TUI). First-time connect or mid-session step-up:

mcp-inspector --cli --catalog configs/mcp.json --server stytch-mcp-demo \
  --client-metadata-url https://www.mcpjam.com/.well-known/oauth/client-metadata.json \
  --method tools/list

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 (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)

Same MCP URL (stytch-as-demo.val.run/mcp or mcp.stytch.dev/mcp), leave CIMD disabled and clientMetadataUrl unset — Inspector registers via registration_endpoint on the Stytch AS (demo: POST https://industrious-dress-4239.customers.stytch.dev/v1/oauth2/register at time of writing). First connect should show that register call in the network log before authorize. Useful when you only want to verify remote DCR + PKCE without hosting CIMD JSON. Token exchange will use the returned connected-app-test-… id as client_id, not an HTTPS metadata URL.

Local fallback (composable test server)

For offline CIMD regression — or a strict fail-with-CIMD-off / succeed-with-CIMD-on pair without Stytch’s DCR fallback — use the shipped oauth-cimd-http.json fixture, which sets supportCIMD: true and supportDCR: false and hosts the client metadata document itself at /client-metadata.json.

# The build script lives in clients/web; both paths below are repo-root-relative,
# so come back up before starting the server.
(cd clients/web && npm run test-servers:build)
node test-servers/build/server-composable.js --config test-servers/configs/oauth-cimd-http.json

The server announces its URL on stderr, and walks to the next free port on EADDRINUSE — read the announced URL rather than assuming 8092.

Since #2305 a loopback http:// metadata URL is a valid persisted client.json setting and a valid entry in the settings form, not a test-only affordance — getCimdClientMetadataUrlError exempts localhost, 127.0.0.1 and [::1], the same three literals the SDK exempts for token endpoints. So clientMetadataUrl can point straight at the document this fixture serves, with no HTTPS listener. Before that change the exemption existed only in ensureCimdClientRegistration, for an already-stored client_id, and driving this by hand needed a self-signed HTTPS listener plus NODE_TLS_REJECT_UNAUTHORIZED=0.

⚠️ The server announces its MCP endpoint, not the metadata URL — the line on stderr ends in /mcp. Take the origin (host and port) from it and append /client-metadata.json: an announced http://127.0.0.1:8092/mcp means clientMetadataUrl is http://127.0.0.1:8092/client-metadata.json. Don't assume 8092; the fixture walks past a taken port.

⚠️ Clear OAuth state before each phase, or neither outcome below is deterministic. Tokens and the registered client persist in ~/.mcp-inspector/storage/oauth.json independently of the install-wide CIMD toggle, and the Inspector reuses valid stored tokens before prompting — so a leftover grant from an earlier CIMD-on run makes the CIMD-off phase connect, and the negative control silently proves nothing. Use Clear OAuth state and disconnect (Server Settings → Authorization) before the off phase and again between the off and on phases. Pointing MCP_STORAGE_DIR at a throwaway directory for the whole run is the stronger version, and also survives a restarted fixture having forgotten a client it once issued.

With that state cleared: with CIMD off in Client Settings, connect + authenticate should fail; with CIMD on and that local metadata URL, it should succeed. docs/test-servers.md has the full walkthrough, including the redirect_uris port trap.


4. EMA (xaa.dev) — cross-reference

Enterprise-managed auth uses a different credential model (IdP in Client Settings + resource AS client on server). Documented in v2_auth_ema.md § Staging validation:

  • IdP: https://idp.xaa.dev
  • Resource AS: https://auth.resource.xaa.dev
  • Local MCP resource: test-servers/configs/xaa-ema-http.json

Do not use xaa.dev for standard DCR/static/CIMD ladder testing unless explicitly testing EMA.

TUI EMA smoke (IdP in client.json, resource server with enterpriseManaged: true in catalog):

mcp-inspector --tui --catalog configs/mcp.json \
  --client-config configs/client.json

Register http://127.0.0.1:6276/oauth/callback on the xaa.dev IdP before leg 1 (default runner callback). IdP clientSecret belongs in client.json (web Client Settings or --client-config fixture). Per-server resource oauth.clientSecret belongs in the OS keychain (web Server Settings save path) or a local fixture — TUI/CLI rehydrate both IdP and resource secrets from keychain on catalog load (see prerequisite §4 above).


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.

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):

    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):

    "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.
  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.
  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).

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.
  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.
  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.
  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) 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. 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. 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
Negative control (mechanism off → fail or wrong client id source) Connection Info / error toast
Clear OAuth state between smoke phases Web: Server Settings → OAuth → Clear stored OAuth state; or Connection Info → Clear and disconnect (standard) / Clear OAuth state (EMA). TUI: Auth tab → S (Clear OAuth State; disconnects when connected)
Positive path (mechanism on → authorized session) Connect + tools/list
401 on first connect without tokens Network / Requests tab
OAuth discovery + correct mechanism (DCR vs static vs CIMD) Network log (auth category)
CIMD: token exchange client_id = metadata URL POST …/v1/oauth2/token request body
CIMD: no DCR register call (after cleared state) Absence of POST …/v1/oauth2/register
Stytch: JWT client_id is internal id (not metadata URL) Decode access token — informational only
Authorization redirect opens Browser
Callback completes (/oauth/callback) Web URL or TUI callback server
Second connect uses stored access token Connect without re-login (until expiry)
tools/list JSON Tools tab / CLI --method tools/list
Connection Info: protocol, authorized, client id, registration kind Connection Info OAuth section

Automated parity

Mode Real server (this doc) CI (in-repo)
DCR example-server.modelcontextprotocol.io (or Stytch MCP) inspectorClient-oauth-e2e.test.ts
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 (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)

Mid-session auth is implemented for web (remote transport), TUI, and CLI — see Mid-session authorization. Remaining: optional idle SSE E2E, v2 SDK transport upgrade for direct silent retry.

See Auth hardening (MCP 2026-07-28) for per-SEP as-built status and automated coverage. Hosted smoke here is complementary for real IdPs — SEPs already covered in CI do not need dedicated smoke scenarios.

  • Mid-session auth: see §5 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).

References