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>
68 KiB
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
- 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 (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:
- 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 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
-
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