Skip to content

Commit 6aea872

Browse files
committed
Accept additional response_types values from OAuth servers
OAuth servers may return additional response_types beyond what the client requested (e.g., ["code", "none"] instead of just ["code"]). Per RFC 7591 Section 3.2.1, servers can modify registration metadata and return all registered values including server-provisioned fields. For example, Keycloak returns ["code", "none"] even when just ["code"] was requested. - Changed response_types field from list[Literal["code"]] to list[str] - Added validation in registration handler to ensure "code" is present - Added tests for response_types flexibility This fixes compatibility issues with OAuth servers that return additional response_types while maintaining MCP's requirement for the "code" flow.
1 parent 07ae8c0 commit 6aea872

File tree

3 files changed

+70
-2
lines changed

3 files changed

+70
-2
lines changed

src/mcp/server/auth/handlers/register.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,17 @@ async def handle(self, request: Request) -> Response:
7777
status_code=400,
7878
)
7979

80+
# The MCP spec requires servers to use the authorization `code` flow
81+
# with PKCE
82+
if "code" not in client_metadata.response_types:
83+
return PydanticJSONResponse(
84+
content=RegistrationErrorResponse(
85+
error="invalid_client_metadata",
86+
error_description="response_types must include 'code' for authorization_code grant",
87+
),
88+
status_code=400,
89+
)
90+
8091
client_id_issued_at = int(time.time())
8192
client_secret_expires_at = (
8293
client_id_issued_at + self.options.client_secret_expiry_seconds

src/mcp/shared/auth.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,9 @@ class OAuthClientMetadata(BaseModel):
5151
"authorization_code",
5252
"refresh_token",
5353
]
54-
# this implementation only supports code; ie: it does not support implicit grants
55-
response_types: list[Literal["code"]] = ["code"]
54+
# The MCP spec requires the "code" response type, but OAuth
55+
# servers may also return additional types they support
56+
response_types: list[str] = ["code"]
5657
scope: str | None = None
5758

5859
# these fields are currently unused, but we support & store them for potential

tests/server/fastmcp/auth/test_auth_integration.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -942,6 +942,62 @@ async def test_client_registration_invalid_grant_type(self, test_client: httpx.A
942942
assert error_data["error"] == "invalid_client_metadata"
943943
assert error_data["error_description"] == "grant_types must be authorization_code and refresh_token"
944944

945+
@pytest.mark.anyio
946+
async def test_client_registration_with_additional_response_types(
947+
self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider
948+
):
949+
"""Test that registration accepts additional response_types values alongside 'code'."""
950+
client_metadata = {
951+
"redirect_uris": ["https://client.example.com/callback"],
952+
"client_name": "Test Client",
953+
"grant_types": ["authorization_code", "refresh_token"],
954+
"response_types": ["code", "none"], # Keycloak-style response with additional value
955+
}
956+
957+
response = await test_client.post("/register", json=client_metadata)
958+
assert response.status_code == 201
959+
data = response.json()
960+
961+
client = await mock_oauth_provider.get_client(data["client_id"])
962+
assert client is not None
963+
assert "code" in client.response_types
964+
965+
@pytest.mark.anyio
966+
async def test_client_registration_response_types_without_code(self, test_client: httpx.AsyncClient):
967+
"""Test that registration rejects response_types that don't include 'code'."""
968+
client_metadata = {
969+
"redirect_uris": ["https://client.example.com/callback"],
970+
"client_name": "Test Client",
971+
"grant_types": ["authorization_code", "refresh_token"],
972+
"response_types": ["token", "none", "nonsense-string"],
973+
}
974+
975+
response = await test_client.post("/register", json=client_metadata)
976+
assert response.status_code == 400
977+
error_data = response.json()
978+
assert "error" in error_data
979+
assert error_data["error"] == "invalid_client_metadata"
980+
assert "response_types must include 'code'" in error_data["error_description"]
981+
982+
@pytest.mark.anyio
983+
async def test_client_registration_default_response_types(
984+
self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider
985+
):
986+
"""Test that registration uses default response_types of ['code'] when not specified."""
987+
client_metadata = {
988+
"redirect_uris": ["https://client.example.com/callback"],
989+
"client_name": "Test Client",
990+
"grant_types": ["authorization_code", "refresh_token"],
991+
# response_types not specified, should default to ["code"]
992+
}
993+
994+
response = await test_client.post("/register", json=client_metadata)
995+
assert response.status_code == 201
996+
data = response.json()
997+
998+
assert "response_types" in data
999+
assert data["response_types"] == ["code"]
1000+
9451001

9461002
class TestAuthorizeEndpointErrors:
9471003
"""Test error handling in the OAuth authorization endpoint."""

0 commit comments

Comments
 (0)