Skip to content

Commit 99b80b5

Browse files
committed
fix: normalize OAuth redirect URI URL subtypes
1 parent 616476f commit 99b80b5

2 files changed

Lines changed: 33 additions & 2 deletions

File tree

src/mcp/shared/auth.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Literal
1+
from typing import Any, Literal, cast
22

33
from pydantic import AnyHttpUrl, AnyUrl, BaseModel, Field, field_validator
44

@@ -85,6 +85,14 @@ def _empty_string_optional_url_to_none(cls, v: object) -> object:
8585
return None
8686
return v
8787

88+
@field_validator("redirect_uris", mode="before")
89+
@classmethod
90+
def _normalize_redirect_uris(cls, v: object) -> object:
91+
if isinstance(v, list | tuple | set | frozenset):
92+
redirect_uris = cast(list[Any] | tuple[Any, ...] | set[Any] | frozenset[Any], v)
93+
return [str(uri) for uri in redirect_uris]
94+
return v
95+
8896
def validate_scope(self, requested_scope: str | None) -> list[str] | None:
8997
if requested_scope is None:
9098
return None

tests/shared/test_auth.py

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

3+
from typing import Any
4+
35
import pytest
4-
from pydantic import ValidationError
6+
from pydantic import AnyHttpUrl, AnyUrl, ValidationError
57

68
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthMetadata
79

@@ -109,6 +111,27 @@ def test_valid_url_passes_through_unchanged():
109111
assert str(metadata.client_uri) == "https://udemy.com/"
110112

111113

114+
@pytest.mark.parametrize(
115+
"redirect_uris",
116+
[
117+
[AnyHttpUrl("https://example.com/callback")],
118+
(AnyHttpUrl("https://example.com/callback"),),
119+
{AnyHttpUrl("https://example.com/callback")},
120+
frozenset({AnyHttpUrl("https://example.com/callback")}),
121+
],
122+
)
123+
def test_redirect_uri_url_subtypes_are_normalized(redirect_uris: Any):
124+
info = OAuthClientInformationFull(
125+
client_id="abc123",
126+
redirect_uris=redirect_uris,
127+
)
128+
129+
incoming = AnyUrl("https://example.com/callback")
130+
131+
assert info.validate_redirect_uri(incoming) == incoming
132+
assert info.model_dump(mode="json")["redirect_uris"] == ["https://example.com/callback"]
133+
134+
112135
def test_information_full_inherits_coercion():
113136
"""OAuthClientInformationFull subclasses OAuthClientMetadata, so the
114137
same coercion applies to DCR responses parsed via the full model."""

0 commit comments

Comments
 (0)