From ffea4aeaa3163265ff165c71dfb818c312a3fa57 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Mon, 1 Jun 2026 07:48:14 +0800 Subject: [PATCH] fix: normalize OAuth redirect URI URL subtypes --- src/mcp/shared/auth.py | 10 +++++++++- tests/shared/test_auth.py | 14 +++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/mcp/shared/auth.py b/src/mcp/shared/auth.py index 3b48152d5b..b0e2a6a91a 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 @@ -85,6 +85,14 @@ def _empty_string_optional_url_to_none(cls, v: object) -> object: return None return v + @field_validator("redirect_uris", mode="before") + @classmethod + def _normalize_redirect_uris(cls, v: object) -> object: + if isinstance(v, list): + redirect_uris = cast(list[Any], v) + return [str(uri) for uri in redirect_uris] + return v + def validate_scope(self, requested_scope: str | None) -> list[str] | None: if requested_scope is None: return None diff --git a/tests/shared/test_auth.py b/tests/shared/test_auth.py index 7463bc5a8a..2fb3f0e974 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 @@ -109,6 +109,18 @@ def test_valid_url_passes_through_unchanged(): assert str(metadata.client_uri) == "https://udemy.com/" +def test_redirect_uri_url_subtypes_are_normalized(): + 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.model_dump(mode="json")["redirect_uris"] == ["https://example.com/callback"] + + def test_information_full_inherits_coercion(): """OAuthClientInformationFull subclasses OAuthClientMetadata, so the same coercion applies to DCR responses parsed via the full model."""