{"record":{"id":"3fb3cdb485b44e8e","repo":"PrefectHQ/fastmcp","slug":"oauth-client-not-found-cached-credentials-may-be","errorCode":null,"errorMessage":"OAuth client not found - cached credentials may be stale","messagePattern":"OAuth client not found - cached credentials may be stale","errorType":"exception","errorClass":"ClientNotFoundError","httpStatus":400,"severity":"error","filePath":"fastmcp_slim/fastmcp/client/auth/oauth.py","lineNumber":389,"sourceCode":"            client_info is not None\n            and client_info.client_secret is not None\n            and client_info.client_secret_expires_at\n            and client_info.client_secret_expires_at <= int(time.time())\n        ):\n            raise ExpiredClientRegistrationError(\n                \"OAuth dynamic registration returned an expired client secret\"\n            )\n        return await super()._perform_authorization()\n\n    async def redirect_handler(self, authorization_url: str) -> None:\n        \"\"\"Open browser for authorization, with pre-flight check for invalid client.\"\"\"\n        # Pre-flight check to detect invalid client_id before opening browser\n        async with self.httpx_client_factory() as client:\n            response = await client.get(authorization_url, follow_redirects=False)\n\n            # Check for client not found error (400 typically means bad client_id)\n            if response.status_code == 400:\n                raise ClientNotFoundError(\n                    \"OAuth client not found - cached credentials may be stale\"\n                )\n\n            # OAuth typically returns redirects, but some providers return 200 with HTML login pages\n            if response.status_code not in (200, 302, 303, 307, 308):\n                raise RuntimeError(\n                    f\"Unexpected authorization response: {response.status_code}\"\n                )\n\n        logger.info(f\"OAuth authorization URL: {authorization_url}\")\n        webbrowser.open(authorization_url)\n\n    async def callback_handler(self) -> AuthorizationCodeResult:\n        \"\"\"Handle OAuth callback and return the authorization code result.\"\"\"\n        # Create result container and event to capture the OAuth response\n        result = OAuthCallbackResult()\n        result_ready = anyio.Event()\n","sourceCodeStart":371,"sourceCodeEnd":407,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/client/auth/oauth.py#L371-L407","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["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).","If you passed static client credentials (static_client_info), verify the client_id/client_secret are actually registered with that server.","Re-run the client; a fresh dynamic client registration will be performed against the current server.","Check you are hitting the correct server URL/environment and that its authorization endpoint is healthy."],"exampleFix":"// before: stale cache directory reused across server restarts\nclient = Client('https://mcp.example.com/mcp', auth=OAuth(mcp_url='https://mcp.example.com/mcp'))\n// after: wipe cached tokens so a fresh registration happens\nimport shutil; shutil.rmtree('~/.fastmcp/oauth-cache', ignore_errors=True)\nclient = Client('https://mcp.example.com/mcp', auth=OAuth(mcp_url='https://mcp.example.com/mcp'))","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"from fastmcp.client.auth import ClientNotFoundError\ntry:\n    async with client:\n        result = await client.list_tools()\nexcept ClientNotFoundError:\n    clear_oauth_cache()  # delete token/client-info storage\n    async with client:   # retry triggers fresh dynamic registration\n        result = await client.list_tools()","preventionTips":["Avoid token-cache directories shared across server redeployments when the server keeps registrations in memory.","Prefer dynamic registration (no static_client_info) so the SDK can self-heal by re-registering.","Keep environment-specific cache directories separate (staging vs prod).","Use a persistent OAuth registration store server-side if clients cache credentials long-term."],"tags":["oauth","auth","stale-credentials","client"],"backgroundTag":"oauth-client-not-found","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}