Skip to content

Commit f43f0b6

Browse files
committed
fix(auth): normalize redirect URI URL subclasses
1 parent ac96f88 commit f43f0b6

2 files changed

Lines changed: 25 additions & 2 deletions

File tree

src/mcp/shared/auth.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,16 @@ class OAuthClientMetadata(BaseModel):
6767
software_id: str | None = None
6868
software_version: str | None = None
6969

70+
@field_validator("redirect_uris", mode="before")
71+
@classmethod
72+
def _coerce_redirect_uris_to_any_url(cls, v: object) -> object:
73+
# Pydantic v2 keeps AnyUrl subclasses such as AnyHttpUrl as-is, while
74+
# AnyUrl equality is type-strict. Store the declared base type so later
75+
# redirect_uri membership checks compare URLs, not URL wrapper classes.
76+
if isinstance(v, list | tuple):
77+
return [str(item) if isinstance(item, AnyUrl) else item for item in v]
78+
return v
79+
7080
@field_validator(
7181
"client_uri",
7282
"logo_uri",

tests/shared/test_auth.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"""Tests for OAuth 2.0 shared code."""
22

33
import pytest
4-
from pydantic import ValidationError
4+
from pydantic import AnyHttpUrl, AnyUrl, ValidationError
55

6-
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthMetadata
6+
from mcp.shared.auth import InvalidRedirectUriError, OAuthClientInformationFull, OAuthClientMetadata, OAuthMetadata
77

88

99
def test_oauth():
@@ -130,6 +130,19 @@ def test_information_full_inherits_coercion():
130130
assert info.jwks_uri is None
131131

132132

133+
def test_redirect_uris_normalize_any_url_subtypes():
134+
info = OAuthClientInformationFull(
135+
client_id="abc123",
136+
redirect_uris=[AnyHttpUrl("https://example.com/callback")],
137+
)
138+
139+
assert info.validate_redirect_uri(AnyUrl("https://example.com/callback")) == AnyUrl("https://example.com/callback")
140+
assert info.model_dump(mode="json")["redirect_uris"] == ["https://example.com/callback"]
141+
142+
with pytest.raises(InvalidRedirectUriError):
143+
info.validate_redirect_uri(AnyUrl("https://example.com/other"))
144+
145+
133146
def test_invalid_non_empty_url_still_rejected():
134147
"""Coercion must only touch empty strings — garbage URLs still raise."""
135148
data = {

0 commit comments

Comments
 (0)