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

Tracks #1509.

Summary

Add support for Enterprise-Managed Authorization (EMA, also referred to as XAA / ID-JAG in client implementations). EMA extends the existing OAuth flow so an enterprise IdP (OIDC) can authenticate the client once; any MCP resource authorization server (AS) configured to trust that IdP can then be accessed with minimal or no user prompting.

Inspector v2 already has OAuth infrastructure (core/auth/, core/mcp/oauthManager.ts, per-server OAuth fields in ~/.mcp-inspector/mcp.json — see Servers file). Phases 1–2 (web connect via authenticate()) are implemented in core/auth/ema/ and documented below. TUI and CLI load client.json and wire OAuth for HTTP/SSE servers; dedicated Client Settings UX and full interactive EMA on CLI remain follow-ups. VS Code reference material in the appendix informs remaining work.

Normative references

Goals

  • Support EMA for HTTP MCP servers, aligned with the MCP EMA extension spec. EMA wire and orchestration live in core/auth/ema/ — the v1 TypeScript SDK does not expose EMA as a named API.
  • Preserve existing standard OAuth behavior for non-EMA servers — no regressions to current connect or configured-credential flows.
  • Reuse existing inspector OAuth configuration and secret storage where possible.
  • Work across web, CLI, and TUI clients via shared core/ auth logic.

Non-goals

  • v1 or v1.5/main backport (v2 only; see issue label).

EMA spec and SDK audit

Audited June 2026 against the EMA extension spec, VS Code's shipped schema, and the TypeScript SDK docs.

Protocol (EMA extension)

  • Extension id: io.modelcontextprotocol/enterprise-managed-authorization (stable as of June 2026).
  • Problem solved: per-server interactive OAuth consent is replaced by enterprise IdP policy — user authenticates to the corporate IdP once; the client obtains an identity assertion and exchanges it for MCP resource tokens without redirecting to each MCP authorization server.
  • Client flow (three legs):
    1. IdP SSO — obtain an OpenID ID Token for the user via OIDC at the enterprise IdP (the MCP protocol also allows SAML assertions; out of scope for initial EMA implementation).
    2. ID-JAG mint — RFC 8693 token exchange at the IdP: present the ID Token + IdP client credentials → receive an Identity Assertion JWT Authorization Grant (ID-JAG).
    3. Resource token — RFC 7523 JWT bearer grant at the MCP resource authorization server: present the ID-JAG + resource AS client credentials → receive the MCP access token.
  • No browser redirect to the MCP AS on the happy path — the MCP AS trusts the IdP-issued assertion.
  • Underlying standards: RFC 8693, RFC 7523, RFC 8707 (resource indicators), RFC 9728 (protected resource metadata).

TypeScript SDK (implemented)

Inspector depends on @modelcontextprotocol/sdk v1.x only (^1.29.0 in root package.json). Standard OAuth and EMA both build on that package — there is no @modelcontextprotocol/client v2 dependency in the tree today.

ConcernPackage / module
Standard OAuthv1 @modelcontextprotocol/sdk/client/auth.js via BaseOAuthClientProvider (core/auth/providers.ts)
EMA leg 1 (IdP OIDC)v1 SDK: startAuthorization, exchangeAuthorization, discoverAuthorizationServerMetadatacore/auth/ema/idpOidc.ts
EMA legs 2–3 (ID-JAG + resource token)Leg 2: SDK discoverAndRequestJwtAuthGrant via exchangeIdJag in core/auth/ema/wire.ts. Leg 3: still local JWT bearer in wire.ts + tokenEndpoint.ts (SDK exchangeJwtAuthGrant lacks resource/scope — see SDK consolidation)
EMA orchestrationcore/auth/ema/emaFlow.tsmintEmaResourceTokens, silent connect, connect/callback completion
EMA 401 / transportcore/auth/ema/transportProvider.tsEmaTransportOAuthProvider wraps v1 BaseOAuthClientProvider; re-runs legs 2–3 on expiry, IdP redirect on missing ID Token
JWT expiry helperscore/auth/ema/jwt.tsjwtExpiresAtMs, isJwtExpired (exp claim only; no signature verification)
Leg 1 in-flight PKCE keycore/auth/ema/storage.tsidpOAuthStorageKey(issuer)ema-idp:{issuer} in per-server servers map (storage key only; not an OAuth state param prefix)
Wire constantscore/auth/ema/constants.ts — grant/type URNs + IDP_OIDC_SCOPES for leg 1

EMA is not in the v1 SDK as a named feature (CrossAppAccess, ID-JAG helpers, etc.). Inspector implements the protocol in core/auth/ema/ instead.

v2 @modelcontextprotocol/client Layer-2 EMA helpers

LegStatus
Leg 2 (discoverAndRequestJwtAuthGrant)AdoptedexchangeIdJag wraps the SDK helper
Leg 3 (exchangeJwtAuthGrant)Not adopted — helper omits resource/scope on the JWT-bearer body; keep local redeemIdJagForAccessToken until the SDK wishlist gap is fixed
CrossAppAccessProviderNot adopted — in-memory tokens; no fit for remoting / durable OAuthStorage / IdP leg 1

Watch items:

  • 401 re-auth — EMA connections re-run legs 2–3 (leg 1 only if ID Token missing/expired); do not fall back to standard resource OAuth redirect.
  • Leg 3 SDK adoption — when exchangeJwtAuthGrant grows resource/scope, swap local wire per the consolidation wishlist.

Configuration

Config shape matches VS Code (verified against mcpConfiguration.ts). Two credential planes — do not merge them:

PlaneScopeWhat it identifies
IdP (client / tenant)Global — one per inspector install or sessionInspector's OIDC client at the enterprise IdP (legs 1–2)
Resource AS (per-server)Per MCP server entryOAuth client trusted by the protected resource (leg 3)

1. Client / tenant IdP credentials (global)

One IdP registration shared across all EMA-enabled servers. VS Code setting key: mcp.enterpriseManagedAuth.idp. Shape:

interface EnterpriseManagedAuthIdpConfig {
  issuer: string;
  clientId: string;
  clientSecret: string;
}

All three fields are the IdP OIDC client credentials used in leg 2 (ID-JAG mint). clientSecret is sensitive — never persist it in mcp.json (server catalog).

Near-term persistence: client.json (+ /api/storage/client)

Until client profiles (see below) land, install-level client config — starting with enterpriseManagedAuth — is stored in ~/.mcp-inspector/storage/client.json (same generic storage API as other install-level blobs):

StoreAPI (web)On diskPurpose
clientGET/POST/DELETE /api/storage/client~/.mcp-inspector/storage/client.jsonConfig — IdP credentials, later client identity/caps

Do not put IdP credentials in the OAuth store or per-server mcp.json. Do not use environment variables for IdP config — all clients read/write the same client.json file.

Runtime auth state (standard OAuth tokens, PKCE, and EMA runtime state: cached IdP ID Token / refresh token, leg-1 in-flight PKCE under ema-idp:{issuer}, and per-server resource tokens tagged enterpriseManaged: true when minted via EMA legs 2–3) uses the OAuthStorage interface (core/auth/storage.ts) — whichever implementation each client passes to InspectorClient (NodeOAuthStorage, RemoteOAuthStorage, etc.). ID-JAG is not cached — legs 2–3 re-mint on each connect or 401 refresh.

Web uses shared file-backed OAuth via RemoteOAuthStorage/api/storage/oauth (same oauth.json as CLI/TUI). Existing sessionStorage OAuth blobs from before that wiring are not migrated automatically — users start fresh or re-authorize once.

ClientConfig (client.json)Runtime auth state (OAuthStorage)
WebRemoteStorage adapter → /api/storage/clientRemoteOAuthStorage/api/storage/oauth~/.mcp-inspector/storage/oauth.json (via getWebRemoteOAuthStorage() in clients/web/src/lib/remoteOAuthStorage.ts)
CLI / TUINodeClientStorage (file adapter)NodeOAuthStorageoauth.json

On-disk shape (initial):

{
  "enterpriseManagedAuth": {
    "idp": {
      "issuer": "https://idp.example.com",
      "clientId": "inspector-idp-client-id",
      "clientSecret": "inspector-idp-client-secret"
    }
  }
}

When the user turns off Enable in Client Settings, credentials are retained but EMA is inactive install-wide:

{
  "enterpriseManagedAuth": {
    "enabled": false,
    "idp": {
      "issuer": "https://idp.example.com",
      "clientId": "inspector-idp-client-id",
      "clientSecret": "inspector-idp-client-secret"
    }
  }
}

isEnterpriseManagedAuthEnabled() / getActiveEnterpriseManagedAuthIdp() in core/client/types.ts treat enabled: false as inactive while preserving the IdP fields for re-enable without re-entry.

Later fields (client identity, capability toggles) extend this document until client profiles replace it.

Core provides:

  • Types: ClientConfig, EnterpriseManagedAuthIdpConfig in core/client/types.ts (imported by core/mcp/types.ts for server/oauth shapes).
  • Load/save helpers with JSON parse/validate — no process.env reads inside InspectorClient.
  • File path: getStoreFilePath(getDefaultStorageDir(), "client") (reuse store-io.ts primitives).

Tests write fixture client.json files directly or via /api/storage/client in integration tests (same as oauth test helpers).

Wiring into InspectorClient

Pass IdP config explicitly on the options surface — do not put it inside per-server options.oauth (that block already means resource OAuth client credentials).

Near-term shape on InspectorClientOptions:

/** Active IdP credentials — omitted when install-level EMA is disabled. */
enterpriseManagedAuth?: { idp: EnterpriseManagedAuthIdpConfig };

/**
 * Full install-level EMA config from client.json (including when disabled).
 * Used to produce friendly errors when a server expects EMA but IdP is inactive.
 */
installEnterpriseManagedAuth?: ClientConfig["enterpriseManagedAuth"];

Callers load client.json once at session startup, then pass into each InspectorClient construction:

const clientConfig = await loadClientConfig(); // client.json
new InspectorClient(serverConfig, {
  ...opts,
  ...(getActiveEnterpriseManagedAuthIdp(clientConfig) && {
    enterpriseManagedAuth: {
      idp: getActiveEnterpriseManagedAuthIdp(clientConfig)!,
    },
  }),
  ...(clientConfig.enterpriseManagedAuth && {
    installEnterpriseManagedAuth: clientConfig.enterpriseManagedAuth,
  }),
});

Issuer validation: parseHttpUrl() in core/auth/utils.ts validates IdP issuer URLs (and other HTTP URLs in EMA paths) with clear, user-facing error labels — e.g. rejects https;//idp.example.com at connect time and during client.json parse.

Future: nest under InspectorClientProfile loaded from the profile store; client.json migrates into that mechanism.

IdP session state (cached ID Token from leg 1, optional refresh token, optional idTokenExpiresAt) lives in the OAuth store via OAuthStorage. Use a store-root idpSessions map keyed by issuer (global across servers — one IdP per install), separate from per-server servers[url] resource OAuth state. Leg-1 in-flight PKCE/metadata uses synthetic server key ema-idp:{issuer} in the servers map:

interface ServerOAuthState {
  // ... existing fields ...
  enterpriseManaged?: boolean; // true when resource tokens came from EMA legs 2–3
}

interface IdpSessionState {
  idToken?: string;
  refreshToken?: string;
  idTokenExpiresAt?: number; // epoch ms
}

interface OAuthStoreState {
  servers: Record<string, ServerOAuthState>; // per-server resource OAuth + leg-1 PKCE at ema-idp:{issuer}
  idpSessions?: Record<string, IdpSessionState>; // issuer → IdP session cache
}

Extend OAuthStorage / store methods accordingly. IdP credentials stay in client.json; only runtime tokens belong in idpSessions. Per-server ServerOAuthState may include enterpriseManaged: true when EMA legs 2–3 persist resource tokens (saveTokens(..., { enterpriseManaged: true })). Sign out clears idpSessions for the configured issuer and removes tagged EMA resource entries from the shared OAuth blob — standard OAuth server entries are not cleared. All implementations (BrowserOAuthStorage, NodeOAuthStorage, RemoteOAuthStorage) extend OAuthStorageBase and serialize the same shape through their persist backends (core/auth/oauth-persist.ts).

Later: client profiles

Inspector is designing a client profile mechanism for install-level client configuration (identity, capabilities, IdP/EMA settings, etc.). That work is not settled yet. When it ships:

  • Migrate client.json content into the profile store (clientSecret in the OS keychain, not plaintext).
  • client.json becomes legacy or an export format.

Web Client Settings (see §UX) is the near-term editor for IdP credentials; client profiles may subsume or extend that dialog later.

Near-term IdP config can be edited in the web Client Settings dialog (primary path), by hand in client.json, via POST /api/storage/client (tests/automation), or through the file adapters on CLI/TUI — no env-var path.

2. Per-server MCP config (~/.mcp-inspector/mcp.json)

Extend the existing per-server oauth block already used for standard OAuth (Servers file). When enterpriseManaged is true, the server-level OAuth fields mean resource authorization server credentials, not IdP credentials:

{
  "mcpServers": {
    "my-enterprise-server": {
      "url": "https://mcp.example.com/mcp",
      "oauth": {
        "enterpriseManaged": true,
        "clientId": "resource-as-client-id",
        "clientSecret": "resource-as-client-secret",
        "scopes": "tools:read tools:execute"
      }
    }
  }
}
FieldWhen enterpriseManaged: falseWhen enterpriseManaged: true
oauth.clientIdOAuth client id (existing behavior)Resource AS client id trusted by the protected resource
oauth.clientSecretOAuth client secret (existing behavior)Resource AS client secret (keychain-backed, same as today)
oauth.scopesOAuth scopes (existing behavior)Scopes for the resource token request
oauth.enterpriseManagedomitted / falsetrue — route to EMA flow instead of standard authorization-code OAuth

Types to extend in core/mcp/types.ts:

  • StoredMCPServer.oauth.enterpriseManaged?: boolean
  • InspectorServerSettings — lift enterpriseManaged alongside existing flat oauthClientId / oauthClientSecret / oauthScopes fields.

Normalizers, server list read/write, and API wire shape follow the same patterns as existing OAuth fields (Servers file).

UX (near-term)

Per-server: enterpriseManaged (in scope for #1509)

Add enterpriseManaged to server settings data and surface it in the web UI:

  • Where: ServerSettingsForm OAuth section (clients/web/src/components/groups/ServerSettingsForm/) — same panel as existing OAuth client id, client secret, and scopes.
  • Control: checkbox "Enterprise-managed authorization", with help text: connect via the configured enterprise IdP instead of interactive OAuth to the MCP authorization server; OAuth fields below are resource authorization server credentials.
  • Behavior: toggling updates InspectorServerSettings.enterpriseManaged and persists to mcp.json under oauth.enterpriseManaged on save. When unchecked, standard OAuth behavior is unchanged.
  • Visibility: OAuth and EMA apply to remote HTTP transports (SSE / streamable-http) only. Implemented: the entire OAuth Settings accordion section is hidden for stdio servers (isOAuthCapableServerType in core/mcp/config.ts; ServerSettingsForm receives serverType from ServerSettingsModal / App.tsx). Stdio entries may still carry legacy oauth fields on disk; they are not used at connect time.

CLI/TUI can read/write enterpriseManaged via catalog/config file without dedicated UX in the first slice; web form is the primary editor.

Client / IdP settings (web — implemented)

Install-level IdP credentials are edited in Client Settings, separate from per-server Server Settings. Same modal pattern as server settings: collapsible accordion sections, controlled draft, debounced persist on change, flush on close.

  • Entry point: gear icon in the top bar (ViewHeader), immediately left of the theme toggle. Available connected and disconnected.
  • Components: ClientSettingsModal + ClientSettingsForm (clients/web/src/components/groups/ClientSettingsModal/, ClientSettingsForm/). Storybook: Groups/ClientSettingsModal.
  • Section (initial): Enterprise-Managed Authorization
    • Enable checkbox — when off, persisted config keeps enterpriseManagedAuth.idp but sets enabled: false so credentials are not lost; when on (or enabled omitted), EMA is active install-wide.
    • When enabled: Issuer, Client ID, Client secret (Inspector's registration with the enterprise IdP).
    • Shows the OAuth redirect URI to register with the IdP ({origin}/oauth/callback), derived from window.location.origin.
  • Persistence: POST /api/storage/client via saveClientConfigRemote / parseClientConfig (core/client/remote.ts, core/client/config-parse.ts). Same on-disk shape as hand-edited client.json (enterpriseManagedAuth.idp, optional enabled).
  • Runtime wiring: App.tsx loads config at startup (loadClientConfigRemote) and passes active IdP via getActiveEnterpriseManagedAuthIdp() plus full install config via installEnterpriseManagedAuth into each InspectorClient. After a successful save, in-memory clientConfig updates so the next connect uses new IdP values without a full page reload.
  • Draft hook: useClientSettingsDraft (core/react/, alongside useSettingsDraft) — mirrors server settings draft behavior; skips debounced persist while EMA is enabled but issuer or client id is still empty (avoids validation errors mid-edit).
  • IdP sign-in state (implemented): when an issuer is configured, Client Settings shows install-level IdP session status derived from OAuthStorage.idpSessions[issuer]:
    • Signed in — cached ID Token is present and not expired (core/auth/ema/idpSession.ts + isJwtExpired).
    • Session expired — cached ID Token exists but is expired (next EMA connect will prompt for IdP login).
    • Not signed in — no cached IdP session (next EMA connect will open IdP login).
    • State is refreshed when the modal opens (useEmaIdpLoginState in core/react/).
  • Sign out (implemented): Sign out button when a cached IdP session exists (signed in or expired). Calls clearEmaIdpSession(storage, issuer) in core/auth/ema/idpSession.ts — a single core entry point; clients do not pass a server catalog. It:
    1. Clears idpSessions[issuer]
    2. Clears leg-1 in-flight state at servers["ema-idp:{issuer}"]
    3. Calls OAuthStorage.clearEnterpriseManagedResourceServers() — scans the shared OAuth blob and removes servers[url] entries where enterpriseManaged === true (set when EMA legs 2–3 saved tokens via saveTokens(url, tokens, { enterpriseManaged: true }) in emaFlow.ts / EmaTransportOAuthProvider). Standard OAuth entries in the same blob are not cleared. Untagged legacy EMA tokens (saved before tagging landed) are not removed until the next EMA connect re-saves with the tag. The next connect to a cleared EMA server has no cached access token, so a 401 triggers leg 1 (IdP login) via authenticate().
  • Web OAuth store singleton: web uses getWebRemoteOAuthStorage() (clients/web/src/lib/remoteOAuthStorage.ts) — memoized RemoteOAuthStorage backed by /api/storage/oauth — so Client Settings sign-out, EMA IdP session, connect, and per-server clear all mutate the same in-memory OAuthStorageBase view as the active InspectorClient.

Future (not implemented): explicit Sign out may additionally invoke the IdP's OIDC end-session / logout endpoint (RP-initiated logout) so the IdP SSO cookie is cleared — not just inspector-local IdP/resource token state. Today sign-out is local-only (clear idpSessions, tagged EMA resource entries, and leg-1 PKCE); the IdP may still treat the browser as signed in and skip the login prompt on the next authorize redirect until the IdP session expires or the user signs out at the IdP.

Copy / UX notes: User-facing text explains enterprise-managed authorization in plain language (org IdP sign-in vs each server's OAuth login). It does not reference protocol jargon (leg 1, resource AS, etc.) or storage filenames in the form. Per-server EMA enablement and MCP-server OAuth credentials remain in Server Settings (see below).

Alternative paths (CLI/TUI, automation, hand-edit):

  • Hand-edit ~/.mcp-inspector/storage/client.json
  • POST /api/storage/client from tests or scripts

Do not store IdP credentials in the OAuth store — only runtime OAuth/EMA state (tokens, PKCE, IdP session cache, etc.) belongs there, via whichever OAuthStorage adapter the client uses.

CLI/TUI do not have a dedicated Client Settings dialog. They load client.json at startup (--client-config, default ~/.mcp-inspector/storage/client.json) and pass IdP/CIMD settings into InspectorClient. Edit install config via the web Client Settings dialog, hand-edit the file, or POST /api/storage/client. Default OAuth callback for TUI/CLI: http://127.0.0.1:6276/oauth/callback — see clients/tui/README.md and clients/cli/README.md.

Connect errors when IdP is missing or disabled (web — implemented)

When the user connects to a server with oauth.enterpriseManaged: true but install-level IdP is not active, OAuthManager.getEmaFlowConfig() throws EmaClientNotConfiguredError (core/auth/ema/clientConfigError.ts) with a reason-specific message:

ReasonWhenUser-facing guidance
not_configuredNo IdP block, or issuer/client id missingOpen Client Settings, enable Enterprise IdP, set issuer, client ID, and secret
disabledenterpriseManagedAuth.enabled === false with IdP credentials retainedEnterprise IdP is turned off in Client Settings — re-enable and retry

installEnterpriseManagedAuth on InspectorClientOptions supplies the full install config so the error can distinguish disabled from never configured. Web surfaces this in connect, OAuth auth, and post-callback reconnect paths (App.tsx): toast title Cannot connect to "<server name>", message from the error, autoClose: false (stays until dismissed).

Connection Info — OAuth snapshot (web — implemented)

InspectorClient.getOAuthState() (core/auth/connection-state.ts) assembles OAuthConnectionState from storage and config only — no network. Used by Connection Info (ConnectionInfoContent, oauthDetailsFromConnectionState.ts) for the active server:

  • Protocolstandard or ema
  • Authorized — whether a usable access token exists in storage
  • Client ID — static, DCR, or CIMD registration kind (see clientRegistrationKind on connection state)
  • IdP session (EMA only) — none / logged_in / expired from idpSessions[issuer]
  • Auth URL — cached from an in-flight or completed OAuth flow (OAuthFlowState), when present
  • Scopes — configured vs granted
  • Access tokenOAuthAccessTokenField: copy (raw), decode JWT in place (core/auth/ema/jwt.ts helpers); multi-line wrap with segment-aware breaking for JWTs

In-flight OAuth flow state uses OAuthFlowState / getOAuthFlowState() / getOAuthFlowStep(). getOAuthState() is the persisted connection snapshot; flow state is separate and ephemeral.

Leg 1 — IdP OIDC (settled)

Leg 1 is OIDC authorization-code login against the enterprise IdP using credentials from client.json (issuer, clientId, clientSecret). Discover IdP endpoints from the issuer (OpenID Provider Metadata). Exchange the authorization code at the IdP token endpoint; persist the resulting ID Token (and refresh token if issued) in OAuthStorage.

Reuse existing OAuth client infrastructure — leg 1 is not a new auth stack. It uses the same per-client mechanisms already wired for standard MCP resource OAuth (OAuthManager, redirect/callback, PKCE, token exchange). Only the target changes: IdP endpoints and client.json credentials instead of the resource authorization server and per-server oauth.* fields.

ClientLeg 1 mechanism (same stack as standard OAuth in that client)
WebBrowser redirect via navigation; callback at /oauth/callback; InspectorClient.completeOAuthFlow(code); RemoteOAuthStorage → shared oauth.json
TUI / CLIOAuthCallbackServer opens local callback; system browser for authorize URL; completeOAuthFlow(code); NodeOAuthStorage

Silent connect: if a valid cached ID Token exists for the configured issuer, skip the interactive redirect and proceed to leg 2.

SAML: defined in the MCP EMA protocol but out of scope for initial EMA implementation.

Implementation notes (leg 1 + OAuthManager)

Leg 1 reuses the existing OAuth redirect/callback/PKCE machinery but targets the IdP, not the resource authorization server. OAuthManager today discovers resource AS metadata and builds authorize URLs against the resource AS — the EMA branch must parameterize the same flow with IdP endpoints (from issuer OpenID Provider Metadata) and IdP credentials from client.json.

Web callback disambiguation: web uses a single /oauth/callback path for both resource OAuth and IdP OIDC. Pending server id is stashed in sessionStorage before redirect; protocol (standard vs EMA) comes from oauth.enterpriseManaged on that server, not from the OAuth state query param. The state param is a 64-char hex CSRF token (generateOAuthState()); parseOAuthState() extracts authId for fetch-log restore on callback.

Auth protocol: AuthProtocol (standard | ema) from server/client config. EMA and standard OAuth both use InspectorClient.authenticate() / completeOAuthFlow().

Scope for leg 2: resourceContext.resolveEmaScopes() — prefer configured per-server oauth.scopes; else join resourceMetadata.scopes_supported when present; else omit. Do not fall back to IdP scopes_supported.

401 re-auth: on EMA connections, re-run legs 2–3 (and leg 1 only if the cached ID Token is missing or expired). Do not fall back to standard resource authorization-code OAuth. Mid-session detection, step-up scopes, and web remote propagation are specified in Mid-session auth.

EMA step-up (web confirmation)

When an EMA server returns 403 + insufficient_scope, OAuthManager.handleAuthChallenge() returns step_up_confirm until the web user clicks Authorize in StepUpAuthModal. Only then does Inspector call trySilentEmaAuth() (legs 2–3 re-mint with SEP-2350 union scopes) or start IdP leg 1 if the organization session is missing.

Why confirm on web? EMA can often upgrade scopes silently when the IdP session is still valid. Inspector deliberately shows the additional scopes first — same pattern as standard OAuth step-up — because it is a testing and exploration client: operators should see permission elevation while validating MCP servers, not discover it only after the fact.

Web UX after Authorize:

OutcomeUser sees
satisfied (silent re-mint)Blue in-progress toast → green “Organization permissions were updated” (+ “Retry your action” when step-up was triggered by a tool/prompt/resource/app)
interactive (IdP redirect)Pre-redirect toast → full-page IdP callback → same resume snapshot behavior as other EMA flows
failedRed toast with error detail; triggering panel shows failure

TUI/CLI still use their existing confirm prompts (Auth tab A/C, CLI y/N) before silent re-mint — no web modal.

EMA resource token tagging + sign-out: per-server oauth.enterpriseManaged in mcp.json is config (routing); the OAuth store does not read the catalog on sign-out. Instead, when EMA legs 2–3 persist a resource access token, the store tags that entry (ServerOAuthState.enterpriseManaged: true via SaveTokensOptions). Sign-out uses that tag to find and clear EMA resource state inside the same OAuthStorage blob without the client enumerating MCP servers. This avoids clearing standard OAuth servers and avoids clearing EMA catalog entries that were never connected.

Inspector mapping

When connecting to an HTTP server with oauth.enterpriseManaged: true:

  1. Validate protected resource metadata (resource, authorization_servers).
  2. Branch from standard OAuth (VS Code: mainThreadMcp — see appendix §A.3).
  3. Load tenant IdP config from client.json (via InspectorClientOptions.enterpriseManagedAuth, loaded at session startup).
  4. Obtain or reuse IdP ID Token (leg 1 — idpOidc.ts, v1 SDK OIDC redirect/callback).
  5. Run leg 2 — wire.exchangeIdJag() (RFC 8693 at IdP) → ID-JAG (scopes from resourceContext.ts; see §Implementation notes).
  6. Run leg 3 — wire.redeemIdJagForAccessToken() (RFC 7523 at resource AS) with per-server oauth.clientId / oauth.clientSecret → MCP access token.
  7. Connect with the resource access token via the existing v1 transport; persist tokens via OAuthStorage.saveTokens(serverUrl, tokens, { enterpriseManaged: true }).

When enterpriseManaged is absent or false, existing standard OAuth path is unchanged.

Client capability: declare EMA support in the MCP initialize request when connecting EMA-enabled servers:

{
  "capabilities": {
    "extensions": {
      "io.modelcontextprotocol/enterprise-managed-authorization": {}
    }
  }
}

Inspector touchpoints to extend:

  • core/client/types.tsClientConfig, EnterpriseManagedAuthIdpConfig, getActiveEnterpriseManagedAuthIdp, isEnterpriseManagedAuthEnabled
  • core/mcp/types.tsenterpriseManaged on server oauth shape; InspectorClientOptions.enterpriseManagedAuth, installEnterpriseManagedAuth
  • core/client/ (or core/storage/) — loadClientConfig / saveClientConfig; NodeClientStorage; remote adapter for /api/storage/client
  • core/auth/utils.tsparseHttpUrl (issuer and EMA URL validation)
  • core/auth/connection-state.tsbuildOAuthConnectionState, OAuthConnectionState
  • core/auth/storage.ts + core/auth/store.ts + core/auth/oauth-storage.ts + core/auth/oauth-persist.tsOAuthStorage / OAuthMemoryStore: store-root idpSessions; ServerOAuthState.enterpriseManaged tag; async getters with auto-load; setters auto-persist; legacy { state, version } envelope promoted on read, plain JSON on write
  • core/auth/ema/idpOidc.ts (leg 1), wire.ts (legs 2–3), emaFlow.ts (orchestration + tagged saveTokens), transportProvider.ts (401 re-auth + tagged saveTokens), resourceContext.ts, idpSession.ts (getEmaIdpLoginState, clearEmaIdpSession), jwt.ts, storage.ts, constants.ts, clientConfigError.ts (EmaClientNotConfiguredError)
  • core/mcp/oauthManager.ts — branch on enterpriseManaged; EMA connect via emaFlow; getOAuthState(); createOAuthProvider() returns EmaTransportOAuthProvider for EMA servers (401 re-auth); standard OAuth unchanged
  • core/mcp/inspectorClient.ts — declare EMA extension in initialize capabilities when enterpriseManaged; getOAuthState()
  • core/react/useEmaIdpLoginState.ts — Client Settings IdP session status; calls clearEmaIdpSession on sign-out (no catalog wiring)
  • clients/web/src/lib/remoteOAuthStorage.tsgetWebRemoteOAuthStorage() singleton (web OAuth runtime store)
  • core/auth/browser/storage.tsBrowserOAuthStorage / getBrowserOAuthStorage() (sessionStorage; reference provider only, not wired in v2 web app)
  • clients/webClient Settings modal; Connection Info OAuth snapshot; friendly EMA connect toasts; load client config at startup; ServerSettingsForm OAuth section (HTTP/SSE only); web callback flow tagging for IdP vs resource OAuth
  • clients/cli, clients/tui — load client.json at startup; pass into InspectorClient; leg 1 via same OAuthCallbackServer + NodeOAuthStorage stack as TUI standard OAuth today

Clients

ClientNotes
WebFirst — implement and test EMA here; Client Settings for IdP + per-server EMA checkbox (HTTP/SSE only); connect via authenticate() on 401 or explicit auth; leg 1 via browser redirect + /oauth/callback (same path as standard OAuth; disambiguate pending flow)
TUIFollow web; extend existing OAuth auth tab (clients/tui/src/App.tsx, AuthTab) for EMA legs 2–3; leg 1 via existing OAuthCallbackServer flow
CLIFollow web/TUI; same Node OAuth stack as TUI when interactive IdP login is required

Implementation order (settled)

Web first for development and testing. Core types, OAuthManager EMA branch, and OAuthStorage extensions live in core/ and are client-agnostic; the shipped #1509 path is web connect plus Client Settings / Server Settings UX. TUI/CLI load install config and support OAuth connect on HTTP/SSE; CLI interactive callback and terminal-native Client Settings remain follow-ups.

Design decisions for EMA are complete. Remaining work is the phased plan and checklist below (client profiles are a separate, later track — not a blocker for #1509).

Phased implementation plan

Phase 1 — Foundation (core, no EMA connect yet)

  1. Add types: ClientConfig, EnterpriseManagedAuthIdpConfig, enterpriseManaged, InspectorClientOptions.enterpriseManagedAuth.
  2. Add enterpriseManaged to server types, normalizers, and mcp.json read/write.
  3. Implement client.json load/save (NodeClientStorage + remote adapter for /api/storage/client).
  4. Extend OAuthStorage / store schema: store-root idpSessions; ServerOAuthState.enterpriseManaged tag; clearEnterpriseManagedResourceServers().
  5. Implement EMA wire + orchestration in core/auth/ema/ (v1 SDK for discovery/OIDC; local wire.ts for legs 2–3).

Phase 2 — Web EMA connect (first end-to-end path)

  1. Wire web to load client.json at session startup → active IdP + installEnterpriseManagedAuth.
  2. Web UX: Client Settings dialog for IdP (ClientSettingsModal / /api/storage/client); IdP sign-in state + sign-out; enterpriseManaged checkbox in ServerSettingsForm OAuth section; friendly connect errors; Connection Info OAuth snapshot.
  3. OAuthManager EMA branch: leg 1 (idpOidc.ts) → legs 2–3 (wire.ts / emaFlow.ts) → tagged saveTokens(..., { enterpriseManaged: true }) → connect.
  4. InspectorClient: declare EMA extension in initialize when enterpriseManaged. EMA 401 re-auth via EmaTransportOAuthProvider in OAuthManager.createOAuthProvider() (re-run legs 2–3, not resource OAuth redirect).
  5. Web callback: disambiguate IdP OIDC vs resource OAuth pending flows at /oauth/callback.

Phase 3 — Integration tests

  1. Tests: EMA wire/orchestration unit tests and mock IdP/AS integration. Implemented: parseClientConfig, clientSettingsValues, idpSessions storage, enterpriseManaged server-list round-trip, OAuth state parsing, clientConfigError, connection-state, OAuthManager EMA not-configured paths, keychain secret migration on server-id rename (servers-route.test.ts), Phase 3b automated tests (§Phase 3b test plan — Layers 1–3). Manual staging: live xaa.dev EMA verified (§Staging validation). Optional follow-up: RemoteOAuthStorage EMA E2E variant, 401 re-auth stretch case.

Phase 4 — Other clients (after web works)

  1. Wire TUI/CLI to load client.json and pass enterpriseManagedAuth into InspectorClient. DoneloadRunnerClientConfig + buildRunnerClientAuthOptions in TUI/CLI entrypoints. EMA sign-out (clearEmaIdpSession) is already client-agnostic in core/ — Phase 4 clients can call it without catalog enumeration once Client Settings / logout UX lands.
  2. TUI interactive EMA + standard OAuth connect Done (TUI) — 401 → authenticate()OAuthCallbackServer on 6276 → reconnect; Auth tab OAuth snapshot + S clear (disconnects when connected). Still open: dedicated Client Settings UX in terminal, CLI interactive OAuth, EMA-specific terminal UX refinements.

Implementation checklist

Design (complete)

  • Audit EMA spec and TypeScript SDK surfaces
  • Finalize inspector config shape (server + tenant IdP)
  • Choose near-term implementation: v1 @modelcontextprotocol/sdk + local EMA wire (core/auth/ema/), v1 transport unchanged
  • Choose near-term persistence: client.json (IdP config) + extend existing OAuthStorage per client (OAuth/EMA runtime state)
  • Leg 1 mechanism: OIDC authorization-code flow via existing per-client OAuth stack (web redirect/callback; TUI/CLI OAuthCallbackServer)
  • Implementation order: web client first for EMA development and testing

Phase 1 — Foundation

  • Implement EMA core: core/auth/ema/ (leg 1 via v1 SDK OIDC; legs 2–3 via wire.ts; orchestration in emaFlow.ts)
  • Add types: ClientConfig, EnterpriseManagedAuthIdpConfig, enterpriseManaged, InspectorClientOptions.enterpriseManagedAuth
  • Add enterpriseManaged to server types, normalizers, and mcp.json read/write
  • Implement client.json load/save (NodeClientStorage + /api/storage/client remote adapter)
  • Extend OAuthStorage / OAuth store schema: idpSessions, ServerOAuthState.enterpriseManaged, clearEnterpriseManagedResourceServers()

Phase 2 — Web EMA connect

  • Wire web to load client config at session startup → active IdP + installEnterpriseManagedAuth
  • Web UX: Client Settings dialog for IdP credentials (ClientSettingsModal, gear icon in header, /api/storage/client)
  • Web UX: IdP sign-in state + Sign out in Client Settings (idpSession.ts, useEmaIdpLoginState, clearEmaIdpSession + tagged resource cleanup)
  • Web UX: enterpriseManaged checkbox and OAuth settings section in ServerSettingsForm (hidden for stdio)
  • Web UX: friendly connect errors when EMA server but IdP missing/disabled (EmaClientNotConfiguredError, persistent toast)
  • Web UX: Connection Info OAuth snapshot (getOAuthState, access token copy/decode)
  • Integrate EMA routing in OAuthManager (branch on enterpriseManaged; leg 1 IdP OIDC; legs 2–3 via wire.ts / emaFlow.ts)
  • Declare EMA extension in initialize when connecting with enterpriseManaged
  • EMA 401 re-auth: re-run legs 2–3 (leg 1 only if ID Token expired/missing) via EmaTransportOAuthProvider
  • Web: disambiguate IdP vs resource OAuth at /oauth/callback (shared callback; completeOAuthFlow branches on enterpriseManaged; protocol from server config; OAuth state is 64-char hex CSRF token)

Phase 3 — Integration tests

  • Manual staging validation — full EMA connect against live xaa.dev (see §Staging validation). Confirms legs 1–3 and web UX outside CI.
  • Automated integration tests — mock IdP + mock resource AS + composable protected-resource server (see §Phase 3b test plan). Live xaa.dev is not required for CI.
  • (optional) RemoteOAuthStorage EMA E2E variant — same happy path via /api/storage/oauth (standard OAuth remote E2E exists; EMA-specific variant optional).
  • (optional) 401 re-auth stretch — invalidate resource token, assert legs 2–3 re-run.

Phase 4 — Other clients

  • Wire TUI/CLI to load client config → InspectorClientOptions.enterpriseManagedAuth (+ CIMD / per-server OAuth via buildRunnerClientAuthOptions)
  • TUI interactive EMA + standard OAuth connect (401 → authenticate → callback → reconnect; Auth tab; keychain secret rehydration via loadServerEntries)
  • TUI/CLI polish: Client Settings dialog, CLI interactive OAuth, terminal EMA UX refinements

Later

  • Remove Zustand from OAuth persistenceOAuthStorageBase + persist backends (#1549); see §OAuth persistence
  • Web shared OAuth storeRemoteOAuthStorage / oauth.json for web + CLI + TUI parity (#1548); see §Shared file-backed OAuth state
  • Client profile persistence (migrate from client.json; may extend or replace web Client Settings)
  • Optional: adopt @modelcontextprotocol/client v2 Layer-2 helpers for legs 2–3 (replace wire.ts) or full v2 transport for EMA
  • Optional: IdP end-session / RP-initiated logout on explicit Sign out (today sign-out clears inspector-local IdP session and tagged EMA resource tokens only; IdP browser SSO may remain active)

Staging validation (manual — verified)

Full end-to-end EMA has been exercised manually against live xaa.dev (June 2026). This validates the #1509 web path outside CI.

Topology

RoleServiceInspector config
Enterprise IdP (legs 1–2)https://idp.xaa.devClient Settings — issuer, requesting-app client id/secret
Resource authorization server (leg 3)https://auth.resource.xaa.dev (xaa.dev resource AS)Per-server Server Settings — resource test client id/secret, EMA enabled, scopes
Protected MCP resourceComposable test server (test-servers/configs/xaa-ema-http.json) — protected-resource mode, local streamable-httpServer catalog entry URL must match registered resource identifier (http://localhost:8080/ — use localhost, not 127.0.0.1)

Registration on xaa.dev: composable test server registered as a resource server; its AS points at xaa.dev (authorizationServers / JWKS in xaa-ema-http.json). IdP and resource AS are distinct hosts on xaa.dev.

Flow exercised

  1. Client Settings — Enterprise IdP configured and enabled.
  2. Server Settings — enterpriseManaged on; resource AS client credentials from xaa.dev resource registration.
  3. Connect — IdP login when needed → ID-JAG mint → resource access token → MCP initialize with EMA extension capability.
  4. Reconnect / 401 — silent or re-auth paths; Connection Info shows EMA OAuth snapshot.

Known-good fixture

test-servers/configs/xaa-ema-http.json — protected-resource OAuth pointing at https://auth.resource.xaa.dev. Use with the composable test server CLI; align Inspector server URL and xaa.dev resource identifier.


Follow-up work (not blocking #1509 close-out)

These items came out of EMA staging and apply beyond EMA. They are not required to ship EMA on web but should be tracked.

OAuth persistence (#1549 — done)

OAuthStorage is implemented by OAuthStorageBase (core/auth/oauth-storage.ts) backed by OAuthMemoryStore (core/auth/store.ts) and an explicit OAuthPersistBackend (core/auth/oauth-persist.ts):

BackendClassWhere
FileNodeOAuthStorageCLI, TUI, default ~/.mcp-inspector/storage/oauth.json
Remote HTTPRemoteOAuthStorageWeb → GET/POST/DELETE /api/storage/oauth on the Hono backend
SessionBrowserOAuthStorageReference/tests only; v2 web uses shared file-backed remote store

On disk / wire: writes plain JSON { servers, idpSessions }. Reads still accept legacy blobs wrapped as { state: { servers, idpSessions }, version } (produced by the old Zustand persist middleware) and promote the inner payload — migrate-on-write on the next save.

API shape: all getters are async (ensureLoaded() internally); setters await persist. load() is optional preload (OAuth callback fail-fast on web), not required before reads. BaseOAuthClientProvider.prepareForAuth() caches scope for the SDK’s sync clientMetadata.scope.

Removed: Zustand dependency, core/storage/adapters/*, public getOAuthStore().

Shared file-backed OAuth state (web + CLI + TUI)

Status (July 2026): Web is wired through RemoteOAuthStorage (environmentFactory.ts, App.tsx, getWebRemoteOAuthStorage()). CLI/TUI continue to use NodeOAuthStorage on the same on-disk file when using the default local backend.

ClientOAuth runtime store
WebRemoteOAuthStorage/api/storage/oauth~/.mcp-inspector/storage/oauth.json
CLI / TUINodeOAuthStorage~/.mcp-inspector/storage/oauth.json

RemoteOAuthStorage (core/auth/remote/storage-remote.ts) talks to GET/POST/DELETE /api/storage/oauth on the local Hono backend — the same generic storage API that persists to disk under ~/.mcp-inspector/storage/. Integration tests use it (inspectorClient-oauth-remote-storage-e2e.test.ts).

Benefits: IdP session and EMA resource tokens survive browser refresh; sign-out in Client Settings and connect use the same store; EMA testing matches CLI/TUI behavior.

Migration: no automatic import from old sessionStorage OAuth blobs; document that local dev uses shared oauth.json by default.

Optional follow-up: navigator.locks for cross-tab single-flight on silent refresh (see Mid-session auth).


Phase 3b test plan (automated integration)

Goal: CI tests that prove EMA legs 1–3 through InspectorClient / OAuthManager without calling live xaa.dev. Manual staging (§Staging validation) already covers live IdP/AS; automation encodes regressions.

Status (June 2026): Layers 1–3 implemented and green in CI (npm run test:integration). Optional items (RemoteOAuthStorage variant, 401 re-auth) remain open.

Principles

  • No live xaa.dev in default CI — flaky, credential-bound, network-dependent. Optional describe.skipIf / manual job later.
  • Reuse existing patternsTestServerHttp + createExternalResourceOAuthTestServerConfig, inspectorClient-oauth-e2e.test.ts, inspectorClient-oauth-remote-storage-e2e.test.ts, jose for JWTs (test-server-protected-resource.test.ts).
  • Mock at HTTP boundaryfetchFn injection on InspectorClient / EmaFlowConfig so wire.ts and idpOidc.ts run real code against local mock servers.
  • Single auth pathauthenticate() / trySilentEmaAuth / completeOAuthFlow() (same as standard OAuth).

Test layers

Layer 1 — wire.ts + emaFlow.ts (unit / near-unit) — done

Files: clients/web/src/test/core/auth/ema/wire.test.ts, emaFlow.test.ts

CaseStatus
exchangeIdJag happy path✅ Mock IdP token endpoint; assert RFC 8693 request shape
exchangeIdJag IdP error✅ Asserts leg-2 error message
redeemIdJagForAccessToken happy path✅ Mock resource AS token endpoint
mintEmaResourceTokens end-to-end✅ In-memory OAuthStorage + mocked fetch; resourceContext bypass
Missing ID Token
Missing resource secret
trySilentEmaAuth saves tagged tokensenterpriseManaged: true on saveTokens
trySilentEmaAuth returns false without IdP session

Uses vi.fn() fetch — no listening ports. Runs in unit project.

Layer 2 — Mock IdP + mock resource AS (local HTTP) — done

File: clients/web/src/test/integration/mcp/ema-mock-servers.ts

Ephemeral node:http servers (port 0 on 127.0.0.1):

  1. Mock IdP/.well-known/oauth-authorization-server + openid-configuration, /token (RFC 8693 token exchange → ID-JAG). Fixture client id/secret: EMA_MOCK_IDP_* constants.
  2. Mock resource AS/.well-known/oauth-authorization-server, /jwks, /token (JWT bearer grant accepts ID-JAG, returns RS256 access token). Fixture client id/secret: EMA_MOCK_RESOURCE_* constants.

Shared helper minimalOAuthAsMetadata() satisfies SDK OAuth metadata schema. createMockIdToken() seeds leg-1 IdP session in storage. Topology mirrors xaa.dev staging — see comment block in helper + test-servers/configs/xaa-ema-http.json.

Layer 3 — InspectorClient EMA connect E2E (integration) — done

File: clients/web/src/test/integration/mcp/inspectorClient-ema-e2e.test.ts

StepStatus
1✅ Protected-resource TestServerHttp + createExternalResourceOAuthTestServerConfig (mock AS JWKS/issuers)
2✅ Mock IdP + mock resource AS on ephemeral ports
3InspectorClient with enterpriseManaged: true, fixture IdP + resource credentials
4✅ Cached ID Token seeded in NodeOAuthStorage (leg 1 shortcut)
5connect()getOAuthState() protocol: "ema", authorized: true, ema.idpSession: "logged_in"
6✅ Second connect reuses silent path; persisted tokens tagged enterpriseManaged: true

Runs under integration project; test-servers:build prerequisite (existing). Transport: streamable-http only (EMA is HTTP-only).

Storage variant (optional — not implemented): same flow with RemoteOAuthStorage + createRemoteApp tmp dir — proves EMA tokens persist via /api/storage/oauth (standard remote OAuth E2E already covers the storage API).

Layer 4 — Protected-resource discovery — partial

File: clients/web/src/test/integration/mcp/test-server-protected-resource.test.ts

Existing coverage: xaa-ema-http.json config load, ExternalAccessTokenValidator, protected-resource metadata advertises external authorization_servers. Layer 3 E2E exercises discoverEmaResourceContext implicitly on connect. Dedicated discoverEmaResourceContext unit test not added (optional).

Out of scope for Phase 3b

  • Live xaa.dev (covered by §Staging validation)
  • Browser / Playwright UI tests
  • TUI/CLI (Phase 4)

Suggested implementation order

  1. Layer 1 (wire / emaFlow unit tests)done
  2. Layer 2 mock servers helperdone
  3. Layer 3 single happy-path E2E (streamable-http)done
  4. Layer 3 storage variant with RemoteOAuthStorage — optional EMA-specific EMA E2E (shared storage already covered by standard remote OAuth tests).
  5. 401 re-auth case — connect, invalidate resource token, assert legs 2–3 re-run (stretch).

Done criteria for Phase 3b checklist

  • Layer 1 tests merged
  • Layer 3 happy-path integration test green in CI
  • Document in test file how mock topology maps to xaa.dev staging (comment block + link to xaa-ema-http.json)
  • (optional) RemoteOAuthStorage EMA E2E variant
  • (optional) 401 re-auth stretch case

Appendix A — VS Code implementation reference

The following is reference material only, adapted from a working document in /Users/bob/Documents/Auth0/vscode-xaa-implementation-reference.md. It describes Microsoft's VS Code TypeScript implementation of enterprise-managed MCP auth (XAA / ID-JAG). Do not treat it as inspector design — use it to locate patterns and source files when implementing #1509.

A.1 Changelist that introduced this

A.2 Where the feature is configured

A.2.1 mcp.json server-level opt-in (oauth.enterpriseManaged)

A.2.2 Tenant-level IdP config (mcp.enterpriseManagedAuth.idp)

A.3 Routing logic: where VS Code chooses XAA vs normal OAuth

In mainThreadMcp, enterprise-managed path does these implementation-specific checks:

  1. Validate resource metadata has resource.
  2. Read resource AS from authorization_servers[0].
  3. Compute XAA scopes from challenge/resource metadata (not IdP scopes_supported fallback).
  4. Resolve issuer from mcp.enterpriseManagedAuth.idp.issuer.
  5. Request shared provider via createOrGetXaaProvider(issuer).
  6. Resolve resource client secret from secret store using resource + resource client id key.
  7. Call session acquisition with audience and resource in options.

All of this is implemented in:

A.4 Provider lifecycle and host plumbing

A.4.1 Authentication service API additions

A.4.2 Main thread registration path

A.4.3 Ext host registration path

A.5 Core XAA implementation class (the most important file)

Key implementation methods to mirror in your app:

  1. getSessions(...)

    • Silent path first.
    • Reuses persisted IdP session.
    • Re-mints resource token silently (no prompts).
  2. createSession(...)

    • Interactive path.
    • Ensures IdP session, then mints resource token.
  3. _mintResourceToken(...)

    • Runs leg 2 + leg 3 logic.
    • Resolves resource client_id from explicit config first, then from JAG claim fallback.
    • Handles per-resource client_secret retrieval/prompt/caching semantics.
  4. _exchangeForIdJag(...)

    • IdP token exchange request construction and execution.
  5. _discoverResourceTokenEndpoint(...)

    • Fetches metadata for audience/resource AS.
  6. _redeemAtResource(...)

    • Resource-side JWT bearer redemption for access token.
  7. Caching helpers

    • cacheKey(...), isExpired(...), in-memory resource token cache keyed by (resource, scopes).

A.6 OAuth wire-format helpers (shared utilities)

Functions to copy conceptually:

  1. buildIdJagExchangeBody(...)
  2. buildResourceRedemptionBody(...)
  3. JWT claims parsing helpers used to infer client_id from assertion

Tests for body semantics:

A.7 Secret storage model in this implementation

A.7.1 Resource client secret prompt + persistence

Behavior details implemented:

  1. undefined means user canceled (do not cache as "no secret").
  2. Empty string means explicit "no secret" (valid for public clients).
  3. Non-empty values stored in OS secret storage.

A.7.2 Secret keying strategy

The feature intentionally keys by resource indicator + resource client id, not only client id.

A.8 Session option surface expansion (audience)

The implementation introduces audience as part of auth session option shapes so XAA provider can receive resource AS context via standard session APIs.

A.9 Tests added in this changelist

A.10 Third-party libraries used for this feature

No new third-party runtime dependency appears to be introduced by this changelist.

The implementation relies on:

  1. Existing VS Code auth/mcp infrastructure.
  2. Web/platform primitives (fetch, URLSearchParams).
  3. Protocol standards (RFC 8693, RFC 7523, RFC 8707, ID-JAG draft semantics).

A.11 Practical architecture notes reusable in inspector

If implementing this pattern in inspector, mirror these design choices:

  1. Split credentials into two planes:

    • IdP client credentials (tenant/global)
    • Resource AS client credentials (per resource/per client id)
  2. Cache provider by issuer:

    • One XAA provider per issuer, shared across resources.
  3. Keep silent and interactive paths separate:

    • getSessions style silent path must not prompt.
    • createSession style path can prompt.
  4. Use audience explicitly:

    • Do not overload resource for both meanings.
  5. Treat cancel as cancel:

    • Distinguish undefined (cancel) from "" (explicit no secret).
  6. Key secrets with resource context:

    • Prevent cross-resource secret confusion.

A.12 Commit-pinned references (stable snapshot)

If you need a frozen reference (won't drift as main changes), use commit-pinned paths under:

For example: