PrefectHQ/fastmcp · error · RuntimeError

OAuth callback handler could not be started

Error message

OAuth callback handler could not be started

What it means

callback_handler runs the local callback server inside an anyio task group; if server.serve exits without the result event ever firing (server failed to start or died immediately), control falls through the task group and the function raises RuntimeError as a catch-all 'could not be started'.

Source

Thrown at fastmcp_slim/fastmcp/client/auth/oauth.py:444

                    await result_ready.wait()
                    if result.error:
                        raise result.error
                    # `result.code` is set once `result_ready` fires without error.
                    return AuthorizationCodeResult(
                        code=result.code,  # type: ignore[arg-type]  # ty:ignore[invalid-argument-type]
                        state=result.state,
                        iss=result.iss,
                    )
            except TimeoutError as e:
                raise TimeoutError(
                    f"OAuth callback timed out after {self._callback_timeout} seconds"
                ) from e
            finally:
                server.should_exit = True
                await anyio.sleep(0.1)  # Allow server to shut down gracefully
                tg.cancel_scope.cancel()

        raise RuntimeError("OAuth callback handler could not be started")

    async def async_auth_flow(
        self, request: httpx2.Request
    ) -> AsyncGenerator[httpx2.Request, httpx2.Response]:
        """HTTPX auth flow with automatic retry on stale cached credentials.

        If the OAuth flow fails due to invalid/stale client credentials,
        clears the cache and retries once with fresh registration.
        """
        if not self._bound:
            raise RuntimeError(
                "OAuth provider has no server URL. Either pass mcp_url to OAuth() "
                "or use it with Client(auth=...) which provides the URL automatically."
            )
        try:
            # First attempt with potentially cached credentials
            async with aclosing(super().async_auth_flow(request)) as gen:
                response = None

View on GitHub (pinned to 1f02114297)

Solutions

  1. Check what is occupying the redirect port (lsof/ss) and free it, or choose a different redirect port.
  2. Pick an unprivileged port (>1024) for the OAuth redirect.
  3. Kill stale callback servers from earlier failed auth attempts.
  4. Retry the auth flow once the port is available.

Example fix

// before: port 80 requires root and bind fails
auth = OAuth(mcp_url='https://mcp.example.com/mcp', redirect_port=80)
// after: unprivileged port
auth = OAuth(mcp_url='https://mcp.example.com/mcp', redirect_port=8080)
Defensive patterns

Strategy: validation

Validate before calling

import socket
s = socket.socket()
try:
    s.bind(('127.0.0.1', redirect_port))
    print('port free')
except OSError as e:
    print('port in use or forbidden:', e)
finally:
    s.close()

Try / catch

try:
    async with client:
        await client.list_tools()
except RuntimeError as e:
    if 'callback handler could not be started' in str(e):
        pick_free_port_and_retry()
    raise

Prevention

When it happens

Trigger: The uvicorn callback server at self.redirect_port fails to bind (port already in use, permission denied on privileged port) or exits immediately, so result_ready never fires and the task group ends without a result.

Common situations: Another process (or a leftover previous OAuth attempt) is already listening on the redirect port; trying to use a privileged port (<1024) without permissions; container networking preventing loopback binds.

Related errors


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/a0c743f3cb2e1a56. Report an issue: GitHub.