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

  1. Pass mcp_url when constructing OAuth: OAuth(mcp_url='https://mcp.example.com/mcp').
  2. Or use the provider via Client(transport_or_url, auth=oauth), which binds the URL automatically.
  3. 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

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


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