From e09658f69115ddc228c7eeaa115f46e5a2c4a075 Mon Sep 17 00:00:00 2001 From: debaditya Date: Thu, 13 Aug 2026 02:32:39 +0530 Subject: [PATCH 1/2] fix(auth): normalize default resource URL ports --- src/mcp/shared/auth_utils.py | 19 ++++++++++++++++++- tests/shared/test_auth_utils.py | 24 ++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/mcp/shared/auth_utils.py b/src/mcp/shared/auth_utils.py index 3ba880f40d..2c5098d053 100644 --- a/src/mcp/shared/auth_utils.py +++ b/src/mcp/shared/auth_utils.py @@ -23,7 +23,24 @@ def resource_url_from_server_url(url: str | HttpUrl | AnyUrl) -> str: # Parse the URL and remove fragment, create canonical form parsed = urlsplit(url_str) - canonical = urlunsplit(parsed._replace(scheme=parsed.scheme.lower(), netloc=parsed.netloc.lower(), fragment="")) + scheme = parsed.scheme.lower() + netloc = parsed.netloc.lower() + default_port = {"http": 80, "https": 443}.get(scheme) + + try: + port = parsed.port + except ValueError: + port = None + + if port == default_port: + hostname = parsed.hostname + if hostname is not None: + if parsed.netloc.rsplit("@", 1)[-1].startswith("["): + hostname = f"[{hostname}]" + userinfo, separator, _ = netloc.rpartition("@") + netloc = userinfo + separator + hostname + + canonical = urlunsplit(parsed._replace(scheme=scheme, netloc=netloc, fragment="")) return canonical diff --git a/tests/shared/test_auth_utils.py b/tests/shared/test_auth_utils.py index 5ae0e22b0c..b6a14c2b58 100644 --- a/tests/shared/test_auth_utils.py +++ b/tests/shared/test_auth_utils.py @@ -28,10 +28,28 @@ def test_resource_url_from_server_url_preserves_query(): assert resource_url_from_server_url("https://example.com/?key=value") == "https://example.com/?key=value" +def test_resource_url_from_server_url_removes_default_ports(): + """Explicit HTTP and HTTPS default ports should be removed from canonical resource URLs.""" + assert resource_url_from_server_url("http://example.com:80/mcp") == "http://example.com/mcp" + assert resource_url_from_server_url("https://example.com:443/mcp") == "https://example.com/mcp" + + def test_resource_url_from_server_url_preserves_port(): """Non-default ports should be preserved.""" assert resource_url_from_server_url("https://example.com:8443/path") == "https://example.com:8443/path" assert resource_url_from_server_url("http://example.com:8080/") == "http://example.com:8080/" + assert resource_url_from_server_url("ftp://example.com:443/path") == "ftp://example.com:443/path" + + +def test_resource_url_from_server_url_removes_default_port_from_ipv6_literal(): + """Default-port removal should preserve bracketed IPv6 authority syntax.""" + assert resource_url_from_server_url("https://[2001:DB8::1]:443/mcp") == "https://[2001:db8::1]/mcp" + + +def test_resource_url_from_server_url_preserves_malformed_port(): + """Malformed ports should retain the existing canonicalization behavior.""" + assert resource_url_from_server_url("https://example.com:abc/mcp") == "https://example.com:abc/mcp" + assert resource_url_from_server_url("https://:443/mcp") == "https://:443/mcp" def test_resource_url_from_server_url_lowercase_scheme_and_host(): @@ -121,3 +139,9 @@ def test_check_resource_allowed_empty_paths(): assert check_resource_allowed("https://example.com", "https://example.com") is True assert check_resource_allowed("https://example.com/", "https://example.com") is True assert check_resource_allowed("https://example.com/api", "https://example.com") is True + + +def test_check_resource_allowed_accepts_canonicalized_default_port(): + """Canonicalized explicit default ports should match equivalent metadata URLs.""" + canonical = resource_url_from_server_url("https://example.com:443/mcp") + assert check_resource_allowed(canonical, "https://example.com/mcp") is True From a9330b0b9a818e04c82383ebf858613011a73672 Mon Sep 17 00:00:00 2001 From: debaditya Date: Thu, 13 Aug 2026 03:07:15 +0530 Subject: [PATCH 2/2] fix(auth): preserve unrelated URL authorities --- src/mcp/shared/auth_utils.py | 2 +- tests/shared/test_auth_utils.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mcp/shared/auth_utils.py b/src/mcp/shared/auth_utils.py index 2c5098d053..3cec219204 100644 --- a/src/mcp/shared/auth_utils.py +++ b/src/mcp/shared/auth_utils.py @@ -32,7 +32,7 @@ def resource_url_from_server_url(url: str | HttpUrl | AnyUrl) -> str: except ValueError: port = None - if port == default_port: + if default_port is not None and port == default_port: hostname = parsed.hostname if hostname is not None: if parsed.netloc.rsplit("@", 1)[-1].startswith("["): diff --git a/tests/shared/test_auth_utils.py b/tests/shared/test_auth_utils.py index b6a14c2b58..4209151abd 100644 --- a/tests/shared/test_auth_utils.py +++ b/tests/shared/test_auth_utils.py @@ -50,6 +50,8 @@ def test_resource_url_from_server_url_preserves_malformed_port(): """Malformed ports should retain the existing canonicalization behavior.""" assert resource_url_from_server_url("https://example.com:abc/mcp") == "https://example.com:abc/mcp" assert resource_url_from_server_url("https://:443/mcp") == "https://:443/mcp" + assert resource_url_from_server_url("ftp://example.com:abc/mcp") == "ftp://example.com:abc/mcp" + assert resource_url_from_server_url("ftp://example.com:/mcp") == "ftp://example.com:/mcp" def test_resource_url_from_server_url_lowercase_scheme_and_host():