mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-23 00:41:40 +00:00
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:
parent
1332eb4fc3
commit
94797e0566
4 changed files with 47 additions and 12 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue