mastra-ai/mastra · error · Error

Failed to start OAuth callback server: ports ${firstPort}-${

Error message

Failed to start OAuth callback server: ports ${firstPort}-${lastPort} are all in use

What it means

Error thrown by createOAuthCallbackServer after attempting to bind each candidate loopback port; if none could be bound, it reports the full candidate port range as in use. The OAuth flow needs a local HTTP server to receive the authorization redirect, so this is fatal to the flow.

Source

Thrown at packages/mcp/src/client/oauth-callback-server.ts:299

  const hostname = candidates[0]!.hostname.replace(/^\[|\]$/g, '');

  let boundUrl: URL | undefined;
  let boundPort: number | undefined;
  for (const candidate of candidates) {
    const candidatePort = Number(candidate.port);
    const error = await listen(server, candidatePort, hostname);
    if (!error) {
      boundUrl = candidate;
      // Preserve the port we actually listened on. Reading URL.port back would
      // return '' for the default 80/443, losing the effective port.
      boundPort = candidatePort;
      break;
    }
  }
  if (!boundUrl || boundPort === undefined) {
    const firstPort = Number(candidates[0]!.port);
    const lastPort = Number(candidates[candidates.length - 1]!.port);
    throw new Error(`Failed to start OAuth callback server: ports ${firstPort}-${lastPort} are all in use`);
  }

  // The bind-time 'error' listener is removed once 'listening' fires, so after
  // this point the server has no 'error' handler. An emitted 'error' (e.g. a
  // post-bind socket failure) with no listener throws and would crash the host
  // process. Keep a persistent listener that settles the flow with the error
  // instead of letting it become an uncaught exception.
  server.on('error', error => {
    settle({ error: error instanceof Error ? error : new Error(String(error)) });
  });

  return {
    url: boundUrl,
    port: boundPort,

    waitForCode({ timeoutMs = DEFAULT_CALLBACK_TIMEOUT_MS } = {}) {
      let timer: NodeJS.Timeout | undefined;
      const timeout = new Promise<never>((_, reject) => {

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Identify and stop the processes occupying the ports (lsof -i :PORT / netstat), or wait for them to exit.
  2. Configure a different or wider callback port range in the provider/redirect URL.
  3. Ensure previous OAuth flows are cancelled/completed so their callback servers are released.
  4. In CI/containers, reserve unique port ranges per test or worker to avoid cross-process collisions.

Example fix

// before
new MCPOAuthClientProvider({ redirectUrl: 'http://localhost:3456/callback' }) // 3456 busy
// after
const freePort = await getFreePort(4000, 4100);
new MCPOAuthClientProvider({ redirectUrl: `http://localhost:${freePort}/callback` })
Defensive patterns

Strategy: retry

Validate before calling

import net from 'node:net';
function isPortFree(port: number): Promise<boolean> {
  return new Promise(res => {
    const s = net.createServer();
    s.once('error', () => res(false));
    s.once('listening', () => s.close(() => res(true)));
    s.listen(port, 'localhost');
  });
}

Try / catch

try {
  await startOAuthFlow();
} catch (e) {
  if (e instanceof Error && e.message.includes('are all in use')) {
    await releaseStaleServers(); // cancel previous flows / kill stale listeners
    await startOAuthFlow(); // retry once
  } else throw e;
}

Prevention

When it happens

Trigger: Starting an OAuth authorization flow when every port in the configured candidate port range is already bound by other processes (or blocked by firewall/permissions).

Common situations: Stale dev servers or previous crashed runs still holding the callback port; multiple concurrent OAuth flows for different servers competing for the same range; containers/CI where the port range collides with other services.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/fbc1684ff125b563. Report an issue: GitHub.