different-ai/openwork · warning · EnterpriseMcpOAuthContractError
MCP_OAUTH_AUTHORIZATION_CLIENT_CHANGED
MCP_OAUTH_AUTHORIZATION_CLIENT_CHANGED
Error message
A different OAuth client registration won a concurrent registration attempt; retry the connection.
What it means
Thrown from saveClientInformation when the persistence layer returns a saved client registration whose client_id differs from the one this session just obtained via dynamic registration. It means two concurrent registration attempts raced and a different client record won the write. The library treats this as a recoverable contract violation: the connection must be retried so it adopts the winning registration.
Source
Thrown at packages/enterprise-mcp-client/src/oauth-provider.ts:367
this.loadedClient = record
return clientInformation
}
async saveClientInformation(
clientInformation: StoredOAuthClientInformation,
context?: OAuthClientInformationContext,
): Promise<void> {
const validated = this.storedClientInformation(clientInformation, context)
const source = this.clientMetadataUrl === validated.client_id ? "client-metadata" : "dynamic"
const saved = await this.persistence.clientRegistrations.save({
context: this.context(),
clientInformation: validated,
redirectUri: this.redirectUri,
expiresAt: clientExpiration(validated),
source,
})
if (saved.clientInformation.client_id !== validated.client_id) {
throw new EnterpriseMcpOAuthContractError(
"MCP_OAUTH_AUTHORIZATION_CLIENT_CHANGED",
"A different OAuth client registration won a concurrent registration attempt; retry the connection.",
)
}
this.loadedClient = saved
}
async tokens(context?: OAuthClientInformationContext): Promise<StoredOAuthTokens | undefined> {
const record = await this.persistence.credentials.load(this.context())
if (!record) {
this.loadedCredential = undefined
return undefined
}
const tokens = this.storedTokens(record.tokens, context)
if (!record.revision.trim()) {
throw new EnterpriseMcpOAuthContractError(
"MCP_OAUTH_PERSISTENCE_INVALID",
"The OAuth credential is missing its persistence revision.",View on GitHub (pinned to 2b7df46e8a)
Solutions
- Retry the whole connection flow; the winning registration is now persisted and will be loaded on the next attempt.
- Serialize connect flows per context so only one registration happens at a time (a lock/mutex in the app layer).
- Verify the persistence backend applies save atomically per context and does not allow divergent concurrent writes.
Example fix
// before
await provider.saveClientInformation(info)
// after — retry the flow on client-changed
try { await provider.saveClientInformation(info) }
catch (e) { if (e instanceof EnterpriseMcpOAuthContractError && e.code === "MCP_OAUTH_AUTHORIZATION_CLIENT_CHANGED") await retryConnection() ; else throw e } Defensive patterns
Strategy: retry
Validate before calling
// Check whether a registration already exists before triggering dynamic registration const existing = await persistence.clientRegistrations.load(context) const shouldRegister = !existing || !existing.revision.trim()
Try / catch
try { await connect() }
catch (e) {
if (e instanceof EnterpriseMcpOAuthContractError && e.code === "MCP_OAUTH_AUTHORIZATION_CLIENT_CHANGED") {
return connect() // winning registration is persisted; retry adopts it
}
throw e
} Prevention
- Serialize connect flows per context with an app-level lock
- Avoid triggering dynamic registration from multiple tabs/processes at once
- Use a persistence backend with atomic compare-and-swap semantics for registrations
When it happens
Trigger: Calling saveClientInformation (usually indirectly via the OAuth flow) while another tab/process/session registers a client for the same context; the clientRegistrations.save call returns a record with a different client_id than the one passed in.
Common situations: Two browser tabs both starting the same MCP connect flow; a background refresh job racing a user-initiated reconnect; multiple worker processes sharing a persistence backend (DB/Redis) and doing dynamic client registration simultaneously.
Related errors
- The active OpenWork Cloud account changed while reconnecting
- AUTH_CLIENT_REGISTRATION
- `${t("providers.no_oauth_prefix")} ${resolved}. ${t("provide
- `${t("providers.not_oauth_flow_prefix")} ${resolved}.`
- t("providers.oauth_method_required")
AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01).
Data as JSON: /api/errors/55b43f9901dc0de0.
Report an issue: GitHub.