fix uvx mcp servers

This commit is contained in:
mubashir1osmani 2026-04-12 08:52:29 -04:00
parent cd9c511df6
commit 79f4d899d4
No known key found for this signature in database
GPG key ID: AB055FF67D0B4D9A
3 changed files with 44 additions and 0 deletions

View file

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

View file

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

View file

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