Skip to content

Commit e09658f

Browse files
committed
fix(auth): normalize default resource URL ports
1 parent 6e30452 commit e09658f

2 files changed

Lines changed: 42 additions & 1 deletion

File tree

src/mcp/shared/auth_utils.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,24 @@ def resource_url_from_server_url(url: str | HttpUrl | AnyUrl) -> str:
2323

2424
# Parse the URL and remove fragment, create canonical form
2525
parsed = urlsplit(url_str)
26-
canonical = urlunsplit(parsed._replace(scheme=parsed.scheme.lower(), netloc=parsed.netloc.lower(), fragment=""))
26+
scheme = parsed.scheme.lower()
27+
netloc = parsed.netloc.lower()
28+
default_port = {"http": 80, "https": 443}.get(scheme)
29+
30+
try:
31+
port = parsed.port
32+
except ValueError:
33+
port = None
34+
35+
if port == default_port:
36+
hostname = parsed.hostname
37+
if hostname is not None:
38+
if parsed.netloc.rsplit("@", 1)[-1].startswith("["):
39+
hostname = f"[{hostname}]"
40+
userinfo, separator, _ = netloc.rpartition("@")
41+
netloc = userinfo + separator + hostname
42+
43+
canonical = urlunsplit(parsed._replace(scheme=scheme, netloc=netloc, fragment=""))
2744

2845
return canonical
2946

tests/shared/test_auth_utils.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,28 @@ def test_resource_url_from_server_url_preserves_query():
2828
assert resource_url_from_server_url("https://example.com/?key=value") == "https://example.com/?key=value"
2929

3030

31+
def test_resource_url_from_server_url_removes_default_ports():
32+
"""Explicit HTTP and HTTPS default ports should be removed from canonical resource URLs."""
33+
assert resource_url_from_server_url("http://example.com:80/mcp") == "http://example.com/mcp"
34+
assert resource_url_from_server_url("https://example.com:443/mcp") == "https://example.com/mcp"
35+
36+
3137
def test_resource_url_from_server_url_preserves_port():
3238
"""Non-default ports should be preserved."""
3339
assert resource_url_from_server_url("https://example.com:8443/path") == "https://example.com:8443/path"
3440
assert resource_url_from_server_url("http://example.com:8080/") == "http://example.com:8080/"
41+
assert resource_url_from_server_url("ftp://example.com:443/path") == "ftp://example.com:443/path"
42+
43+
44+
def test_resource_url_from_server_url_removes_default_port_from_ipv6_literal():
45+
"""Default-port removal should preserve bracketed IPv6 authority syntax."""
46+
assert resource_url_from_server_url("https://[2001:DB8::1]:443/mcp") == "https://[2001:db8::1]/mcp"
47+
48+
49+
def test_resource_url_from_server_url_preserves_malformed_port():
50+
"""Malformed ports should retain the existing canonicalization behavior."""
51+
assert resource_url_from_server_url("https://example.com:abc/mcp") == "https://example.com:abc/mcp"
52+
assert resource_url_from_server_url("https://:443/mcp") == "https://:443/mcp"
3553

3654

3755
def test_resource_url_from_server_url_lowercase_scheme_and_host():
@@ -121,3 +139,9 @@ def test_check_resource_allowed_empty_paths():
121139
assert check_resource_allowed("https://example.com", "https://example.com") is True
122140
assert check_resource_allowed("https://example.com/", "https://example.com") is True
123141
assert check_resource_allowed("https://example.com/api", "https://example.com") is True
142+
143+
144+
def test_check_resource_allowed_accepts_canonicalized_default_port():
145+
"""Canonicalized explicit default ports should match equivalent metadata URLs."""
146+
canonical = resource_url_from_server_url("https://example.com:443/mcp")
147+
assert check_resource_allowed(canonical, "https://example.com/mcp") is True

0 commit comments

Comments
 (0)