From 51aa021c6e1d418c77c48d8adf4971cbe6ce13a0 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:37:26 -0700 Subject: [PATCH 1/2] fix(mcp): return camelCase tool keys from /v1/mcp/tools after the SDK 2 upgrade SDK 2 spells the Tool model's Python attributes in snake_case behind camelCase aliases, so dumping attribute names handed scripts input_schema and output_schema instead of the inputSchema and outputSchema v1.102.0 returned. Dump each tool by its MCP wire aliases, as the other list routes do, and pin the shape with a regression test. Also drop an unused tools dict in the Responses MCP stream iterator. --- .../mcp_management_endpoints.py | 2 +- .../responses/mcp/mcp_streaming_iterator.py | 10 ------- .../test_mcp_management_endpoints.py | 26 +++++++++++++++++++ 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/litellm/proxy/management_endpoints/mcp_management_endpoints.py b/litellm/proxy/management_endpoints/mcp_management_endpoints.py index c1388e8bb81..6e0f0415951 100644 --- a/litellm/proxy/management_endpoints/mcp_management_endpoints.py +++ b/litellm/proxy/management_endpoints/mcp_management_endpoints.py @@ -983,7 +983,7 @@ if MCP_AVAILABLE: mcp_server_auth_headers=None, ) tools: Final = listing.tools - dumped_tools: Final = [dict(tool) for tool in tools] + dumped_tools: Final = [tool.model_dump(by_alias=True) for tool in tools] return {"tools": dumped_tools} diff --git a/litellm/responses/mcp/mcp_streaming_iterator.py b/litellm/responses/mcp/mcp_streaming_iterator.py index c60020ab979..3b5cb85862d 100644 --- a/litellm/responses/mcp/mcp_streaming_iterator.py +++ b/litellm/responses/mcp/mcp_streaming_iterator.py @@ -91,16 +91,6 @@ async def create_mcp_list_tools_events( # Use the pre-processed MCP tools that were already fetched, filtered, and deduplicated by the parent filtered_mcp_tools: Final = pre_processed_mcp_tools - # Convert tools to dict format for the event - _mcp_tools_dict: Final = [ - tool.model_dump() - if hasattr(tool, "model_dump") and callable(getattr(tool, "model_dump", None)) - else tool.__dict__ - if hasattr(tool, "__dict__") - else {"name": getattr(tool, "name", str(tool))} - for tool in filtered_mcp_tools - ] - # Emit list tools completed event completed_event: Final = MCPListToolsCompletedEvent( type=ResponsesAPIStreamEvents.MCP_LIST_TOOLS_COMPLETED, diff --git a/tests/test_litellm/proxy/management_endpoints/test_mcp_management_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_mcp_management_endpoints.py index 80773f314d8..39351ac6dad 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_mcp_management_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_mcp_management_endpoints.py @@ -7824,3 +7824,29 @@ class TestDeleteMCPGatewaySessions: assert result.terminated_sessions == 2 assert {s.user_id for s in result.sessions} == {"bob"} assert "sk-live-bob" not in result.model_dump_json() + + +class TestGetMcpToolsWireShape: + @pytest.mark.asyncio + async def test_get_mcp_tools_returns_each_tool_in_mcp_wire_spelling(self): + """GET /v1/mcp/tools hands scripts each tool in the MCP wire spelling (`inputSchema`, + `outputSchema`, `_meta`), the shape v1.102.0 returned and the shape /mcp-rest/tools/list and the + JSON-RPC tools/list still return. SDK 2 renamed the Tool model's Python attributes to snake_case + behind camelCase aliases, so dumping attribute names leaked `input_schema` to every reader.""" + from mcp.types import ListToolsResult, Tool + + add_schema = {"type": "object", "properties": {"a": {"type": "integer"}}, "required": ["a"]} + listed = ListToolsResult( + tools=[Tool(name="add", description="Add", inputSchema=add_schema, outputSchema={"type": "integer"})] + ) + with patch( + "litellm.proxy._experimental.mcp_server.server._list_mcp_tools", + AsyncMock(return_value=listed), + ): + result = await mgmt_endpoints.get_mcp_tools(user_api_key_dict=generate_mock_user_api_key_auth()) + + (tool,) = result["tools"] + assert tool["inputSchema"] == add_schema + assert tool["outputSchema"] == {"type": "integer"} + assert "_meta" in tool + assert not {"input_schema", "output_schema", "meta"} & tool.keys() From b58c9349dd5854647bcd423bc81057915ac7b9e8 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Mon, 21 Sep 2026 16:15:43 -0700 Subject: [PATCH 2/2] test(mcp): drop the docstring from the wire spelling regression test --- .../management_endpoints/test_mcp_management_endpoints.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/test_litellm/proxy/management_endpoints/test_mcp_management_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_mcp_management_endpoints.py index 39351ac6dad..ef8f5e76219 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_mcp_management_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_mcp_management_endpoints.py @@ -7829,10 +7829,6 @@ class TestDeleteMCPGatewaySessions: class TestGetMcpToolsWireShape: @pytest.mark.asyncio async def test_get_mcp_tools_returns_each_tool_in_mcp_wire_spelling(self): - """GET /v1/mcp/tools hands scripts each tool in the MCP wire spelling (`inputSchema`, - `outputSchema`, `_meta`), the shape v1.102.0 returned and the shape /mcp-rest/tools/list and the - JSON-RPC tools/list still return. SDK 2 renamed the Tool model's Python attributes to snake_case - behind camelCase aliases, so dumping attribute names leaked `input_schema` to every reader.""" from mcp.types import ListToolsResult, Tool add_schema = {"type": "object", "properties": {"a": {"type": "integer"}}, "required": ["a"]}