Skip to content

Commit 47ec422

Browse files
committed
Preserve userinfo case in resource URLs
1 parent e942d00 commit 47ec422

2 files changed

Lines changed: 28 additions & 1 deletion

File tree

src/mcp/shared/auth_utils.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@
66
from pydantic import AnyUrl, HttpUrl
77

88

9+
def _lowercase_host(netloc: str) -> str:
10+
userinfo, separator, hostport = netloc.rpartition("@")
11+
prefix = f"{userinfo}{separator}" if separator else ""
12+
13+
if hostport.startswith("["):
14+
end = hostport.find("]")
15+
if end != -1:
16+
return f"{prefix}{hostport[: end + 1].lower()}{hostport[end + 1 :]}"
17+
18+
host, separator, port = hostport.partition(":")
19+
return f"{prefix}{host.lower()}{separator}{port}"
20+
21+
922
def resource_url_from_server_url(url: str | HttpUrl | AnyUrl) -> str:
1023
"""Convert server URL to canonical resource URL per RFC 8707.
1124
@@ -23,7 +36,13 @@ def resource_url_from_server_url(url: str | HttpUrl | AnyUrl) -> str:
2336

2437
# Parse the URL and remove fragment, create canonical form
2538
parsed = urlsplit(url_str)
26-
canonical = urlunsplit(parsed._replace(scheme=parsed.scheme.lower(), netloc=parsed.netloc.lower(), fragment=""))
39+
canonical = urlunsplit(
40+
parsed._replace(
41+
scheme=parsed.scheme.lower(),
42+
netloc=_lowercase_host(parsed.netloc),
43+
fragment="",
44+
)
45+
)
2746

2847
return canonical
2948

tests/shared/test_auth_utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ def test_resource_url_from_server_url_lowercase_scheme_and_host():
4040
assert resource_url_from_server_url("Http://Example.Com:8080/") == "http://example.com:8080/"
4141

4242

43+
def test_resource_url_from_server_url_preserves_userinfo_case():
44+
"""Only the scheme and host are canonicalized; userinfo is preserved byte-for-byte."""
45+
assert (
46+
resource_url_from_server_url("HTTPS://User:PaSs@EXAMPLE.COM/path#fragment")
47+
== "https://User:PaSs@example.com/path"
48+
)
49+
50+
4351
def test_resource_url_from_server_url_handles_pydantic_urls():
4452
"""Should handle Pydantic URL types."""
4553
url = HttpUrl("https://example.com/path")

0 commit comments

Comments
 (0)