From cac33fe1e2a74b6874d90f5ebd87b8141c73344f Mon Sep 17 00:00:00 2001 From: Joshua Valluru <326636767+joshua-berri@users.noreply.github.com> Date: Sat, 17 Oct 2026 17:42:36 -0700 Subject: [PATCH] fix(mcp): backport SSE event size fix to 1.103 Backport of #47034 (828c770857b294c2a64c99fef0b3853fd08db202) for stable/1.103.x. Extend the existing protocol integration file because this line predates the transport test helpers --- litellm/constants.py | 1 + litellm/experimental_mcp_client/client.py | 8 +++++- pyproject.toml | 8 +++--- .../mcp/test_mcp_protocol_errors.py | 28 ++++++++++++++++++- .../test_mcp_client.py | 4 +-- uv.lock | 22 +++++++-------- 6 files changed, 52 insertions(+), 19 deletions(-) diff --git a/litellm/constants.py b/litellm/constants.py index 8871978d440..392441f61ca 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -181,6 +181,7 @@ MCP_PER_USER_TOKEN_EXPIRY_BUFFER_SECONDS: Final = int(os.getenv("MCP_PER_USER_TO # MCP timeout defaults (seconds). Override via env vars for slow/custom MCP servers. MCP_CLIENT_TIMEOUT: Final = float(os.getenv("LITELLM_MCP_CLIENT_TIMEOUT", "60.0")) +MCP_MAX_SSE_EVENT_SIZE_BYTES: Final = get_env_int("LITELLM_MCP_MAX_SSE_EVENT_SIZE_BYTES", 64 * 1024 * 1024) MCP_TOOL_LISTING_TIMEOUT: Final = float(os.getenv("LITELLM_MCP_TOOL_LISTING_TIMEOUT", "30.0")) MCP_METADATA_TIMEOUT: Final = float(os.getenv("LITELLM_MCP_METADATA_TIMEOUT", "10.0")) MCP_HEALTH_CHECK_TIMEOUT: Final = float(os.getenv("LITELLM_MCP_HEALTH_CHECK_TIMEOUT", "10.0")) diff --git a/litellm/experimental_mcp_client/client.py b/litellm/experimental_mcp_client/client.py index 4b456710057..9f8ff5f1ea8 100644 --- a/litellm/experimental_mcp_client/client.py +++ b/litellm/experimental_mcp_client/client.py @@ -49,7 +49,12 @@ from mcp.types import Tool as MCPTool from pydantic import AnyUrl from litellm._logging import verbose_logger -from litellm.constants import MCP_CLIENT_TIMEOUT, MCP_NPM_CACHE_DIR, MCP_TOOL_LISTING_TIMEOUT +from litellm.constants import ( + MCP_CLIENT_TIMEOUT, + MCP_MAX_SSE_EVENT_SIZE_BYTES, + MCP_NPM_CACHE_DIR, + MCP_TOOL_LISTING_TIMEOUT, +) from litellm.experimental_mcp_client.tools import list_tools_with_pagination from litellm.llms.custom_httpx.http_handler import get_ssl_configuration from litellm.proxy._experimental.mcp_server.mcp_debug import capture_upstream_error_response @@ -395,6 +400,7 @@ class MCPClient: transport_ctx: Final = streamable_http_client( url=self.server_url, http_client=http_client, + max_sse_event_size=MCP_MAX_SSE_EVENT_SIZE_BYTES, ) return transport_ctx, http_client diff --git a/pyproject.toml b/pyproject.toml index bb7cd4e2901..ba11451508f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,8 +68,8 @@ proxy = [ "boto3>=1.43.1,<2.0", "azure-identity>=1.25.2,<2.0", "azure-storage-blob>=12.28.0,<13.0", - "mcp>=2.2.0,<3", - "httpx2>=2.5.0,<3", + "mcp>=2.3.0,<3", + "httpx2>=2.10.0,<3", "pydantic>=2.12.0,<3", "litellm-proxy-extras==0.4.100.post1", "litellm-enterprise==0.1.69.post1", @@ -117,7 +117,7 @@ utils = [ "numpydoc>=1.8.0,<2.0", ] caching = ["diskcache>=5.6.3,<6.0"] -mcp = ["mcp>=2.2.0,<3", "httpx2>=2.5.0,<3", "pydantic>=2.12.0,<3"] +mcp = ["mcp>=2.3.0,<3", "httpx2>=2.10.0,<3", "pydantic>=2.12.0,<3"] # Driver for the MongoDB Atlas vector store; Atlas Vector Search has no HTTP query API. # The floor is 4.9 because that is the release AsyncMongoClient landed in. # SAML SSO for the admin UI. python3-saml pulls in xmlsec/lxml, whose wheels @@ -236,7 +236,7 @@ e2e-dev = [ "websockets>=15.0.1,<16.0", "locust==2.45.0", "psutil==7.2.2", - "mcp>=2.2.0,<3", + "mcp>=2.3.0,<3", ] proxy-dev = [ "prisma==0.11.0", diff --git a/tests/integration/mcp/test_mcp_protocol_errors.py b/tests/integration/mcp/test_mcp_protocol_errors.py index bb06d8c6068..44046ac240e 100644 --- a/tests/integration/mcp/test_mcp_protocol_errors.py +++ b/tests/integration/mcp/test_mcp_protocol_errors.py @@ -4,10 +4,12 @@ import uuid from typing import Final import pytest - +from integration._support.asgi import asgi_server from integration._support.client import Gateway from integration._support.mcp import McpPeer, call_tool, register_mcp, tool_names from integration._support.wire import Reply, Request, wire_server +from mcp.server.mcpserver import MCPServer +from mcp.server.transport_security import TransportSecuritySettings @pytest.mark.covers("other.mcp.errors.protocol_and_malformed_results_cannot_be_empty_success") @@ -84,3 +86,27 @@ def test_jsonrpc_error_and_malformed_tool_result_remain_errors(gateway: Gateway) control: Final = call_tool(gateway, key, identity, names["add"], {"a": 3, "b": 5}) assert control.status_code == 200 and control.json()["isError"] is False, control.text assert control.json()["content"][0]["text"] == "8" + + +def test_tool_result_larger_than_one_mib_in_a_single_sse_event_reaches_the_caller(gateway: Gateway) -> None: + payload: Final = "x" * (3 * 1024 * 1024) + server: Final = MCPServer("large-result") + + @server.tool() + def dump() -> str: + return payload + + app: Final = server.streamable_http_app( + stateless_http=True, + json_response=False, + transport_security=TransportSecuritySettings(enable_dns_rebinding_protection=False), + ) + with asgi_server(app) as url, gateway.scenario() as scenario: + peer: Final = McpPeer(url + "/mcp", queue.Queue()) + identity: Final = register_mcp(scenario, peer, "bigsse" + uuid.uuid4().hex[:8]) + key: Final = scenario.key(object_permission={"mcp_servers": [identity]}) + response: Final = call_tool(gateway, key, identity, "dump", {}) + assert response.status_code == 200, response.text[:500] + body: Final = response.json() + assert body["isError"] is False, response.text[:500] + assert body["content"][0]["text"] == payload 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 7e4598c2e58..89d9c35b0e1 100644 --- a/tests/test_litellm/experimental_mcp_client/test_mcp_client.py +++ b/tests/test_litellm/experimental_mcp_client/test_mcp_client.py @@ -1102,8 +1102,8 @@ def test_mcp_extra_matches_proxy_extra_and_supports_streamable_http(): } specifier: Final = Requirement(mcp_extra["mcp"]).specifier - assert not specifier.contains("1.28.1") - assert specifier.contains("2.2.0") + assert not specifier.contains("2.2.0") + assert specifier.contains("2.3.0") with (pyproject_path.parent / "uv.lock").open("rb") as f: locked = tomllib.load(f) mcp_versions: Final = [package["version"] for package in locked["package"] if package["name"] == "mcp"] diff --git a/uv.lock b/uv.lock index 02994dfe53c..327488b2662 100644 --- a/uv.lock +++ b/uv.lock @@ -4780,8 +4780,8 @@ requires-dist = [ { name = "gunicorn", marker = "extra == 'proxy'", specifier = ">=23.0.0,<24.0" }, { name = "hiredis", marker = "extra == 'proxy'", specifier = ">=3.0.0,<4.0" }, { name = "httpx", extras = ["http2"], specifier = ">=0.28.0,<1.0" }, - { name = "httpx2", marker = "extra == 'mcp'", specifier = ">=2.5.0,<3" }, - { name = "httpx2", marker = "extra == 'proxy'", specifier = ">=2.5.0,<3" }, + { name = "httpx2", marker = "extra == 'mcp'", specifier = ">=2.10.0,<3" }, + { name = "httpx2", marker = "extra == 'proxy'", specifier = ">=2.10.0,<3" }, { name = "importlib-metadata", specifier = ">=8.0.0,<9.0" }, { name = "inquirerpy", marker = "extra == 'cli'", specifier = ">=0.3.4,<1.0" }, { name = "inquirerpy", marker = "extra == 'proxy'", specifier = ">=0.3.4,<1.0" }, @@ -4793,8 +4793,8 @@ 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 = ">=2.2.0,<3" }, - { name = "mcp", marker = "extra == 'proxy'", specifier = ">=2.2.0,<3" }, + { name = "mcp", marker = "extra == 'mcp'", specifier = ">=2.3.0,<3" }, + { name = "mcp", marker = "extra == 'proxy'", specifier = ">=2.3.0,<3" }, { name = "mlflow", marker = "extra == 'mlflow'", specifier = ">=3.11.1,<4.0" }, { name = "numpy", marker = "extra == 'stt-nvidia-riva'", specifier = ">=1.26.0" }, { name = "numpydoc", marker = "extra == 'utils'", specifier = ">=1.8.0,<2.0" }, @@ -4919,7 +4919,7 @@ dev = [ ] e2e-dev = [ { name = "locust", specifier = "==2.45.0" }, - { name = "mcp", specifier = ">=2.2.0,<3" }, + { name = "mcp", specifier = ">=2.3.0,<3" }, { name = "playwright", specifier = "==1.61.0" }, { name = "psutil", specifier = "==7.2.2" }, { name = "websockets", specifier = ">=15.0.1,<16.0" }, @@ -5397,7 +5397,7 @@ wheels = [ [[package]] name = "mcp" -version = "2.2.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -5415,22 +5415,22 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/31/ac54fb0fdd5b37de704486e288bba4fbbb463f24cfcfedbede407b854513/mcp-2.2.0.tar.gz", hash = "sha256:2dc37ecb1974becdcebdbf7561e7c15a07dbbf20ba21ba16c3593b3038b3afbd", size = 4084129, upload-time = "2026-09-07T16:06:23.439Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/8d/e0d339616f4810e9051d4aba6887afab289ab1f81875fe908b606cdfd0e3/mcp-2.3.0.tar.gz", hash = "sha256:8b147a50441cf059dc88c684e0aeed3687f0aa0f39c6cde7b90330effd2b34d8", size = 4171955, upload-time = "2026-10-02T22:06:56.091Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/ff/8e7eade68b8a28f7da0ed1085544341b51f9c935dbf6b95c76b7edfea6a0/mcp-2.2.0-py3-none-any.whl", hash = "sha256:bde982589473a060ae145e3406e9a5333fe538c97229ba841f5a7f92be004f81", size = 365656, upload-time = "2026-09-07T16:06:19.711Z" }, + { url = "https://files.pythonhosted.org/packages/20/d7/4a2878f128df729db3ca04e71bfce26405cc2e0c9ebe19c592feeb4c61b9/mcp-2.3.0-py3-none-any.whl", hash = "sha256:dd0c44c089d16453e8ae31a3877a0054d7a2314caaa81f5e0541b9b1734b2377", size = 368664, upload-time = "2026-10-02T22:06:52.119Z" }, ] [[package]] name = "mcp-types" -version = "2.2.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/91/762d7755d971aff8a28d75f7961656148edf27875c8026e6385aaab08ae7/mcp_types-2.2.0.tar.gz", hash = "sha256:d3ed53703ddd10d9c6399f29d322bb66f3f67ab41348ac8556ba23e07fedefad", size = 65892, upload-time = "2026-09-07T16:06:25.187Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/2d/7c251e34207f6c51000312fc8839111ac45cfe02023f90b44e7f1051dd8e/mcp_types-2.3.0.tar.gz", hash = "sha256:d1e46549edb35ee19a94940fcee6d1addd7e589ab7ea92dda83f5d84781fc362", size = 65898, upload-time = "2026-10-02T22:06:57.863Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/d7/6ffba5d8cd5dd9b8a19478875c50e04945314ba5074e84d749283f27f62d/mcp_types-2.2.0-py3-none-any.whl", hash = "sha256:ea476b73ee86709ab5abc9452385ed36cc05907e582355622e294595c9a04f13", size = 69106, upload-time = "2026-09-07T16:06:21.461Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/32583066b155bb5d4507e7887dc88ee23118cc4de8e2a12870c49719e2a8/mcp_types-2.3.0-py3-none-any.whl", hash = "sha256:968efdbdaedfab06adae40d378a34395f1090c5921d4be3c9cde283aaf76d91d", size = 69108, upload-time = "2026-10-02T22:06:54.099Z" }, ] [[package]]