diff --git a/src/mcp/shared/auth.py b/src/mcp/shared/auth.py index 4fabb1a89..0d473afc9 100644 --- a/src/mcp/shared/auth.py +++ b/src/mcp/shared/auth.py @@ -1,4 +1,4 @@ -from typing import Any, Literal +from typing import Any, Literal, cast from pydantic import AnyHttpUrl, AnyUrl, BaseModel, ConfigDict, Field, field_validator @@ -84,6 +84,14 @@ class OAuthClientMetadata(BaseModel): software_id: str | None = None software_version: str | None = None + @field_validator("redirect_uris", mode="before") + @classmethod + def _normalize_redirect_uri_subtypes(cls, v: object) -> object: + if isinstance(v, list): + # Pydantic URL equality is type-strict, so store redirect URIs as the declared AnyUrl type. + return [str(item) if isinstance(item, AnyUrl) else item for item in cast(list[object], v)] + return v + @field_validator( "client_uri", "logo_uri", diff --git a/tests/shared/test_auth.py b/tests/shared/test_auth.py index 7463bc5a8..814b6e81e 100644 --- a/tests/shared/test_auth.py +++ b/tests/shared/test_auth.py @@ -1,9 +1,14 @@ """Tests for OAuth 2.0 shared code.""" import pytest -from pydantic import ValidationError +from pydantic import AnyHttpUrl, AnyUrl, ValidationError -from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthMetadata +from mcp.shared.auth import ( + InvalidRedirectUriError, + OAuthClientInformationFull, + OAuthClientMetadata, + OAuthMetadata, +) def test_oauth(): @@ -138,3 +143,18 @@ def test_invalid_non_empty_url_still_rejected(): } with pytest.raises(ValidationError): OAuthClientMetadata.model_validate(data) + + +def test_redirect_uri_subtypes_normalized_for_validation(): + client_info = OAuthClientInformationFull( + client_id="test-client", + redirect_uris=[AnyHttpUrl("https://example.com/callback")], + ) + + incoming = AnyUrl("https://example.com/callback") + + assert client_info.validate_redirect_uri(incoming) == incoming + assert client_info.model_dump(mode="json")["redirect_uris"] == ["https://example.com/callback"] + + with pytest.raises(InvalidRedirectUriError): + client_info.validate_redirect_uri(AnyUrl("https://example.com/other"))