From 43e954f5d9aa9ace3673a556ad3422c8e53ad3e5 Mon Sep 17 00:00:00 2001 From: Charlie Tonneslan Date: Tue, 17 Mar 2026 13:11:10 -0400 Subject: [PATCH] fix: preserve existing refresh_token when server omits it in refresh response Per RFC 6749 Section 6, the authorization server MAY issue a new refresh token in the refresh response. When it does not, the client must preserve the existing one. The current implementation replaces current_tokens with the parsed response as-is, which discards the stored refresh_token when the server omits it. After the first successful refresh, can_refresh_token() returns False and all subsequent refreshes fail, forcing full re-authentication. Many OAuth providers omit refresh_token from refresh responses by default (Google, Auth0 without rotation, Okta in persistent mode). Github-Issue: #2270 --- src/mcp/client/auth/oauth2.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/mcp/client/auth/oauth2.py b/src/mcp/client/auth/oauth2.py index 25075dec3..952d2c108 100644 --- a/src/mcp/client/auth/oauth2.py +++ b/src/mcp/client/auth/oauth2.py @@ -458,6 +458,17 @@ async def _handle_refresh_response(self, response: httpx.Response) -> bool: # p content = await response.aread() token_response = OAuthToken.model_validate_json(content) + # Per RFC 6749 Section 6, the server MAY issue a new refresh token. + # If the response omits it, preserve the existing one. + if ( + not token_response.refresh_token + and self.context.current_tokens + and self.context.current_tokens.refresh_token + ): + token_response = token_response.model_copy( + update={"refresh_token": self.context.current_tokens.refresh_token} + ) + self.context.current_tokens = token_response self.context.update_token_expiry(token_response) await self.context.storage.set_tokens(token_response)