hud-python 0.3.4__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of hud-python might be problematic. Click here for more details.

Files changed (192) hide show
  1. hud/__init__.py +22 -89
  2. hud/agents/__init__.py +17 -0
  3. hud/agents/art.py +101 -0
  4. hud/agents/base.py +599 -0
  5. hud/{mcp → agents}/claude.py +373 -321
  6. hud/{mcp → agents}/langchain.py +250 -250
  7. hud/agents/misc/__init__.py +7 -0
  8. hud/{agent → agents}/misc/response_agent.py +80 -80
  9. hud/{mcp → agents}/openai.py +352 -334
  10. hud/agents/openai_chat_generic.py +154 -0
  11. hud/{mcp → agents}/tests/__init__.py +1 -1
  12. hud/agents/tests/test_base.py +742 -0
  13. hud/agents/tests/test_claude.py +324 -0
  14. hud/{mcp → agents}/tests/test_client.py +363 -324
  15. hud/{mcp → agents}/tests/test_openai.py +237 -238
  16. hud/cli/__init__.py +617 -0
  17. hud/cli/__main__.py +8 -0
  18. hud/cli/analyze.py +371 -0
  19. hud/cli/analyze_metadata.py +230 -0
  20. hud/cli/build.py +427 -0
  21. hud/cli/clone.py +185 -0
  22. hud/cli/cursor.py +92 -0
  23. hud/cli/debug.py +392 -0
  24. hud/cli/docker_utils.py +83 -0
  25. hud/cli/init.py +281 -0
  26. hud/cli/interactive.py +353 -0
  27. hud/cli/mcp_server.py +756 -0
  28. hud/cli/pull.py +336 -0
  29. hud/cli/push.py +379 -0
  30. hud/cli/remote_runner.py +311 -0
  31. hud/cli/runner.py +160 -0
  32. hud/cli/tests/__init__.py +3 -0
  33. hud/cli/tests/test_analyze.py +284 -0
  34. hud/cli/tests/test_cli_init.py +265 -0
  35. hud/cli/tests/test_cli_main.py +27 -0
  36. hud/cli/tests/test_clone.py +142 -0
  37. hud/cli/tests/test_cursor.py +253 -0
  38. hud/cli/tests/test_debug.py +453 -0
  39. hud/cli/tests/test_mcp_server.py +139 -0
  40. hud/cli/tests/test_utils.py +388 -0
  41. hud/cli/utils.py +263 -0
  42. hud/clients/README.md +143 -0
  43. hud/clients/__init__.py +16 -0
  44. hud/clients/base.py +354 -0
  45. hud/clients/fastmcp.py +202 -0
  46. hud/clients/mcp_use.py +278 -0
  47. hud/clients/tests/__init__.py +1 -0
  48. hud/clients/tests/test_client_integration.py +111 -0
  49. hud/clients/tests/test_fastmcp.py +342 -0
  50. hud/clients/tests/test_protocol.py +188 -0
  51. hud/clients/utils/__init__.py +1 -0
  52. hud/clients/utils/retry_transport.py +160 -0
  53. hud/datasets.py +322 -192
  54. hud/misc/__init__.py +1 -0
  55. hud/{agent → misc}/claude_plays_pokemon.py +292 -283
  56. hud/otel/__init__.py +35 -0
  57. hud/otel/collector.py +142 -0
  58. hud/otel/config.py +164 -0
  59. hud/otel/context.py +536 -0
  60. hud/otel/exporters.py +366 -0
  61. hud/otel/instrumentation.py +97 -0
  62. hud/otel/processors.py +118 -0
  63. hud/otel/tests/__init__.py +1 -0
  64. hud/otel/tests/test_processors.py +197 -0
  65. hud/server/__init__.py +5 -5
  66. hud/server/context.py +114 -0
  67. hud/server/helper/__init__.py +5 -0
  68. hud/server/low_level.py +132 -0
  69. hud/server/server.py +166 -0
  70. hud/server/tests/__init__.py +3 -0
  71. hud/settings.py +73 -79
  72. hud/shared/__init__.py +5 -0
  73. hud/{exceptions.py → shared/exceptions.py} +180 -180
  74. hud/{server → shared}/requests.py +264 -264
  75. hud/shared/tests/test_exceptions.py +157 -0
  76. hud/{server → shared}/tests/test_requests.py +275 -275
  77. hud/telemetry/__init__.py +25 -30
  78. hud/telemetry/instrument.py +379 -0
  79. hud/telemetry/job.py +309 -141
  80. hud/telemetry/replay.py +74 -0
  81. hud/telemetry/trace.py +83 -0
  82. hud/tools/__init__.py +33 -34
  83. hud/tools/base.py +365 -65
  84. hud/tools/bash.py +161 -137
  85. hud/tools/computer/__init__.py +15 -13
  86. hud/tools/computer/anthropic.py +437 -414
  87. hud/tools/computer/hud.py +376 -328
  88. hud/tools/computer/openai.py +295 -286
  89. hud/tools/computer/settings.py +82 -0
  90. hud/tools/edit.py +314 -290
  91. hud/tools/executors/__init__.py +30 -30
  92. hud/tools/executors/base.py +539 -532
  93. hud/tools/executors/pyautogui.py +621 -619
  94. hud/tools/executors/tests/__init__.py +1 -1
  95. hud/tools/executors/tests/test_base_executor.py +338 -338
  96. hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
  97. hud/tools/executors/xdo.py +511 -503
  98. hud/tools/{playwright_tool.py → playwright.py} +412 -379
  99. hud/tools/tests/__init__.py +3 -3
  100. hud/tools/tests/test_base.py +282 -0
  101. hud/tools/tests/test_bash.py +158 -152
  102. hud/tools/tests/test_bash_extended.py +197 -0
  103. hud/tools/tests/test_computer.py +425 -52
  104. hud/tools/tests/test_computer_actions.py +34 -34
  105. hud/tools/tests/test_edit.py +259 -240
  106. hud/tools/tests/test_init.py +27 -27
  107. hud/tools/tests/test_playwright_tool.py +183 -183
  108. hud/tools/tests/test_tools.py +145 -157
  109. hud/tools/tests/test_utils.py +156 -156
  110. hud/tools/types.py +72 -0
  111. hud/tools/utils.py +50 -50
  112. hud/types.py +136 -89
  113. hud/utils/__init__.py +10 -16
  114. hud/utils/async_utils.py +65 -0
  115. hud/utils/design.py +168 -0
  116. hud/utils/mcp.py +55 -0
  117. hud/utils/progress.py +149 -149
  118. hud/utils/telemetry.py +66 -66
  119. hud/utils/tests/test_async_utils.py +173 -0
  120. hud/utils/tests/test_init.py +17 -21
  121. hud/utils/tests/test_progress.py +261 -225
  122. hud/utils/tests/test_telemetry.py +82 -37
  123. hud/utils/tests/test_version.py +8 -8
  124. hud/version.py +7 -7
  125. hud_python-0.4.0.dist-info/METADATA +474 -0
  126. hud_python-0.4.0.dist-info/RECORD +132 -0
  127. hud_python-0.4.0.dist-info/entry_points.txt +3 -0
  128. {hud_python-0.3.4.dist-info → hud_python-0.4.0.dist-info}/licenses/LICENSE +21 -21
  129. hud/adapters/__init__.py +0 -8
  130. hud/adapters/claude/__init__.py +0 -5
  131. hud/adapters/claude/adapter.py +0 -180
  132. hud/adapters/claude/tests/__init__.py +0 -1
  133. hud/adapters/claude/tests/test_adapter.py +0 -519
  134. hud/adapters/common/__init__.py +0 -6
  135. hud/adapters/common/adapter.py +0 -178
  136. hud/adapters/common/tests/test_adapter.py +0 -289
  137. hud/adapters/common/types.py +0 -446
  138. hud/adapters/operator/__init__.py +0 -5
  139. hud/adapters/operator/adapter.py +0 -108
  140. hud/adapters/operator/tests/__init__.py +0 -1
  141. hud/adapters/operator/tests/test_adapter.py +0 -370
  142. hud/agent/__init__.py +0 -19
  143. hud/agent/base.py +0 -126
  144. hud/agent/claude.py +0 -271
  145. hud/agent/langchain.py +0 -215
  146. hud/agent/misc/__init__.py +0 -3
  147. hud/agent/operator.py +0 -268
  148. hud/agent/tests/__init__.py +0 -1
  149. hud/agent/tests/test_base.py +0 -202
  150. hud/env/__init__.py +0 -11
  151. hud/env/client.py +0 -35
  152. hud/env/docker_client.py +0 -349
  153. hud/env/environment.py +0 -446
  154. hud/env/local_docker_client.py +0 -358
  155. hud/env/remote_client.py +0 -212
  156. hud/env/remote_docker_client.py +0 -292
  157. hud/gym.py +0 -130
  158. hud/job.py +0 -773
  159. hud/mcp/__init__.py +0 -17
  160. hud/mcp/base.py +0 -631
  161. hud/mcp/client.py +0 -312
  162. hud/mcp/tests/test_base.py +0 -512
  163. hud/mcp/tests/test_claude.py +0 -294
  164. hud/task.py +0 -149
  165. hud/taskset.py +0 -237
  166. hud/telemetry/_trace.py +0 -347
  167. hud/telemetry/context.py +0 -230
  168. hud/telemetry/exporter.py +0 -575
  169. hud/telemetry/instrumentation/__init__.py +0 -3
  170. hud/telemetry/instrumentation/mcp.py +0 -259
  171. hud/telemetry/instrumentation/registry.py +0 -59
  172. hud/telemetry/mcp_models.py +0 -270
  173. hud/telemetry/tests/__init__.py +0 -1
  174. hud/telemetry/tests/test_context.py +0 -210
  175. hud/telemetry/tests/test_trace.py +0 -312
  176. hud/tools/helper/README.md +0 -56
  177. hud/tools/helper/__init__.py +0 -9
  178. hud/tools/helper/mcp_server.py +0 -78
  179. hud/tools/helper/server_initialization.py +0 -115
  180. hud/tools/helper/utils.py +0 -58
  181. hud/trajectory.py +0 -94
  182. hud/utils/agent.py +0 -37
  183. hud/utils/common.py +0 -256
  184. hud/utils/config.py +0 -120
  185. hud/utils/deprecation.py +0 -115
  186. hud/utils/misc.py +0 -53
  187. hud/utils/tests/test_common.py +0 -277
  188. hud/utils/tests/test_config.py +0 -129
  189. hud_python-0.3.4.dist-info/METADATA +0 -284
  190. hud_python-0.3.4.dist-info/RECORD +0 -120
  191. /hud/{adapters/common → shared}/tests/__init__.py +0 -0
  192. {hud_python-0.3.4.dist-info → hud_python-0.4.0.dist-info}/WHEEL +0 -0
@@ -1,324 +1,363 @@
1
- """Tests for MCP Client implementation."""
2
-
3
- from __future__ import annotations
4
-
5
- from unittest.mock import AsyncMock, MagicMock, patch
6
-
7
- import pytest
8
- from mcp import types
9
- from pydantic import AnyUrl
10
-
11
- from hud.mcp.client import MCPClient
12
-
13
-
14
- class TestMCPClient:
15
- """Test MCPClient class."""
16
-
17
- @pytest.fixture
18
- def mock_mcp_use_client(self):
19
- """Create a mock MCPUseClient (the internal mcp_use client)."""
20
- # Create a mock instance that will be returned by from_dict
21
- mock_instance = MagicMock()
22
- mock_instance.create_session = AsyncMock()
23
- mock_instance.create_all_sessions = AsyncMock(return_value={})
24
- mock_instance.close_all_sessions = AsyncMock()
25
- mock_instance.get_all_active_sessions = MagicMock(return_value={})
26
-
27
- # Patch MCPUseClient that's imported in hud.mcp.client
28
- with patch("hud.mcp.client.MCPUseClient") as mock_class:
29
- mock_class.from_dict = MagicMock(return_value=mock_instance)
30
- yield mock_instance
31
-
32
- @pytest.mark.asyncio
33
- async def test_init_with_config(self):
34
- """Test client initialization with config dictionary."""
35
- mcp_config = {
36
- "test_server": {
37
- "command": "python",
38
- "args": ["-m", "test_server"],
39
- "env": {"TEST": "true"},
40
- }
41
- }
42
-
43
- with patch("hud.mcp.client.MCPUseClient") as mock_use_client:
44
- client = MCPClient(mcp_config=mcp_config, verbose=True)
45
-
46
- assert client.verbose is True
47
- # Verify MCPUseClient.from_dict was called with proper config
48
- mock_use_client.from_dict.assert_called_once_with({"mcpServers": mcp_config})
49
-
50
- @pytest.mark.asyncio
51
- async def test_connect_single_server(self, mock_mcp_use_client):
52
- """Test connecting to a single server."""
53
- config = {"test_server": {"command": "python", "args": ["-m", "test_server"]}}
54
-
55
- # Create the MCPClient - the fixture already patches MCPUseClient
56
- client = MCPClient(mcp_config=config, verbose=True)
57
-
58
- # Verify internal client was created properly
59
- assert client._mcp_client == mock_mcp_use_client
60
-
61
- # Mock session
62
- mock_session = MagicMock()
63
- mock_session.connector = MagicMock()
64
- mock_session.connector.client_session = MagicMock()
65
-
66
- # Mock list_tools response
67
- async def mock_list_tools():
68
- return types.ListToolsResult(
69
- tools=[
70
- types.Tool(name="tool1", description="Tool 1", inputSchema={"type": "object"}),
71
- types.Tool(name="tool2", description="Tool 2", inputSchema={"type": "object"}),
72
- ]
73
- )
74
-
75
- mock_session.connector.client_session.list_tools = mock_list_tools
76
-
77
- # Mock create_all_sessions to return a dict with our session
78
- mock_mcp_use_client.create_all_sessions = AsyncMock(
79
- return_value={"test_server": mock_session}
80
- )
81
-
82
- # Initialize the client (creates sessions and discovers tools)
83
- await client.initialize()
84
-
85
- # Verify session was created
86
- mock_mcp_use_client.create_all_sessions.assert_called_once()
87
-
88
- # Check tools were discovered
89
- assert len(client._available_tools) == 2
90
- assert len(client._tool_map) == 2
91
- assert "tool1" in client._tool_map
92
- assert "tool2" in client._tool_map
93
-
94
- @pytest.mark.asyncio
95
- async def test_connect_multiple_servers(self, mock_mcp_use_client):
96
- """Test connecting to multiple servers."""
97
- config = {
98
- "server1": {"command": "python", "args": ["-m", "server1"]},
99
- "server2": {"command": "node", "args": ["server2.js"]},
100
- }
101
-
102
- client = MCPClient(mcp_config=config)
103
-
104
- # Mock sessions
105
- mock_session1 = MagicMock()
106
- mock_session1.connector = MagicMock()
107
- mock_session1.connector.client_session = MagicMock()
108
-
109
- mock_session2 = MagicMock()
110
- mock_session2.connector = MagicMock()
111
- mock_session2.connector.client_session = MagicMock()
112
-
113
- # Mock tools for each server
114
- async def mock_list_tools1():
115
- return types.ListToolsResult(
116
- tools=[
117
- types.Tool(name="tool1", description="Tool 1", inputSchema={"type": "object"})
118
- ]
119
- )
120
-
121
- async def mock_list_tools2():
122
- return types.ListToolsResult(
123
- tools=[
124
- types.Tool(name="tool2", description="Tool 2", inputSchema={"type": "object"})
125
- ]
126
- )
127
-
128
- mock_session1.connector.client_session.list_tools = mock_list_tools1
129
- mock_session2.connector.client_session.list_tools = mock_list_tools2
130
-
131
- # Mock create_all_sessions to return both sessions
132
- mock_mcp_use_client.create_all_sessions = AsyncMock(
133
- return_value={"server1": mock_session1, "server2": mock_session2}
134
- )
135
-
136
- await client.initialize()
137
-
138
- # Verify sessions were created
139
- mock_mcp_use_client.create_all_sessions.assert_called_once()
140
-
141
- # Check tools from both servers
142
- assert len(client._tool_map) == 2
143
- assert "tool1" in client._tool_map
144
- assert "tool2" in client._tool_map
145
-
146
- @pytest.mark.asyncio
147
- async def test_call_tool(self, mock_mcp_use_client):
148
- """Test calling a tool."""
149
- config = {"test": {"command": "test"}}
150
- client = MCPClient(mcp_config=config)
151
-
152
- # Setup mock session
153
- mock_session = MagicMock()
154
- mock_session.connector = MagicMock()
155
- mock_session.connector.client_session = MagicMock()
156
-
157
- # Mock tool
158
- tool = types.Tool(
159
- name="calculator", description="Calculator", inputSchema={"type": "object"}
160
- )
161
-
162
- async def mock_list_tools():
163
- return types.ListToolsResult(tools=[tool])
164
-
165
- mock_session.connector.client_session.list_tools = mock_list_tools
166
-
167
- # Mock tool execution
168
- mock_result = types.CallToolResult(
169
- content=[types.TextContent(type="text", text="Result: 42")], isError=False
170
- )
171
-
172
- mock_session.connector.client_session.call_tool = AsyncMock(return_value=mock_result)
173
-
174
- mock_mcp_use_client.create_all_sessions = AsyncMock(return_value={"test": mock_session})
175
-
176
- await client.initialize()
177
-
178
- # Call the tool
179
- result = await client.call_tool("calculator", {"operation": "add", "a": 20, "b": 22})
180
-
181
- assert result == mock_result
182
- mock_session.connector.client_session.call_tool.assert_called_once_with(
183
- name="calculator", arguments={"operation": "add", "a": 20, "b": 22}
184
- )
185
-
186
- @pytest.mark.asyncio
187
- async def test_call_tool_not_found(self, mock_mcp_use_client):
188
- """Test calling a non-existent tool."""
189
- config = {"test": {"command": "test"}}
190
- client = MCPClient(mcp_config=config)
191
-
192
- mock_session = MagicMock()
193
- mock_session.connector = MagicMock()
194
- mock_session.connector.client_session = MagicMock()
195
-
196
- async def mock_list_tools():
197
- return types.ListToolsResult(tools=[])
198
-
199
- mock_session.connector.client_session.list_tools = mock_list_tools
200
- mock_mcp_use_client.create_all_sessions = AsyncMock(return_value={"test": mock_session})
201
-
202
- await client.initialize()
203
-
204
- with pytest.raises(ValueError, match="Tool 'nonexistent' not found"):
205
- await client.call_tool("nonexistent", {})
206
-
207
- @pytest.mark.asyncio
208
- async def test_get_telemetry_data(self, mock_mcp_use_client):
209
- """Test getting telemetry data."""
210
- config = {"test": {"command": "test"}}
211
- client = MCPClient(mcp_config=config)
212
-
213
- mock_session = MagicMock()
214
- mock_session.connector = MagicMock()
215
- mock_session.connector.client_session = MagicMock()
216
-
217
- # Mock tools
218
- async def mock_list_tools():
219
- return types.ListToolsResult(tools=[])
220
-
221
- mock_session.connector.client_session.list_tools = mock_list_tools
222
-
223
- # Mock telemetry resource
224
- mock_telemetry = types.ReadResourceResult(
225
- contents=[
226
- types.TextResourceContents(
227
- uri=AnyUrl("telemetry://live"),
228
- mimeType="application/json",
229
- text='{"events": [{"type": "test", "data": "value"}]}',
230
- )
231
- ]
232
- )
233
-
234
- mock_session.connector.client_session.read_resource = AsyncMock(return_value=mock_telemetry)
235
-
236
- mock_mcp_use_client.create_all_sessions = AsyncMock(return_value={"test": mock_session})
237
-
238
- await client.initialize()
239
-
240
- telemetry_data = client.get_telemetry_data()
241
-
242
- assert "test" in telemetry_data
243
- assert telemetry_data["test"]["events"][0]["type"] == "test"
244
-
245
- @pytest.mark.asyncio
246
- async def test_close(self, mock_mcp_use_client):
247
- """Test closing client connections."""
248
- config = {"test": {"command": "test"}}
249
- client = MCPClient(mcp_config=config)
250
-
251
- mock_session = MagicMock()
252
- mock_session.connector = MagicMock()
253
- mock_session.connector.client_session = MagicMock()
254
-
255
- async def mock_list_tools():
256
- return types.ListToolsResult(tools=[])
257
-
258
- mock_session.connector.client_session.list_tools = mock_list_tools
259
- mock_mcp_use_client.create_session = AsyncMock(return_value=mock_session)
260
- mock_mcp_use_client.close_all_sessions = AsyncMock()
261
-
262
- await client.initialize()
263
- await client.close()
264
-
265
- mock_mcp_use_client.close_all_sessions.assert_called_once()
266
-
267
- @pytest.mark.asyncio
268
- async def test_context_manager(self, mock_mcp_use_client):
269
- """Test using client as context manager."""
270
- mock_session = MagicMock()
271
- mock_session.connector = MagicMock()
272
- mock_session.connector.client_session = MagicMock()
273
-
274
- async def mock_list_tools():
275
- return types.ListToolsResult(tools=[])
276
-
277
- mock_session.connector.client_session.list_tools = mock_list_tools
278
- mock_mcp_use_client.create_all_sessions = AsyncMock(return_value={"test": mock_session})
279
- mock_mcp_use_client.close_all_sessions = AsyncMock()
280
-
281
- config = {"test": {"command": "test"}}
282
- # The fixture already patches MCPUseClient
283
- async with MCPClient(mcp_config=config) as client:
284
- assert client._mcp_client is not None
285
- # Verify that the client uses our mock
286
- assert client._mcp_client == mock_mcp_use_client
287
-
288
- # Verify cleanup was called
289
- mock_mcp_use_client.close_all_sessions.assert_called_once()
290
-
291
- def test_get_available_tools(self, mock_mcp_use_client):
292
- """Test getting available tools."""
293
- config = {"test": {"command": "test"}}
294
- client = MCPClient(mcp_config=config)
295
-
296
- # Manually set tools
297
- client._available_tools = [
298
- types.Tool(name="tool1", description="Tool 1", inputSchema={"type": "object"}),
299
- types.Tool(name="tool2", description="Tool 2", inputSchema={"type": "object"}),
300
- ]
301
-
302
- tools = client.get_available_tools()
303
- assert len(tools) == 2
304
- assert tools[0].name == "tool1"
305
- assert tools[1].name == "tool2"
306
-
307
- def test_get_tool_map(self, mock_mcp_use_client):
308
- """Test getting tool map."""
309
- config = {"test": {"command": "test"}}
310
- client = MCPClient(mcp_config=config)
311
-
312
- # Manually set tool map
313
- tool1 = types.Tool(name="tool1", description="Tool 1", inputSchema={"type": "object"})
314
- tool2 = types.Tool(name="tool2", description="Tool 2", inputSchema={"type": "object"})
315
-
316
- client._tool_map = {
317
- "tool1": ("server1", tool1),
318
- "tool2": ("server2", tool2),
319
- }
320
-
321
- tool_map = client.get_tool_map()
322
- assert len(tool_map) == 2
323
- assert tool_map["tool1"][0] == "server1"
324
- assert tool_map["tool2"][0] == "server2"
1
+ """Tests for MCP Client implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from unittest.mock import AsyncMock, MagicMock, patch
7
+
8
+ import pytest
9
+ from mcp import types
10
+ from pydantic import AnyUrl
11
+
12
+ from hud.clients.mcp_use import MCPUseHUDClient as MCPClient
13
+ from hud.types import MCPToolResult
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ @patch("hud.clients.base.setup_hud_telemetry")
19
+ class TestMCPClient:
20
+ """Test MCPClient class."""
21
+
22
+ @pytest.fixture
23
+ def mock_mcp_use_client(self):
24
+ """Create a mock MCPUseClient (the internal mcp_use client)."""
25
+ # Create a mock instance that will be returned by from_dict
26
+ mock_instance = MagicMock()
27
+ mock_instance.create_session = AsyncMock()
28
+ mock_instance.create_all_sessions = AsyncMock(return_value={})
29
+ mock_instance.close_all_sessions = AsyncMock()
30
+ mock_instance.get_all_active_sessions = MagicMock(return_value={})
31
+
32
+ # Patch MCPUseClient.from_dict at the module level
33
+ with patch("mcp_use.client.MCPClient.from_dict", return_value=mock_instance):
34
+ yield mock_instance
35
+
36
+ @pytest.mark.asyncio
37
+ async def test_init_with_config(self, mock_telemetry):
38
+ """Test client initialization with config dictionary."""
39
+ mcp_config = {
40
+ "test_server": {
41
+ "command": "python",
42
+ "args": ["-m", "test_server"],
43
+ "env": {"TEST": "true"},
44
+ }
45
+ }
46
+
47
+ with patch("mcp_use.client.MCPClient.from_dict") as mock_from_dict:
48
+ mock_instance = MagicMock()
49
+ mock_instance.create_all_sessions = AsyncMock(return_value={})
50
+ mock_from_dict.return_value = mock_instance
51
+ client = MCPClient(mcp_config=mcp_config, verbose=True)
52
+ # Initialize to trigger connection
53
+ await client.initialize()
54
+
55
+ assert client.verbose is True
56
+ # Verify MCPUseClient.from_dict was called with proper config
57
+ mock_from_dict.assert_called_once_with({"mcpServers": mcp_config})
58
+
59
+ @pytest.mark.asyncio
60
+ async def test_connect_single_server(self, mock_telemetry, mock_mcp_use_client):
61
+ """Test connecting to a single server."""
62
+ config = {"test_server": {"command": "python", "args": ["-m", "test_server"]}}
63
+
64
+ # Create the MCPClient - the fixture already patches MCPUseClient
65
+ client = MCPClient(mcp_config=config, verbose=True)
66
+
67
+ # Mock session
68
+ mock_session = MagicMock()
69
+ mock_session.connector = MagicMock()
70
+ mock_session.connector.client_session = MagicMock()
71
+
72
+ # Mock list_tools response
73
+ async def mock_list_tools():
74
+ return types.ListToolsResult(
75
+ tools=[
76
+ types.Tool(name="tool1", description="Tool 1", inputSchema={"type": "object"}),
77
+ types.Tool(name="tool2", description="Tool 2", inputSchema={"type": "object"}),
78
+ ]
79
+ )
80
+
81
+ mock_session.connector.client_session.list_tools = mock_list_tools
82
+
83
+ # Mock create_all_sessions to return a dict with our session
84
+ mock_mcp_use_client.create_all_sessions = AsyncMock(
85
+ return_value={"test_server": mock_session}
86
+ )
87
+
88
+ # Initialize the client (creates sessions and discovers tools)
89
+ await client.initialize()
90
+
91
+ # Internal client created
92
+ assert client._client is not None
93
+
94
+ # Verify session was created
95
+ mock_mcp_use_client.create_all_sessions.assert_called_once()
96
+
97
+ # Check tools were discovered via public API
98
+ tools = await client.list_tools()
99
+ names = {t.name for t in tools}
100
+ assert names == {"tool1", "tool2"}
101
+
102
+ @pytest.mark.asyncio
103
+ async def test_connect_multiple_servers(self, mock_telemetry, mock_mcp_use_client):
104
+ """Test connecting to multiple servers."""
105
+ config = {
106
+ "server1": {"command": "python", "args": ["-m", "server1"]},
107
+ "server2": {"command": "node", "args": ["server2.js"]},
108
+ }
109
+
110
+ client = MCPClient(mcp_config=config)
111
+
112
+ # Mock sessions
113
+ mock_session1 = MagicMock()
114
+ mock_session1.connector = MagicMock()
115
+ mock_session1.connector.client_session = MagicMock()
116
+
117
+ mock_session2 = MagicMock()
118
+ mock_session2.connector = MagicMock()
119
+ mock_session2.connector.client_session = MagicMock()
120
+
121
+ # Mock tools for each server
122
+ async def mock_list_tools1():
123
+ return types.ListToolsResult(
124
+ tools=[
125
+ types.Tool(name="tool1", description="Tool 1", inputSchema={"type": "object"})
126
+ ]
127
+ )
128
+
129
+ async def mock_list_tools2():
130
+ return types.ListToolsResult(
131
+ tools=[
132
+ types.Tool(name="tool2", description="Tool 2", inputSchema={"type": "object"})
133
+ ]
134
+ )
135
+
136
+ mock_session1.connector.client_session.list_tools = mock_list_tools1
137
+ mock_session2.connector.client_session.list_tools = mock_list_tools2
138
+
139
+ # Mock create_all_sessions to return both sessions
140
+ mock_mcp_use_client.create_all_sessions = AsyncMock(
141
+ return_value={"server1": mock_session1, "server2": mock_session2}
142
+ )
143
+
144
+ await client.initialize()
145
+
146
+ # Verify sessions were created
147
+ mock_mcp_use_client.create_all_sessions.assert_called_once()
148
+
149
+ # Check tools from both servers
150
+ tools = await client.list_tools()
151
+ names = {t.name for t in tools}
152
+ assert names == {"tool1", "tool2"}
153
+
154
+ @pytest.mark.asyncio
155
+ async def test_call_tool(self, mock_telemetry, mock_mcp_use_client):
156
+ """Test calling a tool."""
157
+ config = {"test": {"command": "test"}}
158
+ client = MCPClient(mcp_config=config)
159
+
160
+ # Setup mock session
161
+ mock_session = MagicMock()
162
+ mock_session.connector = MagicMock()
163
+ mock_session.connector.client_session = MagicMock()
164
+
165
+ # Mock tool
166
+ tool = types.Tool(
167
+ name="calculator", description="Calculator", inputSchema={"type": "object"}
168
+ )
169
+
170
+ async def mock_list_tools():
171
+ return types.ListToolsResult(tools=[tool])
172
+
173
+ mock_session.connector.client_session.list_tools = mock_list_tools
174
+
175
+ # The session returns CallToolResult, but the client should return MCPToolResult
176
+ mock_call_result = types.CallToolResult(
177
+ content=[types.TextContent(type="text", text="Result: 42")], isError=False
178
+ )
179
+
180
+ mock_session.connector.client_session.call_tool = AsyncMock(return_value=mock_call_result)
181
+
182
+ # Set up the mock to return the session both when creating and when getting sessions
183
+ mock_mcp_use_client.create_all_sessions = AsyncMock(return_value={"test": mock_session})
184
+ mock_mcp_use_client.get_all_active_sessions = MagicMock(return_value={"test": mock_session})
185
+
186
+ await client.initialize()
187
+
188
+ # First discover tools by calling list_tools
189
+ tools = await client.list_tools()
190
+ assert len(tools) == 1
191
+ assert tools[0].name == "calculator"
192
+
193
+ # Call the tool
194
+ result = await client.call_tool(
195
+ name="calculator", arguments={"operation": "add", "a": 20, "b": 22}
196
+ )
197
+
198
+ assert isinstance(result, MCPToolResult)
199
+ assert result.content[0].text == "Result: 42" # type: ignore
200
+ assert result.isError is False
201
+ mock_session.connector.client_session.call_tool.assert_called_once_with(
202
+ name="calculator", arguments={"operation": "add", "a": 20, "b": 22}
203
+ )
204
+
205
+ @pytest.mark.asyncio
206
+ async def test_call_tool_not_found(self, mock_telemetry, mock_mcp_use_client):
207
+ """Test calling a non-existent tool."""
208
+ config = {"test": {"command": "test"}}
209
+ client = MCPClient(mcp_config=config)
210
+
211
+ mock_session = MagicMock()
212
+ mock_session.connector = MagicMock()
213
+ mock_session.connector.client_session = MagicMock()
214
+
215
+ async def mock_list_tools():
216
+ return types.ListToolsResult(tools=[])
217
+
218
+ mock_session.connector.client_session.list_tools = mock_list_tools
219
+ mock_mcp_use_client.create_all_sessions = AsyncMock(return_value={"test": mock_session})
220
+
221
+ await client.initialize()
222
+
223
+ with pytest.raises(ValueError, match="Tool 'nonexistent' not found"):
224
+ await client.call_tool(name="nonexistent", arguments={})
225
+
226
+ @pytest.mark.asyncio
227
+ async def test_get_telemetry_data(self, mock_telemetry, mock_mcp_use_client):
228
+ """Test getting telemetry data."""
229
+ config = {"test": {"command": "test"}}
230
+ client = MCPClient(mcp_config=config)
231
+
232
+ mock_session = MagicMock()
233
+ mock_session.connector = MagicMock()
234
+ mock_session.connector.client_session = MagicMock()
235
+
236
+ # Mock tools
237
+ async def mock_list_tools():
238
+ return types.ListToolsResult(tools=[])
239
+
240
+ mock_session.connector.client_session.list_tools = mock_list_tools
241
+
242
+ # Mock telemetry resource
243
+ mock_telemetry = types.ReadResourceResult(
244
+ contents=[
245
+ types.TextResourceContents(
246
+ uri=AnyUrl("telemetry://live"),
247
+ mimeType="application/json",
248
+ text='{"events": [{"type": "test", "data": "value"}]}',
249
+ )
250
+ ]
251
+ )
252
+
253
+ mock_session.connector.client_session.read_resource = AsyncMock(return_value=mock_telemetry)
254
+
255
+ mock_mcp_use_client.create_all_sessions = AsyncMock(return_value={"test": mock_session})
256
+
257
+ await client.initialize()
258
+
259
+ telemetry_data = client._telemetry_data
260
+ # In the new client, telemetry is a flat dict of fields
261
+ assert isinstance(telemetry_data, dict)
262
+
263
+ @pytest.mark.asyncio
264
+ async def test_close(self, mock_telemetry, mock_mcp_use_client):
265
+ """Test closing client connections."""
266
+ config = {"test": {"command": "test"}}
267
+ client = MCPClient(mcp_config=config)
268
+
269
+ mock_session = MagicMock()
270
+ mock_session.connector = MagicMock()
271
+ mock_session.connector.client_session = MagicMock()
272
+
273
+ async def mock_list_tools():
274
+ return types.ListToolsResult(tools=[])
275
+
276
+ mock_session.connector.client_session.list_tools = mock_list_tools
277
+ mock_mcp_use_client.create_all_sessions = AsyncMock(return_value={"test": mock_session})
278
+ mock_mcp_use_client.close_all_sessions = AsyncMock()
279
+
280
+ await client.initialize()
281
+ await client.shutdown()
282
+
283
+ mock_mcp_use_client.close_all_sessions.assert_called_once()
284
+
285
+ @pytest.mark.asyncio
286
+ async def test_context_manager(self, mock_telemetry, mock_mcp_use_client):
287
+ """Test using client as context manager."""
288
+ mock_session = MagicMock()
289
+ mock_session.connector = MagicMock()
290
+ mock_session.connector.client_session = MagicMock()
291
+
292
+ async def mock_list_tools():
293
+ return types.ListToolsResult(tools=[])
294
+
295
+ mock_session.connector.client_session.list_tools = mock_list_tools
296
+ mock_mcp_use_client.create_all_sessions = AsyncMock(return_value={"test": mock_session})
297
+ mock_mcp_use_client.close_all_sessions = AsyncMock()
298
+
299
+ config = {"test": {"command": "test"}}
300
+ # The fixture already patches MCPUseClient
301
+ async with MCPClient(mcp_config=config) as client:
302
+ assert client._client is not None
303
+ # Verify that the client uses our mock
304
+ assert client._client == mock_mcp_use_client
305
+
306
+ # Verify cleanup was called
307
+ mock_mcp_use_client.close_all_sessions.assert_called_once()
308
+
309
+ @pytest.mark.asyncio
310
+ async def test_get_available_tools(self, mock_telemetry, mock_mcp_use_client):
311
+ """Test getting available tools."""
312
+ config = {"test": {"command": "test"}}
313
+ client = MCPClient(mcp_config=config)
314
+
315
+ # Create tool objects
316
+ tool1 = types.Tool(name="tool1", description="Tool 1", inputSchema={"type": "object"})
317
+ tool2 = types.Tool(name="tool2", description="Tool 2", inputSchema={"type": "object"})
318
+
319
+ # Setup mock session with tools
320
+ mock_session = MagicMock()
321
+ mock_session.connector = MagicMock()
322
+ mock_session.connector.client_session = MagicMock()
323
+
324
+ async def mock_list_tools():
325
+ return types.ListToolsResult(tools=[tool1, tool2])
326
+
327
+ mock_session.connector.client_session.list_tools = mock_list_tools
328
+ mock_mcp_use_client.create_all_sessions = AsyncMock(return_value={"test": mock_session})
329
+
330
+ # Initialize to populate tools
331
+ await client.initialize()
332
+
333
+ tools = await client.list_tools()
334
+ names = {t.name for t in tools}
335
+ assert names == {"tool1", "tool2"}
336
+
337
+ @pytest.mark.asyncio
338
+ async def test_get_tool_map(self, mock_telemetry, mock_mcp_use_client):
339
+ """Test getting tool map."""
340
+ config = {"test": {"command": "test"}}
341
+ client = MCPClient(mcp_config=config)
342
+
343
+ # Create tool objects
344
+ tool1 = types.Tool(name="tool1", description="Tool 1", inputSchema={"type": "object"})
345
+ tool2 = types.Tool(name="tool2", description="Tool 2", inputSchema={"type": "object"})
346
+
347
+ # Setup mock session with tools
348
+ mock_session = MagicMock()
349
+ mock_session.connector = MagicMock()
350
+ mock_session.connector.client_session = MagicMock()
351
+
352
+ async def mock_list_tools():
353
+ return types.ListToolsResult(tools=[tool1, tool2])
354
+
355
+ mock_session.connector.client_session.list_tools = mock_list_tools
356
+ mock_mcp_use_client.create_all_sessions = AsyncMock(return_value={"test": mock_session})
357
+
358
+ # Initialize to populate tools
359
+ await client.initialize()
360
+
361
+ tools = await client.list_tools()
362
+ names = {t.name for t in tools}
363
+ assert names == {"tool1", "tool2"}