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
hud/clients/base.py
CHANGED
|
@@ -1,379 +1,378 @@
|
|
|
1
|
-
"""Base protocol and implementation for HUD MCP clients."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
import logging
|
|
7
|
-
from abc import abstractmethod
|
|
8
|
-
from typing import TYPE_CHECKING, Any, Protocol, overload, runtime_checkable
|
|
9
|
-
|
|
10
|
-
from mcp.types import Implementation
|
|
11
|
-
|
|
12
|
-
from hud.types import MCPToolCall, MCPToolResult
|
|
13
|
-
from hud.utils.mcp import setup_hud_telemetry
|
|
14
|
-
from hud.version import __version__ as hud_version
|
|
15
|
-
|
|
16
|
-
if TYPE_CHECKING:
|
|
17
|
-
import mcp.types as types
|
|
18
|
-
|
|
19
|
-
else:
|
|
20
|
-
pass
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
logger = logging.getLogger(__name__)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@runtime_checkable
|
|
27
|
-
class AgentMCPClient(Protocol):
|
|
28
|
-
"""Minimal interface for MCP clients used by agents.
|
|
29
|
-
|
|
30
|
-
Any custom client must implement this interface.
|
|
31
|
-
|
|
32
|
-
Any custom agent can assume that this will be the interaction protocol.
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
|
-
_initialized: bool
|
|
36
|
-
|
|
37
|
-
@property
|
|
38
|
-
def mcp_config(self) -> dict[str, dict[str, Any]]:
|
|
39
|
-
"""Get the MCP config."""
|
|
40
|
-
...
|
|
41
|
-
|
|
42
|
-
@property
|
|
43
|
-
def is_connected(self) -> bool:
|
|
44
|
-
"""Check if client is connected and initialized."""
|
|
45
|
-
...
|
|
46
|
-
|
|
47
|
-
async def initialize(self, mcp_config: dict[str, dict[str, Any]] | None = None) -> None:
|
|
48
|
-
"""Initialize the client."""
|
|
49
|
-
...
|
|
50
|
-
|
|
51
|
-
async def list_tools(self) -> list[types.Tool]:
|
|
52
|
-
"""List all available tools."""
|
|
53
|
-
...
|
|
54
|
-
|
|
55
|
-
async def call_tool(self, tool_call: MCPToolCall) -> MCPToolResult:
|
|
56
|
-
"""Execute a tool by name."""
|
|
57
|
-
...
|
|
58
|
-
|
|
59
|
-
async def shutdown(self) -> None:
|
|
60
|
-
"""Shutdown the client."""
|
|
61
|
-
...
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
class BaseHUDClient(AgentMCPClient):
|
|
65
|
-
"""Base class with common HUD functionality that adds:
|
|
66
|
-
- Connection management
|
|
67
|
-
- Tool discovery
|
|
68
|
-
- Telemetry fetching (hud environment-specific)
|
|
69
|
-
- Logging
|
|
70
|
-
- Strict tool output validation (optional)
|
|
71
|
-
- Environment analysis (optional)
|
|
72
|
-
|
|
73
|
-
Any custom client should inherit from this class, and implement:
|
|
74
|
-
- _connect: Connect to the MCP server
|
|
75
|
-
- list_tools: List all available tools
|
|
76
|
-
- list_resources: List all available resources
|
|
77
|
-
- call_tool: Execute a tool by name
|
|
78
|
-
- read_resource: Read a resource by URI
|
|
79
|
-
- _disconnect: Disconnect from the MCP server
|
|
80
|
-
- any other MCP client methods
|
|
81
|
-
"""
|
|
82
|
-
|
|
83
|
-
client_info = Implementation(name="hud-mcp", title="hud MCP Client", version=hud_version)
|
|
84
|
-
|
|
85
|
-
def __init__(
|
|
86
|
-
self,
|
|
87
|
-
mcp_config: dict[str, dict[str, Any]] | None = None,
|
|
88
|
-
verbose: bool = False,
|
|
89
|
-
strict_validation: bool = False,
|
|
90
|
-
auto_trace: bool = True,
|
|
91
|
-
) -> None:
|
|
92
|
-
"""
|
|
93
|
-
Initialize base client.
|
|
94
|
-
|
|
95
|
-
Args:
|
|
96
|
-
mcp_config: MCP server configuration dict
|
|
97
|
-
verbose: Enable verbose logging
|
|
98
|
-
strict_validation: Enable strict tool output validation
|
|
99
|
-
"""
|
|
100
|
-
self.verbose = verbose
|
|
101
|
-
self._mcp_config = mcp_config
|
|
102
|
-
self._strict_validation = strict_validation
|
|
103
|
-
self._auto_trace = auto_trace
|
|
104
|
-
|
|
105
|
-
self._initialized = False
|
|
106
|
-
self._telemetry_data = {} # Initialize telemetry data
|
|
107
|
-
|
|
108
|
-
if self.verbose:
|
|
109
|
-
self._setup_verbose_logging()
|
|
110
|
-
|
|
111
|
-
async def initialize(self, mcp_config: dict[str, dict[str, Any]] | None = None) -> None:
|
|
112
|
-
"""Initialize connection and fetch tools."""
|
|
113
|
-
if self._initialized:
|
|
114
|
-
logger.warning(
|
|
115
|
-
"Client already connected, if you want to reconnect or change the configuration, "
|
|
116
|
-
"call shutdown() first. This is especially important if you are using an agent."
|
|
117
|
-
)
|
|
118
|
-
return
|
|
119
|
-
|
|
120
|
-
self._mcp_config = mcp_config or self._mcp_config
|
|
121
|
-
if self._mcp_config is None:
|
|
122
|
-
raise ValueError(
|
|
123
|
-
"An MCP server configuration is required"
|
|
124
|
-
"Either pass it to the constructor or call initialize with a configuration"
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
setup_hud_telemetry(self._mcp_config, auto_trace=self._auto_trace)
|
|
128
|
-
|
|
129
|
-
logger.debug("Initializing MCP client...")
|
|
130
|
-
|
|
131
|
-
try:
|
|
132
|
-
# Subclasses implement connection
|
|
133
|
-
await self._connect(self._mcp_config)
|
|
134
|
-
except RuntimeError as e:
|
|
135
|
-
# Re-raise authentication errors with clear message
|
|
136
|
-
if "Authentication failed" in str(e):
|
|
137
|
-
raise
|
|
138
|
-
raise
|
|
139
|
-
except Exception as e:
|
|
140
|
-
# Check for authentication errors in the exception chain
|
|
141
|
-
error_msg = str(e)
|
|
142
|
-
if "401" in error_msg or "Unauthorized" in error_msg:
|
|
143
|
-
# Check if connecting to HUD API
|
|
144
|
-
for server_config in self._mcp_config.values():
|
|
145
|
-
url = server_config.get("url", "")
|
|
146
|
-
if "mcp.hud.so" in url:
|
|
147
|
-
raise RuntimeError(
|
|
148
|
-
"Authentication failed for HUD API. "
|
|
149
|
-
"Please ensure your HUD_API_KEY environment variable is set correctly. "
|
|
150
|
-
"You can get an API key at https://app.hud.so"
|
|
151
|
-
) from e
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
"
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
""
|
|
254
|
-
logging.getLogger("
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
handler
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
)
|
|
262
|
-
logger.
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
result
|
|
270
|
-
|
|
271
|
-
#
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
-
|
|
301
|
-
-
|
|
302
|
-
-
|
|
303
|
-
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
"
|
|
311
|
-
"
|
|
312
|
-
"
|
|
313
|
-
"
|
|
314
|
-
|
|
315
|
-
"
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
"
|
|
325
|
-
"
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
tool.description
|
|
333
|
-
and "
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
hub_functions
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
"
|
|
347
|
-
"
|
|
348
|
-
"
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
result
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
functions
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
return []
|
|
1
|
+
"""Base protocol and implementation for HUD MCP clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from abc import abstractmethod
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Protocol, overload, runtime_checkable
|
|
9
|
+
|
|
10
|
+
from mcp.types import Implementation
|
|
11
|
+
|
|
12
|
+
from hud.types import MCPToolCall, MCPToolResult
|
|
13
|
+
from hud.utils.mcp import setup_hud_telemetry
|
|
14
|
+
from hud.version import __version__ as hud_version
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
import mcp.types as types
|
|
18
|
+
|
|
19
|
+
else:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@runtime_checkable
|
|
27
|
+
class AgentMCPClient(Protocol):
|
|
28
|
+
"""Minimal interface for MCP clients used by agents.
|
|
29
|
+
|
|
30
|
+
Any custom client must implement this interface.
|
|
31
|
+
|
|
32
|
+
Any custom agent can assume that this will be the interaction protocol.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
_initialized: bool
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def mcp_config(self) -> dict[str, dict[str, Any]]:
|
|
39
|
+
"""Get the MCP config."""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def is_connected(self) -> bool:
|
|
44
|
+
"""Check if client is connected and initialized."""
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
async def initialize(self, mcp_config: dict[str, dict[str, Any]] | None = None) -> None:
|
|
48
|
+
"""Initialize the client."""
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
async def list_tools(self) -> list[types.Tool]:
|
|
52
|
+
"""List all available tools."""
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
async def call_tool(self, tool_call: MCPToolCall) -> MCPToolResult:
|
|
56
|
+
"""Execute a tool by name."""
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
async def shutdown(self) -> None:
|
|
60
|
+
"""Shutdown the client."""
|
|
61
|
+
...
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class BaseHUDClient(AgentMCPClient):
|
|
65
|
+
"""Base class with common HUD functionality that adds:
|
|
66
|
+
- Connection management
|
|
67
|
+
- Tool discovery
|
|
68
|
+
- Telemetry fetching (hud environment-specific)
|
|
69
|
+
- Logging
|
|
70
|
+
- Strict tool output validation (optional)
|
|
71
|
+
- Environment analysis (optional)
|
|
72
|
+
|
|
73
|
+
Any custom client should inherit from this class, and implement:
|
|
74
|
+
- _connect: Connect to the MCP server
|
|
75
|
+
- list_tools: List all available tools
|
|
76
|
+
- list_resources: List all available resources
|
|
77
|
+
- call_tool: Execute a tool by name
|
|
78
|
+
- read_resource: Read a resource by URI
|
|
79
|
+
- _disconnect: Disconnect from the MCP server
|
|
80
|
+
- any other MCP client methods
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
client_info = Implementation(name="hud-mcp", title="hud MCP Client", version=hud_version)
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
mcp_config: dict[str, dict[str, Any]] | None = None,
|
|
88
|
+
verbose: bool = False,
|
|
89
|
+
strict_validation: bool = False,
|
|
90
|
+
auto_trace: bool = True,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""
|
|
93
|
+
Initialize base client.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
mcp_config: MCP server configuration dict
|
|
97
|
+
verbose: Enable verbose logging
|
|
98
|
+
strict_validation: Enable strict tool output validation
|
|
99
|
+
"""
|
|
100
|
+
self.verbose = verbose
|
|
101
|
+
self._mcp_config = mcp_config
|
|
102
|
+
self._strict_validation = strict_validation
|
|
103
|
+
self._auto_trace = auto_trace
|
|
104
|
+
|
|
105
|
+
self._initialized = False
|
|
106
|
+
self._telemetry_data = {} # Initialize telemetry data
|
|
107
|
+
|
|
108
|
+
if self.verbose:
|
|
109
|
+
self._setup_verbose_logging()
|
|
110
|
+
|
|
111
|
+
async def initialize(self, mcp_config: dict[str, dict[str, Any]] | None = None) -> None:
|
|
112
|
+
"""Initialize connection and fetch tools."""
|
|
113
|
+
if self._initialized:
|
|
114
|
+
logger.warning(
|
|
115
|
+
"Client already connected, if you want to reconnect or change the configuration, "
|
|
116
|
+
"call shutdown() first. This is especially important if you are using an agent."
|
|
117
|
+
)
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
self._mcp_config = mcp_config or self._mcp_config
|
|
121
|
+
if self._mcp_config is None:
|
|
122
|
+
raise ValueError(
|
|
123
|
+
"An MCP server configuration is required"
|
|
124
|
+
"Either pass it to the constructor or call initialize with a configuration"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
setup_hud_telemetry(self._mcp_config, auto_trace=self._auto_trace)
|
|
128
|
+
|
|
129
|
+
logger.debug("Initializing MCP client...")
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
# Subclasses implement connection
|
|
133
|
+
await self._connect(self._mcp_config)
|
|
134
|
+
except RuntimeError as e:
|
|
135
|
+
# Re-raise authentication errors with clear message
|
|
136
|
+
if "Authentication failed" in str(e):
|
|
137
|
+
raise
|
|
138
|
+
raise
|
|
139
|
+
except Exception as e:
|
|
140
|
+
# Check for authentication errors in the exception chain
|
|
141
|
+
error_msg = str(e)
|
|
142
|
+
if "401" in error_msg or "Unauthorized" in error_msg:
|
|
143
|
+
# Check if connecting to HUD API
|
|
144
|
+
for server_config in self._mcp_config.values():
|
|
145
|
+
url = server_config.get("url", "")
|
|
146
|
+
if "mcp.hud.so" in url:
|
|
147
|
+
raise RuntimeError(
|
|
148
|
+
"Authentication failed for HUD API. "
|
|
149
|
+
"Please ensure your HUD_API_KEY environment variable is set correctly. "
|
|
150
|
+
"You can get an API key at https://app.hud.so"
|
|
151
|
+
) from e
|
|
152
|
+
raise RuntimeError(
|
|
153
|
+
"Authentication failed (401 Unauthorized). "
|
|
154
|
+
"Please check your credentials or API key."
|
|
155
|
+
) from e
|
|
156
|
+
raise
|
|
157
|
+
|
|
158
|
+
# Common hud behavior - fetch telemetry
|
|
159
|
+
await self._fetch_telemetry()
|
|
160
|
+
|
|
161
|
+
self._initialized = True
|
|
162
|
+
logger.info("Client initialized")
|
|
163
|
+
|
|
164
|
+
async def shutdown(self) -> None:
|
|
165
|
+
"""Disconnect from the MCP server."""
|
|
166
|
+
if self._initialized:
|
|
167
|
+
await self._disconnect()
|
|
168
|
+
self._initialized = False
|
|
169
|
+
logger.info("Client disconnected")
|
|
170
|
+
else:
|
|
171
|
+
logger.warning("Client is not running, cannot disconnect")
|
|
172
|
+
|
|
173
|
+
@overload
|
|
174
|
+
async def call_tool(self, tool_call: MCPToolCall, /) -> MCPToolResult: ...
|
|
175
|
+
@overload
|
|
176
|
+
async def call_tool(
|
|
177
|
+
self,
|
|
178
|
+
*,
|
|
179
|
+
name: str,
|
|
180
|
+
arguments: dict[str, Any] | None = None,
|
|
181
|
+
) -> MCPToolResult: ...
|
|
182
|
+
|
|
183
|
+
async def call_tool(
|
|
184
|
+
self,
|
|
185
|
+
tool_call: MCPToolCall | None = None,
|
|
186
|
+
*,
|
|
187
|
+
name: str | None = None,
|
|
188
|
+
arguments: dict[str, Any] | None = None,
|
|
189
|
+
) -> MCPToolResult:
|
|
190
|
+
if tool_call is not None:
|
|
191
|
+
return await self._call_tool(tool_call)
|
|
192
|
+
elif name is not None:
|
|
193
|
+
return await self._call_tool(MCPToolCall(name=name, arguments=arguments))
|
|
194
|
+
else:
|
|
195
|
+
raise TypeError(
|
|
196
|
+
"call_tool() requires either an MCPToolCall positional arg "
|
|
197
|
+
"or keyword 'name' (and optional 'arguments')."
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
@abstractmethod
|
|
201
|
+
async def _connect(self, mcp_config: dict[str, dict[str, Any]]) -> None:
|
|
202
|
+
"""Subclasses implement their connection logic."""
|
|
203
|
+
raise NotImplementedError
|
|
204
|
+
|
|
205
|
+
@abstractmethod
|
|
206
|
+
async def list_tools(self) -> list[types.Tool]:
|
|
207
|
+
"""List all available tools."""
|
|
208
|
+
raise NotImplementedError
|
|
209
|
+
|
|
210
|
+
@abstractmethod
|
|
211
|
+
async def list_resources(self) -> list[types.Resource]:
|
|
212
|
+
"""List all available resources."""
|
|
213
|
+
raise NotImplementedError
|
|
214
|
+
|
|
215
|
+
@abstractmethod
|
|
216
|
+
async def _call_tool(self, tool_call: MCPToolCall) -> MCPToolResult:
|
|
217
|
+
"""Execute a tool by name."""
|
|
218
|
+
raise NotImplementedError
|
|
219
|
+
|
|
220
|
+
@abstractmethod
|
|
221
|
+
async def read_resource(self, uri: str) -> types.ReadResourceResult | None:
|
|
222
|
+
"""Read a resource by URI."""
|
|
223
|
+
raise NotImplementedError
|
|
224
|
+
|
|
225
|
+
@abstractmethod
|
|
226
|
+
async def _disconnect(self) -> None:
|
|
227
|
+
"""Subclasses implement their disconnection logic."""
|
|
228
|
+
raise NotImplementedError
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def is_connected(self) -> bool:
|
|
232
|
+
"""Check if client is connected and initialized."""
|
|
233
|
+
return self._initialized
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def mcp_config(self) -> dict[str, dict[str, Any]]:
|
|
237
|
+
"""Get the MCP config."""
|
|
238
|
+
if self._mcp_config is None:
|
|
239
|
+
raise ValueError("Please initialize the client with a valid MCP config")
|
|
240
|
+
return self._mcp_config
|
|
241
|
+
|
|
242
|
+
async def __aenter__(self: Any) -> Any:
|
|
243
|
+
"""Async context manager entry."""
|
|
244
|
+
await self.initialize()
|
|
245
|
+
return self
|
|
246
|
+
|
|
247
|
+
async def __aexit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None:
|
|
248
|
+
"""Async context manager exit."""
|
|
249
|
+
await self.shutdown()
|
|
250
|
+
|
|
251
|
+
def _setup_verbose_logging(self) -> None:
|
|
252
|
+
"""Configure verbose logging for debugging."""
|
|
253
|
+
logging.getLogger("mcp").setLevel(logging.DEBUG)
|
|
254
|
+
logging.getLogger("fastmcp").setLevel(logging.DEBUG)
|
|
255
|
+
|
|
256
|
+
if not any(isinstance(h, logging.StreamHandler) for h in logger.handlers):
|
|
257
|
+
handler = logging.StreamHandler()
|
|
258
|
+
handler.setFormatter(
|
|
259
|
+
logging.Formatter("[%(levelname)s] %(asctime)s - %(name)s - %(message)s")
|
|
260
|
+
)
|
|
261
|
+
logger.addHandler(handler)
|
|
262
|
+
logger.setLevel(logging.DEBUG)
|
|
263
|
+
|
|
264
|
+
async def _fetch_telemetry(self) -> None:
|
|
265
|
+
"""Common telemetry fetching for all hud clients."""
|
|
266
|
+
try:
|
|
267
|
+
# Try to read telemetry resource directly
|
|
268
|
+
result = await self.read_resource("telemetry://live")
|
|
269
|
+
if result and result.contents:
|
|
270
|
+
# Parse telemetry data
|
|
271
|
+
telemetry_data = json.loads(result.contents[0].text) # type: ignore
|
|
272
|
+
self._telemetry_data = telemetry_data
|
|
273
|
+
|
|
274
|
+
logger.info("📡 Telemetry data fetched:")
|
|
275
|
+
if "live_url" in telemetry_data:
|
|
276
|
+
logger.info(" 🖥️ Live URL: %s", telemetry_data["live_url"])
|
|
277
|
+
if "cdp_url" in telemetry_data:
|
|
278
|
+
logger.info(" 🦾 CDP URL: %s", telemetry_data["cdp_url"])
|
|
279
|
+
if "status" in telemetry_data:
|
|
280
|
+
logger.info(" 📊 Status: %s", telemetry_data["status"])
|
|
281
|
+
if "services" in telemetry_data:
|
|
282
|
+
logger.debug(" 📋 Services:")
|
|
283
|
+
for service, status in telemetry_data["services"].items():
|
|
284
|
+
status_icon = "✅" if status == "running" else "❌"
|
|
285
|
+
logger.debug(" %s %s: %s", status_icon, service, status)
|
|
286
|
+
|
|
287
|
+
if self.verbose:
|
|
288
|
+
logger.debug("Full telemetry data:\n%s", json.dumps(telemetry_data, indent=2))
|
|
289
|
+
except Exception as e:
|
|
290
|
+
# Telemetry is optional
|
|
291
|
+
if self.verbose:
|
|
292
|
+
logger.debug("No telemetry available: %s", e)
|
|
293
|
+
|
|
294
|
+
async def analyze_environment(self) -> dict[str, Any]:
|
|
295
|
+
"""Complete analysis of the MCP environment.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Dictionary containing:
|
|
299
|
+
- tools: All tools with full schemas
|
|
300
|
+
- hub_tools: Hub structures with subtools
|
|
301
|
+
- telemetry: Telemetry resources and data
|
|
302
|
+
- resources: All available resources
|
|
303
|
+
- metadata: Environment metadata
|
|
304
|
+
"""
|
|
305
|
+
if not self._initialized:
|
|
306
|
+
raise ValueError("Client must be initialized before analyzing the environment")
|
|
307
|
+
|
|
308
|
+
analysis: dict[str, Any] = {
|
|
309
|
+
"tools": [],
|
|
310
|
+
"hub_tools": {},
|
|
311
|
+
"telemetry": self._telemetry_data,
|
|
312
|
+
"resources": [],
|
|
313
|
+
"metadata": {
|
|
314
|
+
"servers": list(self._mcp_config.keys()), # type: ignore
|
|
315
|
+
"initialized": self._initialized,
|
|
316
|
+
},
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
# Get all tools with schemas
|
|
320
|
+
tools = await self.list_tools()
|
|
321
|
+
for tool in tools:
|
|
322
|
+
tool_info = {
|
|
323
|
+
"name": tool.name,
|
|
324
|
+
"description": tool.description,
|
|
325
|
+
"input_schema": tool.inputSchema,
|
|
326
|
+
}
|
|
327
|
+
analysis["tools"].append(tool_info)
|
|
328
|
+
|
|
329
|
+
# Check if this is a hub tool (like setup, evaluate)
|
|
330
|
+
if (
|
|
331
|
+
tool.description
|
|
332
|
+
and "internal" in tool.description.lower()
|
|
333
|
+
and "functions" in tool.description.lower()
|
|
334
|
+
):
|
|
335
|
+
# This is likely a hub dispatcher tool
|
|
336
|
+
hub_functions = await self.get_hub_tools(tool.name)
|
|
337
|
+
if hub_functions:
|
|
338
|
+
analysis["hub_tools"][tool.name] = hub_functions
|
|
339
|
+
|
|
340
|
+
# Get all resources
|
|
341
|
+
try:
|
|
342
|
+
resources = await self.list_resources()
|
|
343
|
+
for resource in resources:
|
|
344
|
+
resource_info = {
|
|
345
|
+
"uri": str(resource.uri),
|
|
346
|
+
"name": resource.name,
|
|
347
|
+
"description": resource.description,
|
|
348
|
+
"mime_type": getattr(resource, "mimeType", None),
|
|
349
|
+
}
|
|
350
|
+
analysis["resources"].append(resource_info)
|
|
351
|
+
except Exception as e:
|
|
352
|
+
if self.verbose:
|
|
353
|
+
logger.debug("Could not list resources: %s", e)
|
|
354
|
+
|
|
355
|
+
return analysis
|
|
356
|
+
|
|
357
|
+
async def get_hub_tools(self, hub_name: str) -> list[str]:
|
|
358
|
+
"""Get all subtools for a specific hub (setup/evaluate).
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
hub_name: Name of the hub (e.g., "setup", "evaluate")
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
List of available function names for the hub
|
|
365
|
+
"""
|
|
366
|
+
try:
|
|
367
|
+
# Read the hub's functions catalogue resource
|
|
368
|
+
result = await self.read_resource(f"file:///{hub_name}/functions")
|
|
369
|
+
if result and result.contents:
|
|
370
|
+
# Parse the JSON list of function names
|
|
371
|
+
import json
|
|
372
|
+
|
|
373
|
+
functions = json.loads(result.contents[0].text) # type: ignore
|
|
374
|
+
return functions
|
|
375
|
+
except Exception as e:
|
|
376
|
+
if self.verbose:
|
|
377
|
+
logger.debug("Could not read hub functions for '%s': %s", hub_name, e)
|
|
378
|
+
return []
|