{"record":{"id":"55b43f9901dc0de0","repo":"different-ai/openwork","slug":"mcp-oauth-authorization-client-changed","errorCode":"MCP_OAUTH_AUTHORIZATION_CLIENT_CHANGED","errorMessage":"A different OAuth client registration won a concurrent registration attempt; retry the connection.","messagePattern":"A different OAuth client registration won a concurrent registration attempt; retry the connection\\.","errorType":"exception","errorClass":"EnterpriseMcpOAuthContractError","httpStatus":null,"severity":"warning","filePath":"packages/enterprise-mcp-client/src/oauth-provider.ts","lineNumber":367,"sourceCode":"    this.loadedClient = record\n    return clientInformation\n  }\n\n  async saveClientInformation(\n    clientInformation: StoredOAuthClientInformation,\n    context?: OAuthClientInformationContext,\n  ): Promise<void> {\n    const validated = this.storedClientInformation(clientInformation, context)\n    const source = this.clientMetadataUrl === validated.client_id ? \"client-metadata\" : \"dynamic\"\n    const saved = await this.persistence.clientRegistrations.save({\n      context: this.context(),\n      clientInformation: validated,\n      redirectUri: this.redirectUri,\n      expiresAt: clientExpiration(validated),\n      source,\n    })\n    if (saved.clientInformation.client_id !== validated.client_id) {\n      throw new EnterpriseMcpOAuthContractError(\n        \"MCP_OAUTH_AUTHORIZATION_CLIENT_CHANGED\",\n        \"A different OAuth client registration won a concurrent registration attempt; retry the connection.\",\n      )\n    }\n    this.loadedClient = saved\n  }\n\n  async tokens(context?: OAuthClientInformationContext): Promise<StoredOAuthTokens | undefined> {\n    const record = await this.persistence.credentials.load(this.context())\n    if (!record) {\n      this.loadedCredential = undefined\n      return undefined\n    }\n    const tokens = this.storedTokens(record.tokens, context)\n    if (!record.revision.trim()) {\n      throw new EnterpriseMcpOAuthContractError(\n        \"MCP_OAUTH_PERSISTENCE_INVALID\",\n        \"The OAuth credential is missing its persistence revision.\",","sourceCodeStart":349,"sourceCodeEnd":385,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/packages/enterprise-mcp-client/src/oauth-provider.ts#L349-L385","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait provider.saveClientInformation(info)\n// after — retry the flow on client-changed\ntry { await provider.saveClientInformation(info) }\ncatch (e) { if (e instanceof EnterpriseMcpOAuthContractError && e.code === \"MCP_OAUTH_AUTHORIZATION_CLIENT_CHANGED\") await retryConnection() ; else throw e }","handlingStrategy":"retry","validationCode":"// Check whether a registration already exists before triggering dynamic registration\nconst existing = await persistence.clientRegistrations.load(context)\nconst shouldRegister = !existing || !existing.revision.trim()","typeGuard":null,"tryCatchPattern":"try { await connect() }\ncatch (e) {\n  if (e instanceof EnterpriseMcpOAuthContractError && e.code === \"MCP_OAUTH_AUTHORIZATION_CLIENT_CHANGED\") {\n    return connect() // winning registration is persisted; retry adopts it\n  }\n  throw e\n}","preventionTips":["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"],"tags":["oauth","concurrency","race-condition","dynamic-client-registration"],"backgroundTag":"oauth-client-registration-race","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}