mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
fix: MCP - inject NPM_CONFIG_CACHE into STDIO MCP subprocess env (#21069)
* fix: inject NPM_CONFIG_CACHE into STDIO MCP subprocess env for Docker npm/npx needs a writable cache directory. In containers the default (~/.npm) may not exist or be read-only, causing STDIO MCP servers launched via npx to fail with ENOENT. Inject NPM_CONFIG_CACHE=/tmp/.npm_mcp_cache into the subprocess env when not already set. * test: add unit test for NPM_CONFIG_CACHE injection in STDIO MCP Verifies that NPM_CONFIG_CACHE is auto-injected when not set, and preserved when explicitly provided. Also moves the import to module level per code style rules. * Update litellm/constants.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Apply suggestion from @greptile-apps[bot] Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
parent
736daf0a7d
commit
5f40f93846
3 changed files with 56 additions and 5 deletions
|
|
@ -101,6 +101,11 @@ MCP_OAUTH2_TOKEN_CACHE_MAX_SIZE = int(
|
|||
MCP_OAUTH2_TOKEN_CACHE_DEFAULT_TTL = int(
|
||||
os.getenv("MCP_OAUTH2_TOKEN_CACHE_DEFAULT_TTL", "3600")
|
||||
)
|
||||
|
||||
# Default npm cache directory for STDIO MCP servers.
|
||||
# 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")
|
||||
MCP_OAUTH2_TOKEN_CACHE_MIN_TTL = int(
|
||||
os.getenv("MCP_OAUTH2_TOKEN_CACHE_MIN_TTL", "10")
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ from typing import Any, Callable, Dict, List, Literal, Optional, Set, Tuple, Uni
|
|||
from urllib.parse import urlparse
|
||||
|
||||
import anyio
|
||||
|
||||
from fastapi import HTTPException
|
||||
from httpx import HTTPStatusError
|
||||
from mcp import ReadResourceResult, Resource
|
||||
|
|
@ -32,6 +31,7 @@ from pydantic import AnyUrl
|
|||
|
||||
import litellm
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.constants import MCP_NPM_CACHE_DIR
|
||||
from litellm.exceptions import BlockedPiiEntityError, GuardrailRaisedException
|
||||
from litellm.experimental_mcp_client.client import MCPClient
|
||||
from litellm.llms.custom_httpx.http_handler import get_async_httpx_client
|
||||
|
|
@ -72,7 +72,9 @@ try:
|
|||
from mcp.shared.tool_name_validation import (
|
||||
validate_tool_name, # pyright: ignore[reportAssignmentType]
|
||||
)
|
||||
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
|
||||
|
||||
|
|
@ -889,8 +891,15 @@ class MCPServerManager:
|
|||
|
||||
# Handle stdio transport
|
||||
if transport == MCPTransport.stdio:
|
||||
# For stdio, we need to get the stdio config from the server
|
||||
resolved_env = stdio_env if stdio_env is not None else server.env or {}
|
||||
resolved_env = stdio_env if stdio_env is not None else dict(server.env or {})
|
||||
|
||||
# Ensure npm-based STDIO MCP servers have a writable cache dir.
|
||||
# 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
|
||||
stdio_config: Optional[MCPStdioConfig] = None
|
||||
if server.command and server.args is not None:
|
||||
stdio_config = MCPStdioConfig(
|
||||
|
|
|
|||
|
|
@ -112,7 +112,44 @@ class TestMCPServerManager:
|
|||
assert client.stdio_config is not None
|
||||
assert client.stdio_config["command"] == "node"
|
||||
assert client.stdio_config["args"] == ["server.js"]
|
||||
assert client.stdio_config["env"] == {"NODE_ENV": "test"}
|
||||
# NPM_CONFIG_CACHE is injected automatically for container compatibility
|
||||
from litellm.constants import MCP_NPM_CACHE_DIR
|
||||
|
||||
assert client.stdio_config["env"]["NODE_ENV"] == "test"
|
||||
assert client.stdio_config["env"]["NPM_CONFIG_CACHE"] == MCP_NPM_CACHE_DIR
|
||||
|
||||
async def test_create_mcp_client_stdio_injects_npm_config_cache(self):
|
||||
"""Test that _create_mcp_client injects NPM_CONFIG_CACHE when not already set,
|
||||
and preserves user-provided NPM_CONFIG_CACHE when present."""
|
||||
from litellm.constants import MCP_NPM_CACHE_DIR
|
||||
|
||||
manager = MCPServerManager()
|
||||
|
||||
# Case 1: NPM_CONFIG_CACHE not set -> should be injected
|
||||
server_no_cache = MCPServer(
|
||||
server_id="stdio-npm-1",
|
||||
name="test_npm_server",
|
||||
url=None,
|
||||
transport=MCPTransport.stdio,
|
||||
command="npx",
|
||||
args=["-y", "@modelcontextprotocol/server-everything"],
|
||||
env={},
|
||||
)
|
||||
client = await manager._create_mcp_client(server_no_cache)
|
||||
assert client.stdio_config["env"]["NPM_CONFIG_CACHE"] == MCP_NPM_CACHE_DIR
|
||||
|
||||
# Case 2: NPM_CONFIG_CACHE already set -> should NOT be overwritten
|
||||
server_with_cache = MCPServer(
|
||||
server_id="stdio-npm-2",
|
||||
name="test_npm_server_custom",
|
||||
url=None,
|
||||
transport=MCPTransport.stdio,
|
||||
command="npx",
|
||||
args=["-y", "@modelcontextprotocol/server-everything"],
|
||||
env={"NPM_CONFIG_CACHE": "/custom/cache"},
|
||||
)
|
||||
client2 = await manager._create_mcp_client(server_with_cache)
|
||||
assert client2.stdio_config["env"]["NPM_CONFIG_CACHE"] == "/custom/cache"
|
||||
|
||||
def test_build_stdio_env_only_accepts_x_prefixed_placeholders(self):
|
||||
"""Ensure only ${X-*} placeholders are substituted from headers."""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue