From d5355602d59d8672b5c417be1b8f06897a7bfd4e Mon Sep 17 00:00:00 2001 From: Shivam Rawat <161387515+shivamrawat1@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:13:41 -0800 Subject: [PATCH] added configurable env for mcp timeouts (#22287) --- docs/my-website/docs/proxy/config_settings.md | 4 ++++ litellm/constants.py | 6 +++++ litellm/experimental_mcp_client/client.py | 5 ++-- .../mcp_server/mcp_server_manager.py | 24 +++++++++++++------ tests/mcp_tests/test_mcp_client_unit.py | 13 ++++++++++ tests/test_litellm/test_constants.py | 4 ++++ 6 files changed, 47 insertions(+), 9 deletions(-) diff --git a/docs/my-website/docs/proxy/config_settings.md b/docs/my-website/docs/proxy/config_settings.md index e302e2171f..7b2011e45d 100644 --- a/docs/my-website/docs/proxy/config_settings.md +++ b/docs/my-website/docs/proxy/config_settings.md @@ -557,6 +557,10 @@ router_settings: | DEFAULT_MCP_SEMANTIC_FILTER_SIMILARITY_THRESHOLD | Default similarity threshold for MCP semantic tool filtering. Default is 0.3 | DEFAULT_MCP_SEMANTIC_FILTER_TOP_K | Default number of top results to return for MCP semantic tool filtering. Default is 10 | MCP_NPM_CACHE_DIR | Directory for npm cache used by STDIO MCP servers. In containers the default (~/.npm) may not exist or be read-only. Default is `/tmp/.npm_mcp_cache` +| LITELLM_MCP_CLIENT_TIMEOUT | MCP client connection timeout in seconds (stdio and HTTP/SSE transports). Default is 60 +| LITELLM_MCP_TOOL_LISTING_TIMEOUT | Timeout in seconds for listing tools from an MCP server. Default is 30 +| LITELLM_MCP_METADATA_TIMEOUT | HTTP client timeout in seconds for OAuth metadata fetching. Default is 10 +| LITELLM_MCP_HEALTH_CHECK_TIMEOUT | Health check timeout in seconds for MCP servers. Default is 10 | MCP_OAUTH2_TOKEN_CACHE_DEFAULT_TTL | Default TTL in seconds for MCP OAuth2 token cache. Default is 3600 | MCP_OAUTH2_TOKEN_CACHE_MAX_SIZE | Maximum number of entries in MCP OAuth2 token cache. Default is 200 | MCP_OAUTH2_TOKEN_CACHE_MIN_TTL | Minimum TTL in seconds for MCP OAuth2 token cache. Default is 10 diff --git a/litellm/constants.py b/litellm/constants.py index 871b7e5a80..c1bb7da1b7 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -137,6 +137,12 @@ MCP_OAUTH2_TOKEN_CACHE_DEFAULT_TTL = int( 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")) +# MCP timeout defaults (seconds). Override via env vars for slow/custom MCP servers. +MCP_CLIENT_TIMEOUT = float(os.getenv("LITELLM_MCP_CLIENT_TIMEOUT", "60.0")) +MCP_TOOL_LISTING_TIMEOUT = float(os.getenv("LITELLM_MCP_TOOL_LISTING_TIMEOUT", "30.0")) +MCP_METADATA_TIMEOUT = float(os.getenv("LITELLM_MCP_METADATA_TIMEOUT", "10.0")) +MCP_HEALTH_CHECK_TIMEOUT = float(os.getenv("LITELLM_MCP_HEALTH_CHECK_TIMEOUT", "10.0")) + LITELLM_UI_ALLOW_HEADERS = [ "x-litellm-semantic-filter", "x-litellm-semantic-filter-tools", diff --git a/litellm/experimental_mcp_client/client.py b/litellm/experimental_mcp_client/client.py index 5e21ff9754..849ce02310 100644 --- a/litellm/experimental_mcp_client/client.py +++ b/litellm/experimental_mcp_client/client.py @@ -30,6 +30,7 @@ from mcp.types import Tool as MCPTool from pydantic import AnyUrl from litellm._logging import verbose_logger +from litellm.constants import MCP_CLIENT_TIMEOUT from litellm.llms.custom_httpx.http_handler import get_ssl_configuration from litellm.types.llms.custom_http import VerifyTypes from litellm.types.mcp import ( @@ -63,7 +64,7 @@ class MCPClient: transport_type: MCPTransportType = MCPTransport.http, auth_type: MCPAuthType = None, auth_value: Optional[Union[str, Dict[str, str]]] = None, - timeout: float = 60.0, + timeout: Optional[float] = None, stdio_config: Optional[MCPStdioConfig] = None, extra_headers: Optional[Dict[str, str]] = None, ssl_verify: Optional[VerifyTypes] = None, @@ -71,7 +72,7 @@ class MCPClient: self.server_url: str = server_url self.transport_type: MCPTransport = transport_type self.auth_type: MCPAuthType = auth_type - self.timeout: float = timeout + self.timeout: float = timeout if timeout is not None else MCP_CLIENT_TIMEOUT self._mcp_auth_value: Optional[Union[str, Dict[str, str]]] = None self.stdio_config: Optional[MCPStdioConfig] = stdio_config self.extra_headers: Optional[Dict[str, str]] = extra_headers diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index 08213f40b4..da29c7804a 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -31,6 +31,12 @@ from pydantic import AnyUrl import litellm from litellm._logging import verbose_logger +from litellm.constants import ( + MCP_CLIENT_TIMEOUT, + MCP_HEALTH_CHECK_TIMEOUT, + MCP_METADATA_TIMEOUT, + MCP_TOOL_LISTING_TIMEOUT, +) 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 @@ -943,7 +949,7 @@ class MCPServerManager: transport_type=transport, auth_type=server.auth_type, auth_value=auth_value, - timeout=60.0, + timeout=MCP_CLIENT_TIMEOUT, stdio_config=stdio_config, extra_headers=extra_headers, ) @@ -955,7 +961,7 @@ class MCPServerManager: transport_type=transport, auth_type=server.auth_type, auth_value=auth_value, - timeout=60.0, + timeout=MCP_CLIENT_TIMEOUT, extra_headers=extra_headers, ) @@ -1334,7 +1340,7 @@ class MCPServerManager: try: client = get_async_httpx_client( llm_provider=httpxSpecialProvider.MCP, - params={"timeout": 10.0}, + params={"timeout": MCP_METADATA_TIMEOUT}, ) response = await client.get(resource_metadata_url) response.raise_for_status() @@ -1430,7 +1436,7 @@ class MCPServerManager: try: client = get_async_httpx_client( llm_provider=httpxSpecialProvider.MCP, - params={"timeout": 10.0}, + params={"timeout": MCP_METADATA_TIMEOUT}, ) response = await client.get(url) response.raise_for_status() @@ -1489,7 +1495,7 @@ class MCPServerManager: List of tools from the server """ try: - with anyio.fail_after(30.0): + with anyio.fail_after(MCP_TOOL_LISTING_TIMEOUT): tools = await client.list_tools() verbose_logger.debug(f"Tools from {server_name}: {tools}") return tools @@ -2508,10 +2514,14 @@ class MCPServerManager: return "ok" # Add timeout wrapper to prevent hanging - await asyncio.wait_for(client.run_with_session(_noop), timeout=10.0) + await asyncio.wait_for( + client.run_with_session(_noop), timeout=MCP_HEALTH_CHECK_TIMEOUT + ) status = "healthy" except asyncio.TimeoutError: - health_check_error = "Health check timed out after 10 seconds" + health_check_error = ( + f"Health check timed out after {MCP_HEALTH_CHECK_TIMEOUT} seconds" + ) status = "unhealthy" except asyncio.CancelledError: health_check_error = "Health check was cancelled" diff --git a/tests/mcp_tests/test_mcp_client_unit.py b/tests/mcp_tests/test_mcp_client_unit.py index c70d0c42cd..9f88fad83e 100644 --- a/tests/mcp_tests/test_mcp_client_unit.py +++ b/tests/mcp_tests/test_mcp_client_unit.py @@ -16,6 +16,19 @@ from litellm.types.mcp import MCPAuth, MCPTransport from mcp.types import Tool as MCPTool, CallToolResult as MCPCallToolResult +def test_mcp_client_uses_configurable_default_timeout(): + """MCPClient should use MCP_CLIENT_TIMEOUT constant when no timeout is passed.""" + with patch( + "litellm.experimental_mcp_client.client.MCP_CLIENT_TIMEOUT", 120.0 + ): + # Client reads constant at runtime when timeout is None + client = MCPClient( + server_url="http://example.com", + transport_type=MCPTransport.sse, + ) + assert client.timeout == 120.0 + + class TestMCPClientUnitTests: """Unit tests for MCPClient functionality.""" diff --git a/tests/test_litellm/test_constants.py b/tests/test_litellm/test_constants.py index 23447a02e0..8fff3ec40d 100644 --- a/tests/test_litellm/test_constants.py +++ b/tests/test_litellm/test_constants.py @@ -41,6 +41,10 @@ def test_all_numeric_constants_can_be_overridden(): # Constants that use a different env var name than the constant name constant_to_env_var = { "MAX_CALLBACKS": "LITELLM_MAX_CALLBACKS", + "MCP_CLIENT_TIMEOUT": "LITELLM_MCP_CLIENT_TIMEOUT", + "MCP_TOOL_LISTING_TIMEOUT": "LITELLM_MCP_TOOL_LISTING_TIMEOUT", + "MCP_METADATA_TIMEOUT": "LITELLM_MCP_METADATA_TIMEOUT", + "MCP_HEALTH_CHECK_TIMEOUT": "LITELLM_MCP_HEALTH_CHECK_TIMEOUT", } # Verify all numeric constants have environment variable support