fix(mcp): plumb INTERNAL_REQUEST to off-diff callers + clarify _ip_blocked log

After fail-closing the IP gate on missing client_ip, three off-diff
callers of get_mcp_server_by_name still defaulted to None and would
silently return no server:

- responses/mcp/litellm_proxy_mcp_handler.py: server-side name vs
  toolset disambiguation; access control is enforced downstream by
  _get_allowed_mcp_servers_from_mcp_server_names. Pass INTERNAL_REQUEST.
- _experimental/mcp_server/mcp_debug.py: passive debug-header builder
  called after upstream access checks. If the upstream request handler
  couldn't extract a client IP, fall back to INTERNAL_REQUEST so debug
  metadata isn't silently dropped.
- management_endpoints/mcp_management_endpoints.py: programmatic
  callers of _get_cached_temporary_mcp_server_or_404 (request=None)
  bypass the gate explicitly; real HTTP callers still get the
  extracted IP.

Also disambiguate the _ip_blocked debug log so operators see the
actionable message: "fix request-IP extraction" when client_ip is
None vs. "set available_on_public_internet: true" when a real IP is
blocked. The previous combined message pointed operators toward
exposing internal servers when the real cause was IP-extraction
failure.
This commit is contained in:
user 2026-05-10 06:34:26 +00:00
parent 1332eb4fc3
commit 94797e0566
No known key found for this signature in database
4 changed files with 47 additions and 12 deletions

View file

@ -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

View file

@ -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(

View file

@ -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(

View file

@ -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