From 79f4d899d41d7e4155c4907cf09c344872d655ca Mon Sep 17 00:00:00 2001 From: mubashir1osmani Date: Sun, 12 Apr 2026 08:52:29 -0400 Subject: [PATCH] fix uvx mcp servers --- litellm/constants.py | 5 +++ .../mcp_server/mcp_server_manager.py | 6 ++++ .../mcp_server/test_mcp_server_manager.py | 33 +++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/litellm/constants.py b/litellm/constants.py index a7d86ddb16b..c1fed5c2a76 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -133,6 +133,11 @@ MCP_OAUTH2_TOKEN_CACHE_DEFAULT_TTL = int( # npm/npx needs a writable cache dir; in containers the default (~/.npm) # may not exist or be read-only. /tmp is always writable. MCP_NPM_CACHE_DIR = os.getenv("MCP_NPM_CACHE_DIR", "/tmp/.npm_mcp_cache") + +# Default uv cache directory for uvx-based STDIO MCP servers. +# uvx needs a writable cache dir; in containers the default (~/.cache/uv) +# may not exist or be read-only, causing uvx to fail when downloading packages. +MCP_UV_CACHE_DIR = os.getenv("MCP_UV_CACHE_DIR", "/tmp/.uv_mcp_cache") MCP_OAUTH2_TOKEN_CACHE_MIN_TTL = int(os.getenv("MCP_OAUTH2_TOKEN_CACHE_MIN_TTL", "10")) # MCP timeout defaults (seconds). Override via env vars for slow/custom MCP servers. diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index 402e12d9356..4da20e0cb5c 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -38,6 +38,7 @@ from litellm.constants import ( MCP_METADATA_TIMEOUT, MCP_NPM_CACHE_DIR, MCP_STDIO_ALLOWED_COMMANDS, + MCP_UV_CACHE_DIR, MCP_TOOL_LISTING_TIMEOUT, ) from litellm.exceptions import BlockedPiiEntityError, GuardrailRaisedException @@ -1123,6 +1124,11 @@ class MCPServerManager: # or be read-only, causing npx to fail with ENOENT. if "NPM_CONFIG_CACHE" not in resolved_env: resolved_env["NPM_CONFIG_CACHE"] = MCP_NPM_CACHE_DIR + # Ensure uvx-based STDIO MCP servers have a writable cache dir. + # In containers the default (~/.cache/uv) may not exist + # or be read-only, causing uvx to fail when downloading packages. + if "UV_CACHE_DIR" not in resolved_env: + resolved_env["UV_CACHE_DIR"] = MCP_UV_CACHE_DIR # Defense-in-depth: block commands not in the allowlist. # The Pydantic validator blocks new servers; this catches legacy # config/DB records predating the allowlist. diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py index 656a9c616e8..d296be5f457 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py @@ -160,6 +160,39 @@ class TestMCPServerManager: client2 = await manager._create_mcp_client(server_with_cache) assert client2.stdio_config["env"]["NPM_CONFIG_CACHE"] == "/custom/cache" + async def test_create_mcp_client_stdio_injects_uv_cache_dir(self): + """Test that _create_mcp_client injects UV_CACHE_DIR for uvx when not already set, + and preserves user-provided UV_CACHE_DIR when present.""" + from litellm.constants import MCP_UV_CACHE_DIR + + manager = MCPServerManager() + + # Case 1: UV_CACHE_DIR not set -> should be injected + server_no_cache = MCPServer( + server_id="stdio-uvx-1", + name="test_uvx_server", + url=None, + transport=MCPTransport.stdio, + command="uvx", + args=["mcp-server-fetch"], + env={}, + ) + client = await manager._create_mcp_client(server_no_cache) + assert client.stdio_config["env"]["UV_CACHE_DIR"] == MCP_UV_CACHE_DIR + + # Case 2: UV_CACHE_DIR already set -> should NOT be overwritten + server_with_cache = MCPServer( + server_id="stdio-uvx-2", + name="test_uvx_server_custom", + url=None, + transport=MCPTransport.stdio, + command="uvx", + args=["mcp-server-fetch"], + env={"UV_CACHE_DIR": "/custom/uv_cache"}, + ) + client2 = await manager._create_mcp_client(server_with_cache) + assert client2.stdio_config["env"]["UV_CACHE_DIR"] == "/custom/uv_cache" + def test_build_stdio_env_only_accepts_x_prefixed_placeholders(self): """Ensure only ${X-*} placeholders are substituted from headers.""" manager = MCPServerManager()