fix(mcp): classify a cancelled per-server fetch instead of reporting a healthy empty server

A cancelled fetch absorbed to [] made that server contribute ServerListOk(tool_count=0), the exact
healthy-but-empty impostor this change removes. Cancellation stays suppressed (the pre-existing
choice); it now carries an internal fault so outcomes stay truthful
This commit is contained in:
Tin Chi Lo 2026-07-13 19:43:11 -07:00
parent f776ea7f9b
commit eefd5e31e5
2 changed files with 22 additions and 2 deletions

View file

@ -3393,9 +3393,9 @@ class MCPServerManager:
except TimeoutError as e:
verbose_logger.warning(f"Timeout while listing tools from {server_name}")
raise MCPServerListError(ServerListFault(tag="timeout"), server_name) from e
except asyncio.CancelledError:
except asyncio.CancelledError as e:
verbose_logger.warning(f"Task cancelled while listing tools from {server_name}")
return []
raise MCPServerListError(ServerListFault(tag="internal"), server_name) from e
except ConnectionError as e:
verbose_logger.warning(f"Connection error while listing tools from {server_name}: {str(e)}")
raise MCPServerListError(ServerListFault(tag="unreachable"), server_name) from e

View file

@ -74,3 +74,23 @@ def test_wire_value_carries_no_prose():
)
def test_single_upstream_http_status_is_truthful(tag, status_code, expected):
assert list_fault_http_status(ServerListFault(tag=tag, status_code=status_code)) == expected
@pytest.mark.asyncio
async def test_cancelled_fetch_is_a_classified_fault_not_a_healthy_empty_server():
"""A cancelled per-server fetch must not masquerade as ok(tool_count=0): cancellation was already
suppressed before the outcome plumbing existed, so it stays suppressed, but as an internal fault
the outcome reporting can see."""
import asyncio
from unittest.mock import AsyncMock, MagicMock
from litellm.proxy._experimental.mcp_server.mcp_server_manager import MCPServerManager
manager = MCPServerManager()
client = MagicMock()
client.list_tools = AsyncMock(side_effect=asyncio.CancelledError())
with pytest.raises(MCPServerListError) as exc_info:
await manager._fetch_tools_with_timeout(client, "cancelled_srv")
assert exc_info.value.fault.tag == "internal"