diff --git a/litellm/proxy/_experimental/mcp_server/mcp_debug.py b/litellm/proxy/_experimental/mcp_server/mcp_debug.py index 254f208e231..e4bbe536c07 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_debug.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_debug.py @@ -293,6 +293,7 @@ class MCPDebug: MCPRequestHandler, ) from litellm.proxy._experimental.mcp_server.mcp_server_manager import ( + INTERNAL_REQUEST, global_mcp_server_manager, ) @@ -300,9 +301,14 @@ class MCPDebug: server_auth_type: Optional[str] = None auth_resolution = "no-auth" + # Debug-header generation is passive observability — access control was + # already enforced upstream when the tool list was fetched. If the + # caller couldn't extract a client IP, don't drop the debug metadata; + # bypass the IP gate explicitly. + gate_arg = client_ip if client_ip is not None else INTERNAL_REQUEST for server_name in mcp_servers or []: server = global_mcp_server_manager.get_mcp_server_by_name( - server_name, client_ip=client_ip + server_name, client_ip=gate_arg ) if server: server_url = server.url diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py index b032bc4de26..009b503976d 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -885,15 +885,28 @@ if MCP_AVAILABLE: 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, - ) + if client_ip is None: + # IP extraction failed (no X-Forwarded-* header, missing + # trusted-proxy config, etc.). Fail-closed at the gate + # silently dropped the servers — tell the operator to fix + # IP forwarding, NOT to expose the server publicly. + verbose_logger.debug( + "MCP IP filtering: %d server(s) hidden because client IP " + "could not be determined for this request. Fix request-IP " + "extraction (X-Forwarded-For + trusted_proxies) so the " + "gate can evaluate access.", + _ip_blocked, + ) + else: + 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/litellm/proxy/management_endpoints/mcp_management_endpoints.py b/litellm/proxy/management_endpoints/mcp_management_endpoints.py index 7bda0f87ccd..abce1597bab 100644 --- a/litellm/proxy/management_endpoints/mcp_management_endpoints.py +++ b/litellm/proxy/management_endpoints/mcp_management_endpoints.py @@ -1557,9 +1557,18 @@ if MCP_AVAILABLE: if server is None: # Fall back to real DB/config server (e.g. for the user-side OAuth flow # which calls these endpoints with a real server_id, not a temp session id). + from litellm.proxy._experimental.mcp_server.mcp_server_manager import ( + INTERNAL_REQUEST, + ) from litellm.proxy.auth.ip_address_utils import IPAddressUtils - client_ip = IPAddressUtils.get_mcp_client_ip(request) if request else None + # Programmatic callers (no Request) bypass the IP gate explicitly. + # Real HTTP callers go through the normal extraction path. + client_ip = ( + IPAddressUtils.get_mcp_client_ip(request) + if request + else INTERNAL_REQUEST + ) server = global_mcp_server_manager.get_mcp_server_by_id( server_id ) or global_mcp_server_manager.get_mcp_server_by_name( diff --git a/litellm/responses/mcp/litellm_proxy_mcp_handler.py b/litellm/responses/mcp/litellm_proxy_mcp_handler.py index 94cff6922b5..2e6a0668517 100644 --- a/litellm/responses/mcp/litellm_proxy_mcp_handler.py +++ b/litellm/responses/mcp/litellm_proxy_mcp_handler.py @@ -191,6 +191,7 @@ class LiteLLM_Proxy_MCP_Handler: List names of allowed MCP servers """ from litellm.proxy._experimental.mcp_server.mcp_server_manager import ( + INTERNAL_REQUEST, global_mcp_server_manager, ) from litellm.proxy._experimental.mcp_server.server import ( @@ -216,7 +217,13 @@ class LiteLLM_Proxy_MCP_Handler: resolved_mcp_servers: List[str] = [] resolved_toolset_ids: List[str] = [] for name in mcp_servers: - if not global_mcp_server_manager.get_mcp_server_by_name(name): + # Server-side disambiguation between MCP server names and toolset + # names. Access control is enforced downstream by + # _get_allowed_mcp_servers_from_mcp_server_names; this lookup is + # purely "is `name` a known server?", so bypass the IP gate. + if not global_mcp_server_manager.get_mcp_server_by_name( + name, client_ip=INTERNAL_REQUEST + ): try: from litellm.proxy.proxy_server import prisma_client