Title: Critical Token Refresh Bugs - Prevents Proactive Refresh · Issue #1318 · modelcontextprotocol/python-sdk · GitHub
Open Graph Title: Critical Token Refresh Bugs - Prevents Proactive Refresh · Issue #1318 · modelcontextprotocol/python-sdk
X Title: Critical Token Refresh Bugs - Prevents Proactive Refresh · Issue #1318 · modelcontextprotocol/python-sdk
Description: Initial Checks I confirm that I'm using the latest version of MCP Python SDK I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening this issue Description tldr: Circular logic i...
Open Graph Description: Initial Checks I confirm that I'm using the latest version of MCP Python SDK I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening this ...
X Description: Initial Checks I confirm that I'm using the latest version of MCP Python SDK I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening t...
Opengraph URL: https://github.com/modelcontextprotocol/python-sdk/issues/1318
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"Critical Token Refresh Bugs - Prevents Proactive Refresh","articleBody":"### Initial Checks\n\n- [x] I confirm that I'm using the latest version of MCP Python SDK\n- [x] I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening this issue\n\n### Description\n\ntldr: Circular logic in `mcp/client/auth.py` creates two critical issues that prevent proactive token refresh; this would cause frequent re-auth for all users but it creates a third, worse issue for Clients that use TokenStorage (especially noticeable in a multi-user setup)\n\n## Background \nIn `mcp/client/auth.py` there are only 3 places where `OAuthContext.token_expiry_time` is set:\n1. During init it is set to None\n2. During full re-auth after receiving a 401 (in call to `_handle_token_response`)\n3. When a token is deemed invalid in `is_token_valid` (at start of `async_auth_flow`) \n\n## Issue 1 - Token validity check treats None as Valid\nThe ultimate problem is that when `token_expiry_time` is None, a token will pass validity checks, per the below. \n\n```python\ndef is_token_valid(self) -\u003e bool:\n \"\"\"Check if current token is valid.\"\"\"\n return bool(\n self.current_tokens\n and self.current_tokens.access_token\n and (not self.token_expiry_time or time.time() \u003c= self.token_expiry_time)\n# ^^^^^^^^^^^^^^^^^^ None makes it perpetually valid so never needs pro-active refresh\n )\n```\nThis is probably intended behavior to enable None to represent 'perpetual tokens'.\n\nIt sets up 2 fatal problems.\n\n## Issue 2 - Expired tokens get converted to 'perpetual tokens'\nIn `update_token_expiry` an expired token with a `token.expires` time of `0` is 'Falsey' which sets `token_expiry_time` to None.\n```python\ndef update_token_expiry(self, token: OAuthToken) -\u003e None:\n \"\"\"Update token expiry time.\"\"\"\n if token.expires_in: # \u003c============================ 0 evaluates to False\n self.token_expiry_time = time.time() + token.expires_in\n else:\n self.token_expiry_time = None # \u003c============================ Makes it perpetually valid\n```\n\nPer **Issue 1**, when `token_expiry_time` is None the token is always valid (i.e. perpetual).\n\n## Undesirable Result - Only full re-auth can refresh a 'perpetual token' that goes stale\nBecause the token is now perpetual, it never gets proactively refreshed so it always requires a 401 and full re-auth to refresh.\n\n## Issue 3 - Clients with storage always start with `token_expiry_time` set to None\nThe OAuth init does not check token expiry:\n```python\nasync def _initialize(self) -\u003e None:\n \"\"\"Load stored tokens and client info.\"\"\"\n self.context.current_tokens = await self.context.storage.get_tokens()\n self.context.client_info = await self.context.storage.get_client_info()\n self._initialized = True\n```\nThis is often fine for Clients that persist.\n\nFor newly or frequently instantiated Clients that use Token Storage, `token_expiry_time` is None by default.\n\nDue to **Issue 1** that None value cause the token to become perpetual.\n\nDue to **Issue 2** the perpetual token is never checked for refresh.\n\nAs a result, tokens from storage (which are probably stale) are never checked prior to their first call. So they always trigger a 401 and revert to full re-auth.\n\n## Proposed Fixes\nToken update should check if token has an expires_in attribute and for an explicit None. This preserves perpetual tokens while treating expired tokens appropriately.\n\n```python\n def update_token_expiry(self, token: OAuthToken) -\u003e None:\n \"\"\"Update token expiry time.\"\"\"\n if hasattr(token, 'expires_in') and token.expires_in is not None: # \u003c================ Fix\n self.token_expiry_time = time.time() + token.expires_in\n else:\n self.token_expiry_time = None\n```\n\nAlso, the validity check should only work when `self.token_expiry_time=None` explicitly instead of when 'Falsey':\n\n```python\ndef is_token_valid(self) -\u003e bool:\n \"\"\"Check if current token is valid.\"\"\"\n return bool(\n self.current_tokens\n and self.current_tokens.access_token\n and (self.token_expiry_time is None or time.time() \u003c= self.token_expiry_time) # \u003c============== Fix\n #^^^^^^^^^^^^^^^^^^^^^^^ None can remain signal for perpetual token\n )\n```\n\nFinally, OAuthContext._initialize should call update_token_expiry so stored tokens aren't treated as perpetual by default:\n\n```python\nasync _initialize(self):\n \"\"\"Initialize and properly set token expiry from stored tokens.\"\"\"\n self.context.current_tokens = await self.context.storage.get_tokens()\n self.context.client_info = await self.context.storage.get_client_info()\n \n # Fix: Update token expiry if tokens loaded from storage\n if self.context.current_tokens:\n self.context.update_token_expiry(self.context.current_tokens)\n self._initialized = True\n```\n\n### Example Code\n\n```Python\n\n```\n\n### Python \u0026 MCP Python SDK\n\n```Text\nI'm on 1.12.4 but the code is still the same for 1.13.1\n```","author":{"url":"https://github.com/Norcim133","@type":"Person","name":"Norcim133"},"datePublished":"2025-08-28T00:16:24.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":9},"url":"https://github.com/1318/python-sdk/issues/1318"}
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:c364cd05-a465-c9dd-7d25-36625eaacceb |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | BD88:144E47:2A8B5B:3A07E5:6A5A5ECD |
| html-safe-nonce | f3545739ea53f6695c0b9c2a0acbcd47a528355949f2fccbbc8753afc46fc67f |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJCRDg4OjE0NEU0NzoyQThCNUI6M0EwN0U1OjZBNUE1RUNEIiwidmlzaXRvcl9pZCI6IjgxOTc5ODA5OTc5NTIzNjQyMzciLCJyZWdpb25fZWRnZSI6ImlhZCIsInJlZ2lvbl9yZW5kZXIiOiJpYWQifQ== |
| visitor-hmac | 4fcb3bfbf4bdd814728a327b67bf33cc6d50131d8a0c66588ec8f93cb87ecc76 |
| hovercard-subject-tag | issue:3361273652 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/modelcontextprotocol/python-sdk/1318/issue_layout |
| twitter:image | https://opengraph.githubassets.com/9f890e39ce5ac5375dbb676341ea6aaf09150424074f14353c00269c96f6e2b0/modelcontextprotocol/python-sdk/issues/1318 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/9f890e39ce5ac5375dbb676341ea6aaf09150424074f14353c00269c96f6e2b0/modelcontextprotocol/python-sdk/issues/1318 |
| og:image:alt | Initial Checks I confirm that I'm using the latest version of MCP Python SDK I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening this ... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | Norcim133 |
| hostname | github.com |
| expected-hostname | github.com |
| None | 05b9ddf6a47d2dbe13944873a99f5fb4b83ba4871f9cb8a8e256793a63ca9687 |
| turbo-cache-control | no-preview |
| go-import | github.com/modelcontextprotocol/python-sdk git https://github.com/modelcontextprotocol/python-sdk.git |
| octolytics-dimension-user_id | 182288589 |
| octolytics-dimension-user_login | modelcontextprotocol |
| octolytics-dimension-repository_id | 862584018 |
| octolytics-dimension-repository_nwo | modelcontextprotocol/python-sdk |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 862584018 |
| octolytics-dimension-repository_network_root_nwo | modelcontextprotocol/python-sdk |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | f8d29d1bd03dda2dd14b3f80b8bc27e1111f43bd |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width