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.
This commit is contained in:
mateo-berri 2026-09-21 15:37:26 -07:00
parent 3d26a29a1a
commit 51aa021c6e
3 changed files with 27 additions and 11 deletions

View file

@ -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}

View file

@ -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,

View file

@ -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()