Repository navigation
Token handler drops RFC 8707 resource on refresh_token (and authorization_code) grants; provider can't see it #3720
Description
Initial Checks
- I confirm that I'm using the newest release of my line (checked on v2.3.0)
- I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening this issue
Release line
2.x (current stable)
Description
The token handler parses the RFC 8707 resource parameter on a refresh_token grant but never passes it to the provider, and OAuthAuthorizationServerProvider.exchange_refresh_token has no way to receive it.
RefreshTokenRequestdeclaresresource(handlers/token.py#L36-L45).- The refresh branch calls
self.provider.exchange_refresh_token(client_info, refresh_token, scopes)and dropstoken_request.resource(L238). exchange_refresh_token(self, client, refresh_token, scopes)has no resource parameter (provider.py#L259-L264).- The JWT bearer branch does forward it (
resource=token_request.resource, L268).
So an authorization server built on the SDK can't apply RFC 8707 §2.2 on refresh ("such policy may limit the acceptable resources to those that were originally granted by the resource owner or a subset thereof") or answer a mismatched resource with invalid_target. The MCP authorization spec requires clients to send resource in token requests, so the value is always there; the provider just can't see it. #3447 added RefreshToken.resource so the granted resource can be stored, but there is still nothing to compare it with.
The same applies to the authorization_code grant: the handler doesn't compare token_request.resource with AuthorizationCode.resource and doesn't pass it to exchange_authorization_code (L200), so a mismatch is accepted silently.
For comparison, the TypeScript SDK's token handler passes resource to exchangeRefreshToken and to the authorization code exchange.
Today the only workaround is to subclass TokenHandler and hand the form value to the provider out of band.
Expected: the provider can see the requested resource on both grants. One backwards-compatible option is an optional resource: str | None = None argument on exchange_refresh_token and exchange_authorization_code (or a params object, as the JWT bearer path does). Optionally, the handler could itself return invalid_target when resource is sent and differs from refresh_token.resource / auth_code.resource.
Example Code
class Provider(OAuthAuthorizationServerProvider[AuthorizationCode, RefreshToken, AccessToken]):
async def exchange_refresh_token(
self, client: OAuthClientInformationFull, refresh_token: RefreshToken, scopes: list[str]
) -> OAuthToken:
# POST /token
# grant_type=refresh_token&refresh_token=...&resource=https://other.example/mcp
# The requested resource is not available here, so the provider can't refuse it
# with invalid_target or check it against refresh_token.resource.
...Python & MCP Python SDK
Python 3.12.12
mcp v2.3.0
Activity
sattyamjjain commentedon Oct 17, 2026
Confirmed on main at 9f66f37: token.py L200 calls
exchange_authorization_code(client_info, auth_code)and L238 callsexchange_refresh_token(client_info, refresh_token, scopes), while the JWT-bearer branch at L268 passesresource=token_request.resource. Same gap on v1.x (52e64dd, L189 and L232).https://github.com/modelcontextprotocol/python-sdk/blob/9f66f376d486e84e9810b368cf8be9d5908661ab/src/mcp/server/auth/handlers/token.py
For reference, the TypeScript SDK v1.x passes it as an optional argument on both grants (
exchangeRefreshToken(client, refreshToken, scopes?, resource?: URL), provider.ts L51; handler token.ts L109-114 and L129-133). It only forwards the value and leaves the check to the provider.If the handler does validate, I think it should be a subset check, not equality. RFC 8707 section 2.2: "In the case of a "refresh_token" or "authorization_code" grant type request, such policy may limit the acceptable resources to those that were originally granted by the resource owner or a subset thereof."
TokenErrorCodein provider.ts already includes "invalid_target", so a provider could reject with it once it can see the value.