-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Description
Description
Summary
The Dynamic Client Registration (DCR) handler is rejecting valid RFC 7591-compliant registration requests that only specify authorization_code in the grant_types field. This is overly restrictive and breaks compatibility with OAuth clients that follow the RFC 7591 specification.
Location
File: mcp/server/auth/handlers/register.py
Line: 71-78
Current Behavior
The registration handler validates that both authorization_code and refresh_token grant types must be present:
if not {"authorization_code", "refresh_token"}.issubset(set(client_metadata.grant_types)):
return PydanticJSONResponse(
content=RegistrationErrorResponse(
error="invalid_client_metadata",
error_description="grant_types must be authorization_code and refresh_token",
),
status_code=400,
)Result: HTTP 400 Bad Request with error:
{
"error": "invalid_client_metadata",
"error_description": "grant_types must be authorization_code and refresh_token"
}Expected Behavior
Per RFC 7591 (OAuth 2.0 Dynamic Client Registration Protocol), the grant_types field should accept any valid OAuth 2.0 grant type, and refresh_token is optional, not required.
The handler should accept registration requests with only authorization_code in grant_types:
{
"client_name": "MCP Compliance Tester",
"redirect_uris": ["http://localhost:3000/callback"],
"grant_types": ["authorization_code"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}Expected Result: HTTP 201 Created with client credentials
RFC 7591 Compliance
From RFC 7591 Section 2:
grant_types: Array of OAuth 2.0 grant type strings that the client can use at the token endpoint. [...] If omitted, the default is that the client will use only the "authorization_code" Grant Type.
The specification does not mandate that refresh_token must be included. In fact, it's listed as a separate, optional grant type.
Impact
This restrictive validation:
- Breaks RFC 7591 compliance - Rejects valid registration requests
- Fails MCP OAuth compliance tests - Specifically test DCR-3.2 (HTTP 201 Registration Response)
- Limits interoperability - Clients that don't want refresh token support cannot register
- Forces unnecessary capabilities - Not all OAuth flows require refresh tokens
Reproduction
Test Request
curl -X POST http://localhost:3010/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "Test Client",
"redirect_uris": ["http://localhost:3000/callback"],
"grant_types": ["authorization_code"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}'Actual Response
{
"error": "invalid_client_metadata",
"error_description": "grant_types must be authorization_code and refresh_token"
}Status: 400 Bad Request
Expected Response
{
"client_id": "550e8400-e29b-41d4-a716-446655440000",
"client_id_issued_at": 1234567890,
"redirect_uris": ["http://localhost:3000/callback"],
"grant_types": ["authorization_code"],
"response_types": ["code"],
"token_endpoint_auth_method": "none",
"client_name": "Test Client"
}Status: 201 Created
Proposed Fix
Change the validation at line 71 to only require authorization_code:
# Before (overly restrictive):
if not {"authorization_code", "refresh_token"}.issubset(set(client_metadata.grant_types)):
return PydanticJSONResponse(
content=RegistrationErrorResponse(
error="invalid_client_metadata",
error_description="grant_types must be authorization_code and refresh_token",
),
status_code=400,
)
# After (RFC 7591 compliant):
if "authorization_code" not in client_metadata.grant_types:
return PydanticJSONResponse(
content=RegistrationErrorResponse(
error="invalid_client_metadata",
error_description="grant_types must include 'authorization_code'",
),
status_code=400,
)This change:
- ✅ Accepts
["authorization_code"](RFC compliant) - ✅ Accepts
["authorization_code", "refresh_token"](still works) - ✅ Rejects requests without
authorization_code(maintains MCP requirement) - ✅ Allows clients to opt-out of refresh tokens if not needed
Functional Impact Analysis
I've analyzed the downstream dependencies and confirmed:
✅ No Breaking Changes to Core Functionality
The rest of the MCP SDK and FastMCP OAuthProxy already handle optional refresh tokens correctly:
-
Token Exchange (
exchange_authorization_code):- Only issues refresh tokens if upstream provider returns one
- Gracefully returns
refresh_token: nullif not available
-
Token Storage:
UpstreamTokenSet.refresh_tokenis already typed asstr | None- All conditional checks use
if idp_tokens.get("refresh_token")
-
Token Refresh Flow:
exchange_refresh_token()method only called if client has refresh token- Returns proper error if refresh not supported:
"Refresh not supported for this token"
-
Client Behavior:
- Clients without refresh tokens simply re-authenticate when access token expires
- This is standard OAuth 2.0 behavior and doesn't break functionality
📊 What Works vs What Doesn't
Works with grant_types: ["authorization_code"] only:
- ✅ Client registration
- ✅ Authorization flow
- ✅ Token exchange (returns access token)
- ✅ Access token validation
- ✅ Token revocation
- ✅ Re-authentication when access token expires
Doesn't work (expected):
- ❌ Token refresh endpoint (client must re-authorize instead)
This is standard OAuth 2.0 behavior - not all clients need refresh tokens.
Additional Context
- MCP Specification: Requires authorization code flow with PKCE (satisfied by requiring
authorization_code) - Refresh Tokens: Should be optional, not mandatory
- Backward Compatibility: This fix is backward compatible - clients currently sending both grant types will continue to work
- Downstream Code: FastMCP OAuthProxy already implements optional refresh token support correctly
Environment
- MCP SDK: Latest version (as of 2025-11-20)
- Python: 3.12.8
- OAuth Provider: FastMCP OAuthProxy pattern
- Test Framework: MCP OAuth Compliance Tester
References
- RFC 7591 - OAuth 2.0 Dynamic Client Registration Protocol
- RFC 7591 Section 2 - Client Metadata
- MCP Specification - OAuth Requirements
Example Code
Version Information
FastMCP version: 2.13.1
MCP version: 1.21.0
Python version: 3.12.8
Platform: Windows-11-10.0.26100-SP0