From 48f549b30bd096cfecbd66bf7349f4366aeec9b8 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Thu, 26 Feb 2026 09:56:44 +0530 Subject: [PATCH] Return Clear error message why no tools are available / IP Filtering occured --- .../mcp_server/mcp_server_manager.py | 30 +++++++--- .../mcp_server/rest_endpoints.py | 41 +++++++++++++- .../proxy/_experimental/mcp_server/server.py | 14 ++++- .../proxy/auth/test_mcp_ip_filtering.py | 55 +++++++++++++++++++ 4 files changed, 128 insertions(+), 12 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index 7e90c64efd..7484de33ce 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -756,14 +756,30 @@ class MCPServerManager: Returns server_ids unchanged when client_ip is None (no filtering). """ + filtered, _ = self.filter_server_ids_by_ip_with_info(server_ids, client_ip) + return filtered + + def filter_server_ids_by_ip_with_info( + self, server_ids: List[str], client_ip: Optional[str] + ) -> Tuple[List[str], int]: + """ + Filter server IDs by client IP — external callers only see public servers. + + Returns (filtered_ids, ip_blocked_count) where ip_blocked_count is the number + of servers that were blocked because the client IP is not allowed to access them. + Returns server_ids unchanged (with 0 blocked) when client_ip is None. + """ if client_ip is None: - return server_ids - return [ - sid - for sid in server_ids - if (s := self.get_mcp_server_by_id(sid)) is not None - and self._is_server_accessible_from_ip(s, client_ip) - ] + return server_ids, 0 + allowed = [] + blocked = 0 + for sid in server_ids: + s = self.get_mcp_server_by_id(sid) + if s is not None and self._is_server_accessible_from_ip(s, client_ip): + allowed.append(sid) + elif s is not None: + blocked += 1 + return allowed, blocked async def get_tools_for_server(self, server_id: str) -> List[MCPTool]: """ diff --git a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py index 6e1a252be7..16f8f83543 100644 --- a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py +++ b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py @@ -10,9 +10,9 @@ from litellm.proxy._experimental.mcp_server.ui_session_utils import ( ) from litellm.proxy._experimental.mcp_server.utils import merge_mcp_headers from litellm.proxy._types import UserAPIKeyAuth -from litellm.proxy.common_utils.http_parsing_utils import _safe_get_request_headers from litellm.proxy.auth.ip_address_utils import IPAddressUtils from litellm.proxy.auth.user_api_key_auth import user_api_key_auth +from litellm.proxy.common_utils.http_parsing_utils import _safe_get_request_headers from litellm.types.mcp import MCPAuth from litellm.types.utils import CallTypes @@ -283,8 +283,10 @@ if MCP_AVAILABLE: ) allowed_server_ids_set.update(servers) - allowed_server_ids = global_mcp_server_manager.filter_server_ids_by_ip( - list(allowed_server_ids_set), _rest_client_ip + allowed_server_ids, _ip_blocked_count = ( + global_mcp_server_manager.filter_server_ids_by_ip_with_info( + list(allowed_server_ids_set), _rest_client_ip + ) ) list_tools_result = [] @@ -293,6 +295,26 @@ if MCP_AVAILABLE: # If server_id is specified, only query that specific server if server_id: if server_id not in allowed_server_ids: + _server = global_mcp_server_manager.get_mcp_server_by_id(server_id) + if ( + _server is not None + and _rest_client_ip is not None + and not global_mcp_server_manager._is_server_accessible_from_ip( + _server, _rest_client_ip + ) + ): + raise HTTPException( + status_code=403, + detail={ + "error": "ip_filtering", + "message": ( + f"MCP server '{server_id}' is not accessible from your IP address " + f"({_rest_client_ip}). This server is restricted to internal " + "networks only. To make it externally accessible, set " + "'available_on_public_internet: true' in the server configuration." + ), + }, + ) raise HTTPException( status_code=403, detail={ @@ -330,6 +352,19 @@ if MCP_AVAILABLE: } else: if not allowed_server_ids: + if _ip_blocked_count > 0: + raise HTTPException( + status_code=403, + detail={ + "error": "ip_filtering", + "message": ( + f"No MCP tools are available for your IP address ({_rest_client_ip}). " + f"{_ip_blocked_count} server(s) are restricted to internal networks only. " + "To make servers externally accessible, set " + "'available_on_public_internet: true' in the server configuration." + ), + }, + ) raise HTTPException( status_code=403, detail={ diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py index e8877b4fff..48c837c1e4 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -771,8 +771,8 @@ if MCP_AVAILABLE: user_api_key_auth ) ) - allowed_mcp_server_ids = ( - global_mcp_server_manager.filter_server_ids_by_ip( + allowed_mcp_server_ids, _ip_blocked = ( + global_mcp_server_manager.filter_server_ids_by_ip_with_info( allowed_mcp_server_ids, client_ip ) ) @@ -780,6 +780,16 @@ if MCP_AVAILABLE: "MCP IP filter: client_ip=%s, allowed_server_ids=%s", client_ip, allowed_mcp_server_ids, ) + if _ip_blocked > 0: + verbose_logger.debug( + "MCP IP filtering: %d server(s) are not accessible from client IP %s " + "because they are restricted to internal networks. " + "No tools from those servers will be returned. " + "To expose a server externally, set 'available_on_public_internet: true' " + "in its configuration.", + _ip_blocked, + client_ip, + ) allowed_mcp_servers: List[MCPServer] = [] for allowed_mcp_server_id in allowed_mcp_server_ids: mcp_server = global_mcp_server_manager.get_mcp_server_by_id( diff --git a/tests/test_litellm/proxy/auth/test_mcp_ip_filtering.py b/tests/test_litellm/proxy/auth/test_mcp_ip_filtering.py index 50e51fbe03..3b13ef3641 100644 --- a/tests/test_litellm/proxy/auth/test_mcp_ip_filtering.py +++ b/tests/test_litellm/proxy/auth/test_mcp_ip_filtering.py @@ -91,3 +91,58 @@ class TestMCPServerIPFiltering: result = manager.filter_server_ids_by_ip(["priv"], client_ip=None) assert result == ["priv"] + + +class TestFilterServerIdsByIpWithInfo: + """Tests that filter_server_ids_by_ip_with_info returns accurate block counts.""" + + @patch("litellm.public_mcp_servers", []) + @patch("litellm.proxy.proxy_server.general_settings", {}) + def test_external_ip_reports_blocked_count(self): + pub = _make_server("pub", available_on_public_internet=True) + priv = _make_server("priv", available_on_public_internet=False) + manager = _make_manager([pub, priv]) + + allowed, blocked = manager.filter_server_ids_by_ip_with_info( + ["pub", "priv"], client_ip="8.8.8.8" + ) + assert allowed == ["pub"] + assert blocked == 1 + + @patch("litellm.public_mcp_servers", []) + @patch("litellm.proxy.proxy_server.general_settings", {}) + def test_internal_ip_reports_zero_blocked(self): + pub = _make_server("pub", available_on_public_internet=True) + priv = _make_server("priv", available_on_public_internet=False) + manager = _make_manager([pub, priv]) + + allowed, blocked = manager.filter_server_ids_by_ip_with_info( + ["pub", "priv"], client_ip="192.168.1.1" + ) + assert allowed == ["pub", "priv"] + assert blocked == 0 + + @patch("litellm.public_mcp_servers", []) + @patch("litellm.proxy.proxy_server.general_settings", {}) + def test_no_ip_returns_all_with_zero_blocked(self): + priv = _make_server("priv", available_on_public_internet=False) + manager = _make_manager([priv]) + + allowed, blocked = manager.filter_server_ids_by_ip_with_info( + ["priv"], client_ip=None + ) + assert allowed == ["priv"] + assert blocked == 0 + + @patch("litellm.public_mcp_servers", []) + @patch("litellm.proxy.proxy_server.general_settings", {}) + def test_all_private_external_ip_reports_all_blocked(self): + priv1 = _make_server("priv1", available_on_public_internet=False) + priv2 = _make_server("priv2", available_on_public_internet=False) + manager = _make_manager([priv1, priv2]) + + allowed, blocked = manager.filter_server_ids_by_ip_with_info( + ["priv1", "priv2"], client_ip="1.2.3.4" + ) + assert allowed == [] + assert blocked == 2