hud-python 0.4.1__py3-none-any.whl → 0.4.3__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 (130) hide show
  1. hud/__init__.py +22 -22
  2. hud/agents/__init__.py +13 -15
  3. hud/agents/base.py +599 -599
  4. hud/agents/claude.py +373 -373
  5. hud/agents/langchain.py +261 -250
  6. hud/agents/misc/__init__.py +7 -7
  7. hud/agents/misc/response_agent.py +82 -80
  8. hud/agents/openai.py +352 -352
  9. hud/agents/openai_chat_generic.py +154 -154
  10. hud/agents/tests/__init__.py +1 -1
  11. hud/agents/tests/test_base.py +742 -742
  12. hud/agents/tests/test_claude.py +324 -324
  13. hud/agents/tests/test_client.py +363 -363
  14. hud/agents/tests/test_openai.py +237 -237
  15. hud/cli/__init__.py +617 -617
  16. hud/cli/__main__.py +8 -8
  17. hud/cli/analyze.py +371 -371
  18. hud/cli/analyze_metadata.py +230 -230
  19. hud/cli/build.py +498 -427
  20. hud/cli/clone.py +185 -185
  21. hud/cli/cursor.py +92 -92
  22. hud/cli/debug.py +392 -392
  23. hud/cli/docker_utils.py +83 -83
  24. hud/cli/init.py +280 -281
  25. hud/cli/interactive.py +353 -353
  26. hud/cli/mcp_server.py +764 -756
  27. hud/cli/pull.py +330 -336
  28. hud/cli/push.py +404 -370
  29. hud/cli/remote_runner.py +311 -311
  30. hud/cli/runner.py +160 -160
  31. hud/cli/tests/__init__.py +3 -3
  32. hud/cli/tests/test_analyze.py +284 -284
  33. hud/cli/tests/test_cli_init.py +265 -265
  34. hud/cli/tests/test_cli_main.py +27 -27
  35. hud/cli/tests/test_clone.py +142 -142
  36. hud/cli/tests/test_cursor.py +253 -253
  37. hud/cli/tests/test_debug.py +453 -453
  38. hud/cli/tests/test_mcp_server.py +139 -139
  39. hud/cli/tests/test_utils.py +388 -388
  40. hud/cli/utils.py +263 -263
  41. hud/clients/README.md +143 -143
  42. hud/clients/__init__.py +16 -16
  43. hud/clients/base.py +378 -379
  44. hud/clients/fastmcp.py +222 -222
  45. hud/clients/mcp_use.py +298 -278
  46. hud/clients/tests/__init__.py +1 -1
  47. hud/clients/tests/test_client_integration.py +111 -111
  48. hud/clients/tests/test_fastmcp.py +342 -342
  49. hud/clients/tests/test_protocol.py +188 -188
  50. hud/clients/utils/__init__.py +1 -1
  51. hud/clients/utils/retry_transport.py +160 -160
  52. hud/datasets.py +327 -322
  53. hud/misc/__init__.py +1 -1
  54. hud/misc/claude_plays_pokemon.py +292 -292
  55. hud/otel/__init__.py +35 -35
  56. hud/otel/collector.py +142 -142
  57. hud/otel/config.py +164 -164
  58. hud/otel/context.py +536 -536
  59. hud/otel/exporters.py +366 -366
  60. hud/otel/instrumentation.py +97 -97
  61. hud/otel/processors.py +118 -118
  62. hud/otel/tests/__init__.py +1 -1
  63. hud/otel/tests/test_processors.py +197 -197
  64. hud/server/__init__.py +5 -5
  65. hud/server/context.py +114 -114
  66. hud/server/helper/__init__.py +5 -5
  67. hud/server/low_level.py +132 -132
  68. hud/server/server.py +170 -166
  69. hud/server/tests/__init__.py +3 -3
  70. hud/settings.py +73 -73
  71. hud/shared/__init__.py +5 -5
  72. hud/shared/exceptions.py +180 -180
  73. hud/shared/requests.py +264 -264
  74. hud/shared/tests/test_exceptions.py +157 -157
  75. hud/shared/tests/test_requests.py +275 -275
  76. hud/telemetry/__init__.py +25 -25
  77. hud/telemetry/instrument.py +379 -379
  78. hud/telemetry/job.py +309 -309
  79. hud/telemetry/replay.py +74 -74
  80. hud/telemetry/trace.py +83 -83
  81. hud/tools/__init__.py +33 -33
  82. hud/tools/base.py +365 -365
  83. hud/tools/bash.py +161 -161
  84. hud/tools/computer/__init__.py +15 -15
  85. hud/tools/computer/anthropic.py +437 -437
  86. hud/tools/computer/hud.py +376 -376
  87. hud/tools/computer/openai.py +295 -295
  88. hud/tools/computer/settings.py +82 -82
  89. hud/tools/edit.py +314 -314
  90. hud/tools/executors/__init__.py +30 -30
  91. hud/tools/executors/base.py +539 -539
  92. hud/tools/executors/pyautogui.py +621 -621
  93. hud/tools/executors/tests/__init__.py +1 -1
  94. hud/tools/executors/tests/test_base_executor.py +338 -338
  95. hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
  96. hud/tools/executors/xdo.py +511 -511
  97. hud/tools/playwright.py +412 -412
  98. hud/tools/tests/__init__.py +3 -3
  99. hud/tools/tests/test_base.py +282 -282
  100. hud/tools/tests/test_bash.py +158 -158
  101. hud/tools/tests/test_bash_extended.py +197 -197
  102. hud/tools/tests/test_computer.py +425 -425
  103. hud/tools/tests/test_computer_actions.py +34 -34
  104. hud/tools/tests/test_edit.py +259 -259
  105. hud/tools/tests/test_init.py +27 -27
  106. hud/tools/tests/test_playwright_tool.py +183 -183
  107. hud/tools/tests/test_tools.py +145 -145
  108. hud/tools/tests/test_utils.py +156 -156
  109. hud/tools/types.py +72 -72
  110. hud/tools/utils.py +50 -50
  111. hud/types.py +136 -136
  112. hud/utils/__init__.py +10 -10
  113. hud/utils/async_utils.py +65 -65
  114. hud/utils/design.py +236 -168
  115. hud/utils/mcp.py +55 -55
  116. hud/utils/progress.py +149 -149
  117. hud/utils/telemetry.py +66 -66
  118. hud/utils/tests/test_async_utils.py +173 -173
  119. hud/utils/tests/test_init.py +17 -17
  120. hud/utils/tests/test_progress.py +261 -261
  121. hud/utils/tests/test_telemetry.py +82 -82
  122. hud/utils/tests/test_version.py +8 -8
  123. hud/version.py +7 -7
  124. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/METADATA +10 -8
  125. hud_python-0.4.3.dist-info/RECORD +131 -0
  126. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/licenses/LICENSE +21 -21
  127. hud/agents/art.py +0 -101
  128. hud_python-0.4.1.dist-info/RECORD +0 -132
  129. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/WHEEL +0 -0
  130. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/entry_points.txt +0 -0
@@ -1,363 +1,363 @@
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"}
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"}