diff --git a/src/mcp/shared/auth.py b/src/mcp/shared/auth.py index bf03a8b8d..eba4ca0e3 100644 --- a/src/mcp/shared/auth.py +++ b/src/mcp/shared/auth.py @@ -68,6 +68,13 @@ class OAuthClientMetadata(BaseModel): software_id: str | None = None software_version: str | None = None + @field_validator("client_uri", "logo_uri", "tos_uri", "policy_uri", "jwks_uri", mode="before") + @classmethod + def normalize_empty_optional_uris(cls, v: Any) -> Any: + if v == "": + return None + return v + def validate_scope(self, requested_scope: str | None) -> list[str] | None: if requested_scope is None: return None diff --git a/tests/client/test_auth.py b/tests/client/test_auth.py index 5aa985e36..c85df032b 100644 --- a/tests/client/test_auth.py +++ b/tests/client/test_auth.py @@ -83,6 +83,25 @@ def valid_tokens(): ) +def test_oauth_client_metadata_treats_empty_optional_uris_as_none(): + metadata = OAuthClientMetadata.model_validate( + { + "redirect_uris": ["http://localhost:3030/callback"], + "client_uri": "", + "logo_uri": "", + "tos_uri": "", + "policy_uri": "", + "jwks_uri": "", + } + ) + + assert metadata.client_uri is None + assert metadata.logo_uri is None + assert metadata.tos_uri is None + assert metadata.policy_uri is None + assert metadata.jwks_uri is None + + @pytest.fixture def oauth_provider(client_metadata: OAuthClientMetadata, mock_storage: MockTokenStorage): async def redirect_handler(url: str) -> None: