{"record":{"id":"405cd90f7c64f592","repo":"vercel/ai","slug":"oauth-client-information-must-be-saveable-for-dyna","errorCode":null,"errorMessage":"OAuth client information must be saveable for dynamic registration","messagePattern":"OAuth client information must be saveable for dynamic registration","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/mcp/src/tool/oauth.ts","lineNumber":1324,"sourceCode":"        clientInformation,\n      });\n    if (storedAuthorizationServerInformation) {\n      assertAuthorizationServerInformationMatches({\n        storedAuthorizationServerInformation,\n        currentAuthorizationServerInformation,\n      });\n    }\n  }\n\n  if (!clientInformation) {\n    if (authorizationCode !== undefined) {\n      throw new Error(\n        'Existing OAuth client information is required when exchanging an authorization code',\n      );\n    }\n\n    if (!provider.saveClientInformation) {\n      throw new Error(\n        'OAuth client information must be saveable for dynamic registration',\n      );\n    }\n\n    const fullInformation = await registerClient(authorizationServerUrl, {\n      metadata,\n      clientMetadata: {\n        ...clientMetadata,\n        scope: selectedScope,\n      },\n      fetchFn,\n    });\n\n    clientInformation = addAuthorizationServerInformationToClientInformation(\n      fullInformation,\n      currentAuthorizationServerInformation,\n    );\n    await provider.saveClientInformation(clientInformation);","sourceCodeStart":1306,"sourceCodeEnd":1342,"githubUrl":"https://github.com/vercel/ai/blob/69428b1f8b037e4d118fb4853428d5c4e620493c/packages/mcp/src/tool/oauth.ts#L1306-L1342","documentation":"During the OAuth authorization-code flow, the MCP client found no stored client registration and needs to dynamically register a new client with the authorization server. Dynamic registration only works if the provider supplies a saveClientInformation callback so the freshly registered client id/secret can be persisted for the subsequent token exchange. Without it, the flow cannot continue safely, so auth() throws.","triggerScenarios":"Calling the MCP OAuth helper (auth() in packages/mcp/src/tool/oauth.ts) when: (1) provider.clientInformation() returns undefined (no previously registered client), and (2) the call carries an authorizationCode === undefined (start of the flow), and (3) provider.saveClientInformation is not implemented in the custom OAuthProvider passed in.","commonSituations":"Developers implementing a minimal custom OAuth provider for an MCP server forget to implement saveClientInformation, or pass an in-memory provider whose storage was cleared between runs, or use a provider built for a pre-registered/static client while the server actually requires dynamic registration.","solutions":["Implement saveClientInformation(clientInformation) on your OAuth provider object (e.g. persist to a config file, keychain, or database) so dynamic client registration can be saved.","Pre-register a client with the authorization server and return it from provider.clientInformation() so the dynamic-registration path is never taken.","If using a static client secret, configure provider.clientSecret / clientInformation so clientInformation() returns a value before authorization starts."],"exampleFix":"// before\nconst provider = {\n  redirectUrl: 'http://localhost:3333/callback',\n  clientInformation: async () => undefined,\n};\n// after\nconst provider = {\n  redirectUrl: 'http://localhost:3333/callback',\n  clientInformation: async () => loadClientInfo(),\n  saveClientInformation: async (info) => {\n    await fs.writeFile('client-info.json', JSON.stringify(info));\n  },\n};","handlingStrategy":"validation","validationCode":"if (!provider.clientInformation && typeof provider.clientInformation === 'function') {\n  const existing = await provider.clientInformation();\n  if (!existing && typeof provider.saveClientInformation !== 'function') {\n    throw new Error('OAuth provider must implement saveClientInformation for dynamic registration');\n  }\n}","typeGuard":"function hasSaveClientInformation(p: object): p is { saveClientInformation: (info: unknown) => Promise<void> } {\n  return typeof (p as any).saveClientInformation === 'function';\n}","tryCatchPattern":"try {\n  await auth(serverUrl, { provider });\n} catch (e) {\n  if (e instanceof Error && e.message.includes('must be saveable for dynamic registration')) {\n    // persist a client registration or supply provider.saveClientInformation\n  }\n  throw e;\n}","preventionTips":["Always implement both clientInformation() and saveClientInformation() on custom OAuth providers","Persist client registrations durably so restarts don't lose them","Prefer pre-registered static clients when the AS supports it","Add a startup assertion that the provider object satisfies the full OAuthProvider interface"],"tags":["oauth","mcp","configuration","dynamic-client-registration"],"backgroundTag":"oauth-dynamic-client-registration","analyzedSha":"69428b1f8b037e4d118fb4853428d5c4e620493c","analyzedAt":"2026-08-30T12:32:21.016Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}