Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2640df0
update MCPAuthenticatedUser
ishaan-jaff Feb 6, 2026
5eee751
add available_on_public_internet for MCPs
ishaan-jaff Feb 6, 2026
95284dd
update claude.md
ishaan-jaff Feb 6, 2026
3c61dd9
init IPAddressUtils
ishaan-jaff Feb 6, 2026
2904ef5
init available_on_public_internet
ishaan-jaff Feb 6, 2026
7e364f4
add on REST endpoints
ishaan-jaff Feb 6, 2026
784342e
filter with IP
ishaan-jaff Feb 6, 2026
a925d41
TestIsInternalIp
ishaan-jaff Feb 6, 2026
46a7cab
_extract_mcp_headers_from_request
ishaan-jaff Feb 6, 2026
fc483ea
init get_mcp_client_ip
ishaan-jaff Feb 6, 2026
769df00
_get_general_settings
ishaan-jaff Feb 6, 2026
4118fad
allowed_server_ids
ishaan-jaff Feb 6, 2026
6d665c2
address PR comments
ishaan-jaff Feb 7, 2026
35614ac
get_mcp_server_by_name fix
ishaan-jaff Feb 7, 2026
0ad94af
fix server
ishaan-jaff Feb 7, 2026
4252e9b
fix review comments
ishaan-jaff Feb 7, 2026
490995d
get_public_mcp_servers
ishaan-jaff Feb 7, 2026
7c48b80
address _get_allowed_mcp_servers
ishaan-jaff Feb 7, 2026
003e15e
test fix
ishaan-jaff Feb 7, 2026
e366f5a
fix linting
ishaan-jaff Feb 7, 2026
93ba86a
inint ui types
ishaan-jaff Feb 7, 2026
e0298d2
add ui for managing MCP private/public
ishaan-jaff Feb 7, 2026
580e54c
add ui
ishaan-jaff Feb 7, 2026
f8e1d31
fixes
ishaan-jaff Feb 7, 2026
c3cc655
add to schema
ishaan-jaff Feb 7, 2026
4731104
add types
ishaan-jaff Feb 7, 2026
de46d7c
fix endpoint
ishaan-jaff Feb 7, 2026
46efea9
add endpoint
ishaan-jaff Feb 7, 2026
04eef68
update manager
ishaan-jaff Feb 7, 2026
6a0321f
test mcp
ishaan-jaff Feb 7, 2026
9936f7d
Merge branch 'main' into litellm_mcp_control_ui
ishaan-jaff Feb 7, 2026
5b38ced
dont use external party for ip address
ishaan-jaff Feb 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions litellm-proxy-extras/litellm_proxy_extras/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ model LiteLLM_MCPServerTable {
token_url String?
registration_url String?
allow_all_keys Boolean @default(false)
available_on_public_internet Boolean @default(false)
}

// Generate Tokens for Proxy
Expand Down
30 changes: 30 additions & 0 deletions litellm/model_prices_and_context_window_backup.json
Original file line number Diff line number Diff line change
Expand Up @@ -1263,6 +1263,36 @@
"supports_vision": true,
"tool_use_system_prompt_tokens": 346
},
"au.anthropic.claude-opus-4-6-v1:0": {
"cache_creation_input_token_cost": 6.875e-06,
"cache_creation_input_token_cost_above_200k_tokens": 1.375e-05,
"cache_read_input_token_cost": 5.5e-07,
"cache_read_input_token_cost_above_200k_tokens": 1.1e-06,
"input_cost_per_token": 5.5e-06,
"input_cost_per_token_above_200k_tokens": 1.1e-05,
"litellm_provider": "bedrock_converse",
"max_input_tokens": 200000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 2.75e-05,
"output_cost_per_token_above_200k_tokens": 4.125e-05,
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
"search_context_size_medium": 0.01
},
"supports_assistant_prefill": false,
"supports_computer_use": true,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 346
},
"anthropic.claude-sonnet-4-20250514-v1:0": {
"cache_creation_input_token_cost": 3.75e-06,
"cache_read_input_token_cost": 3e-07,
Expand Down
8 changes: 2 additions & 6 deletions litellm/proxy/_experimental/mcp_server/mcp_server_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@
from mcp.shared.tool_name_validation import (
SEP_986_URL,
)
from mcp.shared.tool_name_validation import SEP_986_URL
except ImportError:
from pydantic import BaseModel

Expand All @@ -82,7 +81,7 @@ class _ToolNameValidationResult(BaseModel):
is_valid: bool = True
warnings: list = []

def validate_tool_name(name: str) -> _ToolNameValidationResult:
def validate_tool_name(name: str) -> _ToolNameValidationResult: # type: ignore[misc]
return _ToolNameValidationResult()


Expand Down Expand Up @@ -2304,28 +2303,24 @@ def get_mcp_server_by_name(
non-public servers are hidden from external IPs.
"""
registry = self.get_registry()

# Pass 1: Match by alias (highest priority)
for server in registry.values():
if server.alias == server_name:
if not self._is_server_accessible_from_ip(server, client_ip):
return None
return server

# Pass 2: Match by server_name
for server in registry.values():
if server.server_name == server_name:
if not self._is_server_accessible_from_ip(server, client_ip):
return None
return server

# Pass 3: Match by name (lowest priority)
for server in registry.values():
if server.name == server_name:
if not self._is_server_accessible_from_ip(server, client_ip):
return None
return server

return None

def get_filtered_registry(
Expand Down Expand Up @@ -2576,6 +2571,7 @@ def _build_mcp_server_table(self, server: MCPServer) -> LiteLLM_MCPServerTable:
token_url=server.token_url,
registration_url=server.registration_url,
allow_all_keys=server.allow_all_keys,
available_on_public_internet=server.available_on_public_internet,
)

async def get_all_mcp_servers_unfiltered(self) -> List[LiteLLM_MCPServerTable]:
Expand Down
8 changes: 3 additions & 5 deletions litellm/proxy/_experimental/mcp_server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ async def mcp_server_tool_call(
host_token = getattr(host_ctx.meta, 'progressToken', None)
if host_token and hasattr(host_ctx, 'session') and host_ctx.session:
host_session = host_ctx.session

async def forward_progress(progress: float, total: float | None):
"""Forward progress notifications from external MCP to Host"""
try:
Expand All @@ -315,7 +315,7 @@ async def forward_progress(progress: float, total: float | None):
verbose_logger.debug(f"Forwarded progress {progress}/{total} to Host")
except Exception as e:
verbose_logger.error(f"Failed to forward progress to Host: {e}")

host_progress_callback = forward_progress
verbose_logger.debug(f"Host progressToken captured: {host_token[:8]}...")
except Exception as e:
Expand Down Expand Up @@ -725,7 +725,6 @@ def filter_tools_by_allowed_tools(
def _get_client_ip_from_context() -> Optional[str]:
"""
Extract client_ip from auth context.

Returns None if context not set (caller should handle this as "no IP filtering").
"""
try:
Expand All @@ -748,7 +747,6 @@ async def _get_allowed_mcp_servers(
mcp_servers: Optional list of server names to filter to.
client_ip: Client IP for IP-based access control. If None, falls back to
auth context. Pass explicitly from request handlers for safety.

Note: If client_ip is None and auth context is not set, IP filtering is skipped.
This is intentional for internal callers but may indicate a bug if called
from a request handler without proper context setup.
Expand Down Expand Up @@ -1781,7 +1779,7 @@ async def _handle_managed_mcp_tool(
oauth2_headers: Optional[Dict[str, str]] = None,
raw_headers: Optional[Dict[str, str]] = None,
litellm_logging_obj: Optional[Any] = None,
host_progress_callback: Optional[Callable] = None,
host_progress_callback: Optional[Callable] = None,
) -> CallToolResult:
"""Handle tool execution for managed server tools"""
# Import here to avoid circular import
Expand Down
11 changes: 11 additions & 0 deletions litellm/proxy/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -1071,6 +1071,7 @@ class NewMCPServerRequest(LiteLLMPydanticObjectBase):
token_url: Optional[str] = None
registration_url: Optional[str] = None
allow_all_keys: bool = False
available_on_public_internet: bool = False

@model_validator(mode="before")
@classmethod
Expand Down Expand Up @@ -1132,6 +1133,7 @@ class UpdateMCPServerRequest(LiteLLMPydanticObjectBase):
token_url: Optional[str] = None
registration_url: Optional[str] = None
allow_all_keys: bool = False
available_on_public_internet: bool = False

@model_validator(mode="before")
@classmethod
Expand Down Expand Up @@ -1185,6 +1187,7 @@ class LiteLLM_MCPServerTable(LiteLLMPydanticObjectBase):
token_url: Optional[str] = None
registration_url: Optional[str] = None
allow_all_keys: bool = False
available_on_public_internet: bool = False


class MakeMCPServersPublicRequest(LiteLLMPydanticObjectBase):
Expand Down Expand Up @@ -2096,6 +2099,14 @@ class ConfigGeneralSettings(LiteLLMPydanticObjectBase):
None,
description="Maximum retention period for spend logs (e.g., '7d' for 7 days). Logs older than this will be deleted.",
)
mcp_internal_ip_ranges: Optional[List[str]] = Field(
None,
description="Custom CIDR ranges that define internal/private networks for MCP access control. When set, only these ranges are treated as internal. Defaults to RFC 1918 private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8).",
)
mcp_trusted_proxy_ranges: Optional[List[str]] = Field(
None,
description="CIDR ranges of trusted reverse proxies. When set, X-Forwarded-For headers are only trusted from these IPs.",
)


class ConfigYAML(LiteLLMPydanticObjectBase):
Expand Down
19 changes: 14 additions & 5 deletions litellm/proxy/auth/ip_address_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ def parse_trusted_proxy_networks(
) -> List[Union[ipaddress.IPv4Network, ipaddress.IPv6Network]]:
"""
Parse trusted proxy CIDR ranges for XFF validation.

Returns empty list if not configured (XFF will not be trusted).
"""
if not configured_ranges:
Expand Down Expand Up @@ -121,12 +120,23 @@ def get_mcp_client_ip(

Args:
request: FastAPI request object
general_settings: Optional settings dict. If not provided, uses cached reference.
general_settings: Optional settings dict. If not provided, imports from proxy_server.
"""
from litellm.proxy.proxy_server import general_settings
if general_settings is None:
try:
from litellm.proxy.proxy_server import (
general_settings as proxy_general_settings,
)
general_settings = proxy_general_settings
except ImportError:
general_settings = {}

# Handle case where general_settings is still None after import
if general_settings is None:
general_settings = {}

use_xff = general_settings.get("use_x_forwarded_for", False)

# If XFF is enabled, validate the request comes from a trusted proxy
if use_xff and "x-forwarded-for" in request.headers:
trusted_ranges = general_settings.get("mcp_trusted_proxy_ranges")
Expand All @@ -142,5 +152,4 @@ def get_mcp_client_ip(
"XFF header from untrusted IP %s, ignoring", direct_ip
)
return direct_ip

return _get_request_ip_address(request, use_x_forwarded_for=use_xff)
15 changes: 14 additions & 1 deletion litellm/proxy/management_endpoints/mcp_management_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class _ToolNameValidationResult(BaseModel):
is_valid: bool = True
warnings: list = []

def validate_tool_name(name: str) -> _ToolNameValidationResult:
def validate_tool_name(name: str) -> _ToolNameValidationResult: # type: ignore[misc]
return _ToolNameValidationResult()

from litellm.proxy._experimental.mcp_server.db import (
Expand Down Expand Up @@ -333,6 +333,7 @@ def _build_temporary_mcp_server_record(
token_url=payload.token_url,
registration_url=payload.registration_url,
allow_all_keys=payload.allow_all_keys,
available_on_public_internet=payload.available_on_public_internet,
)

def get_prisma_client_or_throw(message: str):
Expand Down Expand Up @@ -421,6 +422,18 @@ async def get_mcp_access_groups(
access_groups_list = sorted(list(access_groups))
return {"access_groups": access_groups_list}

@router.get(
"/network/client-ip",
tags=["mcp"],
dependencies=[Depends(user_api_key_auth)],
description="Returns the caller's IP address as seen by the proxy.",
)
async def get_client_ip(request: Request):
from litellm.proxy.auth.ip_address_utils import IPAddressUtils

client_ip = IPAddressUtils.get_mcp_client_ip(request)
return {"ip": client_ip}

@router.get(
"/registry.json",
tags=["mcp"],
Expand Down
8 changes: 7 additions & 1 deletion litellm/proxy/proxy_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -10904,6 +10904,8 @@ async def get_config_list(
"pass_through_endpoints": {"type": "PydanticModel"},
"store_prompts_in_spend_logs": {"type": "Boolean"},
"maximum_spend_logs_retention_period": {"type": "String"},
"mcp_internal_ip_ranges": {"type": "List"},
"mcp_trusted_proxy_ranges": {"type": "List"},
}

return_val = []
Expand Down Expand Up @@ -10973,11 +10975,15 @@ async def get_config_list(
elif field_name in general_settings:
_stored_in_db = False

_field_value = general_settings.get(field_name, None)
if _field_value is None and field_name in db_general_settings_dict:
_field_value = db_general_settings_dict[field_name]

_response_obj = ConfigList(
field_name=field_name,
field_type=allowed_args[field_name]["type"],
field_description=field_info.description or "",
field_value=general_settings.get(field_name, None),
field_value=_field_value,
stored_in_db=_stored_in_db,
field_default_value=field_info.default,
nested_fields=nested_fields,
Expand Down
1 change: 1 addition & 0 deletions litellm/proxy/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ model LiteLLM_MCPServerTable {
token_url String?
registration_url String?
allow_all_keys Boolean @default(false)
available_on_public_internet Boolean @default(false)
}

// Generate Tokens for Proxy
Expand Down
1 change: 1 addition & 0 deletions schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ model LiteLLM_MCPServerTable {
token_url String?
registration_url String?
allow_all_keys Boolean @default(false)
available_on_public_internet Boolean @default(false)
}

// Generate Tokens for Proxy
Expand Down
Loading
Loading