mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-06 20:19:03 +00:00
fix(mcp): isolate tool discovery failures (#3772)
This commit is contained in:
parent
b0ce4b513c
commit
73874a9b35
@ -615,9 +615,20 @@ async def get_mcp_tools() -> list[BaseTool]:
|
|||||||
tool_name_prefix=True,
|
tool_name_prefix=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get all tools from all servers (discovers tool definitions via
|
async def load_server_tools(server_name: str) -> list[BaseTool]:
|
||||||
# temporary sessions – the persistent-session wrapping is applied below).
|
try:
|
||||||
tools = await client.get_tools()
|
return await client.get_tools(server_name=server_name)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Skipping MCP server '{server_name}' after tool discovery failed: {e}",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Get tools from each server independently so one broken MCP server does
|
||||||
|
# not prevent healthy servers from contributing their tools.
|
||||||
|
tools_by_server = await asyncio.gather(*(load_server_tools(name) for name in servers_config))
|
||||||
|
tools = [tool for server_tools in tools_by_server for tool in server_tools]
|
||||||
logger.info(f"Successfully loaded {len(tools)} tool(s) from MCP servers")
|
logger.info(f"Successfully loaded {len(tools)} tool(s) from MCP servers")
|
||||||
|
|
||||||
# Wrap each tool with persistent-session logic.
|
# Wrap each tool with persistent-session logic.
|
||||||
|
|||||||
@ -889,7 +889,15 @@ async def test_http_transport_tools_not_pooled():
|
|||||||
patch("langchain_mcp_adapters.sessions.create_session", return_value=mock_cm),
|
patch("langchain_mcp_adapters.sessions.create_session", return_value=mock_cm),
|
||||||
):
|
):
|
||||||
mock_client_instance = MockClient.return_value
|
mock_client_instance = MockClient.return_value
|
||||||
mock_client_instance.get_tools = AsyncMock(return_value=[http_tool, stdio_tool])
|
|
||||||
|
async def get_tools_for_server(*, server_name: str | None = None):
|
||||||
|
if server_name == "myserver":
|
||||||
|
return [http_tool]
|
||||||
|
if server_name == "playwright":
|
||||||
|
return [stdio_tool]
|
||||||
|
raise AssertionError(f"unexpected server_name: {server_name}")
|
||||||
|
|
||||||
|
mock_client_instance.get_tools = AsyncMock(side_effect=get_tools_for_server)
|
||||||
|
|
||||||
tools = await get_mcp_tools()
|
tools = await get_mcp_tools()
|
||||||
|
|
||||||
|
|||||||
@ -53,6 +53,46 @@ def test_mcp_tool_sync_wrapper_generation():
|
|||||||
assert result == "result: 42"
|
assert result == "result: 42"
|
||||||
|
|
||||||
|
|
||||||
|
def test_mcp_tool_loading_skips_failed_server():
|
||||||
|
"""A broken MCP server should not drop tools from healthy servers."""
|
||||||
|
|
||||||
|
async def mock_coro(x: int):
|
||||||
|
return f"result: {x}"
|
||||||
|
|
||||||
|
good_tool = StructuredTool(
|
||||||
|
name="good-server_search",
|
||||||
|
description="search from healthy server",
|
||||||
|
args_schema=MockArgs,
|
||||||
|
func=None,
|
||||||
|
coroutine=mock_coro,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_tools_for_server(*, server_name: str | None = None):
|
||||||
|
if server_name == "good-server":
|
||||||
|
return [good_tool]
|
||||||
|
if server_name == "bad-server":
|
||||||
|
raise RuntimeError("SSE endpoint returned text/html")
|
||||||
|
raise AssertionError(f"unexpected server_name: {server_name}")
|
||||||
|
|
||||||
|
mock_client_instance = MagicMock()
|
||||||
|
mock_client_instance.get_tools = AsyncMock(side_effect=get_tools_for_server)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("langchain_mcp_adapters.client.MultiServerMCPClient", return_value=mock_client_instance),
|
||||||
|
patch("deerflow.config.extensions_config.ExtensionsConfig.from_file", return_value=MagicMock(model_extra={})),
|
||||||
|
patch("deerflow.mcp.tools.build_servers_config", return_value={"good-server": {}, "bad-server": {}}),
|
||||||
|
patch("deerflow.mcp.tools.get_initial_oauth_headers", new_callable=AsyncMock, return_value={}),
|
||||||
|
patch("deerflow.mcp.tools.build_oauth_tool_interceptor", return_value=None),
|
||||||
|
patch("deerflow.mcp.tools.logger.warning") as mock_warning,
|
||||||
|
):
|
||||||
|
tools = asyncio.run(get_mcp_tools())
|
||||||
|
|
||||||
|
assert [tool.name for tool in tools] == ["good-server_search"]
|
||||||
|
assert tools[0].func is not None
|
||||||
|
mock_warning.assert_called_once()
|
||||||
|
assert "bad-server" in mock_warning.call_args[0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_mcp_tool_sync_wrapper_in_running_loop():
|
def test_mcp_tool_sync_wrapper_in_running_loop():
|
||||||
"""Test the shared sync wrapper from production code."""
|
"""Test the shared sync wrapper from production code."""
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user