From 4df4c9cf004b635da1bf79334f89bf6d763e0d98 Mon Sep 17 00:00:00 2001 From: kigland Date: Sun, 7 Jun 2026 10:39:16 +0800 Subject: [PATCH] fix mcp list result parsing --- litellm/experimental_mcp_client/client.py | 21 ++++++++-- .../test_mcp_client.py | 38 +++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/litellm/experimental_mcp_client/client.py b/litellm/experimental_mcp_client/client.py index c6d427e7f09..039ac428352 100644 --- a/litellm/experimental_mcp_client/client.py +++ b/litellm/experimental_mcp_client/client.py @@ -18,6 +18,7 @@ from typing import ( Union, ) import httpx +import mcp.types as types from mcp import ClientSession, ReadResourceResult, Resource, StdioServerParameters from mcp.client.sse import sse_client from mcp.client.stdio import stdio_client @@ -41,7 +42,7 @@ from mcp.types import ( TextContent, ) from mcp.types import Tool as MCPTool -from pydantic import AnyUrl +from pydantic import AnyUrl, Field from litellm._logging import verbose_logger from litellm.constants import MCP_CLIENT_TIMEOUT, MCP_NPM_CACHE_DIR from litellm.llms.custom_httpx.http_handler import get_ssl_configuration @@ -81,6 +82,14 @@ def _first_non_cancelled_cause(exc: BaseException) -> Optional[BaseException]: return None +class _LenientListPromptsResult(types.ListPromptsResult): + prompts: List[Prompt] = Field(default_factory=list) + + +class _LenientListResourcesResult(types.ListResourcesResult): + resources: List[Resource] = Field(default_factory=list) + + TSessionResult = TypeVar("TSessionResult") @@ -628,7 +637,10 @@ class MCPClient: ) async def _list_prompts_operation(session: ClientSession): - return await session.list_prompts() + return await session.send_request( + types.ClientRequest(types.ListPromptsRequest(params=None)), + _LenientListPromptsResult, + ) try: result = await self.run_with_session(_list_prompts_operation) @@ -713,7 +725,10 @@ class MCPClient: ) async def _list_resources_operation(session: ClientSession): - return await session.list_resources() + return await session.send_request( + types.ClientRequest(types.ListResourcesRequest(params=None)), + _LenientListResourcesResult, + ) try: result = await self.run_with_session(_list_resources_operation) 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 c9e500b4a5b..468fe61d2e5 100644 --- a/tests/test_litellm/experimental_mcp_client/test_mcp_client.py +++ b/tests/test_litellm/experimental_mcp_client/test_mcp_client.py @@ -542,6 +542,44 @@ class TestExecuteSessionOperationSurfacesTransportError: result = await client._execute_session_operation(transport_ctx, _op) assert result == "done" + @pytest.mark.asyncio + async def test_list_resources_tolerates_missing_resources_field(self): + client = MCPClient(server_url="http://example.com/mcp", transport_type="http") + session = AsyncMock() + + async def send_request(_request, result_type): + assert result_type is mcp_client_module._LenientListResourcesResult + return result_type.model_validate({}) + + session.send_request = AsyncMock(side_effect=send_request) + + async def run_with_session(operation): + return await operation(session) + + client.run_with_session = AsyncMock(side_effect=run_with_session) + + assert await client.list_resources() == [] + session.send_request.assert_awaited_once() + + @pytest.mark.asyncio + async def test_list_prompts_tolerates_missing_prompts_field(self): + client = MCPClient(server_url="http://example.com/mcp", transport_type="http") + session = AsyncMock() + + async def send_request(_request, result_type): + assert result_type is mcp_client_module._LenientListPromptsResult + return result_type.model_validate({}) + + session.send_request = AsyncMock(side_effect=send_request) + + async def run_with_session(operation): + return await operation(session) + + client.run_with_session = AsyncMock(side_effect=run_with_session) + + assert await client.list_prompts() == [] + session.send_request.assert_awaited_once() + if __name__ == "__main__": pytest.main([__file__])