Files
crewAI/lib/crewai/tests/crew/test_mcp_crew_integration.py
2025-10-20 00:13:22 -07:00

503 lines
20 KiB
Python

"""Tests for Crew MCP integration functionality."""
import pytest
from unittest.mock import Mock, patch, MagicMock
# Import from the source directory
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../src'))
from crewai.agent import Agent
from crewai.crew import Crew
from crewai.task import Task
from crewai.tools.mcp_tool_wrapper import MCPToolWrapper
class TestCrewMCPIntegration:
"""Test suite for Crew MCP integration functionality."""
@pytest.fixture
def mcp_agent(self):
"""Create an agent with MCP tools."""
return Agent(
role="MCP Research Agent",
goal="Research using MCP tools",
backstory="Agent with access to MCP tools for research",
mcps=["https://api.example.com/mcp"]
)
@pytest.fixture
def regular_agent(self):
"""Create a regular agent without MCP tools."""
return Agent(
role="Regular Agent",
goal="Regular tasks without MCP",
backstory="Standard agent without MCP access"
)
@pytest.fixture
def sample_task(self, mcp_agent):
"""Create a sample task for testing."""
return Task(
description="Research AI frameworks using available tools",
expected_output="Comprehensive research report",
agent=mcp_agent
)
@pytest.fixture
def mock_mcp_tools(self):
"""Create mock MCP tools."""
tool1 = Mock(spec=MCPToolWrapper)
tool1.name = "mcp_server_search_tool"
tool1.description = "Search tool from MCP server"
tool2 = Mock(spec=MCPToolWrapper)
tool2.name = "mcp_server_analysis_tool"
tool2.description = "Analysis tool from MCP server"
return [tool1, tool2]
def test_crew_creation_with_mcp_agent(self, mcp_agent, sample_task):
"""Test crew creation with MCP-enabled agent."""
crew = Crew(
agents=[mcp_agent],
tasks=[sample_task],
verbose=False
)
assert crew is not None
assert len(crew.agents) == 1
assert len(crew.tasks) == 1
assert crew.agents[0] == mcp_agent
def test_crew_add_mcp_tools_method_exists(self):
"""Test that Crew class has _add_mcp_tools method."""
crew = Crew(agents=[], tasks=[])
assert hasattr(crew, '_add_mcp_tools')
assert callable(crew._add_mcp_tools)
def test_crew_inject_mcp_tools_method_exists(self):
"""Test that Crew class has _inject_mcp_tools method."""
crew = Crew(agents=[], tasks=[])
assert hasattr(crew, '_inject_mcp_tools')
assert callable(crew._inject_mcp_tools)
def test_inject_mcp_tools_with_mcp_agent(self, mcp_agent, mock_mcp_tools):
"""Test MCP tools injection with MCP-enabled agent."""
crew = Crew(agents=[mcp_agent], tasks=[])
initial_tools = []
with patch.object(mcp_agent, 'get_mcp_tools', return_value=mock_mcp_tools):
result_tools = crew._inject_mcp_tools(initial_tools, mcp_agent)
# Should merge MCP tools with existing tools
assert len(result_tools) == len(mock_mcp_tools)
# Verify get_mcp_tools was called with agent's mcps
mcp_agent.get_mcp_tools.assert_called_once_with(mcps=mcp_agent.mcps)
def test_inject_mcp_tools_with_regular_agent(self, regular_agent):
"""Test MCP tools injection with regular agent (no MCP tools)."""
crew = Crew(agents=[regular_agent], tasks=[])
initial_tools = [Mock(name="existing_tool")]
# Regular agent has no mcps attribute
result_tools = crew._inject_mcp_tools(initial_tools, regular_agent)
# Should return original tools unchanged
assert result_tools == initial_tools
def test_inject_mcp_tools_empty_mcps_list(self, mcp_agent):
"""Test MCP tools injection with empty mcps list."""
crew = Crew(agents=[mcp_agent], tasks=[])
# Agent with empty mcps list
mcp_agent.mcps = []
initial_tools = [Mock(name="existing_tool")]
result_tools = crew._inject_mcp_tools(initial_tools, mcp_agent)
# Should return original tools unchanged
assert result_tools == initial_tools
def test_inject_mcp_tools_none_mcps(self, mcp_agent):
"""Test MCP tools injection with None mcps."""
crew = Crew(agents=[mcp_agent], tasks=[])
# Agent with None mcps
mcp_agent.mcps = None
initial_tools = [Mock(name="existing_tool")]
result_tools = crew._inject_mcp_tools(initial_tools, mcp_agent)
# Should return original tools unchanged
assert result_tools == initial_tools
def test_add_mcp_tools_with_task_agent(self, mcp_agent, sample_task, mock_mcp_tools):
"""Test _add_mcp_tools method with task agent."""
crew = Crew(agents=[mcp_agent], tasks=[sample_task])
initial_tools = [Mock(name="task_tool")]
with patch.object(crew, '_inject_mcp_tools', return_value=initial_tools + mock_mcp_tools) as mock_inject:
result_tools = crew._add_mcp_tools(sample_task, initial_tools)
# Should call _inject_mcp_tools with task agent
mock_inject.assert_called_once_with(initial_tools, sample_task.agent)
assert len(result_tools) == len(initial_tools) + len(mock_mcp_tools)
def test_add_mcp_tools_with_no_agent_task(self):
"""Test _add_mcp_tools method with task that has no agent."""
crew = Crew(agents=[], tasks=[])
# Task without agent
task_no_agent = Task(
description="Task without agent",
expected_output="Some output",
agent=None
)
initial_tools = [Mock(name="task_tool")]
result_tools = crew._add_mcp_tools(task_no_agent, initial_tools)
# Should return original tools unchanged
assert result_tools == initial_tools
def test_mcp_tools_integration_in_task_preparation_flow(self, mcp_agent, sample_task, mock_mcp_tools):
"""Test MCP tools integration in the task preparation flow."""
crew = Crew(agents=[mcp_agent], tasks=[sample_task])
# Mock the crew's tool preparation methods
with patch.object(crew, '_add_platform_tools', return_value=[Mock(name="platform_tool")]) as mock_platform, \
patch.object(crew, '_add_mcp_tools', return_value=mock_mcp_tools) as mock_mcp, \
patch.object(crew, '_add_multimodal_tools', return_value=mock_mcp_tools) as mock_multimodal:
# This tests the integration point where MCP tools are added to task tools
# We can't easily test the full _prepare_tools_for_task method due to complexity,
# but we can verify our _add_mcp_tools integration point works
result = crew._add_mcp_tools(sample_task, [])
assert result == mock_mcp_tools
mock_mcp.assert_called_once_with(sample_task, [])
def test_mcp_tools_merge_with_existing_tools(self, mcp_agent, mock_mcp_tools):
"""Test that MCP tools merge correctly with existing tools."""
from crewai.tools import BaseTool
class ExistingTool(BaseTool):
name: str = "existing_search"
description: str = "Existing search tool"
def _run(self, **kwargs):
return "Existing search result"
existing_tools = [ExistingTool()]
crew = Crew(agents=[mcp_agent], tasks=[])
with patch.object(mcp_agent, 'get_mcp_tools', return_value=mock_mcp_tools):
merged_tools = crew._inject_mcp_tools(existing_tools, mcp_agent)
# Should have both existing tools and MCP tools
total_expected = len(existing_tools) + len(mock_mcp_tools)
assert len(merged_tools) == total_expected
# Verify existing tools are preserved
existing_names = [tool.name for tool in existing_tools]
merged_names = [tool.name for tool in merged_tools]
for existing_name in existing_names:
assert existing_name in merged_names
def test_mcp_tools_available_in_crew_context(self, mcp_agent, sample_task, mock_mcp_tools):
"""Test that MCP tools are available in crew execution context."""
crew = Crew(agents=[mcp_agent], tasks=[sample_task])
with patch.object(mcp_agent, 'get_mcp_tools', return_value=mock_mcp_tools):
# Test that crew can access MCP tools through agent
agent_tools = crew._inject_mcp_tools([], mcp_agent)
assert len(agent_tools) == len(mock_mcp_tools)
assert all(tool in agent_tools for tool in mock_mcp_tools)
def test_crew_with_mixed_agents_mcp_and_regular(self, mcp_agent, regular_agent, mock_mcp_tools):
"""Test crew with both MCP-enabled and regular agents."""
task1 = Task(
description="Task for MCP agent",
expected_output="MCP-powered result",
agent=mcp_agent
)
task2 = Task(
description="Task for regular agent",
expected_output="Regular result",
agent=regular_agent
)
crew = Crew(
agents=[mcp_agent, regular_agent],
tasks=[task1, task2]
)
# Test MCP tools injection for MCP agent
with patch.object(mcp_agent, 'get_mcp_tools', return_value=mock_mcp_tools):
mcp_tools = crew._inject_mcp_tools([], mcp_agent)
assert len(mcp_tools) == len(mock_mcp_tools)
# Test MCP tools injection for regular agent
regular_tools = crew._inject_mcp_tools([], regular_agent)
assert len(regular_tools) == 0
def test_crew_mcp_tools_error_handling_during_execution_prep(self, mcp_agent, sample_task):
"""Test crew error handling when MCP tools fail during execution preparation."""
crew = Crew(agents=[mcp_agent], tasks=[sample_task])
# Mock MCP tools failure during crew execution preparation
with patch.object(mcp_agent, 'get_mcp_tools', side_effect=Exception("MCP tools failed")):
# Crew operations should continue despite MCP failure
try:
crew._inject_mcp_tools([], mcp_agent)
# If we get here, the error was handled gracefully by returning empty tools
except Exception as e:
# If exception propagates, it should be an expected one
assert "MCP tools failed" in str(e)
def test_crew_task_execution_flow_includes_mcp_tools(self, mcp_agent, sample_task):
"""Test that crew task execution flow includes MCP tools integration."""
crew = Crew(agents=[mcp_agent], tasks=[sample_task])
# Verify that crew has the necessary methods for MCP integration
assert hasattr(crew, '_add_mcp_tools')
assert hasattr(crew, '_inject_mcp_tools')
# Test the task has an agent with MCP capabilities
assert sample_task.agent == mcp_agent
assert hasattr(sample_task.agent, 'mcps')
assert hasattr(sample_task.agent, 'get_mcp_tools')
def test_mcp_tools_do_not_interfere_with_platform_tools(self, mock_mcp_tools):
"""Test that MCP tools don't interfere with platform tools integration."""
agent_with_both = Agent(
role="Multi-Tool Agent",
goal="Use both platform and MCP tools",
backstory="Agent with access to multiple tool types",
apps=["gmail", "slack"],
mcps=["https://api.example.com/mcp"]
)
task = Task(
description="Use both platform and MCP tools",
expected_output="Combined tool usage result",
agent=agent_with_both
)
crew = Crew(agents=[agent_with_both], tasks=[task])
platform_tools = [Mock(name="gmail_tool"), Mock(name="slack_tool")]
# Test platform tools injection
with patch.object(crew, '_inject_platform_tools', return_value=platform_tools):
result_platform = crew._inject_platform_tools([], agent_with_both)
assert len(result_platform) == len(platform_tools)
# Test MCP tools injection
with patch.object(agent_with_both, 'get_mcp_tools', return_value=mock_mcp_tools):
result_mcp = crew._inject_mcp_tools(platform_tools, agent_with_both)
assert len(result_mcp) == len(platform_tools) + len(mock_mcp_tools)
def test_crew_task_execution_order_includes_mcp_tools(self, mcp_agent, sample_task):
"""Test that crew task execution order includes MCP tools at the right point."""
crew = Crew(agents=[mcp_agent], tasks=[sample_task])
# Mock the various tool addition methods to verify call order
call_order = []
def track_platform_tools(*args, **kwargs):
call_order.append("platform")
return []
def track_mcp_tools(*args, **kwargs):
call_order.append("mcp")
return []
def track_multimodal_tools(*args, **kwargs):
call_order.append("multimodal")
return []
with patch.object(crew, '_add_platform_tools', side_effect=track_platform_tools), \
patch.object(crew, '_add_mcp_tools', side_effect=track_mcp_tools), \
patch.object(crew, '_add_multimodal_tools', side_effect=track_multimodal_tools):
# Test the crew's task preparation flow
# We check that MCP tools are added in the right sequence
# These methods are called in the task preparation flow
crew._add_platform_tools(sample_task, [])
crew._add_mcp_tools(sample_task, [])
assert "platform" in call_order
assert "mcp" in call_order
def test_crew_handles_agent_without_get_mcp_tools_method(self):
"""Test crew handles agents that don't implement get_mcp_tools method."""
# Create a mock agent that doesn't have get_mcp_tools
mock_agent = Mock()
mock_agent.mcps = ["https://api.example.com/mcp"]
# Explicitly don't add get_mcp_tools method
crew = Crew(agents=[], tasks=[])
# Should handle gracefully when agent doesn't have get_mcp_tools
result_tools = crew._inject_mcp_tools([], mock_agent)
# Should return empty list since agent doesn't have get_mcp_tools
assert result_tools == []
def test_crew_handles_agent_get_mcp_tools_exception(self, mcp_agent):
"""Test crew handles exceptions from agent's get_mcp_tools method."""
crew = Crew(agents=[mcp_agent], tasks=[])
with patch.object(mcp_agent, 'get_mcp_tools', side_effect=Exception("MCP tools failed")):
# Should handle exception gracefully
result_tools = crew._inject_mcp_tools([], mcp_agent)
# Depending on implementation, should either return empty list or re-raise
# Since get_mcp_tools handles errors internally, this should return empty list
assert isinstance(result_tools, list)
def test_crew_mcp_tools_merge_functionality(self, mock_mcp_tools):
"""Test crew's tool merging functionality with MCP tools."""
crew = Crew(agents=[], tasks=[])
existing_tools = [Mock(name="existing_tool_1"), Mock(name="existing_tool_2")]
# Test _merge_tools method with MCP tools
merged_tools = crew._merge_tools(existing_tools, mock_mcp_tools)
total_expected = len(existing_tools) + len(mock_mcp_tools)
assert len(merged_tools) == total_expected
# Verify all tools are present
all_tool_names = [tool.name for tool in merged_tools]
assert "existing_tool_1" in all_tool_names
assert "existing_tool_2" in all_tool_names
assert mock_mcp_tools[0].name in all_tool_names
assert mock_mcp_tools[1].name in all_tool_names
def test_crew_workflow_integration_conditions(self, mcp_agent, sample_task):
"""Test the conditions for MCP tools integration in crew workflows."""
crew = Crew(agents=[mcp_agent], tasks=[sample_task])
# Test condition: agent exists and has mcps attribute
assert hasattr(sample_task.agent, 'mcps')
assert sample_task.agent.mcps is not None
# Test condition: agent has get_mcp_tools method
assert hasattr(sample_task.agent, 'get_mcp_tools')
# Test condition: mcps list is not empty
sample_task.agent.mcps = ["https://api.example.com/mcp"]
assert len(sample_task.agent.mcps) > 0
def test_crew_mcp_integration_performance_impact(self, mcp_agent, sample_task, mock_mcp_tools):
"""Test that MCP integration doesn't significantly impact crew performance."""
crew = Crew(agents=[mcp_agent], tasks=[sample_task])
import time
# Test tool injection performance
start_time = time.time()
with patch.object(mcp_agent, 'get_mcp_tools', return_value=mock_mcp_tools):
# Multiple tool injection calls should be fast due to caching
for _ in range(5):
tools = crew._inject_mcp_tools([], mcp_agent)
end_time = time.time()
total_time = end_time - start_time
# Should complete quickly (less than 1 second for 5 operations)
assert total_time < 1.0
assert len(tools) == len(mock_mcp_tools)
def test_crew_task_tool_availability_with_mcp(self, mcp_agent, sample_task, mock_mcp_tools):
"""Test that MCP tools are available during task execution."""
crew = Crew(agents=[mcp_agent], tasks=[sample_task])
with patch.object(mcp_agent, 'get_mcp_tools', return_value=mock_mcp_tools):
# Simulate task tool preparation
prepared_tools = crew._inject_mcp_tools([], mcp_agent)
# Verify tools are properly prepared for task execution
assert len(prepared_tools) == len(mock_mcp_tools)
# Each tool should be a valid BaseTool-compatible object
for tool in prepared_tools:
assert hasattr(tool, 'name')
assert hasattr(tool, 'description')
def test_crew_handles_mcp_tool_name_conflicts(self, mock_mcp_tools):
"""Test crew handling of potential tool name conflicts."""
# Create MCP agent with tools that might conflict
agent1 = Agent(
role="Agent 1",
goal="Test conflicts",
backstory="First agent with MCP tools",
mcps=["https://server1.com/mcp"]
)
agent2 = Agent(
role="Agent 2",
goal="Test conflicts",
backstory="Second agent with MCP tools",
mcps=["https://server2.com/mcp"]
)
# Mock tools with same original names but different server prefixes
server1_tools = [Mock(name="server1_com_mcp_search_tool")]
server2_tools = [Mock(name="server2_com_mcp_search_tool")]
task1 = Task(description="Task 1", expected_output="Result 1", agent=agent1)
task2 = Task(description="Task 2", expected_output="Result 2", agent=agent2)
crew = Crew(agents=[agent1, agent2], tasks=[task1, task2])
with patch.object(agent1, 'get_mcp_tools', return_value=server1_tools), \
patch.object(agent2, 'get_mcp_tools', return_value=server2_tools):
# Each agent should get its own prefixed tools
tools1 = crew._inject_mcp_tools([], agent1)
tools2 = crew._inject_mcp_tools([], agent2)
assert len(tools1) == 1
assert len(tools2) == 1
assert tools1[0].name != tools2[0].name # Names should be different due to prefixing
def test_crew_mcp_integration_with_verbose_mode(self, mcp_agent, sample_task):
"""Test MCP integration works with crew verbose mode."""
crew = Crew(
agents=[mcp_agent],
tasks=[sample_task],
verbose=True # Enable verbose mode
)
# Should work the same regardless of verbose mode
assert crew.verbose is True
assert hasattr(crew, '_inject_mcp_tools')
# MCP integration should not be affected by verbose mode
with patch.object(mcp_agent, 'get_mcp_tools', return_value=[Mock()]):
tools = crew._inject_mcp_tools([], mcp_agent)
assert len(tools) == 1