Skip to content

_handle_refresh_response discards existing refresh_token when server omits it #2270

@oedokumaci

Description

@oedokumaci

Initial Checks

Description

_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.

Per RFC 6749 Section 6, issuing a new refresh token in the refresh response is optional:

"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."

The implicit corollary is that if the server does not issue a new refresh token, the client must preserve the existing one.

Many 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.

Current Code

async def _handle_refresh_response(self, response: httpx.Response) -> bool:
    ...
    token_response = OAuthToken.model_validate_json(content)

    self.context.current_tokens = token_response  # overwrites old refresh_token
    self.context.update_token_expiry(token_response)
    await self.context.storage.set_tokens(token_response)
    ...

Proposed Fix

Preserve the existing refresh_token when the refresh response omits one:

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
...

Related Issues

Python & MCP Python SDK

v1.26.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions