Why does MCP Python SDK re-authenticate on first request instead of refreshing expired stored tokens?
In the MCP Python SDK (reported against the latest version as of Aug 2025), when a client is created with a TokenStorage holding an expired access token, the first request gets a 401 and triggers full re-auth instead of a proactive refresh. token_expiry_time stays None after init, so the token is treated as perpetually valid. Refresh can also use the wrong token URL when oauth_metadata is not restored from storage, which breaks proxy server setups.
Originally asked on GitHub: https://github.com/modelcontextprotocol/python-sdk/issues/1318
4 answers
Status: open. As of the last comments in the thread, the fixes are not merged; two PRs target the two bugs.
Root cause: in
mcp/client/auth.py,OAuthContext.is_token_validtreats aNonetoken_expiry_timeas valid forever, and_initializeloads tokens from storage without restoring their expiry. A stored, expired token therefore looks perpetual, proactive refresh never runs, the first request gets a 401, and the client falls back to full re-auth. A second bug: when a client is re-instantiated from storage,oauth_metadatais not restored, so_refresh_tokenbuilds the token URL asserver_url + /token, which is wrong for proxy servers that serve third-party OAuth endpoints.Two PRs address these: #1784 (restores
token_expiry_timeon_initialize, a small low-risk fix) and #2492 (adds optionalget/set_oauth_metadatatoTokenStorage, backward compatible, so refresh uses the real token endpoint).Workaround suggested by a contributor: after loading tokens in
_initialize, set the expiry:pythonasync def _initialize(self): self.context.current_tokens = await self.context.storage.get_tokens() self.context.client_info = await self.context.storage.get_client_info() if self.context.current_tokens: self.context.update_token_expiry(self.context.current_tokens) self._initialized = TrueNote this is a suggested patch, not a verified fix.
update_token_expiryrelies onexpires_in, so the stored token must retain it; the thread notes the TokenStorage protocol does not explicitly require persisting expiry timestamps. For proxy setups, persist OAuth metadata alongside tokens so refresh hits the correct endpoint. Until the fix lands, one user switched to therequests-oauth2clientlibrary to bypass the SDK's OAuth flow.Did this work for you?What @keurcien tried on GitHub (Outcome unclear)
A contributor suggested restoring the expiry inside _initialize by calling self.context.update_token_expiry(self.context.current_tokens) after loading tokens from storage. The thread does not report a test result for this patch.
Did this work for you?What @Norcim133 tried on GitHub (Outcome unclear)
A commenter suggested forcing OAuth discovery during _initialize so oauth_metadata is populated before refresh, since a proxy server's well-known endpoint otherwise leads to a wrong /token URL (404 and full re-auth). No test result is reported.
Did this work for you?What @levderquantori tried on GitHub (Outcome unclear)
One user reported deciding to use the requests-oauth2client library to implement OAuth2 instead of the SDK's flow. The thread does not state whether the switch succeeded.
Did this work for you?
Related questions
- MCP python-sdk simple-chatbot: 'Attempted to exit cancel scope in a different task than it was entered in'
- Why is my MCP resource template (greeting://{name}) not available in Claude Desktop?
- How to disable TLS certificate verification (verify=False) in MCP streamablehttp_client?
- anyio.ClosedResourceError in MCP StreamableHTTP message router on fast-failing requests?
- Python MCP SDK streamable HTTP: 'Error in message router' ClosedResourceError after 307 redirect