mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
Merge pull request #38399 from BerriAI/litellm_mcp_http_extra
fix(mcp): add litellm[mcp] extra and actionable error when streamable_http_client is missing
This commit is contained in:
commit
147fcf767e
4 changed files with 69 additions and 3 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,47 @@ 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( # test-quality-ok: simulates mcp<1.24.0 whose module lacks this import-time symbol
|
||||
mcp_client_module, "streamable_http_client", None
|
||||
):
|
||||
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():
|
||||
try:
|
||||
import tomllib
|
||||
except ImportError:
|
||||
tomllib = pytest.importorskip("tomli")
|
||||
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")
|
||||
|
|
|
|||
8
uv.lock
generated
8
uv.lock
generated
|
|
@ -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 = [
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue