Skip to content

Commit 0bbe6de

Browse files
committed
New tests for MCP implementation
1 parent ccd3c21 commit 0bbe6de

File tree

15 files changed

+4415
-1
lines changed

15 files changed

+4415
-1
lines changed

docs/en/concepts/agents.mdx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ mode: "wide"
1010
In the CrewAI framework, an `Agent` is an autonomous unit that can:
1111
- Perform specific tasks
1212
- Make decisions based on its role and goal
13-
- Use tools to accomplish objectives
13+
- Use apps, tools and MCP Servers to accomplish objectives
1414
- Communicate and collaborate with other agents
1515
- Maintain memory of interactions
1616
- Delegate tasks when allowed
@@ -40,6 +40,7 @@ The Visual Agent Builder enables:
4040
| **Backstory** | `backstory` | `str` | Provides context and personality to the agent, enriching interactions. |
4141
| **LLM** _(optional)_ | `llm` | `Union[str, LLM, Any]` | Language model that powers the agent. Defaults to the model specified in `OPENAI_MODEL_NAME` or "gpt-4". |
4242
| **Tools** _(optional)_ | `tools` | `List[BaseTool]` | Capabilities or functions available to the agent. Defaults to an empty list. |
43+
| **MCP Servers** _(optional)_ | `mcps` | `Optional[List[str]]` | MCP server references for automatic tool integration. Supports HTTPS URLs and CrewAI AMP marketplace references. |
4344
| **Function Calling LLM** _(optional)_ | `function_calling_llm` | `Optional[Any]` | Language model for tool calling, overrides crew's LLM if specified. |
4445
| **Max Iterations** _(optional)_ | `max_iter` | `int` | Maximum iterations before the agent must provide its best answer. Default is 20. |
4546
| **Max RPM** _(optional)_ | `max_rpm` | `Optional[int]` | Maximum requests per minute to avoid rate limits. |
@@ -194,6 +195,25 @@ research_agent = Agent(
194195
)
195196
```
196197

198+
#### Research Agent with MCP Integration
199+
```python Code
200+
mcp_research_agent = Agent(
201+
role="Advanced Research Analyst",
202+
goal="Conduct comprehensive research using multiple data sources",
203+
backstory="Expert researcher with access to web search, academic papers, and real-time data",
204+
mcps=[
205+
"https://mcp.exa.ai/mcp?api_key=your_key&profile=research",
206+
"crewai-amp:academic-research#pubmed_search", # CrewAI AMP Router for specific action in an MCP Server
207+
"crewai-amp:market-intelligence" # CrewAI AMP Router for a specific MCP server
208+
],
209+
verbose=True
210+
)
211+
```
212+
213+
<Note>
214+
The `mcps` field automatically discovers and integrates tools from MCP servers. Tools are cached for performance and connections are made on-demand. See [MCP DSL Integration](/en/mcp/dsl-integration) for detailed usage.
215+
</Note>
216+
197217
#### Code Development Agent
198218
```python Code
199219
dev_agent = Agent(
Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
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

Comments
 (0)