From db4cb9377027e1a61238cd3de64d247c723b4113 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 20 Dec 2025 13:00:47 +0000 Subject: [PATCH] fix: inject MCP tools in standalone agent execution (fixes #4133) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The LLM was not seeing MCP tools when using standalone agent execution (without Crew) because the prepare_tools function in agent/utils.py did not inject MCP tools. Changes: - Modified prepare_tools() in agent/utils.py to inject MCP tools when agent.mcps is configured, with graceful error handling - Fixed Agent.kickoff_async() to inject MCP tools like kickoff() does - Added comprehensive tests for MCP tool injection in prepare_tools The fix ensures MCP tools are visible to the LLM in both: 1. Standalone agent execution via execute_task/aexecute_task 2. Async agent execution via kickoff_async Co-Authored-By: João --- lib/crewai/src/crewai/agent/core.py | 10 ++ lib/crewai/src/crewai/agent/utils.py | 24 ++- lib/crewai/tests/mcp/test_mcp_config.py | 199 ++++++++++++++++++++++++ 3 files changed, 232 insertions(+), 1 deletion(-) diff --git a/lib/crewai/src/crewai/agent/core.py b/lib/crewai/src/crewai/agent/core.py index f59724343..d0a60d182 100644 --- a/lib/crewai/src/crewai/agent/core.py +++ b/lib/crewai/src/crewai/agent/core.py @@ -1576,7 +1576,17 @@ class Agent(BaseAgent): Returns: LiteAgentOutput: The result of the agent execution. """ + if self.apps: + platform_tools = self.get_platform_tools(self.apps) + if platform_tools and self.tools is not None: + self.tools.extend(platform_tools) + if self.mcps: + mcps = self.get_mcp_tools(self.mcps) + if mcps and self.tools is not None: + self.tools.extend(mcps) + lite_agent = LiteAgent( + id=self.id, role=self.role, goal=self.goal, backstory=self.backstory, diff --git a/lib/crewai/src/crewai/agent/utils.py b/lib/crewai/src/crewai/agent/utils.py index 59d92e302..65492385d 100644 --- a/lib/crewai/src/crewai/agent/utils.py +++ b/lib/crewai/src/crewai/agent/utils.py @@ -251,6 +251,10 @@ def prepare_tools( ) -> list[BaseTool]: """Prepare tools for task execution and create agent executor. + This function prepares tools for task execution, including injecting MCP tools + if the agent has MCP server configurations. MCP tools are merged with existing + tools, with MCP tools replacing any existing tools with the same name. + Args: agent: The agent instance. tools: Optional list of tools. @@ -259,7 +263,25 @@ def prepare_tools( Returns: The list of tools to use. """ - final_tools = tools or agent.tools or [] + # Create a copy to avoid mutating the original list + final_tools = list(tools or agent.tools or []) + + # Inject MCP tools if agent has mcps configured + if hasattr(agent, "mcps") and agent.mcps: + try: + mcp_tools = agent.get_mcp_tools(agent.mcps) + if mcp_tools: + # Merge tools: MCP tools replace existing tools with the same name + mcp_tool_names = {tool.name for tool in mcp_tools} + final_tools = [ + tool for tool in final_tools if tool.name not in mcp_tool_names + ] + final_tools.extend(mcp_tools) + except Exception as e: + agent._logger.log( + "warning", f"Failed to get MCP tools, continuing without them: {e}" + ) + agent.create_agent_executor(tools=final_tools, task=task) return final_tools diff --git a/lib/crewai/tests/mcp/test_mcp_config.py b/lib/crewai/tests/mcp/test_mcp_config.py index e55a7d504..d20f9a2c6 100644 --- a/lib/crewai/tests/mcp/test_mcp_config.py +++ b/lib/crewai/tests/mcp/test_mcp_config.py @@ -3,7 +3,9 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest from crewai.agent.core import Agent +from crewai.agent.utils import prepare_tools from crewai.mcp.config import MCPServerHTTP, MCPServerSSE, MCPServerStdio +from crewai.task import Task from crewai.tools.base_tool import BaseTool @@ -198,3 +200,200 @@ async def test_mcp_tool_execution_in_async_context(mock_tool_definitions): assert result == "test result" mock_client.call_tool.assert_called() + + +def test_prepare_tools_injects_mcp_tools(mock_tool_definitions): + """Test that prepare_tools injects MCP tools when agent has mcps configured. + + This is the core fix for issue #4133 - LLM doesn't see MCP tools when + using standalone agent execution (without Crew). + """ + http_config = MCPServerHTTP(url="https://api.example.com/mcp") + + with patch("crewai.agent.core.MCPClient") as mock_client_class: + mock_client = AsyncMock() + mock_client.list_tools = AsyncMock(return_value=mock_tool_definitions) + mock_client.connected = False + mock_client.connect = AsyncMock() + mock_client.disconnect = AsyncMock() + mock_client_class.return_value = mock_client + + agent = Agent( + role="Test Agent", + goal="Test goal", + backstory="Test backstory", + mcps=[http_config], + ) + + task = Task( + description="Test task", + expected_output="Test output", + agent=agent, + ) + + final_tools = prepare_tools(agent, None, task) + + assert len(final_tools) == 2 + assert all(isinstance(tool, BaseTool) for tool in final_tools) + tool_names = [tool.name for tool in final_tools] + assert any("test_tool_1" in name for name in tool_names) + assert any("test_tool_2" in name for name in tool_names) + + +def test_prepare_tools_merges_mcp_tools_with_existing_tools(mock_tool_definitions): + """Test that prepare_tools merges MCP tools with existing agent tools. + + MCP tools are added alongside existing tools. Note that MCP tools have + prefixed names (based on server URL), so they won't conflict with + existing tools that have the same base name. + """ + http_config = MCPServerHTTP(url="https://api.example.com/mcp") + + class ExistingTool(BaseTool): + name: str = "existing_tool" + description: str = "An existing tool" + + def _run(self, **kwargs): + return "existing result" + + class AnotherTool(BaseTool): + name: str = "another_tool" + description: str = "Another existing tool" + + def _run(self, **kwargs): + return "another result" + + with patch("crewai.agent.core.MCPClient") as mock_client_class: + mock_client = AsyncMock() + mock_client.list_tools = AsyncMock(return_value=mock_tool_definitions) + mock_client.connected = False + mock_client.connect = AsyncMock() + mock_client.disconnect = AsyncMock() + mock_client_class.return_value = mock_client + + agent = Agent( + role="Test Agent", + goal="Test goal", + backstory="Test backstory", + tools=[ExistingTool(), AnotherTool()], + mcps=[http_config], + ) + + task = Task( + description="Test task", + expected_output="Test output", + agent=agent, + ) + + final_tools = prepare_tools(agent, None, task) + + assert len(final_tools) == 4 + tool_names = [tool.name for tool in final_tools] + assert "existing_tool" in tool_names + assert "another_tool" in tool_names + assert any("test_tool_1" in name for name in tool_names) + assert any("test_tool_2" in name for name in tool_names) + + +def test_prepare_tools_does_not_mutate_original_tools_list(mock_tool_definitions): + """Test that prepare_tools does not mutate the original tools list.""" + http_config = MCPServerHTTP(url="https://api.example.com/mcp") + + class ExistingTool(BaseTool): + name: str = "existing_tool" + description: str = "An existing tool" + + def _run(self, **kwargs): + return "existing result" + + original_tools = [ExistingTool()] + original_tools_copy = list(original_tools) + + with patch("crewai.agent.core.MCPClient") as mock_client_class: + mock_client = AsyncMock() + mock_client.list_tools = AsyncMock(return_value=mock_tool_definitions) + mock_client.connected = False + mock_client.connect = AsyncMock() + mock_client.disconnect = AsyncMock() + mock_client_class.return_value = mock_client + + agent = Agent( + role="Test Agent", + goal="Test goal", + backstory="Test backstory", + tools=original_tools, + mcps=[http_config], + ) + + task = Task( + description="Test task", + expected_output="Test output", + agent=agent, + ) + + final_tools = prepare_tools(agent, original_tools, task) + + assert len(original_tools) == len(original_tools_copy) + assert len(final_tools) == 3 + + +def test_prepare_tools_handles_mcp_failure_gracefully(mock_tool_definitions): + """Test that prepare_tools continues without MCP tools if get_mcp_tools fails.""" + http_config = MCPServerHTTP(url="https://api.example.com/mcp") + + class ExistingTool(BaseTool): + name: str = "existing_tool" + description: str = "An existing tool" + + def _run(self, **kwargs): + return "existing result" + + with patch("crewai.agent.core.MCPClient") as mock_client_class: + mock_client_class.side_effect = Exception("Connection failed") + + agent = Agent( + role="Test Agent", + goal="Test goal", + backstory="Test backstory", + tools=[ExistingTool()], + mcps=[http_config], + ) + + task = Task( + description="Test task", + expected_output="Test output", + agent=agent, + ) + + final_tools = prepare_tools(agent, None, task) + + assert len(final_tools) == 1 + assert final_tools[0].name == "existing_tool" + + +def test_prepare_tools_without_mcps(): + """Test that prepare_tools works normally when agent has no mcps configured.""" + class ExistingTool(BaseTool): + name: str = "existing_tool" + description: str = "An existing tool" + + def _run(self, **kwargs): + return "existing result" + + agent = Agent( + role="Test Agent", + goal="Test goal", + backstory="Test backstory", + tools=[ExistingTool()], + ) + + task = Task( + description="Test task", + expected_output="Test output", + agent=agent, + ) + + final_tools = prepare_tools(agent, None, task) + + assert len(final_tools) == 1 + assert final_tools[0].name == "existing_tool"