From fc018a9fcace6d60d94c7aae0d83d92aaf25e048 Mon Sep 17 00:00:00 2001 From: user <70670632+stuxf@users.noreply.github.com> Date: Sun, 10 May 2026 02:18:07 +0000 Subject: [PATCH] chore(mcp): address mypy + Greptile feedback on PR 27575 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cast client_ip to str at the bottom of _is_server_accessible_from_ip so mypy sees the narrowing the early returns already perform. The runtime shape is unchanged — INTERNAL_REQUEST and None are both handled in the early branches above. - Distinguish "client_ip_unknown" from "ip_filtering" in the rest_endpoints 403 response. When IP extraction fails on a public server the previous error told the operator to set available_on_public_internet=True, which was already set; the new message points them at use_x_forwarded_for / mcp_trusted_proxy_ranges. - Preserve the explicit port in the Host header when litellm.user_url_validation is False. The opt-out path previously dropped the port, sending a Host that didn't match the connection target for non-default-port URLs. --- .../mcp_server/discoverable_endpoints.py | 5 +++- .../mcp_server/mcp_server_manager.py | 6 +++-- .../mcp_server/rest_endpoints.py | 26 +++++++++++++++++++ .../mcp_server/test_discoverable_endpoints.py | 9 ++++++- 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py b/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py index 1fd186c50d0..bcbaea54457 100644 --- a/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py +++ b/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py @@ -39,7 +39,10 @@ def _validate_mcp_oauth_outbound_url( operator who points an MCP server at an internal IdP doesn't unintentionally expose it as an SSRF probe via the proxy.""" if not getattr(litellm, "user_url_validation", True): - return url, urlparse(url).hostname or "" + parsed = urlparse(url) + host = parsed.hostname or "" + host_header = f"{host}:{parsed.port}" if parsed.port else host + return url, host_header try: return validate_url(url) except SSRFError as exc: diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index 2cdd2ca2b94..1d45fbefa8f 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -3109,12 +3109,14 @@ class MCPServerManager: public_ids = set(litellm.public_mcp_servers or []) if server.server_id in public_ids: return True - # Non-public server: only accessible from internal IPs + # Non-public server: only accessible from internal IPs. The two + # early returns above narrow client_ip to ``str`` for the type + # checker; cast keeps mypy happy without a runtime assert. general_settings = self._get_general_settings() internal_networks = IPAddressUtils.parse_internal_networks( general_settings.get("mcp_internal_ip_ranges") ) - return IPAddressUtils.is_internal_ip(client_ip, internal_networks) + return IPAddressUtils.is_internal_ip(cast(str, client_ip), internal_networks) def get_mcp_server_by_id(self, server_id: str) -> Optional[MCPServer]: """ diff --git a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py index 759e4924c44..7327f72bf40 100644 --- a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py +++ b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py @@ -397,6 +397,19 @@ if MCP_AVAILABLE: _server, rest_client_ip ) ): + if rest_client_ip is None: + raise HTTPException( + status_code=403, + detail={ + "error": "client_ip_unknown", + "message": ( + "Cannot determine client IP for IP-based access " + "control. If the proxy is behind a load balancer " + "or reverse proxy, configure use_x_forwarded_for " + "and mcp_trusted_proxy_ranges in general_settings." + ), + }, + ) raise HTTPException( status_code=403, detail={ @@ -486,6 +499,19 @@ if MCP_AVAILABLE: _server, rest_client_ip ) ): + if rest_client_ip is None: + raise HTTPException( + status_code=403, + detail={ + "error": "client_ip_unknown", + "message": ( + "Cannot determine client IP for IP-based access " + "control. If the proxy is behind a load balancer " + "or reverse proxy, configure use_x_forwarded_for " + "and mcp_trusted_proxy_ranges in general_settings." + ), + }, + ) raise HTTPException( status_code=403, detail={ diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py index 53922d58966..927e7caf7da 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py @@ -2530,7 +2530,14 @@ class TestOutboundOAuthURLValidation: "http://10.0.0.1:5000/token", role="token" ) assert url == "http://10.0.0.1:5000/token" - assert host == "10.0.0.1" + # Host header includes the explicit port from the URL. + assert host == "10.0.0.1:5000" + + # When the URL omits the port, the Host header omits it too. + url2, host2 = _validate_mcp_oauth_outbound_url( + "http://10.0.0.1/token", role="token" + ) + assert host2 == "10.0.0.1" finally: litellm.user_url_validation = original