diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index f5dee5ac5b8..402e12d9356 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -10,6 +10,7 @@ import asyncio import datetime import hashlib import json +import os import re from typing import Any, Callable, Dict, List, Literal, Optional, Set, Tuple, Union, cast from urllib.parse import urlparse @@ -35,6 +36,8 @@ from litellm.constants import ( MCP_CLIENT_TIMEOUT, MCP_HEALTH_CHECK_TIMEOUT, MCP_METADATA_TIMEOUT, + MCP_NPM_CACHE_DIR, + MCP_STDIO_ALLOWED_COMMANDS, MCP_TOOL_LISTING_TIMEOUT, ) from litellm.exceptions import BlockedPiiEntityError, GuardrailRaisedException @@ -1119,25 +1122,17 @@ class MCPServerManager: # In containers the default (~/.npm or /app/.npm) may not exist # or be read-only, causing npx to fail with ENOENT. if "NPM_CONFIG_CACHE" not in resolved_env: - from litellm.constants import MCP_NPM_CACHE_DIR - resolved_env["NPM_CONFIG_CACHE"] = MCP_NPM_CACHE_DIR - # Defense-in-depth: warn for commands not in the allowlist. + # Defense-in-depth: block commands not in the allowlist. # The Pydantic validator blocks new servers; this catches legacy # config/DB records predating the allowlist. if server.command: - import os as _os - - from litellm.constants import MCP_STDIO_ALLOWED_COMMANDS - - base_command = _os.path.basename(server.command) + base_command = os.path.basename(server.command) if base_command not in MCP_STDIO_ALLOWED_COMMANDS: - verbose_logger.warning( - "MCP stdio command '%s' is not in the allowlist (%s). " - "Add it to LITELLM_MCP_STDIO_EXTRA_COMMANDS to suppress this warning. " - "A future release may block non-allowlisted commands.", - server.command, - sorted(MCP_STDIO_ALLOWED_COMMANDS), + raise HTTPException( + status_code=403, + detail=f"MCP stdio command '{server.command}' is not in the allowlist ({sorted(MCP_STDIO_ALLOWED_COMMANDS)}). " + f"Add it to LITELLM_MCP_STDIO_EXTRA_COMMANDS to allow this command.", ) stdio_config: Optional[MCPStdioConfig] = None diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 70378a1db15..cf99c5cd9fa 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -16,6 +16,7 @@ from pydantic import ( from typing_extensions import Required, TypedDict from litellm._uuid import uuid +from litellm.constants import MCP_STDIO_ALLOWED_COMMANDS from litellm.types.integrations.slack_alerting import AlertType from litellm.types.llms.openai import ( AllMessageValues, @@ -1164,8 +1165,6 @@ class NewMCPServerRequest(LiteLLMPydanticObjectBase): if not values.get("args"): raise ValueError("args is required for stdio transport") # Validate command against allowlist to prevent arbitrary execution - from litellm.constants import MCP_STDIO_ALLOWED_COMMANDS - base_command = os.path.basename(values["command"]) if base_command not in MCP_STDIO_ALLOWED_COMMANDS: raise ValueError( @@ -1233,8 +1232,6 @@ class UpdateMCPServerRequest(LiteLLMPydanticObjectBase): if not values.get("args"): raise ValueError("args is required for stdio transport") # Validate command against allowlist to prevent arbitrary execution - from litellm.constants import MCP_STDIO_ALLOWED_COMMANDS - base_command = os.path.basename(values["command"]) if base_command not in MCP_STDIO_ALLOWED_COMMANDS: raise ValueError(