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

  1. Retry the whole connection flow; the winning registration is now persisted and will be loaded on the next attempt.
  2. Serialize connect flows per context so only one registration happens at a time (a lock/mutex in the app layer).
  3. 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

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


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/55b43f9901dc0de0. Report an issue: GitHub.