From ac16a584762c07e2e44ff961a101a70a786bb663 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Wed, 27 May 2026 19:00:07 +0800 Subject: [PATCH] fix: normalize oauth redirect uri url types --- src/mcp/shared/auth.py | 13 ++++++++++++- tests/shared/test_auth.py | 14 +++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/mcp/shared/auth.py b/src/mcp/shared/auth.py index ebf534d792..a43b293e21 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, Field, field_validator @@ -67,6 +67,17 @@ class OAuthClientMetadata(BaseModel): software_id: str | None = None software_version: str | None = None + @field_validator( + "redirect_uris", + mode="before", + ) + @classmethod + def _normalize_redirect_uri_types(cls, v: object) -> object: + if isinstance(v, list): + uris = cast(list[Any], v) + return [str(uri) if isinstance(uri, AnyUrl) else uri for uri in uris] + return v + @field_validator( "client_uri", "logo_uri", diff --git a/tests/shared/test_auth.py b/tests/shared/test_auth.py index 7463bc5a8a..2e6dd6cc08 100644 --- a/tests/shared/test_auth.py +++ b/tests/shared/test_auth.py @@ -1,7 +1,7 @@ """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 @@ -130,6 +130,18 @@ def test_information_full_inherits_coercion(): assert info.jwks_uri is None +def test_redirect_uri_url_subtypes_validate_by_canonical_url(): + info = OAuthClientInformationFull( + client_id="abc123", + redirect_uris=[AnyHttpUrl("https://example.com/callback")], + ) + incoming = AnyUrl("https://example.com/callback") + + assert info.validate_redirect_uri(incoming) == incoming + assert info.redirect_uris == [incoming] + assert info.model_dump(mode="json")["redirect_uris"] == ["https://example.com/callback"] + + def test_invalid_non_empty_url_still_rejected(): """Coercion must only touch empty strings — garbage URLs still raise.""" data = {