Repository navigation
refactor(mcp): upgrade SDK2 while preserving legacy gateway behavior - #41718
Conversation
|
@greptileai Please review commit 6aa921c for dependency isolation, reproducible installation checks, SDK1 preservation, and the new CI workflow |
|
bugbot run Please review commit 6aa921c for dependency isolation, snapshot validation, clean test execution, and existing SDK1 compatibility |
|
@veria-ai Please review commit 6aa921c for isolation, artifact integrity, workflow permissions, and exposure from test-only minimum dependency fixtures |
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
@greptileai Please review commit 21beb9b, including the Codecov key URL repair, pinned uploader, and package-name normalization regression tests |
|
bugbot run. Please review commit 21beb9b and verify the package-name normalization repair against dotted names and repeated punctuation |
|
@veria-ai Please review commit 21beb9b for signature verification, isolated dependency inventories, workflow permissions, and the existing minimum-fixture exposure boundaries |
|
@greptileai Please review dfd0474, which preserves repository-relative coverage paths while keeping every measured line hit and branch result unchanged |
|
bugbot run. Please review dfd0474 and check the reporting-only coverage configuration, file paths, and preservation of measured results |
|
@veria-ai Please review dfd0474, including the reporting-only coverage export, signature verification, and previously reviewed isolated dependency gate boundaries |
Replace the bespoke dependency-install CI gate with a real migration: require mcp>=2.2.0,<3 alongside httpx2>=2.5.0,<3 and pydantic>=2.12.0,<3 in the proxy and mcp extras, drop langchain-mcp-adapters (pins mcp<2) from the dev group, and remove the dependency-install workflow and tests/mcp_dependency_tests that only exercised the old pins. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
McpError -> MCPError (new code/message/data constructor), camelCase model attributes and constructor kwargs -> snake_case, RequestResponder -> ClientSession message handler receiving ServerNotification | Exception, RequestContext -> ClientRequestContext, read_timeout_seconds -> float, server_capabilities property, JSONRPCMessage union parsed via TypeAdapter, and httpx -> httpx2 for every object handed to the SDK transports (MCPSigV4Auth, the httpx client factory, outbound_credentials auth classes and resolver return types). Helpers that serve both litellm httpx clients and the SDK's httpx2 transport accept both response types. The SDK read-timeout code is now the JSON-RPC REQUEST_TIMEOUT (-32001) instead of HTTP 408; as_mcp_read_timeout keeps the TimeoutError context discriminator. Upstream transport exceptions and responses found in exception trees are matched as httpx2 alongside httpx. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Rename McpError/isError/inputSchema-style references to the SDK 2 spellings, parse the JSONRPCMessage union with a TypeAdapter, and drive the SDK transports off httpx2 MockTransport injection where respx can no longer intercept. Adjust for SDK 2 behavior: the initialize handshake negotiates handshake-era protocol versions only, an empty SSE stream surfaces CONNECTION_CLOSED, non-2xx tool responses surface INTERNAL_ERROR MCPError instead of HTTPStatusError, and the SDK read timeout carries the JSON-RPC REQUEST_TIMEOUT code. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
bugbot run. Please review fb56a14 after merging main and confirm the earlier session-expiry and guardrail serialization fixes remain sound |
|
@veria-ai Please review fb56a14 after merging main, including preserved legacy authentication, session handling, and refreshed live compatibility evidence |
|
@greptileai Please review 8f3562e: dedicated MCP workflow removed, integration coverage retained in shared CI, isolated SDK1 smoke test preserved |
|
bugbot run. Please review 8f3562e for shared MCP test setup, single execution, isolated legacy peer installation, and coverage reporting |
|
@veria-ai Please review 8f3562e: MCP integration tests now use shared CI, retaining isolated SDK1 compatibility checks and existing security coverage |
|
@greptileai Please review 78e1103: legacy peer installation now shares the existing dependency setup deadline; workflow safety and compatibility checks pass |
|
bugbot run. Please review 78e1103, confirming isolated SDK1 setup remains limited to the MCP shard without increasing shared setup timeouts |
|
@veria-ai Please review 78e1103: isolated legacy peer setup now runs inside the existing dependency step, preserving timeout and compatibility safeguards |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 78e1103. Configure here.
TLDR
Problem this solves:
litellm[proxy]pinsmcp<2, so operators cannot install MCP SDK 2How it solves it:
MCP-Protocol-Versionheaders with HTTP 400 for nowThis finishes LIT-7737 as the dependency upgrade plus the minimum legacy parity work. The HTTPX2 transport and auth, signing, and server-import changes are required compatibility work; the remaining LIT-7738, LIT-7739, and LIT-7740 functionality keeps its planned order, and LIT-3467 and LIT-4506 keep their independent test ownership. The Phase 0 release, rollback demonstration, and LIT-7754 canary observation remain required before modern behavior is enabled
Review-driven changes on this branch:
.github/workflows/test-mcp.ymlis gone and the existing unit test matrix runstests/mcp_testsonce asmcp-integrationwith the shared runner and coverage upload, the only job that installs the isolated SDK 1 peer, inside the existing dependency setup allowance. HTTP 404 responses now reach the SDK's own protocol handling, with regressions for an expired session, a JSON-RPC error carried by HTTP 404, an initial HTTP 404, and a fresh operation on a new session, and no automatic replay of tool side effects. Cisco inspection serializes image, audio, and embedded-resource fields with MCP wire aliases for direct SDK results and logging wrappers. Configuration-only tests no longer launch background upstream discovery, shared request-context fixtures replace repeated helpers, and the new dependency matrix runs install and import checks without multiplying the full suites by Python version, skipping its expensive steps for unrelated backend, UI, or docs changes while keeping check reporting. Main was merged at 6718fd6 without conflictsUser Flow
Before: an operator who needs MCP SDK 2 in the gateway's Python environment cannot install it, so they stay on SDK 1
uv pip install 'litellm[proxy]==1.101.0' 'mcp>=2.2,<3'No solution found when resolving dependencies: litellm[proxy]==1.101.0 depends on mcp>=1.28.1,<2.0, and nothing is installedinitializebody naming"protocolVersion": "2025-06-18"and gets HTTP 200 with"protocolVersion": "2025-06-18"tools/listthentools/callformath_stdio-addwith{"a": 3, "b": 4}on the same route and gets the tool list and{"content":[{"type":"text","text":"7"}],"structuredContent":{"result":7},"isError":false}MCP-Protocol-Version: 2026-07-28to that POST gets HTTP 200, the header ignored, and the same legacy session as beforeAfter: the same operator installs SDK 2 and every existing client gets the same responses it got before
uv pip install 'litellm[proxy]' 'mcp>=2.2,<3'mcp2.2,httpx2, andpydantic2.12 or newerinitializebody and gets HTTP 200 with"protocolVersion": "2025-06-18"tools/listandtools/callreturn the same tool list and the same{"content":[{"type":"text","text":"7"}],"structuredContent":{"result":7},"isError":false}, and an SDK 1 LangChain client running in another environment lists and calls the tools unchangedMCP-Protocol-Version: 2026-07-28now gets HTTP 400 with JSON-RPC error-32600Unsupported MCP-Protocol-Version 2026-07-28; supported: 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25, until modern MCP behavior ships in a later phaseRelevant issues
Includes the Python 3.14 dependency-floor prerequisites from #41706, which is closed in favor of this PR. The agreed overlap and remaining acceptance are recorded in LIT-7737, LIT-7738, LIT-7739, and LIT-7740
Affected release
Linear ticket
Resolves LIT-7737
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
QA run at 78e1103, verdict PASS: the merge base cannot install MCP SDK 2 next to the proxy and this PR can, while Claude Code, an unchanged SDK 1 LangChain client, a stock SDK 2 client, and raw curl get the same results on both legs
Each leg is one proxy booted from a source checkout with 2 uvicorn workers and no database, in front of the same two upstream MCP servers: an SDK 1 (
mcp==1.28.1) math server over stdio, and the same server over streamable HTTP started withMCP_ADD_OFFSET=100, so add(3,4) is 7 over stdio and 107 over HTTP. Before is the merge base 6718fd6 withmcp1.28.1 onhttp://127.0.0.1:58904. After is this PR's head withmcp2.2.0,httpx22.13.0, andpydantic2.13.4 onhttp://127.0.0.1:58906.sk-1234is a local fixture key and no LLM call is involvedProxy config for both legs, where only the upstream port differs (58905 before, 58907 after):
upstream_server.pyis a copy of the merge base'stests/mcp_tests/mcp_server.py. SetPto the leg's URL and define:The SDK 1 LangChain client, run from an isolated
mcp==1.28.1pluslangchain-mcp-adapters==0.2.1environment:The stock SDK 2 client, run from an
mcp==2.2.0environment:Claude Code v2.1.278 was driven interactively in its TUI under tmux on both legs, started with
claude --mcp-config claude-mcp.json --strict-mcp-config, whereclaude-mcp.jsonis:{"mcpServers":{"litellm":{"type":"http","url":"http://127.0.0.1:<port>/mcp","headers":{"Authorization":"Bearer sk-1234"}}}}Before (6718fd6,
mcp1.28.1,http://127.0.0.1:58904)/mcpshowslitellmconnected with 6 tools/mcp, wheretools/listis trimmed to its status and tool namesMCP-Protocol-Versionheader on a legacy body is ignoredAfter (78e1103,
mcp2.2.0,http://127.0.0.1:58906)/mcpshowslitellmconnected with 6 toolsMCP-Protocol-Versionheader on a legacy body is now rejected, the one intended behavior changeWhat the run noticed:
tools/callworks without initialize or session id, left aloneapplication/jsoncomes first, left alonestructuredContent, not the text block, left aloneAuthor's checks at the same head that this run did not repeat
The no-database legs above cannot mint a key without server grants, and the last two checks exercise the SDK and a guardrail hook directly, so they stay here as the author reported them
tools/callformath_stdio-addwith a key that has no server grants returns HTTP 200 withisError:trueandError: User not allowed to call this tool.on both legsMcp-Session-Id, listing tools fails withMcpError: Session terminatedbefore andMCPError: Not Found: Session has been terminatedafter, and a fresh session returns7on bothObject of type AnyUrl is not JSON serializable, and after both captured requests carrymimeTypewithimage/png,audio/wav, and nestedtext/plainType
🐛 Bug Fix
🧹 Refactoring
🚄 Infrastructure
✅ Test
Caveats (if any)
Severe
langchain-mcp-adapters==0.2.1uses removed SDK 1 APIs although its metadata allows MCP 2; it stays tested here as an isolated network clientMedium
Low
QA runbook
The only tests/e2e change is the shared OAuth helper's port to SDK 2 (
httpx2client,AuthorizationCodeResultcallback result, two-stream transport tuple). No e2e test was added or changed, so tests/e2e/mcp/test_mcp_chat_completion_oauth_e2e.py keeps its two cases and there is nothing new to reproduce by handFinal Attestation