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

376 lines
11 KiB
Python

"""Shared fixtures for MCP testing."""
import pytest
from unittest.mock import Mock
# 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.tools.mcp_tool_wrapper import MCPToolWrapper
from tests.mocks.mcp_server_mock import MockMCPServerFactory
@pytest.fixture
def sample_mcp_agent():
"""Create a sample agent with MCP configuration for testing."""
return Agent(
role="Test MCP Agent",
goal="Test MCP functionality",
backstory="Agent designed for MCP testing",
mcps=["https://api.test.com/mcp"]
)
@pytest.fixture
def multi_mcp_agent():
"""Create agent with multiple MCP configurations."""
return Agent(
role="Multi-MCP Agent",
goal="Test multiple MCP server integration",
backstory="Agent with access to multiple MCP servers",
mcps=[
"https://search.server.com/mcp",
"https://analysis.server.com/mcp#specific_tool",
"crewai-amp:research-tools",
"crewai-amp:financial-data#stock_prices"
]
)
@pytest.fixture
def mcp_agent_no_tools():
"""Create agent without MCP configuration."""
return Agent(
role="No MCP Agent",
goal="Test without MCP tools",
backstory="Standard agent without MCP access"
)
@pytest.fixture
def sample_mcp_tool_wrapper():
"""Create a sample MCPToolWrapper for testing."""
return MCPToolWrapper(
mcp_server_params={"url": "https://test.server.com/mcp"},
tool_name="test_tool",
tool_schema={
"description": "Test tool for MCP integration",
"args_schema": None
},
server_name="test_server_com_mcp"
)
@pytest.fixture
def mock_mcp_tool_schemas():
"""Provide mock MCP tool schemas for testing."""
return {
"search_web": {
"description": "Search the web for information",
"args_schema": None
},
"analyze_data": {
"description": "Analyze provided data and generate insights",
"args_schema": None
},
"get_weather": {
"description": "Get weather information for a location",
"args_schema": None
}
}
@pytest.fixture
def mock_exa_like_tools():
"""Provide mock tools similar to Exa MCP server."""
tools = []
# Web search tool
web_search = Mock(spec=MCPToolWrapper)
web_search.name = "mcp_exa_ai_mcp_web_search_exa"
web_search.description = "Search the web using Exa AI"
web_search.original_tool_name = "web_search_exa"
web_search.server_name = "mcp_exa_ai_mcp"
tools.append(web_search)
# Code context tool
code_context = Mock(spec=MCPToolWrapper)
code_context.name = "mcp_exa_ai_mcp_get_code_context_exa"
code_context.description = "Get code context using Exa"
code_context.original_tool_name = "get_code_context_exa"
code_context.server_name = "mcp_exa_ai_mcp"
tools.append(code_context)
return tools
@pytest.fixture
def mock_weather_like_tools():
"""Provide mock tools similar to weather MCP server."""
tools = []
weather_tools = [
("get_current_weather", "Get current weather conditions"),
("get_forecast", "Get weather forecast for next 5 days"),
("get_alerts", "Get active weather alerts")
]
for tool_name, description in weather_tools:
tool = Mock(spec=MCPToolWrapper)
tool.name = f"weather_server_com_mcp_{tool_name}"
tool.description = description
tool.original_tool_name = tool_name
tool.server_name = "weather_server_com_mcp"
tools.append(tool)
return tools
@pytest.fixture
def mock_amp_mcp_responses():
"""Provide mock responses for CrewAI AMP MCP API calls."""
return {
"research-tools": [
{"url": "https://amp.crewai.com/mcp/research/v1"},
{"url": "https://amp.crewai.com/mcp/research/v2"}
],
"financial-data": [
{"url": "https://amp.crewai.com/mcp/financial/main"}
],
"weather-service": [
{"url": "https://amp.crewai.com/mcp/weather/api"}
]
}
@pytest.fixture
def performance_test_mcps():
"""Provide MCP configurations for performance testing."""
return [
"https://fast-server.com/mcp",
"https://medium-server.com/mcp",
"https://reliable-server.com/mcp"
]
@pytest.fixture
def error_scenario_mcps():
"""Provide MCP configurations for error scenario testing."""
return [
"https://timeout-server.com/mcp",
"https://auth-fail-server.com/mcp",
"https://json-error-server.com/mcp",
"https://not-found-server.com/mcp"
]
@pytest.fixture
def mixed_quality_mcps():
"""Provide mixed quality MCP server configurations for resilience testing."""
return [
"https://excellent-server.com/mcp", # Always works
"https://intermittent-server.com/mcp", # Sometimes works
"https://slow-but-working-server.com/mcp", # Slow but reliable
"https://completely-broken-server.com/mcp" # Never works
]
@pytest.fixture
def server_name_test_cases():
"""Provide test cases for server name extraction."""
return [
# (input_url, expected_server_name)
("https://api.example.com/mcp", "api_example_com_mcp"),
("https://mcp.exa.ai/api/v1", "mcp_exa_ai_api_v1"),
("https://simple.com", "simple_com"),
("https://complex-domain.co.uk/deep/path/mcp", "complex-domain_co_uk_deep_path_mcp"),
("https://localhost:8080/mcp", "localhost:8080_mcp"),
]
@pytest.fixture
def mcp_reference_parsing_cases():
"""Provide test cases for MCP reference parsing."""
return [
# (mcp_ref, expected_type, expected_server, expected_tool)
("https://api.example.com/mcp", "external", "https://api.example.com/mcp", None),
("https://api.example.com/mcp#search", "external", "https://api.example.com/mcp", "search"),
("crewai-amp:weather-service", "amp", "weather-service", None),
("crewai-amp:financial-data#stock_price", "amp", "financial-data", "stock_price"),
]
@pytest.fixture
def cache_test_scenarios():
"""Provide scenarios for cache testing."""
return {
"cache_hit": {
"initial_time": 1000,
"subsequent_time": 1100, # Within 300s TTL
"expected_calls": 1
},
"cache_miss": {
"initial_time": 1000,
"subsequent_time": 1400, # Beyond 300s TTL
"expected_calls": 2
},
"cache_boundary": {
"initial_time": 1000,
"subsequent_time": 1300, # Exactly at 300s TTL boundary
"expected_calls": 2
}
}
@pytest.fixture
def timeout_test_scenarios():
"""Provide scenarios for timeout testing."""
return {
"connection_timeout": {
"timeout_type": "connection",
"delay": 15, # Exceeds 10s connection timeout
"expected_error": "timed out"
},
"execution_timeout": {
"timeout_type": "execution",
"delay": 35, # Exceeds 30s execution timeout
"expected_error": "timed out"
},
"discovery_timeout": {
"timeout_type": "discovery",
"delay": 20, # Exceeds 15s discovery timeout
"expected_error": "timed out"
}
}
@pytest.fixture
def mcp_error_scenarios():
"""Provide various MCP error scenarios for testing."""
return {
"connection_refused": {
"error": ConnectionRefusedError("Connection refused"),
"expected_msg": "network connection failed",
"retryable": True
},
"auth_failed": {
"error": Exception("Authentication failed"),
"expected_msg": "authentication failed",
"retryable": False
},
"json_parse_error": {
"error": ValueError("JSON decode error"),
"expected_msg": "server response parsing error",
"retryable": True
},
"tool_not_found": {
"error": Exception("Tool not found"),
"expected_msg": "not found",
"retryable": False
},
"server_error": {
"error": Exception("Internal server error"),
"expected_msg": "mcp execution error",
"retryable": False
}
}
@pytest.fixture(autouse=True)
def clear_mcp_cache():
"""Automatically clear MCP cache before each test."""
from crewai.agent import _mcp_schema_cache
_mcp_schema_cache.clear()
yield
_mcp_schema_cache.clear()
@pytest.fixture
def mock_successful_mcp_execution():
"""Provide a mock for successful MCP tool execution."""
def _mock_execution(**kwargs):
return f"Successful MCP execution with args: {kwargs}"
return _mock_execution
@pytest.fixture
def performance_benchmarks():
"""Provide performance benchmarks for MCP operations."""
return {
"agent_creation_max_time": 0.5, # 500ms
"tool_discovery_max_time": 2.0, # 2 seconds
"cache_hit_max_time": 0.01, # 10ms
"tool_execution_max_time": 35.0, # 35 seconds (includes timeout buffer)
"crew_integration_max_time": 0.1 # 100ms
}
# Convenience functions for common test setup
def setup_successful_mcp_environment():
"""Set up a complete successful MCP test environment."""
mock_server = MockMCPServerFactory.create_exa_like_server("https://mock-exa.com/mcp")
agent = Agent(
role="Success Test Agent",
goal="Test successful MCP operations",
backstory="Agent for testing successful scenarios",
mcps=["https://mock-exa.com/mcp"]
)
return agent, mock_server
def setup_error_prone_mcp_environment():
"""Set up an MCP test environment with various error conditions."""
agents = {}
# Different agents for different error scenarios
agents["timeout"] = Agent(
role="Timeout Agent",
goal="Test timeout scenarios",
backstory="Agent for timeout testing",
mcps=["https://slow-server.com/mcp"]
)
agents["auth_fail"] = Agent(
role="Auth Fail Agent",
goal="Test auth failures",
backstory="Agent for auth testing",
mcps=["https://secure-server.com/mcp"]
)
agents["mixed"] = Agent(
role="Mixed Results Agent",
goal="Test mixed success/failure",
backstory="Agent for mixed scenario testing",
mcps=[
"https://working-server.com/mcp",
"https://failing-server.com/mcp",
"crewai-amp:working-service",
"crewai-amp:failing-service"
]
)
return agents
def create_test_crew_with_mcp_agents(agents, task_descriptions=None):
"""Create a test crew with MCP-enabled agents."""
if task_descriptions is None:
task_descriptions = ["Generic test task" for _ in agents]
tasks = []
for i, agent in enumerate(agents):
task = Task(
description=task_descriptions[i] if i < len(task_descriptions) else f"Task for {agent.role}",
expected_output=f"Output from {agent.role}",
agent=agent
)
tasks.append(task)
return Crew(agents=agents, tasks=tasks)