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
- EMA extension spec
- MCP blog: Enterprise-managed auth (announced stable June 18, 2026)
- GitHub issue: #1509
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):
- 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).
- 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).
- 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.
| Concern | Package / module |
|---|---|
| Standard OAuth | v1 @modelcontextprotocol/sdk/client/auth.js via BaseOAuthClientProvider (core/auth/providers.ts) |
| EMA leg 1 (IdP OIDC) | v1 SDK: startAuthorization, exchangeAuthorization, discoverAuthorizationServerMetadata — core/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 orchestration | core/auth/ema/emaFlow.ts — mintEmaResourceTokens, silent connect, connect/callback completion |
| EMA 401 / transport | core/auth/ema/transportProvider.ts — EmaTransportOAuthProvider wraps v1 BaseOAuthClientProvider; re-runs legs 2–3 on expiry, IdP redirect on missing ID Token |
| JWT expiry helpers | core/auth/ema/jwt.ts — jwtExpiresAtMs, isJwtExpired (exp claim only; no signature verification) |
| Leg 1 in-flight PKCE key | core/auth/ema/storage.ts — idpOAuthStorageKey(issuer) → ema-idp:{issuer} in per-server servers map (storage key only; not an OAuth state param prefix) |
| Wire constants | core/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
| Leg | Status |
|---|---|
Leg 2 (discoverAndRequestJwtAuthGrant) | Adopted — exchangeIdJag 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 |
CrossAppAccessProvider | Not 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
exchangeJwtAuthGrantgrowsresource/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:
| Plane | Scope | What it identifies |
|---|---|---|
| IdP (client / tenant) | Global — one per inspector install or session | Inspector's OIDC client at the enterprise IdP (legs 1–2) |
| Resource AS (per-server) | Per MCP server entry | OAuth 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):
| Store | API (web) | On disk | Purpose |
|---|---|---|---|
client | GET/POST/DELETE /api/storage/client | ~/.mcp-inspector/storage/client.json | Config — 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.
| Client | Config (client.json) | Runtime auth state (OAuthStorage) |
|---|---|---|
| Web | RemoteStorage adapter → /api/storage/client | RemoteOAuthStorage → /api/storage/oauth → ~/.mcp-inspector/storage/oauth.json (via getWebRemoteOAuthStorage() in clients/web/src/lib/remoteOAuthStorage.ts) |
| CLI / TUI | NodeClientStorage (file adapter) | NodeOAuthStorage → oauth.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,EnterpriseManagedAuthIdpConfigincore/client/types.ts(imported bycore/mcp/types.tsfor server/oauth shapes). - Load/save helpers with JSON parse/validate — no
process.envreads insideInspectorClient. - File path:
getStoreFilePath(getDefaultStorageDir(), "client")(reusestore-io.tsprimitives).
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.jsoncontent into the profile store (clientSecretin the OS keychain, not plaintext). client.jsonbecomes 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"
}
}
}
}
| Field | When enterpriseManaged: false | When enterpriseManaged: true |
|---|---|---|
oauth.clientId | OAuth client id (existing behavior) | Resource AS client id trusted by the protected resource |
oauth.clientSecret | OAuth client secret (existing behavior) | Resource AS client secret (keychain-backed, same as today) |
oauth.scopes | OAuth scopes (existing behavior) | Scopes for the resource token request |
oauth.enterpriseManaged | omitted / false | true — route to EMA flow instead of standard authorization-code OAuth |
Types to extend in core/mcp/types.ts:
StoredMCPServer.oauth.enterpriseManaged?: booleanInspectorServerSettings— liftenterpriseManagedalongside existing flatoauthClientId/oauthClientSecret/oauthScopesfields.
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:
ServerSettingsFormOAuth 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.enterpriseManagedand persists tomcp.jsonunderoauth.enterpriseManagedon 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 (
isOAuthCapableServerTypeincore/mcp/config.ts;ServerSettingsFormreceivesserverTypefromServerSettingsModal/App.tsx). Stdio entries may still carry legacyoauthfields 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.idpbut setsenabled: falseso credentials are not lost; when on (orenabledomitted), 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 fromwindow.location.origin.
- Enable checkbox — when off, persisted config keeps
- Persistence:
POST /api/storage/clientviasaveClientConfigRemote/parseClientConfig(core/client/remote.ts,core/client/config-parse.ts). Same on-disk shape as hand-editedclient.json(enterpriseManagedAuth.idp, optionalenabled). - Runtime wiring:
App.tsxloads config at startup (loadClientConfigRemote) and passes active IdP viagetActiveEnterpriseManagedAuthIdp()plus full install config viainstallEnterpriseManagedAuthinto eachInspectorClient. After a successful save, in-memoryclientConfigupdates so the next connect uses new IdP values without a full page reload. - Draft hook:
useClientSettingsDraft(core/react/, alongsideuseSettingsDraft) — 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 (
useEmaIdpLoginStateincore/react/).
- Signed in — cached ID Token is present and not expired (
- Sign out (implemented): Sign out button when a cached IdP session exists (signed in or expired). Calls
clearEmaIdpSession(storage, issuer)incore/auth/ema/idpSession.ts— a single core entry point; clients do not pass a server catalog. It:- Clears
idpSessions[issuer] - Clears leg-1 in-flight state at
servers["ema-idp:{issuer}"] - Calls
OAuthStorage.clearEnterpriseManagedResourceServers()— scans the shared OAuth blob and removesservers[url]entries whereenterpriseManaged === true(set when EMA legs 2–3 saved tokens viasaveTokens(url, tokens, { enterpriseManaged: true })inemaFlow.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) viaauthenticate().
- Clears
- Web OAuth store singleton: web uses
getWebRemoteOAuthStorage()(clients/web/src/lib/remoteOAuthStorage.ts) — memoizedRemoteOAuthStoragebacked by/api/storage/oauth— so Client Settings sign-out, EMA IdP session, connect, and per-server clear all mutate the same in-memoryOAuthStorageBaseview as the activeInspectorClient.
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/clientfrom 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:
| Reason | When | User-facing guidance |
|---|---|---|
not_configured | No IdP block, or issuer/client id missing | Open Client Settings, enable Enterprise IdP, set issuer, client ID, and secret |
disabled | enterpriseManagedAuth.enabled === false with IdP credentials retained | Enterprise 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:
- Protocol —
standardorema - Authorized — whether a usable access token exists in storage
- Client ID — static, DCR, or CIMD registration kind (see
clientRegistrationKindon connection state) - IdP session (EMA only) —
none/logged_in/expiredfromidpSessions[issuer] - Auth URL — cached from an in-flight or completed OAuth flow (
OAuthFlowState), when present - Scopes — configured vs granted
- Access token —
OAuthAccessTokenField: copy (raw), decode JWT in place (core/auth/ema/jwt.tshelpers); 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.
| Client | Leg 1 mechanism (same stack as standard OAuth in that client) |
|---|---|
| Web | Browser redirect via navigation; callback at /oauth/callback; InspectorClient.completeOAuthFlow(code); RemoteOAuthStorage → shared oauth.json |
| TUI / CLI | OAuthCallbackServer 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:
| Outcome | User sees |
|---|---|
satisfied (silent re-mint) | Blue in-progress toast → green “Organization permissions were updated” (+ “Retry your action” when step-up was triggered by a tool/prompt/resource/app) |
interactive (IdP redirect) | Pre-redirect toast → full-page IdP callback → same resume snapshot behavior as other EMA flows |
failed | Red toast with error detail; triggering panel shows failure |
TUI/CLI still use their existing confirm prompts (Auth tab A/C, CLI y/N) before silent re-mint — no web modal.
EMA resource token tagging + sign-out: per-server oauth.enterpriseManaged in mcp.json is config (routing); the OAuth store does not read the catalog on sign-out. Instead, when EMA legs 2–3 persist a resource access token, the store tags that entry (ServerOAuthState.enterpriseManaged: true via SaveTokensOptions). Sign-out uses that tag to find and clear EMA resource state inside the same OAuthStorage blob without the client enumerating MCP servers. This avoids clearing standard OAuth servers and avoids clearing EMA catalog entries that were never connected.
Inspector mapping
When connecting to an HTTP server with oauth.enterpriseManaged: true:
- Validate protected resource metadata (
resource,authorization_servers). - Branch from standard OAuth (VS Code:
mainThreadMcp— see appendix §A.3). - Load tenant IdP config from
client.json(viaInspectorClientOptions.enterpriseManagedAuth, loaded at session startup). - Obtain or reuse IdP ID Token (leg 1 —
idpOidc.ts, v1 SDK OIDC redirect/callback). - Run leg 2 —
wire.exchangeIdJag()(RFC 8693 at IdP) → ID-JAG (scopes fromresourceContext.ts; see §Implementation notes). - Run leg 3 —
wire.redeemIdJagForAccessToken()(RFC 7523 at resource AS) with per-serveroauth.clientId/oauth.clientSecret→ MCP access token. - 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.ts—ClientConfig,EnterpriseManagedAuthIdpConfig,getActiveEnterpriseManagedAuthIdp,isEnterpriseManagedAuthEnabledcore/mcp/types.ts—enterpriseManagedon server oauth shape;InspectorClientOptions.enterpriseManagedAuth,installEnterpriseManagedAuthcore/client/(orcore/storage/) —loadClientConfig/saveClientConfig;NodeClientStorage; remote adapter for/api/storage/clientcore/auth/utils.ts—parseHttpUrl(issuer and EMA URL validation)core/auth/connection-state.ts—buildOAuthConnectionState,OAuthConnectionStatecore/auth/storage.ts+core/auth/store.ts+core/auth/oauth-storage.ts+core/auth/oauth-persist.ts—OAuthStorage/OAuthMemoryStore: store-rootidpSessions;ServerOAuthState.enterpriseManagedtag; async getters with auto-load; setters auto-persist; legacy{ state, version }envelope promoted on read, plain JSON on writecore/auth/ema/—idpOidc.ts(leg 1),wire.ts(legs 2–3),emaFlow.ts(orchestration + taggedsaveTokens),transportProvider.ts(401 re-auth + taggedsaveTokens),resourceContext.ts,idpSession.ts(getEmaIdpLoginState,clearEmaIdpSession),jwt.ts,storage.ts,constants.ts,clientConfigError.ts(EmaClientNotConfiguredError)core/mcp/oauthManager.ts— branch onenterpriseManaged; EMA connect viaemaFlow;getOAuthState();createOAuthProvider()returnsEmaTransportOAuthProviderfor EMA servers (401 re-auth); standard OAuth unchangedcore/mcp/inspectorClient.ts— declare EMA extension ininitializecapabilities whenenterpriseManaged;getOAuthState()core/react/useEmaIdpLoginState.ts— Client Settings IdP session status; callsclearEmaIdpSessionon sign-out (no catalog wiring)clients/web/src/lib/remoteOAuthStorage.ts—getWebRemoteOAuthStorage()singleton (web OAuth runtime store)core/auth/browser/storage.ts—BrowserOAuthStorage/getBrowserOAuthStorage()(sessionStorage; reference provider only, not wired in v2 web app)clients/web— Client Settings modal; Connection Info OAuth snapshot; friendly EMA connect toasts; load client config at startup;ServerSettingsFormOAuth section (HTTP/SSE only); web callback flow tagging for IdP vs resource OAuthclients/cli,clients/tui— loadclient.jsonat startup; pass intoInspectorClient; leg 1 via sameOAuthCallbackServer+NodeOAuthStoragestack as TUI standard OAuth today
Clients
| Client | Notes |
|---|---|
| Web | First — 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) |
| TUI | Follow web; extend existing OAuth auth tab (clients/tui/src/App.tsx, AuthTab) for EMA legs 2–3; leg 1 via existing OAuthCallbackServer flow |
| CLI | Follow 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)
- Add types:
ClientConfig,EnterpriseManagedAuthIdpConfig,enterpriseManaged,InspectorClientOptions.enterpriseManagedAuth. - Add
enterpriseManagedto server types, normalizers, andmcp.jsonread/write. - Implement
client.jsonload/save (NodeClientStorage+ remote adapter for/api/storage/client). - Extend
OAuthStorage/ store schema: store-rootidpSessions;ServerOAuthState.enterpriseManagedtag;clearEnterpriseManagedResourceServers(). - Implement EMA wire + orchestration in
core/auth/ema/(v1 SDK for discovery/OIDC; localwire.tsfor legs 2–3).
Phase 2 — Web EMA connect (first end-to-end path)
- Wire web to load
client.jsonat session startup → active IdP +installEnterpriseManagedAuth. - Web UX: Client Settings dialog for IdP (
ClientSettingsModal//api/storage/client); IdP sign-in state + sign-out;enterpriseManagedcheckbox inServerSettingsFormOAuth section; friendly connect errors; Connection Info OAuth snapshot. OAuthManagerEMA branch: leg 1 (idpOidc.ts) → legs 2–3 (wire.ts/emaFlow.ts) → taggedsaveTokens(..., { enterpriseManaged: true })→ connect.InspectorClient: declare EMA extension ininitializewhenenterpriseManaged. EMA 401 re-auth viaEmaTransportOAuthProviderinOAuthManager.createOAuthProvider()(re-run legs 2–3, not resource OAuth redirect).- Web callback: disambiguate IdP OIDC vs resource OAuth pending flows at
/oauth/callback.
Phase 3 — Integration tests
- Tests: EMA wire/orchestration unit tests and mock IdP/AS integration. Implemented:
parseClientConfig,clientSettingsValues,idpSessionsstorage,enterpriseManagedserver-list round-trip, OAuthstateparsing,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:RemoteOAuthStorageEMA E2E variant, 401 re-auth stretch case.
Phase 4 — Other clients (after web works)
Wire TUI/CLI to loadDone —client.jsonand passenterpriseManagedAuthintoInspectorClient.loadRunnerClientConfig+buildRunnerClientAuthOptionsin TUI/CLI entrypoints. EMA sign-out (clearEmaIdpSession) is already client-agnostic incore/— Phase 4 clients can call it without catalog enumeration once Client Settings / logout UX lands.TUI interactive EMA + standard OAuth connectDone (TUI) — 401 →authenticate()→OAuthCallbackServeron6276→ 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 existingOAuthStorageper 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 viawire.ts; orchestration inemaFlow.ts) - Add types:
ClientConfig,EnterpriseManagedAuthIdpConfig,enterpriseManaged,InspectorClientOptions.enterpriseManagedAuth - Add
enterpriseManagedto server types, normalizers, andmcp.jsonread/write - Implement
client.jsonload/save (NodeClientStorage+/api/storage/clientremote 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:
enterpriseManagedcheckbox and OAuth settings section inServerSettingsForm(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 onenterpriseManaged; leg 1 IdP OIDC; legs 2–3 viawire.ts/emaFlow.ts) - Declare EMA extension in
initializewhen connecting withenterpriseManaged - 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;completeOAuthFlowbranches onenterpriseManaged; protocol from server config; OAuthstateis 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)
RemoteOAuthStorageEMA 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 viabuildRunnerClientAuthOptions) - 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 persistence —
OAuthStorageBase+ persist backends (#1549); see §OAuth persistence - Web shared OAuth store —
RemoteOAuthStorage/oauth.jsonfor 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/clientv2 Layer-2 helpers for legs 2–3 (replacewire.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
| Role | Service | Inspector config |
|---|---|---|
| Enterprise IdP (legs 1–2) | https://idp.xaa.dev | Client 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 resource | Composable test server (test-servers/configs/xaa-ema-http.json) — protected-resource mode, local streamable-http | Server 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
- Client Settings — Enterprise IdP configured and enabled.
- Server Settings —
enterpriseManagedon; resource AS client credentials from xaa.dev resource registration. - Connect — IdP login when needed → ID-JAG mint → resource access token → MCP
initializewith EMA extension capability. - 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):
| Backend | Class | Where |
|---|---|---|
| File | NodeOAuthStorage | CLI, TUI, default ~/.mcp-inspector/storage/oauth.json |
| Remote HTTP | RemoteOAuthStorage | Web → GET/POST/DELETE /api/storage/oauth on the Hono backend |
| Session | BrowserOAuthStorage | Reference/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.
| Client | OAuth runtime store |
|---|---|
| Web | RemoteOAuthStorage → /api/storage/oauth → ~/.mcp-inspector/storage/oauth.json |
| CLI / TUI | NodeOAuthStorage → ~/.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 patterns —
TestServerHttp+createExternalResourceOAuthTestServerConfig,inspectorClient-oauth-e2e.test.ts,inspectorClient-oauth-remote-storage-e2e.test.ts,josefor JWTs (test-server-protected-resource.test.ts). - Mock at HTTP boundary —
fetchFninjection onInspectorClient/EmaFlowConfigsowire.tsandidpOidc.tsrun real code against local mock servers. - Single auth path —
authenticate()/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
| Case | Status |
|---|---|
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 tokens | ✅ enterpriseManaged: 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):
- Mock IdP —
/.well-known/oauth-authorization-server+openid-configuration,/token(RFC 8693 token exchange → ID-JAG). Fixture client id/secret:EMA_MOCK_IDP_*constants. - 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
| Step | Status |
|---|---|
| 1 | ✅ Protected-resource TestServerHttp + createExternalResourceOAuthTestServerConfig (mock AS JWKS/issuers) |
| 2 | ✅ Mock IdP + mock resource AS on ephemeral ports |
| 3 | ✅ InspectorClient with enterpriseManaged: true, fixture IdP + resource credentials |
| 4 | ✅ Cached ID Token seeded in NodeOAuthStorage (leg 1 shortcut) |
| 5 | ✅ connect() → 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
Layer 1 (— donewire/emaFlowunit tests)Layer 2 mock servers helper— doneLayer 3 single happy-path E2E (— donestreamable-http)- Layer 3 storage variant with
RemoteOAuthStorage— optional EMA-specific EMA E2E (shared storage already covered by standard remote OAuth tests). - 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)
RemoteOAuthStorageEMA 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
- PR: https://github.com/microsoft/vscode/pull/318067
- Merge commit: https://github.com/microsoft/vscode/commit/2d95154af0f50db2a41ceee431acfc6d79638aab
- Release notes section (1.123): https://code.visualstudio.com/updates/v1_123#_enterprise-managed-mcp-authentication-preview
A.2 Where the feature is configured
A.2.1 mcp.json server-level opt-in (oauth.enterpriseManaged)
- MCP schema (
enterpriseManagedunder HTTP oauth): - MCP types (
enterpriseManaged?: boolean):
A.2.2 Tenant-level IdP config (mcp.enterpriseManagedAuth.idp)
- Setting key + TS interface (
issuer,clientId,clientSecret): - Registration of setting in configuration registry (policy-backed, app-scoped, hidden):
- Policy metadata for enterprise deployment:
A.3 Routing logic: where VS Code chooses XAA vs normal OAuth
- MCP HTTP launch data sends enterprise-managed flag to main thread:
- Main routing decision (
if (authDetails.enterpriseManaged) ...):
In mainThreadMcp, enterprise-managed path does these implementation-specific checks:
- Validate resource metadata has
resource. - Read resource AS from
authorization_servers[0]. - Compute XAA scopes from challenge/resource metadata (not IdP scopes_supported fallback).
- Resolve issuer from
mcp.enterpriseManagedAuth.idp.issuer. - Request shared provider via
createOrGetXaaProvider(issuer). - Resolve resource client secret from secret store using resource + resource client id key.
- Call session acquisition with
audienceandresourcein options.
All of this is implemented in:
A.4 Provider lifecycle and host plumbing
A.4.1 Authentication service API additions
- New
createOrGetXaaProvider(issuer)API contract: - Browser implementation (one provider per issuer id):
A.4.2 Main thread registration path
- Creates XAA provider from issuer metadata; prefers configured IdP credentials over cached:
A.4.3 Ext host registration path
- New RPC methods in protocol (
$registerXaaAuthProvider, enterpriseManaged metadata shape, prompt for resource secret): - Ext host registration of XAA provider (
$registerXaaAuthProvider): - Node ext host binds XAA mixin to node provider implementation:
A.5 Core XAA implementation class (the most important file)
- Main implementation:
Key implementation methods to mirror in your app:
-
getSessions(...)- Silent path first.
- Reuses persisted IdP session.
- Re-mints resource token silently (no prompts).
-
createSession(...)- Interactive path.
- Ensures IdP session, then mints resource token.
-
_mintResourceToken(...)- Runs leg 2 + leg 3 logic.
- Resolves resource
client_idfrom explicit config first, then from JAG claim fallback. - Handles per-resource
client_secretretrieval/prompt/caching semantics.
-
_exchangeForIdJag(...)- IdP token exchange request construction and execution.
-
_discoverResourceTokenEndpoint(...)- Fetches metadata for audience/resource AS.
-
_redeemAtResource(...)- Resource-side JWT bearer redemption for access token.
-
Caching helpers
cacheKey(...),isExpired(...), in-memory resource token cache keyed by(resource, scopes).
A.6 OAuth wire-format helpers (shared utilities)
- OAuth constants + body builders used by XAA flow:
Functions to copy conceptually:
buildIdJagExchangeBody(...)buildResourceRedemptionBody(...)- JWT claims parsing helpers used to infer
client_idfrom assertion
Tests for body semantics:
A.7 Secret storage model in this implementation
A.7.1 Resource client secret prompt + persistence
- Prompt and persistence (
$promptForResourceClientSecret):
Behavior details implemented:
undefinedmeans user canceled (do not cache as "no secret").- Empty string means explicit "no secret" (valid for public clients).
- Non-empty values stored in OS secret storage.
A.7.2 Secret keying strategy
- Secret key utility is in MCP types (
mcpOAuthClientSecretStorageKey):
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.
- Internal service types (
IAuthenticationCreateSessionOptions,IAuthenticationGetSessionsOptions, provider session options): - Proposed VS Code API file:
- Proposal registry inclusion:
A.9 Tests added in this changelist
- XAA helper tests (
cacheKey,isExpired,IDP_SCOPES): - OAuth wire-format tests for ID-JAG and redemption bodies:
- Integration test shape update for new RPC method:
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:
- Existing VS Code auth/mcp infrastructure.
- Web/platform primitives (
fetch,URLSearchParams). - 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:
-
Split credentials into two planes:
- IdP client credentials (tenant/global)
- Resource AS client credentials (per resource/per client id)
-
Cache provider by issuer:
- One XAA provider per issuer, shared across resources.
-
Keep silent and interactive paths separate:
getSessionsstyle silent path must not prompt.createSessionstyle path can prompt.
-
Use
audienceexplicitly:- Do not overload
resourcefor both meanings.
- Do not overload
-
Treat cancel as cancel:
- Distinguish
undefined(cancel) from""(explicit no secret).
- Distinguish
-
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:
- https://github.com/microsoft/vscode/blob/2d95154af0f50db2a41ceee431acfc6d79638aab/src/vs/workbench/api/common/extHostXaaAuthProvider.ts
- https://github.com/microsoft/vscode/blob/2d95154af0f50db2a41ceee431acfc6d79638aab/src/vs/workbench/api/browser/mainThreadMcp.ts
- https://github.com/microsoft/vscode/blob/2d95154af0f50db2a41ceee431acfc6d79638aab/src/vs/workbench/api/browser/mainThreadAuthentication.ts