{"record":{"id":"74266bbd2d197895","repo":"different-ai/openwork","slug":"mcp-lifecycle-deadline","errorCode":"MCP_LIFECYCLE_DEADLINE","errorMessage":"The enterprise MCP lifecycle expired before OAuth persistence could continue.","messagePattern":"The enterprise MCP lifecycle expired before OAuth persistence could continue\\.","errorType":"error_code","errorClass":"EnterpriseMcpOAuthContractError","httpStatus":null,"severity":"error","filePath":"packages/enterprise-mcp-client/src/oauth-provider.ts","lineNumber":122,"sourceCode":"    this.connectionId = input.connectionId\n    this.persistence = input.persistence\n    this.flow = input.flow\n    this.clientName = input.clientName\n    this.clock = input.clock\n    this.lifecycle = input.lifecycle\n    this.authorizationTransactionTtlMs = input.authorizationTransactionTtlMs\n    this.expirationSkewMs = input.expirationSkewMs\n    this.applicationType = input.oauthConfiguration?.applicationType ?? \"web\"\n    this.clientMetadataUrl = input.oauthConfiguration?.clientMetadataUrl\n    this.authorizationServerIssuer = input.oauthConfiguration?.authorizationServerIssuer\n    this.requestedScopes = [...new Set(input.oauthConfiguration?.requestedScopes ?? [])]\n    this.fetch = input.fetch\n  }\n\n  private context(): EnterpriseMcpPersistenceContext {\n    const now = this.clock.now()\n    if (this.lifecycle.signal.aborted || now >= this.lifecycle.expiresAt) {\n      throw new EnterpriseMcpOAuthContractError(\n        \"MCP_LIFECYCLE_DEADLINE\",\n        \"The enterprise MCP lifecycle expired before OAuth persistence could continue.\",\n      )\n    }\n    return {\n      connectionId: this.connectionId,\n      commitExpiresAt: this.lifecycle.expiresAt,\n      signal: this.lifecycle.signal,\n    }\n  }\n\n  get redirectUrl(): string {\n    return this.redirectUri\n  }\n\n  state(): string {\n    if (this.flow.kind !== \"connect\" || !this.flow.authorizationId) {\n      throw new EnterpriseMcpOAuthContractError(","sourceCodeStart":104,"sourceCodeEnd":140,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/packages/enterprise-mcp-client/src/oauth-provider.ts#L104-L140","documentation":"context() checks the lifecycle before every OAuth persistence operation: if the lifecycle AbortSignal is aborted or clock.now() has reached lifecycle.expiresAt, it throws EnterpriseMcpOAuthContractError with code MCP_LIFECYCLE_DEADLINE. The operation ran out of its allotted time budget (or was cancelled), so the library refuses to continue persisting OAuth state rather than commit work past the deadline.","triggerScenarios":"Any persistence-touching provider method (discoveryState, saveDiscoveryState, clientInformation, tokens, saveTokens, saveCodeVerifier, invalidateCredentials, etc.) invoked after the lifecycle deadline passed or after lifecycle.signal.abort() — e.g. a long OAuth discovery/registration flow exceeding the operation timeout, or an external abort triggered mid-flow.","commonSituations":"Slow authorization servers making dynamic client registration + discovery exceed the lifecycle window; users leaving the OAuth page open too long before completing the callback; upstream code aborting the signal on request cancellation; very tight lifecycle timeouts configured for large discovery chains.","solutions":["Retry the whole connect/completeAuthorization flow with a fresh lifecycle — a new authorization transaction is required after a deadline abort.","Increase the lifecycle expiresAt window (operationTimeoutMs / lifecycle option) to cover slow discovery + registration + token exchange.","Check what called lifecycle.signal.abort() and whether it fires prematurely (request cancellation, page unload, parent timeout).","Speed up or cache discovery/registration (persisted discovery state, pre-registered client) so the flow fits the budget."],"exampleFix":"// before\nconst lifecycle = { signal: controller.signal, expiresAt: clock.now() + 5_000 }\n// after\nconst lifecycle = { signal: controller.signal, expiresAt: clock.now() + 30_000 }","handlingStrategy":"retry","validationCode":"function lifecycleHasBudget(lifecycle, clock) {\n  return !lifecycle.signal.aborted && clock.now() < lifecycle.expiresAt;\n}\n// check before starting any OAuth flow","typeGuard":"function isLifecycleDeadlineError(e: unknown): e is { code: \"MCP_LIFECYCLE_DEADLINE\" } {\n  return typeof e === \"object\" && e !== null && (e as { code?: string }).code === \"MCP_LIFECYCLE_DEADLINE\";\n}","tryCatchPattern":"try {\n  await client.connect(input);\n} catch (e) {\n  if (e.code === \"MCP_LIFECYCLE_DEADLINE\") {\n    // restart the flow with a fresh, longer-lived lifecycle and a new authorizationId\n    return client.connect({ ...input, authorizationId: newSignedId() });\n  }\n  throw e;\n}","preventionTips":["Size the lifecycle window to cover discovery + registration + redirect + callback with slack.","Never reuse an expired/aborted lifecycle for a new flow — create a fresh one per attempt.","Audit abort() callers so the signal isn't cancelled prematurely.","Cache/persist discovery state and use pre-registered clients to shorten flows."],"tags":["timeout","lifecycle","oauth","abort"],"backgroundTag":"lifecycle-deadline-exceeded","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}