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 = NoneView on GitHub (pinned to 1f02114297)
Solutions
- Check what is occupying the redirect port (lsof/ss) and free it, or choose a different redirect port.
- Pick an unprivileged port (>1024) for the OAuth redirect.
- Kill stale callback servers from earlier failed auth attempts.
- 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
- Check the redirect port is free before starting the flow (lsof/ss or a socket bind probe).
- Always use an unprivileged port (>1024).
- Clean up orphaned callback servers from previous failed auth attempts.
- In containers, confirm loopback binds are permitted.
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
- OAuth callback timed out after {self._callback_timeout} seco
- The device authorization request expired
- The device authorization request was denied
- Device authorization failed
- OAuth client not found - cached credentials may be stale
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/a0c743f3cb2e1a56.
Report an issue: GitHub.