Inspector V2 Auth
July 20, 2026 · View on GitHub
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. |
| 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
- Inspector web, TUI, or CLI with OAuth environment wired (
environment.oauth— web always; TUI/CLI for HTTP/SSE servers). - Redirect URI must match what the authorization server expects:
- Web (dev):
http://localhost:6274/oauth/callback(defaultCLIENT_PORT; seeclients/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) — defaulthttp://127.0.0.1:6276/oauth/callback(port 6276 ≈ T9 “MCPO”, MCP OAuth; separate from web6274). Override with--callback-urlorMCP_OAUTH_CALLBACK_URL. Usehttp://127.0.0.1:0/oauth/callbackfor an OS-assigned ephemeral port when the AS registers redirect URIs dynamically (DCR).
- Web (dev):
- Catalog entry in
~/.mcp-inspector/mcp.json(or ad-hoc connect) withtype: "streamable-http"and the server URL. - 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 webGET /api/servers; see Servers file §Secret storage). For static/EMA smokes you can also pass--client-secret(CLI/TUI), use a local hand-editedmcp.jsonwith 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 supportedclient_id_metadata_document_supported: not advertised → CIMD not available on this hosttoken_endpoint_auth_methods_supported:["none"]→ public client after DCRcode_challenge_methods_supported:["S256"]→ PKCE required
Procedure (web)
- Add catalog entry (no
oauthblock needed for DCR). - Start Inspector web (
mcp-inspector --web --devor launcher). - Connect → expect 401, then OAuth redirect (or use Connection flow that calls
authenticate()on 401). - Complete browser authorization on the example AS.
- Pass:
tools/listsucceeds; Connection Info shows authorized + dynamic client id from DCR.
Procedure (TUI)
mcp-inspector --tuiwith catalog containing the server.- Press C to connect — OAuth starts automatically when the server returns 401.
- Complete browser flow against callback server (
http://127.0.0.1:6276/oauth/callbackby default). - 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 viaRemoteOAuthStorage→ same file on the local backend). - Reconnect should reuse stored DCR
client_idunless 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_endpointfor 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/oauthClientSecretin 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:
-
Open GitHub Developer settings → OAuth Apps → New OAuth App.
-
Suggested values for local web smoke:
Field Value Application name MCP Inspector (local dev)Homepage URL http://localhost:6274Authorization callback URL http://localhost:6274/oauth/callbackFor 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. -
After creation, copy Client ID and generate a Client secret.
-
In Inspector Server Settings → OAuth for the GitHub MCP entry:
- Client ID →
oauth.clientIdon 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 fortools/list; tighter scopes reduce consent friction)
- Client ID →
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)
- Add the GitHub MCP catalog entry with no Server Settings OAuth fields (no
oauthblock on disk). - Connect → 401 → authenticate → complete or attempt GitHub login.
- 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)
- Create the GitHub OAuth App and fill Server Settings → OAuth (Client ID, Client secret, scopes) as above.
- Clear OAuth state for this server again (Server Settings or Connection Info as above).
- Connect → 401 → authenticate → GitHub login/consent for your OAuth App.
- Pass: connected;
tools/listreturns GitHub tools; Connection Info shows Client registration = Static (preregistered) and your OAuth App client id.
Procedure (TUI)
Same two phases:
- Off: catalog entry without
oauthblock → connect, A to authenticate → expect failure (no DCR on GitHub). - On: add
oauthblock 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/mcpand 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.devclient_id_metadata_document_supported: true → CIMDregistration_endpoint:…/v1/oauth2/register→ DCR also works on the same servertoken_endpoint:…/v1/oauth2/tokenauthorization_endpoint:https://stytch-as-demo.val.run/oauth/authorize(notstytch.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):
POST https://stytch-as-demo.val.run/mcp→ 401 (no token)GET …/.well-known/oauth-protected-resource/mcp→ PRMGET …/.well-known/oauth-authorization-server→ AS metadata (on the issuer host from PRM)- Browser redirect to
https://stytch-as-demo.val.run/oauth/authorize?client_id=…(not logged as an Inspector fetch) - Callback to
http://localhost:6274/oauth/callback POST …/v1/oauth2/token→ 200 (see Verifying CIMD for the request body)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 → CIMDregistration_endpoint: present → DCR also works on the same serverauthorization_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. On authorize, Stytch (or any CIMD-capable AS) fetches that JSON and checks:
- The document’s
client_idfield matches the URL. - The
redirect_urion the request appears exactly inredirect_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)
- Client Settings → Client ID Metadata Document: paste the MCPJam URL but leave Use Client ID Metadata Document unchecked (URL stays saved for Phase 2).
- Add Stytch demo MCP (
https://stytch-as-demo.val.run/mcp) with no per-server OAuth fields. - Connect → 401 → authenticate.
- 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)
- Enable Use Client ID Metadata Document (MCPJam URL from above).
- Clear OAuth state for the Stytch server again.
- Connect → 401 → authenticate → demo login/consent at
stytch-as-demo.val.run(orstytch.comif using Management MCP). - Pass: connected;
tools/listsucceeds; Connection Info shows client id = MCPJam metadata URL and Client registration = Client ID Metadata (CIMD);POST …/v1/oauth2/tokenrequest body shows the same URL asclient_id(see Verifying CIMD).
Procedure (TUI)
Same two phases (prefer stytch-mcp-demo URL):
- Off: launch without CIMD enabled in
client.jsonand without--client-metadata-url→ connect (C) → complete browser auth if prompted → expect failure or DCR client id as in Phase 1 (web). - On:
Or enable CIMD inmcp-inspector --tui --catalog configs/mcp.json \ --client-metadata-url https://www.mcpjam.com/.well-known/oauth/client-metadata.jsonclient.jsonand 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 in-repo TestServerHttp with supportCIMD: true and supportDCR: false (see inspectorClient-oauth-e2e.test.ts). ensureCimdClientRegistration allows http://127.0.0.1 metadata URLs in tests only.
cd test-servers && npm run build
node build/server-composable.js --config path/to/oauth-cimd-only.json
Example composable OAuth flags: "supportCIMD": true, "supportDCR": false. With CIMD off in Client Settings, connect + authenticate should fail; with CIMD on and a valid local metadata URL, it should succeed.
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
-
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.jsonConfirm stderr:
Composable server listening at http://127.0.0.1:8081/mcp. -
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" } } -
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 freshHOME(CLI)
-
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.
- Connect to oauth-step-up-demo → browser OAuth → connected.
- Tools → echo → Run → success.
- Tools → get_temp (city
NYC, unitsC) → Run. - Expect: “Additional permissions required” modal (scopes include
weather:read); no immediate redirect. - Cancel → modal closes, error shown, still connected → echo still works.
- Run get_temp again → Authorize → redirect to AS on
:8081→ approve →http://localhost:6274/oauth/callback→ return to app. - Expect: Tools tab, get_temp form restored, toast “Step-up authorization succeeded. Retry your action.”, network log from pre-redirect still visible.
- Run get_temp → temperature result.
W2 — Connect-time OAuth with no prior session (optional)
- Clear OAuth state, disconnect.
- Connect → full-page OAuth → callback → toast “Authentication succeeded. Retry your action.”
W3 — Silent mid-session refresh (optional)
- Connect, run echo successfully.
- Invalidate access token only (devtools →
sessionStorage→mcp-inspector-oauth), keep refresh token. - 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)
- Tab A: connect, start step-up on get_temp, do not click Authorize yet.
- Tab B: same server, run echo → should not redirect Tab B to OAuth while Tab A’s modal is open.
- Tab A: Authorize → complete flow.
W5 — Step-up pre-redirect toast (recommended, MR-219)
- Complete W1 steps 1–3 to open the step-up modal.
- Click Authorize.
- Expect: blue toast “Step-up authorization for …” / “Redirecting to authorize additional permissions…” appears before the browser leaves the page.
- Complete OAuth → W1 steps 7–8.
W6 — Abandoned step-up → re-auth banner (recommended, MR-111)
- Complete W1 steps 1–3; click Authorize and reach the AS consent page on
:8081. - Do not approve — navigate back to
http://localhost:6274/(or close the OAuth tab and open the inspector URL without callback query params). - Expect: persistent red Re-authentication required banner mentioning step-up was not completed.
- 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).
- Confirm Connection toggle still shows connected (session not torn down).
- Click banner Re-authenticate.
- Expect: no disconnect/reconnect cycle; browser OAuth opens (or silent restore if tokens still valid).
- After successful OAuth, banner clears; echo still works without manual reconnect.
W8 — Background-tab defer + resume (recommended, MR-104)
- Connect; open step-up modal on get_temp (W1 steps 1–3) but do not click Authorize yet.
- 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). - Return to the inspector tab (or disable emulation).
- Expect: deferred recovery runs — step-up modal or OAuth flow resumes without losing the connected session.
- Complete step-up; run get_temp successfully.
W9 — Concurrent step-up blocked (recommended, MR-110)
- Connect; open step-up modal on get_temp; leave it open.
- 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).
- Expect: yellow toast “Step-up authorization in progress…”; first modal remains; no second redirect.
W10 — Partial consent after step-up (optional)
- Complete step-up Authorize flow but on the AS consent screen deny or omit
weather:readif the AS offers granular scopes. - Expect: return to app with warning toast about permissions not granted — not the green “Step-up authorization succeeded…” toast.
- 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.
- Connect with
mcp tools:readonly. - Prompts → run a scoped Get (or Resources → Read, or Apps → open) that returns
403 insufficient_scope. - 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.
- Clear OAuth state (Auth tab → S, or fresh
oauth.json). - Select oauth-step-up-demo → Connect (
c). - 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. - 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.
- Tools tab → get_temp → run with city/units.
- Expect: focus moves to Auth tab; message that extra scopes are needed; prompts A (authorize) / C (cancel).
- Press C → message “Authorization cancelled.” → still connected → echo still works.
- Run get_temp again → press A → browser OAuth → callback on 6276.
- Expect: Auth message “Step-up authorization succeeded. Retry your action.”
- Run get_temp again → success.
T3 — Tool test modal auth recovery (optional)
- From Tools, open test modal for get_temp, trigger step-up as in T2.
- Expect: same Auth tab step-up flow (not a separate modal).
T4 — Clear OAuth state (required, quick)
- While connected, Auth tab → S.
- Expect: tokens cleared, disconnected if was connected.
T5 — Step-up while on another tab (optional, MR-004)
- Connect (T1); stay on Tools tab (not Auth).
- Run get_temp → step-up needed.
- 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)
- Connect; open step-up prompt on Auth tab (T2 step 2); leave it open.
- Trigger another step-up (run get_temp again).
- 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.
- Shared setup steps 1–3 (web dev not required).
-
mcp-inspector --cli --catalog ~/.mcp-inspector/mcp.json --server oauth-step-up-demo \ --method tools/list - Expect stdout:
Please navigate to: http://127.0.0.1:8081/oauth/authorize?... - Open URL → approve → redirect to 6276 → “OAuth complete”.
- Expect stderr:
Authorization complete. - Expect stdout: JSON with
echoandget_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.
-
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"' - Expect stderr: scope message +
Proceed with step-up authorization? [y/N]. - Type
n→ exit ≠ 0, “Step-up authorization declined.” - Re-run; type
y→ authorize URL on stdout → browser → 6276 callback. - 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-processcliOAuth.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
- MCP authorization spec
- example-remote-server
- GitHub MCP remote server
- Stytch MCP demo — local CIMD/DCR smoke (test login at demo
/oauth/authorize) - Stytch MCP — Management API MCP (production-style
stytch.comlogin) - MCPJam CIMD metadata — shared dev metadata URL
- CIMD for MCP, Stytch client types
- client.dev — CIMD format reference
- Inspector e2e:
clients/web/src/test/integration/mcp/inspectorClient-oauth-e2e.test.ts - Local OAuth AS:
test-servers/src/test-server-oauth.ts,createOAuthTestServerConfig()intest-server-fixtures.ts