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.
- hud/__init__.py +22 -22
- hud/agents/__init__.py +13 -15
- hud/agents/base.py +599 -599
- hud/agents/claude.py +373 -373
- hud/agents/langchain.py +261 -250
- hud/agents/misc/__init__.py +7 -7
- hud/agents/misc/response_agent.py +82 -80
- hud/agents/openai.py +352 -352
- hud/agents/openai_chat_generic.py +154 -154
- hud/agents/tests/__init__.py +1 -1
- hud/agents/tests/test_base.py +742 -742
- hud/agents/tests/test_claude.py +324 -324
- hud/agents/tests/test_client.py +363 -363
- hud/agents/tests/test_openai.py +237 -237
- hud/cli/__init__.py +617 -617
- hud/cli/__main__.py +8 -8
- hud/cli/analyze.py +371 -371
- hud/cli/analyze_metadata.py +230 -230
- hud/cli/build.py +498 -427
- hud/cli/clone.py +185 -185
- hud/cli/cursor.py +92 -92
- hud/cli/debug.py +392 -392
- hud/cli/docker_utils.py +83 -83
- hud/cli/init.py +280 -281
- hud/cli/interactive.py +353 -353
- hud/cli/mcp_server.py +764 -756
- hud/cli/pull.py +330 -336
- hud/cli/push.py +404 -370
- hud/cli/remote_runner.py +311 -311
- hud/cli/runner.py +160 -160
- hud/cli/tests/__init__.py +3 -3
- hud/cli/tests/test_analyze.py +284 -284
- hud/cli/tests/test_cli_init.py +265 -265
- hud/cli/tests/test_cli_main.py +27 -27
- hud/cli/tests/test_clone.py +142 -142
- hud/cli/tests/test_cursor.py +253 -253
- hud/cli/tests/test_debug.py +453 -453
- hud/cli/tests/test_mcp_server.py +139 -139
- hud/cli/tests/test_utils.py +388 -388
- hud/cli/utils.py +263 -263
- hud/clients/README.md +143 -143
- hud/clients/__init__.py +16 -16
- hud/clients/base.py +378 -379
- hud/clients/fastmcp.py +222 -222
- hud/clients/mcp_use.py +298 -278
- hud/clients/tests/__init__.py +1 -1
- hud/clients/tests/test_client_integration.py +111 -111
- hud/clients/tests/test_fastmcp.py +342 -342
- hud/clients/tests/test_protocol.py +188 -188
- hud/clients/utils/__init__.py +1 -1
- hud/clients/utils/retry_transport.py +160 -160
- hud/datasets.py +327 -322
- hud/misc/__init__.py +1 -1
- hud/misc/claude_plays_pokemon.py +292 -292
- hud/otel/__init__.py +35 -35
- hud/otel/collector.py +142 -142
- hud/otel/config.py +164 -164
- hud/otel/context.py +536 -536
- hud/otel/exporters.py +366 -366
- hud/otel/instrumentation.py +97 -97
- hud/otel/processors.py +118 -118
- hud/otel/tests/__init__.py +1 -1
- hud/otel/tests/test_processors.py +197 -197
- hud/server/__init__.py +5 -5
- hud/server/context.py +114 -114
- hud/server/helper/__init__.py +5 -5
- hud/server/low_level.py +132 -132
- hud/server/server.py +170 -166
- hud/server/tests/__init__.py +3 -3
- hud/settings.py +73 -73
- hud/shared/__init__.py +5 -5
- hud/shared/exceptions.py +180 -180
- hud/shared/requests.py +264 -264
- hud/shared/tests/test_exceptions.py +157 -157
- hud/shared/tests/test_requests.py +275 -275
- hud/telemetry/__init__.py +25 -25
- hud/telemetry/instrument.py +379 -379
- hud/telemetry/job.py +309 -309
- hud/telemetry/replay.py +74 -74
- hud/telemetry/trace.py +83 -83
- hud/tools/__init__.py +33 -33
- hud/tools/base.py +365 -365
- hud/tools/bash.py +161 -161
- hud/tools/computer/__init__.py +15 -15
- hud/tools/computer/anthropic.py +437 -437
- hud/tools/computer/hud.py +376 -376
- hud/tools/computer/openai.py +295 -295
- hud/tools/computer/settings.py +82 -82
- hud/tools/edit.py +314 -314
- hud/tools/executors/__init__.py +30 -30
- hud/tools/executors/base.py +539 -539
- hud/tools/executors/pyautogui.py +621 -621
- hud/tools/executors/tests/__init__.py +1 -1
- hud/tools/executors/tests/test_base_executor.py +338 -338
- hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
- hud/tools/executors/xdo.py +511 -511
- hud/tools/playwright.py +412 -412
- hud/tools/tests/__init__.py +3 -3
- hud/tools/tests/test_base.py +282 -282
- hud/tools/tests/test_bash.py +158 -158
- hud/tools/tests/test_bash_extended.py +197 -197
- hud/tools/tests/test_computer.py +425 -425
- hud/tools/tests/test_computer_actions.py +34 -34
- hud/tools/tests/test_edit.py +259 -259
- hud/tools/tests/test_init.py +27 -27
- hud/tools/tests/test_playwright_tool.py +183 -183
- hud/tools/tests/test_tools.py +145 -145
- hud/tools/tests/test_utils.py +156 -156
- hud/tools/types.py +72 -72
- hud/tools/utils.py +50 -50
- hud/types.py +136 -136
- hud/utils/__init__.py +10 -10
- hud/utils/async_utils.py +65 -65
- hud/utils/design.py +236 -168
- hud/utils/mcp.py +55 -55
- hud/utils/progress.py +149 -149
- hud/utils/telemetry.py +66 -66
- hud/utils/tests/test_async_utils.py +173 -173
- hud/utils/tests/test_init.py +17 -17
- hud/utils/tests/test_progress.py +261 -261
- hud/utils/tests/test_telemetry.py +82 -82
- hud/utils/tests/test_version.py +8 -8
- hud/version.py +7 -7
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/METADATA +10 -8
- hud_python-0.4.3.dist-info/RECORD +131 -0
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/licenses/LICENSE +21 -21
- hud/agents/art.py +0 -101
- hud_python-0.4.1.dist-info/RECORD +0 -132
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/WHEEL +0 -0
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/entry_points.txt +0 -0
|
@@ -1,342 +1,342 @@
|
|
|
1
|
-
"""Tests for FastMCP client implementation."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from unittest.mock import ANY, AsyncMock, MagicMock, patch
|
|
6
|
-
|
|
7
|
-
import pytest
|
|
8
|
-
from mcp import types
|
|
9
|
-
from pydantic.networks import AnyUrl
|
|
10
|
-
|
|
11
|
-
from hud.clients.fastmcp import FastMCPHUDClient
|
|
12
|
-
from hud.types import MCPToolCall, MCPToolResult
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class TestFastMCPHUDClient:
|
|
16
|
-
"""Test FastMCP HUD client."""
|
|
17
|
-
|
|
18
|
-
def test_initialization(self):
|
|
19
|
-
"""Test client initialization."""
|
|
20
|
-
config = {"server1": {"command": "python", "args": ["server.py"]}}
|
|
21
|
-
|
|
22
|
-
# Client is just instantiated, not connected yet
|
|
23
|
-
client = FastMCPHUDClient(config)
|
|
24
|
-
|
|
25
|
-
# Check that the client has the config stored
|
|
26
|
-
assert client._mcp_config == config
|
|
27
|
-
assert client._client is None # Not connected yet
|
|
28
|
-
|
|
29
|
-
@pytest.mark.asyncio
|
|
30
|
-
async def test_connect_creates_client(self):
|
|
31
|
-
"""Test that _connect creates the FastMCP client."""
|
|
32
|
-
config = {"server1": {"command": "test"}}
|
|
33
|
-
|
|
34
|
-
with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
|
|
35
|
-
mock_fastmcp = AsyncMock()
|
|
36
|
-
mock_client_class.return_value = mock_fastmcp
|
|
37
|
-
|
|
38
|
-
client = FastMCPHUDClient(config)
|
|
39
|
-
await client._connect(config)
|
|
40
|
-
|
|
41
|
-
# Check FastMCP client was created
|
|
42
|
-
mock_client_class.assert_called_once()
|
|
43
|
-
|
|
44
|
-
# Check it was created with correct transport and client info
|
|
45
|
-
call_args = mock_client_class.call_args
|
|
46
|
-
assert call_args[0][0] == {"mcpServers": config}
|
|
47
|
-
assert call_args[1]["client_info"].name == "hud-fastmcp"
|
|
48
|
-
|
|
49
|
-
@pytest.mark.asyncio
|
|
50
|
-
async def test_connect_logs_info(self):
|
|
51
|
-
"""Test that connect logs info message."""
|
|
52
|
-
config = {"server1": {"command": "test"}}
|
|
53
|
-
|
|
54
|
-
with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
|
|
55
|
-
mock_fastmcp = AsyncMock()
|
|
56
|
-
mock_client_class.return_value = mock_fastmcp
|
|
57
|
-
|
|
58
|
-
client = FastMCPHUDClient(config)
|
|
59
|
-
|
|
60
|
-
with patch("hud.clients.fastmcp.logger") as mock_logger:
|
|
61
|
-
await client._connect(config)
|
|
62
|
-
|
|
63
|
-
# Check info was logged
|
|
64
|
-
mock_logger.info.assert_called_with("FastMCP client connected")
|
|
65
|
-
|
|
66
|
-
@pytest.mark.asyncio
|
|
67
|
-
async def test_list_tools(self):
|
|
68
|
-
"""Test listing tools."""
|
|
69
|
-
config = {"server1": {"command": "test"}}
|
|
70
|
-
|
|
71
|
-
with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
|
|
72
|
-
mock_fastmcp = AsyncMock()
|
|
73
|
-
mock_tools = [
|
|
74
|
-
MagicMock(spec=types.Tool, name="tool1"),
|
|
75
|
-
MagicMock(spec=types.Tool, name="tool2"),
|
|
76
|
-
]
|
|
77
|
-
mock_fastmcp.list_tools.return_value = mock_tools
|
|
78
|
-
mock_client_class.return_value = mock_fastmcp
|
|
79
|
-
|
|
80
|
-
client = FastMCPHUDClient(config)
|
|
81
|
-
client._initialized = True # Skip initialization
|
|
82
|
-
client._client = mock_fastmcp # Set the mock client
|
|
83
|
-
|
|
84
|
-
tools = await client.list_tools()
|
|
85
|
-
|
|
86
|
-
assert tools == mock_tools
|
|
87
|
-
mock_fastmcp.list_tools.assert_called_once()
|
|
88
|
-
|
|
89
|
-
@pytest.mark.asyncio
|
|
90
|
-
async def test_call_tool(self):
|
|
91
|
-
"""Test calling a tool."""
|
|
92
|
-
config = {"server1": {"command": "test"}}
|
|
93
|
-
|
|
94
|
-
with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
|
|
95
|
-
mock_fastmcp = AsyncMock()
|
|
96
|
-
|
|
97
|
-
# Mock FastMCP result
|
|
98
|
-
mock_result = MagicMock()
|
|
99
|
-
mock_result.content = [types.TextContent(type="text", text="result")]
|
|
100
|
-
mock_result.is_error = False
|
|
101
|
-
mock_result.structured_content = {"key": "value"}
|
|
102
|
-
|
|
103
|
-
mock_fastmcp.call_tool.return_value = mock_result
|
|
104
|
-
mock_client_class.return_value = mock_fastmcp
|
|
105
|
-
|
|
106
|
-
client = FastMCPHUDClient(config)
|
|
107
|
-
client._initialized = True
|
|
108
|
-
client._client = mock_fastmcp # Set the mock client
|
|
109
|
-
|
|
110
|
-
result = await client.call_tool(name="test_tool", arguments={"arg": "value"})
|
|
111
|
-
|
|
112
|
-
assert isinstance(result, MCPToolResult)
|
|
113
|
-
assert result.content == mock_result.content
|
|
114
|
-
assert result.isError is False
|
|
115
|
-
assert result.structuredContent == {"key": "value"}
|
|
116
|
-
|
|
117
|
-
mock_fastmcp.call_tool.assert_called_once_with(
|
|
118
|
-
name="test_tool",
|
|
119
|
-
arguments={"arg": "value"},
|
|
120
|
-
raise_on_error=False,
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
@pytest.mark.asyncio
|
|
124
|
-
async def test_call_tool_with_mcp_tool_call(self):
|
|
125
|
-
"""Test calling a tool with MCPToolCall object."""
|
|
126
|
-
config = {"server1": {"command": "test"}}
|
|
127
|
-
|
|
128
|
-
with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
|
|
129
|
-
mock_fastmcp = AsyncMock()
|
|
130
|
-
|
|
131
|
-
# Mock FastMCP result
|
|
132
|
-
mock_result = MagicMock()
|
|
133
|
-
mock_result.content = [types.TextContent(type="text", text="result")]
|
|
134
|
-
mock_result.is_error = False
|
|
135
|
-
mock_result.structured_content = {"key": "value"}
|
|
136
|
-
|
|
137
|
-
mock_fastmcp.call_tool.return_value = mock_result
|
|
138
|
-
mock_client_class.return_value = mock_fastmcp
|
|
139
|
-
|
|
140
|
-
client = FastMCPHUDClient(config)
|
|
141
|
-
client._initialized = True
|
|
142
|
-
client._client = mock_fastmcp # Set the mock client
|
|
143
|
-
|
|
144
|
-
# Test with MCPToolCall object
|
|
145
|
-
tool_call = MCPToolCall(name="test_tool", arguments={"arg": "value"})
|
|
146
|
-
result = await client.call_tool(tool_call)
|
|
147
|
-
|
|
148
|
-
assert isinstance(result, MCPToolResult)
|
|
149
|
-
assert result.content == mock_result.content
|
|
150
|
-
assert result.isError is False
|
|
151
|
-
assert result.structuredContent == {"key": "value"}
|
|
152
|
-
|
|
153
|
-
mock_fastmcp.call_tool.assert_called_once_with(
|
|
154
|
-
name="test_tool",
|
|
155
|
-
arguments={"arg": "value"},
|
|
156
|
-
raise_on_error=False,
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
@pytest.mark.asyncio
|
|
160
|
-
async def test_call_tool_no_arguments(self):
|
|
161
|
-
"""Test calling a tool without arguments."""
|
|
162
|
-
config = {"server1": {"command": "test"}}
|
|
163
|
-
|
|
164
|
-
with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
|
|
165
|
-
mock_fastmcp = AsyncMock()
|
|
166
|
-
mock_result = MagicMock()
|
|
167
|
-
mock_result.content = []
|
|
168
|
-
mock_result.is_error = True
|
|
169
|
-
mock_result.structured_content = None
|
|
170
|
-
|
|
171
|
-
mock_fastmcp.call_tool.return_value = mock_result
|
|
172
|
-
mock_client_class.return_value = mock_fastmcp
|
|
173
|
-
|
|
174
|
-
client = FastMCPHUDClient(config)
|
|
175
|
-
client._initialized = True
|
|
176
|
-
client._client = mock_fastmcp # Set the mock client
|
|
177
|
-
|
|
178
|
-
await client.call_tool(name="test_tool", arguments={})
|
|
179
|
-
|
|
180
|
-
# Should pass empty dict for arguments
|
|
181
|
-
mock_fastmcp.call_tool.assert_called_once_with(
|
|
182
|
-
name="test_tool",
|
|
183
|
-
arguments={},
|
|
184
|
-
raise_on_error=False,
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
@pytest.mark.asyncio
|
|
188
|
-
async def test_list_resources(self):
|
|
189
|
-
"""Test listing resources."""
|
|
190
|
-
config = {"server1": {"command": "test"}}
|
|
191
|
-
|
|
192
|
-
with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
|
|
193
|
-
mock_fastmcp = AsyncMock()
|
|
194
|
-
mock_resources = [
|
|
195
|
-
MagicMock(spec=types.Resource, uri="file:///test1"),
|
|
196
|
-
MagicMock(spec=types.Resource, uri="file:///test2"),
|
|
197
|
-
]
|
|
198
|
-
mock_fastmcp.list_resources.return_value = mock_resources
|
|
199
|
-
mock_client_class.return_value = mock_fastmcp
|
|
200
|
-
|
|
201
|
-
client = FastMCPHUDClient(config)
|
|
202
|
-
client._initialized = True
|
|
203
|
-
client._client = mock_fastmcp # Set the mock client
|
|
204
|
-
|
|
205
|
-
resources = await client.list_resources()
|
|
206
|
-
|
|
207
|
-
assert resources == mock_resources
|
|
208
|
-
mock_fastmcp.list_resources.assert_called_once()
|
|
209
|
-
|
|
210
|
-
@pytest.mark.asyncio
|
|
211
|
-
async def test_read_resource_internal_success(self):
|
|
212
|
-
"""Test reading a resource successfully."""
|
|
213
|
-
config = {"server1": {"command": "test"}}
|
|
214
|
-
|
|
215
|
-
# Create proper resource contents that ReadResourceResult expects
|
|
216
|
-
mock_contents = [
|
|
217
|
-
types.TextResourceContents(
|
|
218
|
-
uri=AnyUrl("file:///test"),
|
|
219
|
-
mimeType="text/plain",
|
|
220
|
-
text="resource content",
|
|
221
|
-
)
|
|
222
|
-
]
|
|
223
|
-
|
|
224
|
-
with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
|
|
225
|
-
# Create a mock FastMCP client
|
|
226
|
-
mock_fastmcp = AsyncMock()
|
|
227
|
-
mock_fastmcp.read_resource.return_value = mock_contents
|
|
228
|
-
mock_client_class.return_value = mock_fastmcp
|
|
229
|
-
|
|
230
|
-
# Now create the HUD client - it will use our mocked FastMCP client
|
|
231
|
-
client = FastMCPHUDClient(config)
|
|
232
|
-
client._initialized = True
|
|
233
|
-
client._client = mock_fastmcp # Set the mock client
|
|
234
|
-
|
|
235
|
-
result = await client.read_resource("file:///test")
|
|
236
|
-
|
|
237
|
-
assert isinstance(result, types.ReadResourceResult)
|
|
238
|
-
assert result.contents == mock_contents
|
|
239
|
-
mock_fastmcp.read_resource.assert_called_once_with("file:///test")
|
|
240
|
-
|
|
241
|
-
@pytest.mark.asyncio
|
|
242
|
-
async def test_read_resource_internal_error_verbose(self):
|
|
243
|
-
"""Test reading a resource with error in verbose mode."""
|
|
244
|
-
config = {"server1": {"command": "test"}}
|
|
245
|
-
|
|
246
|
-
with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
|
|
247
|
-
mock_fastmcp = AsyncMock()
|
|
248
|
-
mock_fastmcp.read_resource.side_effect = Exception("Read failed")
|
|
249
|
-
mock_client_class.return_value = mock_fastmcp
|
|
250
|
-
|
|
251
|
-
client = FastMCPHUDClient(config, verbose=True)
|
|
252
|
-
client._initialized = True
|
|
253
|
-
client._client = mock_fastmcp # Set the mock client
|
|
254
|
-
|
|
255
|
-
with patch("hud.clients.fastmcp.logger") as mock_logger:
|
|
256
|
-
result = await client.read_resource("file:///bad")
|
|
257
|
-
|
|
258
|
-
assert result is None
|
|
259
|
-
mock_logger.warning.assert_called_with(
|
|
260
|
-
"Unexpected error reading resource '%s': %s", "file:///bad", ANY
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
@pytest.mark.asyncio
|
|
264
|
-
async def test_read_resource_internal_error_not_verbose(self):
|
|
265
|
-
"""Test reading a resource with error in non-verbose mode."""
|
|
266
|
-
config = {"server1": {"command": "test"}}
|
|
267
|
-
|
|
268
|
-
with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
|
|
269
|
-
mock_fastmcp = AsyncMock()
|
|
270
|
-
mock_fastmcp.read_resource.side_effect = Exception("Read failed")
|
|
271
|
-
mock_client_class.return_value = mock_fastmcp
|
|
272
|
-
|
|
273
|
-
client = FastMCPHUDClient(config, verbose=False)
|
|
274
|
-
client._initialized = True
|
|
275
|
-
client._client = mock_fastmcp # Set the mock client
|
|
276
|
-
|
|
277
|
-
with patch("hud.clients.fastmcp.logger") as mock_logger:
|
|
278
|
-
result = await client.read_resource("file:///bad")
|
|
279
|
-
|
|
280
|
-
assert result is None
|
|
281
|
-
# Should not log in non-verbose mode
|
|
282
|
-
mock_logger.debug.assert_not_called()
|
|
283
|
-
|
|
284
|
-
@pytest.mark.asyncio
|
|
285
|
-
async def test_shutdown(self):
|
|
286
|
-
"""Test shutting down the client."""
|
|
287
|
-
config = {"server1": {"command": "test"}}
|
|
288
|
-
|
|
289
|
-
with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
|
|
290
|
-
mock_fastmcp = AsyncMock()
|
|
291
|
-
mock_client_class.return_value = mock_fastmcp
|
|
292
|
-
|
|
293
|
-
client = FastMCPHUDClient(config)
|
|
294
|
-
|
|
295
|
-
# Set up stack and client
|
|
296
|
-
mock_stack = AsyncMock()
|
|
297
|
-
client._stack = mock_stack
|
|
298
|
-
client._initialized = True
|
|
299
|
-
client._client = mock_fastmcp # Set the mock client
|
|
300
|
-
|
|
301
|
-
with patch("hud.clients.fastmcp.logger") as mock_logger:
|
|
302
|
-
await client.shutdown()
|
|
303
|
-
|
|
304
|
-
mock_stack.aclose.assert_called_once()
|
|
305
|
-
assert client._stack is None
|
|
306
|
-
assert client._initialized is False
|
|
307
|
-
mock_logger.debug.assert_called_with("FastMCP client closed")
|
|
308
|
-
|
|
309
|
-
@pytest.mark.asyncio
|
|
310
|
-
async def test_shutdown_no_stack(self):
|
|
311
|
-
"""Test shutting down when no stack exists."""
|
|
312
|
-
config = {"server1": {"command": "test"}}
|
|
313
|
-
|
|
314
|
-
with patch("hud.clients.fastmcp.FastMCPClient"):
|
|
315
|
-
client = FastMCPHUDClient(config)
|
|
316
|
-
client._stack = None
|
|
317
|
-
|
|
318
|
-
# Should not raise error
|
|
319
|
-
await client.shutdown()
|
|
320
|
-
|
|
321
|
-
assert client._stack is None
|
|
322
|
-
|
|
323
|
-
@pytest.mark.asyncio
|
|
324
|
-
async def test_context_manager(self):
|
|
325
|
-
"""Test using client as async context manager."""
|
|
326
|
-
config = {"server1": {"command": "test"}}
|
|
327
|
-
|
|
328
|
-
with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
|
|
329
|
-
mock_fastmcp = AsyncMock()
|
|
330
|
-
mock_client_class.return_value = mock_fastmcp
|
|
331
|
-
|
|
332
|
-
client = FastMCPHUDClient(config)
|
|
333
|
-
|
|
334
|
-
with (
|
|
335
|
-
patch.object(client, "initialize", new_callable=AsyncMock) as mock_init,
|
|
336
|
-
patch.object(client, "shutdown", new_callable=AsyncMock) as mock_close,
|
|
337
|
-
):
|
|
338
|
-
async with client as ctx:
|
|
339
|
-
assert ctx is client
|
|
340
|
-
mock_init.assert_called_once()
|
|
341
|
-
|
|
342
|
-
mock_close.assert_called_once()
|
|
1
|
+
"""Tests for FastMCP client implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from unittest.mock import ANY, AsyncMock, MagicMock, patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from mcp import types
|
|
9
|
+
from pydantic.networks import AnyUrl
|
|
10
|
+
|
|
11
|
+
from hud.clients.fastmcp import FastMCPHUDClient
|
|
12
|
+
from hud.types import MCPToolCall, MCPToolResult
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestFastMCPHUDClient:
|
|
16
|
+
"""Test FastMCP HUD client."""
|
|
17
|
+
|
|
18
|
+
def test_initialization(self):
|
|
19
|
+
"""Test client initialization."""
|
|
20
|
+
config = {"server1": {"command": "python", "args": ["server.py"]}}
|
|
21
|
+
|
|
22
|
+
# Client is just instantiated, not connected yet
|
|
23
|
+
client = FastMCPHUDClient(config)
|
|
24
|
+
|
|
25
|
+
# Check that the client has the config stored
|
|
26
|
+
assert client._mcp_config == config
|
|
27
|
+
assert client._client is None # Not connected yet
|
|
28
|
+
|
|
29
|
+
@pytest.mark.asyncio
|
|
30
|
+
async def test_connect_creates_client(self):
|
|
31
|
+
"""Test that _connect creates the FastMCP client."""
|
|
32
|
+
config = {"server1": {"command": "test"}}
|
|
33
|
+
|
|
34
|
+
with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
|
|
35
|
+
mock_fastmcp = AsyncMock()
|
|
36
|
+
mock_client_class.return_value = mock_fastmcp
|
|
37
|
+
|
|
38
|
+
client = FastMCPHUDClient(config)
|
|
39
|
+
await client._connect(config)
|
|
40
|
+
|
|
41
|
+
# Check FastMCP client was created
|
|
42
|
+
mock_client_class.assert_called_once()
|
|
43
|
+
|
|
44
|
+
# Check it was created with correct transport and client info
|
|
45
|
+
call_args = mock_client_class.call_args
|
|
46
|
+
assert call_args[0][0] == {"mcpServers": config}
|
|
47
|
+
assert call_args[1]["client_info"].name == "hud-fastmcp"
|
|
48
|
+
|
|
49
|
+
@pytest.mark.asyncio
|
|
50
|
+
async def test_connect_logs_info(self):
|
|
51
|
+
"""Test that connect logs info message."""
|
|
52
|
+
config = {"server1": {"command": "test"}}
|
|
53
|
+
|
|
54
|
+
with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
|
|
55
|
+
mock_fastmcp = AsyncMock()
|
|
56
|
+
mock_client_class.return_value = mock_fastmcp
|
|
57
|
+
|
|
58
|
+
client = FastMCPHUDClient(config)
|
|
59
|
+
|
|
60
|
+
with patch("hud.clients.fastmcp.logger") as mock_logger:
|
|
61
|
+
await client._connect(config)
|
|
62
|
+
|
|
63
|
+
# Check info was logged
|
|
64
|
+
mock_logger.info.assert_called_with("FastMCP client connected")
|
|
65
|
+
|
|
66
|
+
@pytest.mark.asyncio
|
|
67
|
+
async def test_list_tools(self):
|
|
68
|
+
"""Test listing tools."""
|
|
69
|
+
config = {"server1": {"command": "test"}}
|
|
70
|
+
|
|
71
|
+
with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
|
|
72
|
+
mock_fastmcp = AsyncMock()
|
|
73
|
+
mock_tools = [
|
|
74
|
+
MagicMock(spec=types.Tool, name="tool1"),
|
|
75
|
+
MagicMock(spec=types.Tool, name="tool2"),
|
|
76
|
+
]
|
|
77
|
+
mock_fastmcp.list_tools.return_value = mock_tools
|
|
78
|
+
mock_client_class.return_value = mock_fastmcp
|
|
79
|
+
|
|
80
|
+
client = FastMCPHUDClient(config)
|
|
81
|
+
client._initialized = True # Skip initialization
|
|
82
|
+
client._client = mock_fastmcp # Set the mock client
|
|
83
|
+
|
|
84
|
+
tools = await client.list_tools()
|
|
85
|
+
|
|
86
|
+
assert tools == mock_tools
|
|
87
|
+
mock_fastmcp.list_tools.assert_called_once()
|
|
88
|
+
|
|
89
|
+
@pytest.mark.asyncio
|
|
90
|
+
async def test_call_tool(self):
|
|
91
|
+
"""Test calling a tool."""
|
|
92
|
+
config = {"server1": {"command": "test"}}
|
|
93
|
+
|
|
94
|
+
with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
|
|
95
|
+
mock_fastmcp = AsyncMock()
|
|
96
|
+
|
|
97
|
+
# Mock FastMCP result
|
|
98
|
+
mock_result = MagicMock()
|
|
99
|
+
mock_result.content = [types.TextContent(type="text", text="result")]
|
|
100
|
+
mock_result.is_error = False
|
|
101
|
+
mock_result.structured_content = {"key": "value"}
|
|
102
|
+
|
|
103
|
+
mock_fastmcp.call_tool.return_value = mock_result
|
|
104
|
+
mock_client_class.return_value = mock_fastmcp
|
|
105
|
+
|
|
106
|
+
client = FastMCPHUDClient(config)
|
|
107
|
+
client._initialized = True
|
|
108
|
+
client._client = mock_fastmcp # Set the mock client
|
|
109
|
+
|
|
110
|
+
result = await client.call_tool(name="test_tool", arguments={"arg": "value"})
|
|
111
|
+
|
|
112
|
+
assert isinstance(result, MCPToolResult)
|
|
113
|
+
assert result.content == mock_result.content
|
|
114
|
+
assert result.isError is False
|
|
115
|
+
assert result.structuredContent == {"key": "value"}
|
|
116
|
+
|
|
117
|
+
mock_fastmcp.call_tool.assert_called_once_with(
|
|
118
|
+
name="test_tool",
|
|
119
|
+
arguments={"arg": "value"},
|
|
120
|
+
raise_on_error=False,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
@pytest.mark.asyncio
|
|
124
|
+
async def test_call_tool_with_mcp_tool_call(self):
|
|
125
|
+
"""Test calling a tool with MCPToolCall object."""
|
|
126
|
+
config = {"server1": {"command": "test"}}
|
|
127
|
+
|
|
128
|
+
with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
|
|
129
|
+
mock_fastmcp = AsyncMock()
|
|
130
|
+
|
|
131
|
+
# Mock FastMCP result
|
|
132
|
+
mock_result = MagicMock()
|
|
133
|
+
mock_result.content = [types.TextContent(type="text", text="result")]
|
|
134
|
+
mock_result.is_error = False
|
|
135
|
+
mock_result.structured_content = {"key": "value"}
|
|
136
|
+
|
|
137
|
+
mock_fastmcp.call_tool.return_value = mock_result
|
|
138
|
+
mock_client_class.return_value = mock_fastmcp
|
|
139
|
+
|
|
140
|
+
client = FastMCPHUDClient(config)
|
|
141
|
+
client._initialized = True
|
|
142
|
+
client._client = mock_fastmcp # Set the mock client
|
|
143
|
+
|
|
144
|
+
# Test with MCPToolCall object
|
|
145
|
+
tool_call = MCPToolCall(name="test_tool", arguments={"arg": "value"})
|
|
146
|
+
result = await client.call_tool(tool_call)
|
|
147
|
+
|
|
148
|
+
assert isinstance(result, MCPToolResult)
|
|
149
|
+
assert result.content == mock_result.content
|
|
150
|
+
assert result.isError is False
|
|
151
|
+
assert result.structuredContent == {"key": "value"}
|
|
152
|
+
|
|
153
|
+
mock_fastmcp.call_tool.assert_called_once_with(
|
|
154
|
+
name="test_tool",
|
|
155
|
+
arguments={"arg": "value"},
|
|
156
|
+
raise_on_error=False,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
@pytest.mark.asyncio
|
|
160
|
+
async def test_call_tool_no_arguments(self):
|
|
161
|
+
"""Test calling a tool without arguments."""
|
|
162
|
+
config = {"server1": {"command": "test"}}
|
|
163
|
+
|
|
164
|
+
with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
|
|
165
|
+
mock_fastmcp = AsyncMock()
|
|
166
|
+
mock_result = MagicMock()
|
|
167
|
+
mock_result.content = []
|
|
168
|
+
mock_result.is_error = True
|
|
169
|
+
mock_result.structured_content = None
|
|
170
|
+
|
|
171
|
+
mock_fastmcp.call_tool.return_value = mock_result
|
|
172
|
+
mock_client_class.return_value = mock_fastmcp
|
|
173
|
+
|
|
174
|
+
client = FastMCPHUDClient(config)
|
|
175
|
+
client._initialized = True
|
|
176
|
+
client._client = mock_fastmcp # Set the mock client
|
|
177
|
+
|
|
178
|
+
await client.call_tool(name="test_tool", arguments={})
|
|
179
|
+
|
|
180
|
+
# Should pass empty dict for arguments
|
|
181
|
+
mock_fastmcp.call_tool.assert_called_once_with(
|
|
182
|
+
name="test_tool",
|
|
183
|
+
arguments={},
|
|
184
|
+
raise_on_error=False,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
@pytest.mark.asyncio
|
|
188
|
+
async def test_list_resources(self):
|
|
189
|
+
"""Test listing resources."""
|
|
190
|
+
config = {"server1": {"command": "test"}}
|
|
191
|
+
|
|
192
|
+
with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
|
|
193
|
+
mock_fastmcp = AsyncMock()
|
|
194
|
+
mock_resources = [
|
|
195
|
+
MagicMock(spec=types.Resource, uri="file:///test1"),
|
|
196
|
+
MagicMock(spec=types.Resource, uri="file:///test2"),
|
|
197
|
+
]
|
|
198
|
+
mock_fastmcp.list_resources.return_value = mock_resources
|
|
199
|
+
mock_client_class.return_value = mock_fastmcp
|
|
200
|
+
|
|
201
|
+
client = FastMCPHUDClient(config)
|
|
202
|
+
client._initialized = True
|
|
203
|
+
client._client = mock_fastmcp # Set the mock client
|
|
204
|
+
|
|
205
|
+
resources = await client.list_resources()
|
|
206
|
+
|
|
207
|
+
assert resources == mock_resources
|
|
208
|
+
mock_fastmcp.list_resources.assert_called_once()
|
|
209
|
+
|
|
210
|
+
@pytest.mark.asyncio
|
|
211
|
+
async def test_read_resource_internal_success(self):
|
|
212
|
+
"""Test reading a resource successfully."""
|
|
213
|
+
config = {"server1": {"command": "test"}}
|
|
214
|
+
|
|
215
|
+
# Create proper resource contents that ReadResourceResult expects
|
|
216
|
+
mock_contents = [
|
|
217
|
+
types.TextResourceContents(
|
|
218
|
+
uri=AnyUrl("file:///test"),
|
|
219
|
+
mimeType="text/plain",
|
|
220
|
+
text="resource content",
|
|
221
|
+
)
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
|
|
225
|
+
# Create a mock FastMCP client
|
|
226
|
+
mock_fastmcp = AsyncMock()
|
|
227
|
+
mock_fastmcp.read_resource.return_value = mock_contents
|
|
228
|
+
mock_client_class.return_value = mock_fastmcp
|
|
229
|
+
|
|
230
|
+
# Now create the HUD client - it will use our mocked FastMCP client
|
|
231
|
+
client = FastMCPHUDClient(config)
|
|
232
|
+
client._initialized = True
|
|
233
|
+
client._client = mock_fastmcp # Set the mock client
|
|
234
|
+
|
|
235
|
+
result = await client.read_resource("file:///test")
|
|
236
|
+
|
|
237
|
+
assert isinstance(result, types.ReadResourceResult)
|
|
238
|
+
assert result.contents == mock_contents
|
|
239
|
+
mock_fastmcp.read_resource.assert_called_once_with("file:///test")
|
|
240
|
+
|
|
241
|
+
@pytest.mark.asyncio
|
|
242
|
+
async def test_read_resource_internal_error_verbose(self):
|
|
243
|
+
"""Test reading a resource with error in verbose mode."""
|
|
244
|
+
config = {"server1": {"command": "test"}}
|
|
245
|
+
|
|
246
|
+
with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
|
|
247
|
+
mock_fastmcp = AsyncMock()
|
|
248
|
+
mock_fastmcp.read_resource.side_effect = Exception("Read failed")
|
|
249
|
+
mock_client_class.return_value = mock_fastmcp
|
|
250
|
+
|
|
251
|
+
client = FastMCPHUDClient(config, verbose=True)
|
|
252
|
+
client._initialized = True
|
|
253
|
+
client._client = mock_fastmcp # Set the mock client
|
|
254
|
+
|
|
255
|
+
with patch("hud.clients.fastmcp.logger") as mock_logger:
|
|
256
|
+
result = await client.read_resource("file:///bad")
|
|
257
|
+
|
|
258
|
+
assert result is None
|
|
259
|
+
mock_logger.warning.assert_called_with(
|
|
260
|
+
"Unexpected error reading resource '%s': %s", "file:///bad", ANY
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
@pytest.mark.asyncio
|
|
264
|
+
async def test_read_resource_internal_error_not_verbose(self):
|
|
265
|
+
"""Test reading a resource with error in non-verbose mode."""
|
|
266
|
+
config = {"server1": {"command": "test"}}
|
|
267
|
+
|
|
268
|
+
with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
|
|
269
|
+
mock_fastmcp = AsyncMock()
|
|
270
|
+
mock_fastmcp.read_resource.side_effect = Exception("Read failed")
|
|
271
|
+
mock_client_class.return_value = mock_fastmcp
|
|
272
|
+
|
|
273
|
+
client = FastMCPHUDClient(config, verbose=False)
|
|
274
|
+
client._initialized = True
|
|
275
|
+
client._client = mock_fastmcp # Set the mock client
|
|
276
|
+
|
|
277
|
+
with patch("hud.clients.fastmcp.logger") as mock_logger:
|
|
278
|
+
result = await client.read_resource("file:///bad")
|
|
279
|
+
|
|
280
|
+
assert result is None
|
|
281
|
+
# Should not log in non-verbose mode
|
|
282
|
+
mock_logger.debug.assert_not_called()
|
|
283
|
+
|
|
284
|
+
@pytest.mark.asyncio
|
|
285
|
+
async def test_shutdown(self):
|
|
286
|
+
"""Test shutting down the client."""
|
|
287
|
+
config = {"server1": {"command": "test"}}
|
|
288
|
+
|
|
289
|
+
with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
|
|
290
|
+
mock_fastmcp = AsyncMock()
|
|
291
|
+
mock_client_class.return_value = mock_fastmcp
|
|
292
|
+
|
|
293
|
+
client = FastMCPHUDClient(config)
|
|
294
|
+
|
|
295
|
+
# Set up stack and client
|
|
296
|
+
mock_stack = AsyncMock()
|
|
297
|
+
client._stack = mock_stack
|
|
298
|
+
client._initialized = True
|
|
299
|
+
client._client = mock_fastmcp # Set the mock client
|
|
300
|
+
|
|
301
|
+
with patch("hud.clients.fastmcp.logger") as mock_logger:
|
|
302
|
+
await client.shutdown()
|
|
303
|
+
|
|
304
|
+
mock_stack.aclose.assert_called_once()
|
|
305
|
+
assert client._stack is None
|
|
306
|
+
assert client._initialized is False
|
|
307
|
+
mock_logger.debug.assert_called_with("FastMCP client closed")
|
|
308
|
+
|
|
309
|
+
@pytest.mark.asyncio
|
|
310
|
+
async def test_shutdown_no_stack(self):
|
|
311
|
+
"""Test shutting down when no stack exists."""
|
|
312
|
+
config = {"server1": {"command": "test"}}
|
|
313
|
+
|
|
314
|
+
with patch("hud.clients.fastmcp.FastMCPClient"):
|
|
315
|
+
client = FastMCPHUDClient(config)
|
|
316
|
+
client._stack = None
|
|
317
|
+
|
|
318
|
+
# Should not raise error
|
|
319
|
+
await client.shutdown()
|
|
320
|
+
|
|
321
|
+
assert client._stack is None
|
|
322
|
+
|
|
323
|
+
@pytest.mark.asyncio
|
|
324
|
+
async def test_context_manager(self):
|
|
325
|
+
"""Test using client as async context manager."""
|
|
326
|
+
config = {"server1": {"command": "test"}}
|
|
327
|
+
|
|
328
|
+
with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
|
|
329
|
+
mock_fastmcp = AsyncMock()
|
|
330
|
+
mock_client_class.return_value = mock_fastmcp
|
|
331
|
+
|
|
332
|
+
client = FastMCPHUDClient(config)
|
|
333
|
+
|
|
334
|
+
with (
|
|
335
|
+
patch.object(client, "initialize", new_callable=AsyncMock) as mock_init,
|
|
336
|
+
patch.object(client, "shutdown", new_callable=AsyncMock) as mock_close,
|
|
337
|
+
):
|
|
338
|
+
async with client as ctx:
|
|
339
|
+
assert ctx is client
|
|
340
|
+
mock_init.assert_called_once()
|
|
341
|
+
|
|
342
|
+
mock_close.assert_called_once()
|