Describe the bug
AuthorizationManager::refresh_token() replaces stored credentials with the refresh response "as-is". If the authorization server returns a new access token but omits refresh_token, the existing refresh token is lost.
Per RFC 6749 section 6, issuing a new refresh token during refresh is optional. If none is issued, the client should retain the existing refresh token.
To Reproduce
Steps to reproduce the behavior:
- Configure an OAuth-backed MCP server whose token endpoint returns a refresh_token on the initial authorization_code exchange, but does not rotate refresh tokens on refresh.
- Store credentials containing an access token and refresh token.
- Let the access token expire, or otherwise force
AuthorizationManager::refresh_token() to run.
- Have the refresh token response return a valid new access token, token type, and expiry, but
omit refresh_token, for example:
{
"access_token": "new-access-token",
"token_type": "Bearer",
"expires_in": 3600
}
- Observe that
AuthorizationManager::refresh_token() persists token_response: Some(token_result.clone()), so the stored credentials no longer contain the original refresh token.
Expected behavior
If the refresh response omits refresh_token, preserve the previous refresh token before storing/returning the token response.
Logs
If applicable, add logs to help explain your problem.
Additional context
The same bug pattern existed in the Python MCP SDK and was reported as modelcontextprotocol/python-sdk#2270. This behavior was seen on the Guru MCP server with Codex CLI using rust-sdk v1.7.0.
Describe the bug
AuthorizationManager::refresh_token()replaces stored credentials with the refresh response "as-is". If the authorization server returns a new access token but omitsrefresh_token, the existing refresh token is lost.Per RFC 6749 section 6, issuing a new refresh token during refresh is optional. If none is issued, the client should retain the existing refresh token.
To Reproduce
Steps to reproduce the behavior:
AuthorizationManager::refresh_token()to run.omit
refresh_token, for example:AuthorizationManager::refresh_token()persiststoken_response: Some(token_result.clone()), so the stored credentials no longer contain the original refresh token.Expected behavior
If the refresh response omits
refresh_token, preserve the previous refresh token before storing/returning the token response.Logs
If applicable, add logs to help explain your problem.
Additional context
The same bug pattern existed in the Python MCP SDK and was reported as modelcontextprotocol/python-sdk#2270. This behavior was seen on the Guru MCP server with Codex CLI using rust-sdk v1.7.0.