Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 19fd744

Browse files
authored
test: unit tests for MCPAgent (#281)
* unit tests for MCPAgent (init, run, stream) * fix linting and formatting
1 parent 1a7c234 commit 19fd744

File tree

1 file changed

+234
-0
lines changed

1 file changed

+234
-0
lines changed

tests/unit/test_agent.py

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
"""
2+
Unit tests for the MCPAgent class.
3+
"""
4+
5+
from unittest.mock import MagicMock, patch
6+
7+
import pytest
8+
from langchain.schema import HumanMessage
9+
from langchain_core.agents import AgentFinish
10+
11+
from mcp_use.agents.mcpagent import MCPAgent
12+
from mcp_use.client import MCPClient
13+
from mcp_use.connectors.base import BaseConnector
14+
15+
16+
class TestMCPAgentInitialization:
17+
"""Tests for MCPAgent initialization"""
18+
19+
def _mock_llm(self):
20+
llm = MagicMock()
21+
llm._llm_type = "test-provider"
22+
llm._identifying_params = {"model": "test-model"}
23+
return llm
24+
25+
def test_init_with_llm_and_client(self):
26+
"""Initializing locally with LLM and client."""
27+
llm = self._mock_llm()
28+
client = MagicMock(spec=MCPClient)
29+
30+
agent = MCPAgent(llm=llm, client=client)
31+
32+
assert agent.llm is llm
33+
assert agent.client is client
34+
assert agent._is_remote is False
35+
assert agent._initialized is False
36+
assert agent._agent_executor is None
37+
assert isinstance(agent.tools_used_names, list)
38+
39+
def test_init_requires_llm_for_local(self):
40+
"""Omitting LLM for local execution raises ValueError."""
41+
with pytest.raises(ValueError) as exc:
42+
MCPAgent(client=MagicMock(spec=MCPClient))
43+
assert "llm is required for local execution" in str(exc.value)
44+
45+
def test_init_requires_client_or_connectors(self):
46+
"""LLM present but no client/connectors raises ValueError."""
47+
llm = self._mock_llm()
48+
with pytest.raises(ValueError) as exc:
49+
MCPAgent(llm=llm)
50+
assert "Either client or connector must be provided" in str(exc.value)
51+
52+
def test_init_with_connectors_only(self):
53+
"""LLM with connectors initializes without client."""
54+
llm = self._mock_llm()
55+
connector = MagicMock(spec=BaseConnector)
56+
57+
agent = MCPAgent(llm=llm, connectors=[connector])
58+
59+
assert agent.client is None
60+
assert agent.connectors == [connector]
61+
assert agent._is_remote is False
62+
63+
def test_server_manager_requires_client(self):
64+
"""Using server manager without client raises ValueError."""
65+
llm = self._mock_llm()
66+
with pytest.raises(ValueError) as exc:
67+
MCPAgent(llm=llm, connectors=[MagicMock(spec=BaseConnector)], use_server_manager=True)
68+
assert "Client must be provided when using server manager" in str(exc.value)
69+
70+
def test_init_remote_mode_with_agent_id(self):
71+
"""Providing agent_id enables remote mode and skips local requirements."""
72+
with patch("mcp_use.agents.mcpagent.RemoteAgent") as MockRemote:
73+
agent = MCPAgent(agent_id="abc123", api_key="k", base_url="https://x")
74+
75+
MockRemote.assert_called_once()
76+
assert agent._is_remote is True
77+
assert agent._remote_agent is not None
78+
79+
80+
class TestMCPAgentRun:
81+
"""Tests for MCPAgent.run"""
82+
83+
def _mock_llm(self):
84+
llm = MagicMock()
85+
llm._llm_type = "test-provider"
86+
llm._identifying_params = {"model": "test-model"}
87+
llm.with_structured_output = MagicMock(return_value=llm)
88+
return llm
89+
90+
@pytest.mark.asyncio
91+
async def test_run_remote_delegates(self):
92+
"""In remote mode, run delegates to RemoteAgent.run and returns its result."""
93+
with patch("mcp_use.agents.mcpagent.RemoteAgent") as MockRemote:
94+
remote_instance = MockRemote.return_value
95+
remote_instance.run = MagicMock()
96+
97+
async def _arun(*args, **kwargs):
98+
return "remote-result"
99+
100+
remote_instance.run.side_effect = _arun
101+
102+
agent = MCPAgent(agent_id="abc123", api_key="k", base_url="https://x")
103+
104+
result = await agent.run("hello", max_steps=3, external_history=["h"], output_schema=None)
105+
106+
remote_instance.run.assert_called_once()
107+
assert result == "remote-result"
108+
109+
@pytest.mark.asyncio
110+
async def test_run_local_calls_stream_and_consume(self):
111+
"""Local run creates stream generator and consumes it via _consume_and_return."""
112+
llm = self._mock_llm()
113+
client = MagicMock(spec=MCPClient)
114+
115+
agent = MCPAgent(llm=llm, client=client)
116+
117+
async def dummy_gen():
118+
if False:
119+
yield None
120+
121+
with (
122+
patch.object(MCPAgent, "stream", return_value=dummy_gen()) as mock_stream,
123+
patch.object(MCPAgent, "_consume_and_return") as mock_consume,
124+
):
125+
126+
async def _aconsume(gen):
127+
return ("ok", 1)
128+
129+
mock_consume.side_effect = _aconsume
130+
131+
result = await agent.run("query", max_steps=2, manage_connector=True, external_history=None)
132+
133+
mock_stream.assert_called_once()
134+
mock_consume.assert_called_once()
135+
assert result == "ok"
136+
137+
138+
class TestMCPAgentStream:
139+
"""Tests for MCPAgent.stream"""
140+
141+
def _mock_llm(self):
142+
llm = MagicMock()
143+
llm._llm_type = "test-provider"
144+
llm._identifying_params = {"model": "test-model"}
145+
llm.with_structured_output = MagicMock(return_value=llm)
146+
return llm
147+
148+
@pytest.mark.asyncio
149+
async def test_stream_remote_delegates(self):
150+
"""In remote mode, stream delegates to RemoteAgent.stream and yields its items."""
151+
152+
async def _astream(*args, **kwargs):
153+
yield "remote-yield-1"
154+
yield "remote-yield-2"
155+
156+
with patch("mcp_use.agents.mcpagent.RemoteAgent") as MockRemote:
157+
remote_instance = MockRemote.return_value
158+
remote_instance.stream = MagicMock(side_effect=_astream)
159+
160+
agent = MCPAgent(agent_id="abc123", api_key="k", base_url="https://x")
161+
162+
outputs = []
163+
async for item in agent.stream("hello", max_steps=2):
164+
outputs.append(item)
165+
166+
remote_instance.stream.assert_called_once()
167+
assert outputs == ["remote-yield-1", "remote-yield-2"]
168+
169+
@pytest.mark.asyncio
170+
async def test_stream_initializes_and_finishes(self):
171+
"""When not initialized, stream calls initialize, sets max_steps, and yields final output on AgentFinish."""
172+
llm = self._mock_llm()
173+
client = MagicMock(spec=MCPClient)
174+
agent = MCPAgent(llm=llm, client=client)
175+
agent.callbacks = []
176+
agent.telemetry = MagicMock()
177+
178+
executor = MagicMock()
179+
executor.max_iterations = None
180+
181+
async def _init_side_effect():
182+
agent._agent_executor = executor
183+
agent._initialized = True
184+
185+
with patch.object(MCPAgent, "initialize", side_effect=_init_side_effect) as mock_init:
186+
187+
async def _atake_next_step(**kwargs):
188+
return AgentFinish(return_values={"output": "done"}, log="")
189+
190+
executor._atake_next_step = MagicMock(side_effect=_atake_next_step)
191+
192+
outputs = []
193+
async for item in agent.stream("q", max_steps=3):
194+
outputs.append(item)
195+
196+
mock_init.assert_called_once()
197+
assert executor.max_iterations == 3
198+
assert outputs[-1] == "done"
199+
agent.telemetry.track_agent_execution.assert_called_once()
200+
201+
@pytest.mark.asyncio
202+
async def test_stream_uses_external_history_and_sets_max_steps(self):
203+
"""External history should be used, and executor.max_iterations should reflect max_steps arg."""
204+
llm = self._mock_llm()
205+
client = MagicMock(spec=MCPClient)
206+
agent = MCPAgent(llm=llm, client=client)
207+
agent.callbacks = []
208+
agent.telemetry = MagicMock()
209+
210+
external_history = [HumanMessage(content="past")]
211+
212+
executor = MagicMock()
213+
executor.max_iterations = None
214+
215+
async def _init_side_effect():
216+
agent._agent_executor = executor
217+
agent._initialized = True
218+
219+
with patch.object(MCPAgent, "initialize", side_effect=_init_side_effect):
220+
221+
async def _asserting_step(
222+
name_to_tool_map=None, color_mapping=None, inputs=None, intermediate_steps=None, run_manager=None
223+
):
224+
assert inputs["chat_history"] == [msg for msg in external_history]
225+
return AgentFinish(return_values={"output": "ok"}, log="")
226+
227+
executor._atake_next_step = MagicMock(side_effect=_asserting_step)
228+
229+
outputs = []
230+
async for item in agent.stream("query", max_steps=4, external_history=external_history):
231+
outputs.append(item)
232+
233+
assert executor.max_iterations == 4
234+
assert outputs[-1] == "ok"

0 commit comments

Comments
 (0)