mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-02 21:58:11 +00:00
fix(mcp): warn and return empty when native MCP server returns no tools
This commit is contained in:
@@ -374,6 +374,7 @@ class MCPToolResolver:
|
||||
"MCP connection failed due to event loop cleanup issues. "
|
||||
"This may be due to authentication errors or server unavailability."
|
||||
) from e
|
||||
raise
|
||||
except asyncio.CancelledError as e:
|
||||
raise ConnectionError(
|
||||
"MCP connection was cancelled. This may indicate an authentication "
|
||||
@@ -401,6 +402,13 @@ class MCPToolResolver:
|
||||
filtered_tools.append(tool)
|
||||
tools_list = filtered_tools
|
||||
|
||||
if not tools_list:
|
||||
self._logger.log(
|
||||
"warning",
|
||||
f"No tools discovered from MCP server: {server_name}",
|
||||
)
|
||||
return cast(list[BaseTool], []), []
|
||||
|
||||
def _client_factory() -> MCPClient:
|
||||
transport, _ = self._create_transport(mcp_config)
|
||||
return MCPClient(
|
||||
|
||||
99
lib/crewai/tests/mcp/test_tool_resolver_native.py
Normal file
99
lib/crewai/tests/mcp/test_tool_resolver_native.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""Tests for MCPToolResolver native (non-AMP) resolution paths."""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from crewai.agent.core import Agent
|
||||
from crewai.mcp.config import MCPServerHTTP
|
||||
from crewai.mcp.tool_resolver import MCPToolResolver
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def agent():
|
||||
return Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def resolver(agent):
|
||||
return MCPToolResolver(agent=agent, logger=agent._logger)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def http_config():
|
||||
return MCPServerHTTP(url="https://mcp.example.com/api")
|
||||
|
||||
|
||||
class TestResolveNativeEmptyTools:
|
||||
@patch("crewai.mcp.tool_resolver.MCPClient")
|
||||
def test_logs_warning_and_returns_empty_when_server_has_no_tools(
|
||||
self, mock_client_class, resolver, http_config
|
||||
):
|
||||
mock_client = AsyncMock()
|
||||
mock_client.list_tools = AsyncMock(return_value=[])
|
||||
mock_client.connected = False
|
||||
mock_client.connect = AsyncMock()
|
||||
mock_client.disconnect = AsyncMock()
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
mock_log = MagicMock()
|
||||
resolver._logger = MagicMock(log=mock_log)
|
||||
|
||||
tools, clients = resolver._resolve_native(http_config)
|
||||
|
||||
assert tools == []
|
||||
assert clients == []
|
||||
warning_calls = [
|
||||
call for call in mock_log.call_args_list if call.args[0] == "warning"
|
||||
]
|
||||
assert any(
|
||||
"No tools discovered from MCP server" in call.args[1]
|
||||
for call in warning_calls
|
||||
)
|
||||
|
||||
@patch("crewai.mcp.tool_resolver.MCPClient")
|
||||
def test_logs_warning_when_tool_filter_removes_all_tools(
|
||||
self, mock_client_class, resolver
|
||||
):
|
||||
mock_client = AsyncMock()
|
||||
mock_client.list_tools = AsyncMock(
|
||||
return_value=[{"name": "search", "description": "Search"}]
|
||||
)
|
||||
mock_client.connected = False
|
||||
mock_client.connect = AsyncMock()
|
||||
mock_client.disconnect = AsyncMock()
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
config = MCPServerHTTP(
|
||||
url="https://mcp.example.com/api",
|
||||
tool_filter=lambda _tool: False,
|
||||
)
|
||||
|
||||
mock_log = MagicMock()
|
||||
resolver._logger = MagicMock(log=mock_log)
|
||||
|
||||
tools, clients = resolver._resolve_native(config)
|
||||
|
||||
assert tools == []
|
||||
assert clients == []
|
||||
warning_calls = [
|
||||
call for call in mock_log.call_args_list if call.args[0] == "warning"
|
||||
]
|
||||
assert any(
|
||||
"No tools discovered from MCP server" in call.args[1]
|
||||
for call in warning_calls
|
||||
)
|
||||
|
||||
|
||||
class TestResolveNativeRuntimeError:
|
||||
@patch("crewai.mcp.tool_resolver.asyncio.run")
|
||||
def test_unmatched_runtime_error_is_wrapped_not_swallowed(
|
||||
self, mock_asyncio_run, resolver, http_config
|
||||
):
|
||||
mock_asyncio_run.side_effect = RuntimeError("some other failure")
|
||||
|
||||
with pytest.raises(RuntimeError, match="Failed to get native MCP tools"):
|
||||
resolver._resolve_native(http_config)
|
||||
Reference in New Issue
Block a user