diff --git a/litellm/experimental_mcp_client/client.py b/litellm/experimental_mcp_client/client.py index 11b15a63484..be9d2b88e99 100644 --- a/litellm/experimental_mcp_client/client.py +++ b/litellm/experimental_mcp_client/client.py @@ -7,6 +7,7 @@ import base64 import os from collections.abc import Awaitable, Callable, Generator from datetime import timedelta +from importlib import metadata from typing import Any, Final, TypeVar import httpx @@ -21,6 +22,18 @@ try: streamable_http_client = getattr(streamable_http_module, "streamable_http_client", None) except ImportError: pass + +MCP_STREAMABLE_HTTP_REQUIREMENT: Final = "mcp>=1.28.1" + + +def missing_streamable_http_client_error() -> ImportError: + return ImportError( + f"MCP streamable HTTP transport requires {MCP_STREAMABLE_HTTP_REQUIREMENT}, but the installed " + f"mcp {metadata.version('mcp')} does not provide streamable_http_client. " + "Fix with: pip install 'litellm[mcp]' (or upgrade mcp directly: pip install -U mcp)" + ) + + from mcp.types import CallToolRequestParams as MCPCallToolRequestParams from mcp.types import CallToolResult as MCPCallToolResult from mcp.types import ( @@ -323,7 +336,7 @@ class MCPClient: ) # HTTP transport (default) if streamable_http_client is None: - raise ImportError("streamable_http_client is not available. Please install mcp with HTTP support.") + raise missing_streamable_http_client_error() headers = self._get_auth_headers() httpx_client_factory = self._create_httpx_client_factory() verbose_logger.debug("litellm headers for streamable_http_client: %s", headers) diff --git a/pyproject.toml b/pyproject.toml index 37b00373f8a..1c3f5a4875c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,6 +106,7 @@ utils = [ "numpydoc>=1.8.0,<2.0", ] caching = ["diskcache>=5.6.3,<6.0"] +mcp = ["mcp>=1.28.1,<2.0"] # SAML SSO for the admin UI. python3-saml pulls in xmlsec/lxml, whose wheels # bundle the native libxmlsec1/libxml2 libraries, so no system packages are # required. Kept out of the base `proxy` extra so it stays optional. diff --git a/tests/test_litellm/experimental_mcp_client/test_mcp_client.py b/tests/test_litellm/experimental_mcp_client/test_mcp_client.py index 51fdfa4ce31..53fbaf8ddb7 100644 --- a/tests/test_litellm/experimental_mcp_client/test_mcp_client.py +++ b/tests/test_litellm/experimental_mcp_client/test_mcp_client.py @@ -2,6 +2,8 @@ import asyncio import base64 import os import sys +from importlib import metadata +from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch import anyio @@ -24,9 +26,11 @@ from mcp.types import ( import litellm.experimental_mcp_client.client as mcp_client_module from litellm.experimental_mcp_client.client import ( + MCP_STREAMABLE_HTTP_REQUIREMENT, MCPClient, _as_read_timeout, _first_non_cancelled_cause, + missing_streamable_http_client_error, strip_auth_scheme, ) from litellm.proxy._experimental.mcp_server.faults.list_outcomes import ( @@ -1047,3 +1051,42 @@ def test_openapi_byok_auth_header_emits_exactly_one_scheme(auth_type, auth_value assert server.is_byok is False assert _format_byok_openapi_auth_header(server, auth_value) == expected + + +def test_missing_streamable_http_client_error_names_requirement_and_remedy(): + message = str(missing_streamable_http_client_error()) + + assert MCP_STREAMABLE_HTTP_REQUIREMENT in message + assert "pip install 'litellm[mcp]'" in message + assert metadata.version("mcp") in message + + +@pytest.mark.asyncio +async def test_http_transport_without_streamable_http_client_raises_actionable_import_error(): + client = MCPClient( + server_url="https://mcp-server.example.com", + transport_type=MCPTransport.http, + ) + + with patch.object(mcp_client_module, "streamable_http_client", None): # test-quality-ok: simulates mcp<1.24.0 whose module lacks this import-time symbol; no HTTP boundary exists before the raise + with pytest.raises(ImportError, match=r"pip install 'litellm\[mcp\]'"): + await client.list_tools(raise_on_error=True) + + +def test_mcp_extra_matches_proxy_extra_and_supports_streamable_http(): + tomllib = pytest.importorskip("tomllib") + from packaging.requirements import Requirement + + pyproject_path = Path(__file__).parents[3] / "pyproject.toml" + with pyproject_path.open("rb") as f: + extras = tomllib.load(f)["project"]["optional-dependencies"] + + mcp_extra = extras["mcp"] + assert len(mcp_extra) == 1 + + proxy_mcp_requirements = [req for req in extras["proxy"] if Requirement(req).name == "mcp"] + assert mcp_extra == proxy_mcp_requirements + + specifier = Requirement(mcp_extra[0]).specifier + assert not specifier.contains("1.23.0") + assert specifier.contains("1.28.1") diff --git a/uv.lock b/uv.lock index eeb1ced69e2..ca5c4eb8c3c 100644 --- a/uv.lock +++ b/uv.lock @@ -10,7 +10,7 @@ resolution-markers = [ ] [options] -exclude-newer = "2026-08-23T02:27:57.028643Z" +exclude-newer = "2026-08-23T20:15:58.934396Z" exclude-newer-span = "P3D" [manifest] @@ -4315,6 +4315,9 @@ google = [ grpc = [ { name = "grpcio" }, ] +mcp = [ + { name = "mcp" }, +] mlflow = [ { name = "mlflow" }, ] @@ -4526,6 +4529,7 @@ requires-dist = [ { name = "litellm-proxy-extras", marker = "extra == 'proxy'", editable = "litellm-proxy-extras" }, { name = "llm-sandbox", marker = "extra == 'proxy-runtime'", specifier = ">=0.3.39,<1.0" }, { name = "mangum", marker = "extra == 'proxy-runtime'", specifier = ">=0.17.0,<1.0" }, + { name = "mcp", marker = "extra == 'mcp'", specifier = ">=1.28.1,<2.0" }, { name = "mcp", marker = "extra == 'proxy'", specifier = ">=1.28.1,<2.0" }, { name = "mlflow", marker = "extra == 'mlflow'", specifier = ">=3.11.1,<4.0" }, { name = "numpy", marker = "extra == 'stt-nvidia-riva'", specifier = ">=1.26.0" }, @@ -4569,7 +4573,7 @@ requires-dist = [ { name = "uvloop", marker = "sys_platform != 'win32' and extra == 'proxy'", specifier = ">=0.21.0,<1.0" }, { name = "websockets", marker = "extra == 'proxy'", specifier = ">=15.0.1,<16.0" }, ] -provides-extras = ["proxy", "cli", "extra-proxy", "utils", "caching", "saml", "semantic-router", "mlflow", "grpc", "stt-nvidia-riva", "google", "bedrock-realtime", "proxy-runtime"] +provides-extras = ["proxy", "cli", "extra-proxy", "utils", "caching", "mcp", "saml", "semantic-router", "mlflow", "grpc", "stt-nvidia-riva", "google", "bedrock-realtime", "proxy-runtime"] [package.metadata.requires-dev] ci = [