MCP OAuth State Management#
react-mcp implements a layered OAuth state system in createOAuthProvider.ts that handles two competing concerns: sharing state across provider instances that address the same server, and isolating state that is specific to one provider's configuration.
Persisted State Shape#
All OAuth state is stored via MCPStorage, keyed by serverId. The MCPPersistedAuthState record holds:
| Field | Purpose |
|---|---|
serverUrl | URL the state belongs to — used to reject mismatches |
tokens | Access/refresh token pair |
clientInformation | DCR-registered client credentials |
codeVerifier | PKCE verifier for the in-flight auth flow |
state | OAuth state parameter, tied to the verifier |
discoveryState | Authorization server metadata cache |
token | Host-persisted bearer token (non-OAuth path) |
serverUrl is mandatory when any credentials are present. isAuthStateForServerUrl rejects persisted state whose serverUrl doesn't normalize-match the current server URL, preventing tokens issued for one endpoint from being reused against another.
Shared Cache Architecture#
McpServerResource rebuilds a fresh OAuthClientProvider for every transport, so state cannot live on the provider object itself. createOAuthProvider solves this with a two-level in-memory structure:
persistenceByIdentity— aWeakMap<object, Map<serverId, OAuthProviderPersistence>>that holds the write queue and the endpoint cache for every(storage identity, serverId)pair .OAuthProviderPersistence— per-(identity, serverId)record containing the serialized writequeueand the currentendpointcache entry .OAuthProviderEndpointCache— per-serverUrlin-memory mirror of the persisted state, withcached,cachePromise, and aninvalidatedflag .
All providers sharing the same storage identity and serverId read and write through this shared cache, so tokens saved by one provider are immediately visible to a replacement without a round-trip to storage.
Storage Identity and scopeId#
Two MCPStorage instances that read/write the same backing store must declare the same scopeId. getStorageIdentity maps each storage to a shared anchor object via anchorByScope, using a FinalizationRegistry to release the anchor once all storages for that scope are garbage collected. Without scopeId, identity falls back to object identity, meaning a storage rebuilt on every render gets its own unshared cache.
Static vs. Dynamically Registered Clients#
MCPAuthConfig with type: "oauth" may optionally include a clientId (and clientSecret). When present, createOAuthProvider assembles a staticClientInformation object but deliberately does not write it into the shared cache. The comment explains why :
The cache is shared with every other provider for this storage, server id, and server URL, so a statically configured client stays a read-time overlay owned by this provider. Writing it into the cache would leak this provider's registration to a replacement built for a different, or absent, clientId.
The clientInformation() getter resolves this by returning the static overlay when present, falling through to the shared cache otherwise . Dynamic (DCR) registrations are persisted normally.
OAuth State Parameter and PKCE#
The state() method encodes the serverId into the OAuth state parameter using a aui-mcp:-prefixed base64url encoding , enabling the callback handler to route the response back to the correct MCPServerResource without any app-level wiring. The verifier and state are linked: saveCodeVerifier writes both the PKCE verifier and the pending state into the cache atomically, and saveTokens clears the state field once a token is obtained.
decodeServerIdFromState is exported for use by the callback route to extract the serverId from the OAuth state query parameter.
Invalidation and Cleanup#
clearOAuthProviderAuthState performs a fenced clear:
- Marks both
persistence.invalidatedandendpoint.invalidatedimmediately, ensuring any in-flight or queued writes see the fence and abort. - Removes the entry from
persistenceByIdentitybefore awaiting, so a provider constructed during the clear starts on a fresh generation rather than inheriting the fenced one. - Awaits the in-flight cache load and the full write queue before calling
storage.clearAuthState, guaranteeing no queued write recreates the record after the clear.
invalidateCredentials provides scoped invalidation ("tokens", "client", "verifier", "discovery", or "all") that surgically clears fields from the in-memory cache and re-persists, without touching unrelated state.
Key Entry Points#
| Symbol | File | Purpose |
|---|---|---|
createOAuthProvider | createOAuthProvider.ts | Builds an OAuthClientProvider instance |
clearOAuthProviderAuthState | createOAuthProvider.ts | Fenced full state clear |
decodeServerIdFromState | createOAuthProvider.ts | Callback routing helper |
MCPPersistedAuthState | types.ts | Persisted record shape |
MCPStorage | storage/types.ts | Storage interface |
isAuthStateForServerUrl | serverUrl.ts | URL-mismatch guard |
hasPersistedCredentials | serverUrl.ts | Credential presence check |