Title: _handle_refresh_response discards existing refresh_token when server omits it · Issue #2270 · modelcontextprotocol/python-sdk · GitHub
Open Graph Title: _handle_refresh_response discards existing refresh_token when server omits it · Issue #2270 · modelcontextprotocol/python-sdk
X Title: _handle_refresh_response discards existing refresh_token when server omits it · Issue #2270 · 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 _handle_refresh_respon...
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/2270
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"_handle_refresh_response discards existing refresh_token when server omits it","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\n`_handle_refresh_response()` in `src/mcp/client/auth/oauth2.py` replaces the stored token object with the parsed refresh response as-is. When the authorization server does not return a new `refresh_token` in the refresh response, the previously stored `refresh_token` is lost. After the first successful refresh, `can_refresh_token()` returns `False` and all subsequent refreshes fail, forcing full re-authentication.\n\nPer [RFC 6749 Section 6](https://datatracker.ietf.org/doc/html/rfc6749#section-6), issuing a new refresh token in the refresh response is optional:\n\n\u003e \"The authorization server MAY issue a new refresh token, in which case the client MUST discard the old refresh token and replace it with the new refresh token.\"\n\nThe implicit corollary is that if the server does not issue a new refresh token, the client must preserve the existing one.\n\nMany OAuth providers omit the `refresh_token` from refresh responses by default (e.g., Google, Auth0 without rotation enabled, Okta in persistent token mode). This makes the current behavior a practical issue for a wide range of OAuth servers.\n\n### Current Code\n\n```python\nasync def _handle_refresh_response(self, response: httpx.Response) -\u003e bool:\n ...\n token_response = OAuthToken.model_validate_json(content)\n\n self.context.current_tokens = token_response # overwrites old refresh_token\n self.context.update_token_expiry(token_response)\n await self.context.storage.set_tokens(token_response)\n ...\n```\n\n### Proposed Fix\n\nPreserve the existing `refresh_token` when the refresh response omits one:\n\n```python\ntoken_response = OAuthToken.model_validate_json(content)\n\n# Per RFC 6749 Section 6, the server MAY issue a new refresh token.\n# If the response omits it, preserve the existing one.\nif not token_response.refresh_token and self.context.current_tokens and self.context.current_tokens.refresh_token:\n token_response = token_response.model_copy(\n update={\"refresh_token\": self.context.current_tokens.refresh_token}\n )\n\nself.context.current_tokens = token_response\n...\n```\n\n### Related Issues\n\n- #1318 - Critical Token Refresh Bugs (token expiry tracking, P1)\n- #1250 - Framework does not handle refreshed tokens correctly (P1)\n- PR #2039 - SEP-2207: Refresh token guidance (adds `offline_access` scope, does not address preservation)\n\n### Python \u0026 MCP Python SDK\n\n```Text\nv1.26.0\n```","author":{"url":"https://github.com/oedokumaci","@type":"Person","name":"oedokumaci"},"datePublished":"2026-03-11T13:59:01.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":1},"url":"https://github.com/2270/python-sdk/issues/2270"}
| 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:bf5b3143-9d04-8331-4278-727341373298 |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | 8284:2E6F7D:2B9BB5:3B6EF9:6A591EB7 |
| html-safe-nonce | 2339b6c451cdde18c5e599115327c65f1e93f05d5f1e866773c65e3999544422 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiI4Mjg0OjJFNkY3RDoyQjlCQjU6M0I2RUY5OjZBNTkxRUI3IiwidmlzaXRvcl9pZCI6IjgzNjE4NzQ1MDkzMDQzNzgxNSIsInJlZ2lvbl9lZGdlIjoiaWFkIiwicmVnaW9uX3JlbmRlciI6ImlhZCJ9 |
| visitor-hmac | a14da357f2b96b553b3fc46654ecd4f83f36cdb7957b49766b35b1f11de95afd |
| hovercard-subject-tag | issue:4058407633 |
| 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/2270/issue_layout |
| twitter:image | https://opengraph.githubassets.com/8b9b14d083dde5a04817217bc412b942b4d96c90d3f9d075c9d3f5f9e4d4fd1c/modelcontextprotocol/python-sdk/issues/2270 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/8b9b14d083dde5a04817217bc412b942b4d96c90d3f9d075c9d3f5f9e4d4fd1c/modelcontextprotocol/python-sdk/issues/2270 |
| 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 | oedokumaci |
| hostname | github.com |
| expected-hostname | github.com |
| None | de7b53b927e5bfbf02672a5b2960bf82d1608d91c6d7888039c826ebf8d4ca76 |
| 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 | 020f8e5d3a42ae1a22d3f297198ccfca124edd1d |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width