PrefectHQ/fastmcp · error · ClientNotFoundError

OAuth client not found - cached credentials may be stale

Error message

OAuth client not found - cached credentials may be stale

What it means

During the OAuth browser flow, FastMCP pre-flights the authorization URL with a no-redirect GET before opening the browser. A 400 from the authorization server is interpreted as the server not recognizing the client_id, meaning cached dynamic-registration credentials are stale or invalid. ClientNotFoundError is raised so the auth flow can clear the cache and retry with a fresh registration.

Source

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

            client_info is not None
            and client_info.client_secret is not None
            and client_info.client_secret_expires_at
            and client_info.client_secret_expires_at <= int(time.time())
        ):
            raise ExpiredClientRegistrationError(
                "OAuth dynamic registration returned an expired client secret"
            )
        return await super()._perform_authorization()

    async def redirect_handler(self, authorization_url: str) -> None:
        """Open browser for authorization, with pre-flight check for invalid client."""
        # Pre-flight check to detect invalid client_id before opening browser
        async with self.httpx_client_factory() as client:
            response = await client.get(authorization_url, follow_redirects=False)

            # Check for client not found error (400 typically means bad client_id)
            if response.status_code == 400:
                raise ClientNotFoundError(
                    "OAuth client not found - cached credentials may be stale"
                )

            # OAuth typically returns redirects, but some providers return 200 with HTML login pages
            if response.status_code not in (200, 302, 303, 307, 308):
                raise RuntimeError(
                    f"Unexpected authorization response: {response.status_code}"
                )

        logger.info(f"OAuth authorization URL: {authorization_url}")
        webbrowser.open(authorization_url)

    async def callback_handler(self) -> AuthorizationCodeResult:
        """Handle OAuth callback and return the authorization code result."""
        # Create result container and event to capture the OAuth response
        result = OAuthCallbackResult()
        result_ready = anyio.Event()

View on GitHub (pinned to 1f02114297)

Solutions

  1. Clear the cached OAuth state (delete the token storage / cache directory, or let the library do it: async_auth_flow catches ClientNotFoundError for dynamic clients and auto-clears + retries once).
  2. If you passed static client credentials (static_client_info), verify the client_id/client_secret are actually registered with that server.
  3. Re-run the client; a fresh dynamic client registration will be performed against the current server.
  4. Check you are hitting the correct server URL/environment and that its authorization endpoint is healthy.

Example fix

// before: stale cache directory reused across server restarts
client = Client('https://mcp.example.com/mcp', auth=OAuth(mcp_url='https://mcp.example.com/mcp'))
// after: wipe cached tokens so a fresh registration happens
import shutil; shutil.rmtree('~/.fastmcp/oauth-cache', ignore_errors=True)
client = Client('https://mcp.example.com/mcp', auth=OAuth(mcp_url='https://mcp.example.com/mcp'))
Defensive patterns

Strategy: retry

Try / catch

from fastmcp.client.auth import ClientNotFoundError
try:
    async with client:
        result = await client.list_tools()
except ClientNotFoundError:
    clear_oauth_cache()  # delete token/client-info storage
    async with client:   # retry triggers fresh dynamic registration
        result = await client.list_tools()

Prevention

When it happens

Trigger: Calling Client(auth=OAuth(mcp_url)) (or auth='oauth') and hitting redirect_handler when GET <authorization_url> returns HTTP 400 — typically because the stored OAuthClientInformationFull in the token storage no longer matches a client registered at the server (server restarted with in-memory registration storage, tokens cache predates a server redeploy, or the provider purged the client).

Common situations: Pointing a client at a dev MCP server that stores dynamic clients in memory after the server restarted; switching environments (staging vs prod) while reusing a token cache directory; a provider that rotates/evicts dynamically registered clients.

Related errors


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