|
| 1 | +"""Tests for BaseAgent MCP field validation and functionality.""" |
| 2 | + |
| 3 | +import pytest |
| 4 | +from unittest.mock import Mock, patch |
| 5 | +from pydantic import ValidationError |
| 6 | + |
| 7 | +# Import from the source directory |
| 8 | +import sys |
| 9 | +import os |
| 10 | +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../src')) |
| 11 | + |
| 12 | +from crewai.agents.agent_builder.base_agent import BaseAgent |
| 13 | +from crewai.agent import Agent |
| 14 | + |
| 15 | + |
| 16 | +class TestMCPAgent(BaseAgent): |
| 17 | + """Test implementation of BaseAgent for MCP testing.""" |
| 18 | + |
| 19 | + def execute_task(self, task, context=None, tools=None): |
| 20 | + return "Test execution" |
| 21 | + |
| 22 | + def create_agent_executor(self, tools=None): |
| 23 | + pass |
| 24 | + |
| 25 | + def get_delegation_tools(self, agents): |
| 26 | + return [] |
| 27 | + |
| 28 | + def get_platform_tools(self, apps): |
| 29 | + return [] |
| 30 | + |
| 31 | + def get_mcp_tools(self, mcps): |
| 32 | + return [] |
| 33 | + |
| 34 | + |
| 35 | +class TestBaseAgentMCPField: |
| 36 | + """Test suite for BaseAgent MCP field validation and functionality.""" |
| 37 | + |
| 38 | + def test_mcp_field_exists(self): |
| 39 | + """Test that mcps field exists on BaseAgent.""" |
| 40 | + agent = TestMCPAgent( |
| 41 | + role="Test Agent", |
| 42 | + goal="Test MCP field", |
| 43 | + backstory="Testing BaseAgent MCP field" |
| 44 | + ) |
| 45 | + |
| 46 | + assert hasattr(agent, 'mcps') |
| 47 | + assert agent.mcps is None # Default value |
| 48 | + |
| 49 | + def test_mcp_field_accepts_none(self): |
| 50 | + """Test that mcps field accepts None value.""" |
| 51 | + agent = TestMCPAgent( |
| 52 | + role="Test Agent", |
| 53 | + goal="Test MCP field", |
| 54 | + backstory="Testing BaseAgent MCP field", |
| 55 | + mcps=None |
| 56 | + ) |
| 57 | + |
| 58 | + assert agent.mcps is None |
| 59 | + |
| 60 | + def test_mcp_field_accepts_empty_list(self): |
| 61 | + """Test that mcps field accepts empty list.""" |
| 62 | + agent = TestMCPAgent( |
| 63 | + role="Test Agent", |
| 64 | + goal="Test MCP field", |
| 65 | + backstory="Testing BaseAgent MCP field", |
| 66 | + mcps=[] |
| 67 | + ) |
| 68 | + |
| 69 | + assert agent.mcps == [] |
| 70 | + |
| 71 | + def test_mcp_field_accepts_valid_https_urls(self): |
| 72 | + """Test that mcps field accepts valid HTTPS URLs.""" |
| 73 | + valid_urls = [ |
| 74 | + "https://api.example.com/mcp", |
| 75 | + "https://mcp.server.org/endpoint", |
| 76 | + "https://localhost:8080/mcp" |
| 77 | + ] |
| 78 | + |
| 79 | + agent = TestMCPAgent( |
| 80 | + role="Test Agent", |
| 81 | + goal="Test MCP field", |
| 82 | + backstory="Testing BaseAgent MCP field", |
| 83 | + mcps=valid_urls |
| 84 | + ) |
| 85 | + |
| 86 | + # Field validator may reorder items due to set() deduplication |
| 87 | + assert len(agent.mcps) == len(valid_urls) |
| 88 | + assert all(url in agent.mcps for url in valid_urls) |
| 89 | + |
| 90 | + def test_mcp_field_accepts_valid_crewai_amp_references(self): |
| 91 | + """Test that mcps field accepts valid CrewAI AMP references.""" |
| 92 | + valid_amp_refs = [ |
| 93 | + "crewai-amp:weather-service", |
| 94 | + "crewai-amp:financial-data", |
| 95 | + "crewai-amp:research-tools" |
| 96 | + ] |
| 97 | + |
| 98 | + agent = TestMCPAgent( |
| 99 | + role="Test Agent", |
| 100 | + goal="Test MCP field", |
| 101 | + backstory="Testing BaseAgent MCP field", |
| 102 | + mcps=valid_amp_refs |
| 103 | + ) |
| 104 | + |
| 105 | + # Field validator may reorder items due to set() deduplication |
| 106 | + assert len(agent.mcps) == len(valid_amp_refs) |
| 107 | + assert all(ref in agent.mcps for ref in valid_amp_refs) |
| 108 | + |
| 109 | + def test_mcp_field_accepts_mixed_valid_references(self): |
| 110 | + """Test that mcps field accepts mixed valid references.""" |
| 111 | + mixed_refs = [ |
| 112 | + "https://api.example.com/mcp", |
| 113 | + "crewai-amp:weather-service", |
| 114 | + "https://mcp.exa.ai/mcp?api_key=test", |
| 115 | + "crewai-amp:financial-data" |
| 116 | + ] |
| 117 | + |
| 118 | + agent = TestMCPAgent( |
| 119 | + role="Test Agent", |
| 120 | + goal="Test MCP field", |
| 121 | + backstory="Testing BaseAgent MCP field", |
| 122 | + mcps=mixed_refs |
| 123 | + ) |
| 124 | + |
| 125 | + # Field validator may reorder items due to set() deduplication |
| 126 | + assert len(agent.mcps) == len(mixed_refs) |
| 127 | + assert all(ref in agent.mcps for ref in mixed_refs) |
| 128 | + |
| 129 | + def test_mcp_field_rejects_invalid_formats(self): |
| 130 | + """Test that mcps field rejects invalid URL formats.""" |
| 131 | + invalid_refs = [ |
| 132 | + "http://insecure.com/mcp", # HTTP not allowed |
| 133 | + "invalid-format", # No protocol |
| 134 | + "ftp://example.com/mcp", # Wrong protocol |
| 135 | + "crewai:invalid", # Wrong AMP format |
| 136 | + "", # Empty string |
| 137 | + ] |
| 138 | + |
| 139 | + for invalid_ref in invalid_refs: |
| 140 | + with pytest.raises(ValidationError, match="Invalid MCP reference"): |
| 141 | + TestMCPAgent( |
| 142 | + role="Test Agent", |
| 143 | + goal="Test MCP field", |
| 144 | + backstory="Testing BaseAgent MCP field", |
| 145 | + mcps=[invalid_ref] |
| 146 | + ) |
| 147 | + |
| 148 | + def test_mcp_field_removes_duplicates(self): |
| 149 | + """Test that mcps field removes duplicate references.""" |
| 150 | + mcps_with_duplicates = [ |
| 151 | + "https://api.example.com/mcp", |
| 152 | + "crewai-amp:weather-service", |
| 153 | + "https://api.example.com/mcp", # Duplicate |
| 154 | + "crewai-amp:weather-service" # Duplicate |
| 155 | + ] |
| 156 | + |
| 157 | + agent = TestMCPAgent( |
| 158 | + role="Test Agent", |
| 159 | + goal="Test MCP field", |
| 160 | + backstory="Testing BaseAgent MCP field", |
| 161 | + mcps=mcps_with_duplicates |
| 162 | + ) |
| 163 | + |
| 164 | + # Should contain only unique references |
| 165 | + assert len(agent.mcps) == 2 |
| 166 | + assert "https://api.example.com/mcp" in agent.mcps |
| 167 | + assert "crewai-amp:weather-service" in agent.mcps |
| 168 | + |
| 169 | + def test_mcp_field_validates_list_type(self): |
| 170 | + """Test that mcps field validates list type.""" |
| 171 | + with pytest.raises(ValidationError): |
| 172 | + TestMCPAgent( |
| 173 | + role="Test Agent", |
| 174 | + goal="Test MCP field", |
| 175 | + backstory="Testing BaseAgent MCP field", |
| 176 | + mcps="not-a-list" # Should be list[str] |
| 177 | + ) |
| 178 | + |
| 179 | + def test_abstract_get_mcp_tools_method_exists(self): |
| 180 | + """Test that get_mcp_tools abstract method exists.""" |
| 181 | + assert hasattr(BaseAgent, 'get_mcp_tools') |
| 182 | + |
| 183 | + # Verify it's abstract by checking it's in __abstractmethods__ |
| 184 | + assert 'get_mcp_tools' in BaseAgent.__abstractmethods__ |
| 185 | + |
| 186 | + def test_concrete_implementation_must_implement_get_mcp_tools(self): |
| 187 | + """Test that concrete implementations must implement get_mcp_tools.""" |
| 188 | + # This should work - TestMCPAgent implements get_mcp_tools |
| 189 | + agent = TestMCPAgent( |
| 190 | + role="Test Agent", |
| 191 | + goal="Test MCP field", |
| 192 | + backstory="Testing BaseAgent MCP field" |
| 193 | + ) |
| 194 | + |
| 195 | + assert hasattr(agent, 'get_mcp_tools') |
| 196 | + assert callable(agent.get_mcp_tools) |
| 197 | + |
| 198 | + def test_copy_method_excludes_mcps_field(self): |
| 199 | + """Test that copy method excludes mcps field from being copied.""" |
| 200 | + agent = TestMCPAgent( |
| 201 | + role="Test Agent", |
| 202 | + goal="Test MCP field", |
| 203 | + backstory="Testing BaseAgent MCP field", |
| 204 | + mcps=["https://api.example.com/mcp"] |
| 205 | + ) |
| 206 | + |
| 207 | + copied_agent = agent.copy() |
| 208 | + |
| 209 | + # MCP field should be excluded from copy |
| 210 | + assert copied_agent.mcps is None or copied_agent.mcps == [] |
| 211 | + |
| 212 | + def test_model_validation_pipeline_with_mcps(self): |
| 213 | + """Test model validation pipeline with mcps field.""" |
| 214 | + # Test validation runs correctly through entire pipeline |
| 215 | + agent = TestMCPAgent( |
| 216 | + role="Test Agent", |
| 217 | + goal="Test MCP field", |
| 218 | + backstory="Testing BaseAgent MCP field", |
| 219 | + mcps=["https://api.example.com/mcp", "crewai-amp:test-service"] |
| 220 | + ) |
| 221 | + |
| 222 | + # Verify all required fields are set |
| 223 | + assert agent.role == "Test Agent" |
| 224 | + assert agent.goal == "Test MCP field" |
| 225 | + assert agent.backstory == "Testing BaseAgent MCP field" |
| 226 | + assert len(agent.mcps) == 2 |
| 227 | + |
| 228 | + def test_mcp_field_description_is_correct(self): |
| 229 | + """Test that mcps field has correct description.""" |
| 230 | + # Get field info from model |
| 231 | + fields = BaseAgent.model_fields |
| 232 | + mcps_field = fields.get('mcps') |
| 233 | + |
| 234 | + assert mcps_field is not None |
| 235 | + assert "MCP server references" in mcps_field.description |
| 236 | + assert "https://" in mcps_field.description |
| 237 | + assert "crewai-amp:" in mcps_field.description |
| 238 | + assert "#tool_name" in mcps_field.description |
| 239 | + |
| 240 | + |
| 241 | +class TestAgentMCPFieldIntegration: |
| 242 | + """Test MCP field integration with concrete Agent class.""" |
| 243 | + |
| 244 | + def test_agent_class_has_mcp_field(self): |
| 245 | + """Test that concrete Agent class inherits MCP field.""" |
| 246 | + agent = Agent( |
| 247 | + role="Test Agent", |
| 248 | + goal="Test MCP integration", |
| 249 | + backstory="Testing Agent MCP field", |
| 250 | + mcps=["https://api.example.com/mcp"] |
| 251 | + ) |
| 252 | + |
| 253 | + assert hasattr(agent, 'mcps') |
| 254 | + assert agent.mcps == ["https://api.example.com/mcp"] |
| 255 | + |
| 256 | + def test_agent_class_implements_get_mcp_tools(self): |
| 257 | + """Test that concrete Agent class implements get_mcp_tools.""" |
| 258 | + agent = Agent( |
| 259 | + role="Test Agent", |
| 260 | + goal="Test MCP integration", |
| 261 | + backstory="Testing Agent MCP field" |
| 262 | + ) |
| 263 | + |
| 264 | + assert hasattr(agent, 'get_mcp_tools') |
| 265 | + assert callable(agent.get_mcp_tools) |
| 266 | + |
| 267 | + # Test it can be called |
| 268 | + result = agent.get_mcp_tools([]) |
| 269 | + assert isinstance(result, list) |
| 270 | + |
| 271 | + def test_agent_mcp_field_validation_integration(self): |
| 272 | + """Test MCP field validation works with concrete Agent class.""" |
| 273 | + # Valid case |
| 274 | + agent = Agent( |
| 275 | + role="Test Agent", |
| 276 | + goal="Test MCP integration", |
| 277 | + backstory="Testing Agent MCP field", |
| 278 | + mcps=["https://mcp.exa.ai/mcp", "crewai-amp:research-tools"] |
| 279 | + ) |
| 280 | + |
| 281 | + assert len(agent.mcps) == 2 |
| 282 | + |
| 283 | + # Invalid case |
| 284 | + with pytest.raises(ValidationError, match="Invalid MCP reference"): |
| 285 | + Agent( |
| 286 | + role="Test Agent", |
| 287 | + goal="Test MCP integration", |
| 288 | + backstory="Testing Agent MCP field", |
| 289 | + mcps=["invalid-format"] |
| 290 | + ) |
| 291 | + |
| 292 | + def test_agent_docstring_mentions_mcps(self): |
| 293 | + """Test that Agent class docstring mentions mcps field.""" |
| 294 | + docstring = Agent.__doc__ |
| 295 | + |
| 296 | + assert docstring is not None |
| 297 | + assert "mcps" in docstring.lower() |
| 298 | + |
| 299 | + @patch('crewai.agent.create_llm') |
| 300 | + def test_agent_initialization_with_mcps_field(self, mock_create_llm): |
| 301 | + """Test complete Agent initialization with mcps field.""" |
| 302 | + mock_create_llm.return_value = Mock() |
| 303 | + |
| 304 | + agent = Agent( |
| 305 | + role="MCP Test Agent", |
| 306 | + goal="Test complete MCP integration", |
| 307 | + backstory="Agent for testing MCP functionality", |
| 308 | + mcps=[ |
| 309 | + "https://mcp.exa.ai/mcp?api_key=test", |
| 310 | + "crewai-amp:financial-data#get_stock_price" |
| 311 | + ], |
| 312 | + verbose=True |
| 313 | + ) |
| 314 | + |
| 315 | + # Verify agent is properly initialized |
| 316 | + assert agent.role == "MCP Test Agent" |
| 317 | + assert len(agent.mcps) == 2 |
| 318 | + assert agent.verbose is True |
| 319 | + |
| 320 | + # Verify MCP-specific functionality is available |
| 321 | + assert hasattr(agent, 'get_mcp_tools') |
| 322 | + assert hasattr(agent, '_get_external_mcp_tools') |
| 323 | + assert hasattr(agent, '_get_amp_mcp_tools') |
0 commit comments