PrefectHQ/fastmcp · error · RuntimeError
OAuth provider has no server URL. Either pass mcp_url to OAu
Error message
OAuth provider has no server URL. Either pass mcp_url to OAuth() or use it with Client(auth=...) which provides the URL automatically.
What it means
The OAuth provider must know the MCP server URL to perform discovery, registration, and token exchange. If OAuth() was constructed without mcp_url and was never bound by being attached to a Client (which supplies the URL automatically), async_auth_flow raises this RuntimeError because the flow cannot proceed unbound.
Source
Thrown at fastmcp_slim/fastmcp/client/auth/oauth.py:455
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
while True:
try:
# First iteration sends None, subsequent iterations send response
yielded_request = await gen.asend(response) # ty: ignore[invalid-argument-type]
response = yield yielded_request
except StopAsyncIteration:
break
except (ClientNotFoundError, ExpiredClientRegistrationError) as exc:
# Static credentials are fixed — retrying won't help. Surface the
# error so the user can correct their client_id / client_secret.View on GitHub (pinned to 1f02114297)
Solutions
- Pass mcp_url when constructing OAuth: OAuth(mcp_url='https://mcp.example.com/mcp').
- Or use the provider via Client(transport_or_url, auth=oauth), which binds the URL automatically.
- Ensure the flow is invoked through the Client rather than a bare httpx client.
Example fix
// before: unbound provider used with raw httpx
auth = OAuth()
httpx.get('https://mcp.example.com/mcp', auth=auth)
// after: bind via Client, which supplies the URL
client = Client('https://mcp.example.com/mcp', auth=OAuth())
async with client: await client.list_tools() Defensive patterns
Strategy: validation
Validate before calling
auth = OAuth() # or OAuth(mcp_url=...)
if not getattr(auth, 'mcp_url', None) and not getattr(auth, '_bound', False):
raise ValueError('Pass mcp_url to OAuth() or use Client(auth=...) to bind the URL') Type guard
def is_oauth_bound(auth) -> bool:
return bool(getattr(auth, 'mcp_url', None)) or bool(getattr(auth, '_bound', False)) Try / catch
try:
async with client:
await client.list_tools()
except RuntimeError as e:
if 'no server URL' in str(e):
raise ValueError('Construct OAuth with mcp_url or attach it via Client(auth=...)') from e
raise Prevention
- Always construct OAuth with an explicit mcp_url, or always attach it through Client(auth=...).
- Never pass an OAuth provider to a raw httpx client; route requests through fastmcp's Client.
- Don't reuse a single OAuth auth object detached from its client.
- Assert the provider is bound in tests before exercising the auth flow.
When it happens
Trigger: Creating OAuth() with no mcp_url and then using it as an httpx auth outside of Client(auth=...) — e.g. passing it directly to a raw httpx client, or reusing the auth object detached from its client — so self._bound remains False when async_auth_flow runs.
Common situations: Hand-rolling an HTTPX request with the OAuth object instead of going through fastmcp's Client; constructing OAuth() for later configuration but calling the flow before binding; copying an auth object between clients.
Understand the failure class
Background: "X is required", "must be set", "cannot be empty": the missing-required-config error family, from Vertex AI project/location to WeChat keys — this error's family across 18 libraries.
Related errors
- OAuth client not found - cached credentials may be stale
- OAuth server rejected the static client credentials. Verify
- mode must be 'legacy', 'auto', or one of {list(MODERN_PROTOC
- jwt_signing_key is required when upstream_client_secret is n
- Unsupported token_endpoint_auth_method: {method!r}. Supporte
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/9e1163bdef84190b.
Report an issue: GitHub.