vercel/ai · error

OAuth client information must be saveable for dynamic…

Error message

OAuth client information must be saveable for dynamic registration

What it means

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.

Solutions

  1. 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.
  2. Pre-register a client with the authorization server and return it from provider.clientInformation() so the dynamic-registration path is never taken.
  3. If using a static client secret, configure provider.clientSecret / clientInformation so clientInformation() returns a value before authorization starts.

Example fix

// before
const provider = {
  redirectUrl: 'http://localhost:3333/callback',
  clientInformation: async () => undefined,
};
// after
const provider = {
  redirectUrl: 'http://localhost:3333/callback',
  clientInformation: async () => loadClientInfo(),
  saveClientInformation: async (info) => {
    await fs.writeFile('client-info.json', JSON.stringify(info));
  },
};
Defensive patterns

Strategy: validation

Validate before calling

if (!provider.clientInformation && typeof provider.clientInformation === 'function') {
  const existing = await provider.clientInformation();
  if (!existing && typeof provider.saveClientInformation !== 'function') {
    throw new Error('OAuth provider must implement saveClientInformation for dynamic registration');
  }
}

Type guard

function hasSaveClientInformation(p: object): p is { saveClientInformation: (info: unknown) => Promise<void> } {
  return typeof (p as any).saveClientInformation === 'function';
}

Try / catch

try {
  await auth(serverUrl, { provider });
} catch (e) {
  if (e instanceof Error && e.message.includes('must be saveable for dynamic registration')) {
    // persist a client registration or supply provider.saveClientInformation
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30). Data as JSON: /api/errors/405cd90f7c64f592. Report an issue: GitHub.

Appendix: source

Thrown at packages/mcp/src/tool/oauth.ts:1324

        clientInformation,
      });
    if (storedAuthorizationServerInformation) {
      assertAuthorizationServerInformationMatches({
        storedAuthorizationServerInformation,
        currentAuthorizationServerInformation,
      });
    }
  }

  if (!clientInformation) {
    if (authorizationCode !== undefined) {
      throw new Error(
        'Existing OAuth client information is required when exchanging an authorization code',
      );
    }

    if (!provider.saveClientInformation) {
      throw new Error(
        'OAuth client information must be saveable for dynamic registration',
      );
    }

    const fullInformation = await registerClient(authorizationServerUrl, {
      metadata,
      clientMetadata: {
        ...clientMetadata,
        scope: selectedScope,
      },
      fetchFn,
    });

    clientInformation = addAuthorizationServerInformationToClientInformation(
      fullInformation,
      currentAuthorizationServerInformation,
    );
    await provider.saveClientInformation(clientInformation);

View on GitHub (pinned to 69428b1f8b)