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/mcp_use.py
CHANGED
|
@@ -1,278 +1,298 @@
|
|
|
1
|
-
"""MCP-use based client implementation (legacy)."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import logging
|
|
6
|
-
from typing import TYPE_CHECKING, Any
|
|
7
|
-
|
|
8
|
-
from mcp import Implementation
|
|
9
|
-
from mcp.shared.exceptions import McpError
|
|
10
|
-
from
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
from hud.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
from
|
|
20
|
-
from mcp_use.session import MCPSession as MCPUseSession
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
#
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if
|
|
128
|
-
for
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
if
|
|
183
|
-
raise ValueError("Client
|
|
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
|
-
if
|
|
251
|
-
logger.debug(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
1
|
+
"""MCP-use based client implementation (legacy)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from mcp import Implementation
|
|
9
|
+
from mcp.shared.exceptions import McpError
|
|
10
|
+
from pydantic import AnyUrl
|
|
11
|
+
|
|
12
|
+
from hud.types import MCPToolCall, MCPToolResult
|
|
13
|
+
from hud.version import __version__ as hud_version
|
|
14
|
+
|
|
15
|
+
from .base import BaseHUDClient
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from mcp import types
|
|
19
|
+
from mcp_use.client import MCPClient as MCPUseClient
|
|
20
|
+
from mcp_use.session import MCPSession as MCPUseSession
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from mcp_use.client import MCPClient as MCPUseClient
|
|
24
|
+
from mcp_use.session import MCPSession as MCPUseSession
|
|
25
|
+
except ImportError:
|
|
26
|
+
MCPUseClient = None # type: ignore[misc, assignment]
|
|
27
|
+
MCPUseSession = None # type: ignore[misc, assignment]
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MCPUseHUDClient(BaseHUDClient):
|
|
33
|
+
"""MCP-use based implementation of HUD MCP client."""
|
|
34
|
+
|
|
35
|
+
client_info = Implementation(
|
|
36
|
+
name="hud-mcp-use", title="hud MCP-use Client", version=hud_version
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def __init__(self, mcp_config: dict[str, dict[str, Any]] | None = None, **kwargs: Any) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Initialize MCP-use client.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
mcp_config: MCP server configuration dict
|
|
45
|
+
**kwargs: Additional arguments passed to base class
|
|
46
|
+
"""
|
|
47
|
+
super().__init__(mcp_config=mcp_config, **kwargs)
|
|
48
|
+
|
|
49
|
+
if MCPUseClient is None or MCPUseSession is None:
|
|
50
|
+
raise ImportError(
|
|
51
|
+
"MCP-use dependencies are not available. "
|
|
52
|
+
"Please install the optional agent dependencies: pip install 'hud-python[agent]'"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
self._sessions: dict[str, Any] = {} # Will be MCPUseSession when available
|
|
56
|
+
self._tool_map: dict[str, tuple[str, types.Tool]] = {}
|
|
57
|
+
self._client: Any | None = None # Will be MCPUseClient when available
|
|
58
|
+
|
|
59
|
+
async def _connect(self, mcp_config: dict[str, dict[str, Any]]) -> None:
|
|
60
|
+
"""Create all sessions for MCP-use client."""
|
|
61
|
+
if self._client is not None:
|
|
62
|
+
logger.warning("Client is already connected, cannot connect again")
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
config = {"mcpServers": mcp_config}
|
|
66
|
+
if MCPUseClient is None:
|
|
67
|
+
raise ImportError("MCPUseClient is not available")
|
|
68
|
+
self._client = MCPUseClient.from_dict(config)
|
|
69
|
+
try:
|
|
70
|
+
assert self._client is not None # For type checker
|
|
71
|
+
self._sessions = await self._client.create_all_sessions()
|
|
72
|
+
logger.info("Created %d MCP sessions", len(self._sessions))
|
|
73
|
+
|
|
74
|
+
# Configure validation for all sessions based on client setting
|
|
75
|
+
try:
|
|
76
|
+
from mcp.client.session import ValidationOptions # type: ignore[import-not-found]
|
|
77
|
+
|
|
78
|
+
for session in self._sessions.values():
|
|
79
|
+
if (
|
|
80
|
+
hasattr(session, "connector")
|
|
81
|
+
and hasattr(session.connector, "client_session")
|
|
82
|
+
and session.connector.client_session is not None
|
|
83
|
+
):
|
|
84
|
+
session.connector.client_session._validation_options = ValidationOptions(
|
|
85
|
+
strict_output_validation=self._strict_validation
|
|
86
|
+
)
|
|
87
|
+
except ImportError:
|
|
88
|
+
# ValidationOptions may not be available in some mcp versions
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
# Log session details in verbose mode
|
|
92
|
+
if self.verbose and self._sessions:
|
|
93
|
+
for name, session in self._sessions.items():
|
|
94
|
+
logger.debug(" - %s: %s", name, type(session).__name__)
|
|
95
|
+
|
|
96
|
+
except McpError as e:
|
|
97
|
+
# Protocol error - the server is reachable but rejecting our request
|
|
98
|
+
logger.error("MCP protocol error: %s", e)
|
|
99
|
+
logger.error("This typically means:")
|
|
100
|
+
logger.error("- Invalid or missing initialization parameters")
|
|
101
|
+
logger.error("- Incompatible protocol version")
|
|
102
|
+
logger.error("- Server-side configuration issues")
|
|
103
|
+
raise
|
|
104
|
+
except Exception as e:
|
|
105
|
+
# Transport or other errors
|
|
106
|
+
logger.error("Failed to create sessions: %s", e)
|
|
107
|
+
if self.verbose:
|
|
108
|
+
logger.info("Check that the MCP server is running and accessible")
|
|
109
|
+
raise
|
|
110
|
+
|
|
111
|
+
async def list_tools(self) -> list[types.Tool]:
|
|
112
|
+
"""List all available tools from all sessions."""
|
|
113
|
+
if self._client is None or not self._sessions:
|
|
114
|
+
raise ValueError("Client is not connected, call initialize() first")
|
|
115
|
+
|
|
116
|
+
all_tools = []
|
|
117
|
+
self._tool_map = {}
|
|
118
|
+
|
|
119
|
+
for server_name, session in self._sessions.items():
|
|
120
|
+
try:
|
|
121
|
+
# Ensure session is initialized
|
|
122
|
+
if not hasattr(session, "connector") or not hasattr(
|
|
123
|
+
session.connector, "client_session"
|
|
124
|
+
):
|
|
125
|
+
await session.initialize()
|
|
126
|
+
|
|
127
|
+
if session.connector.client_session is None:
|
|
128
|
+
logger.warning("Client session not initialized for %s", server_name)
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
# List tools
|
|
132
|
+
tools_result = await session.connector.client_session.list_tools()
|
|
133
|
+
|
|
134
|
+
logger.info(
|
|
135
|
+
"Discovered %d tools from '%s': %s",
|
|
136
|
+
len(tools_result.tools),
|
|
137
|
+
server_name,
|
|
138
|
+
[tool.name for tool in tools_result.tools],
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Add to collections
|
|
142
|
+
for tool in tools_result.tools:
|
|
143
|
+
all_tools.append(tool)
|
|
144
|
+
self._tool_map[tool.name] = (server_name, tool)
|
|
145
|
+
|
|
146
|
+
# Log detailed tool info in verbose mode
|
|
147
|
+
if self.verbose:
|
|
148
|
+
for tool in tools_result.tools:
|
|
149
|
+
description = tool.description or ""
|
|
150
|
+
logger.debug(
|
|
151
|
+
" Tool '%s': %s",
|
|
152
|
+
tool.name,
|
|
153
|
+
description[:100] + "..." if len(description) > 100 else description,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
except Exception as e:
|
|
157
|
+
logger.error("Error discovering tools from '%s': %s", server_name, e)
|
|
158
|
+
if self.verbose:
|
|
159
|
+
logger.exception("Full error details:")
|
|
160
|
+
|
|
161
|
+
return all_tools
|
|
162
|
+
|
|
163
|
+
async def _call_tool(self, tool_call: MCPToolCall) -> MCPToolResult:
|
|
164
|
+
"""Execute a tool by name."""
|
|
165
|
+
if self._client is None or not self._initialized:
|
|
166
|
+
raise ValueError("Client is not connected, call initialize() first")
|
|
167
|
+
|
|
168
|
+
if tool_call.name not in self._tool_map:
|
|
169
|
+
raise ValueError(f"Tool '{tool_call.name}' not found")
|
|
170
|
+
|
|
171
|
+
server_name, _ = self._tool_map[tool_call.name]
|
|
172
|
+
session = self._sessions[server_name]
|
|
173
|
+
|
|
174
|
+
if self.verbose:
|
|
175
|
+
logger.debug(
|
|
176
|
+
"Calling tool '%s' on server '%s' with arguments: %s",
|
|
177
|
+
tool_call.name,
|
|
178
|
+
server_name,
|
|
179
|
+
tool_call.arguments,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if session.connector.client_session is None:
|
|
183
|
+
raise ValueError(f"Client session not initialized for {server_name}")
|
|
184
|
+
|
|
185
|
+
result = await session.connector.client_session.call_tool(
|
|
186
|
+
name=tool_call.name,
|
|
187
|
+
arguments=tool_call.arguments or {},
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if self.verbose:
|
|
191
|
+
logger.debug("Tool '%s' result: %s", tool_call.name, result)
|
|
192
|
+
|
|
193
|
+
# MCP-use already returns the correct type, but we need to ensure it's MCPToolResult
|
|
194
|
+
return MCPToolResult(
|
|
195
|
+
content=result.content,
|
|
196
|
+
isError=result.isError,
|
|
197
|
+
structuredContent=result.structuredContent,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
async def list_resources(self) -> list[types.Resource]:
|
|
201
|
+
"""List all available resources."""
|
|
202
|
+
if self._client is None or not self._sessions:
|
|
203
|
+
raise ValueError("Client is not connected, call initialize() first")
|
|
204
|
+
|
|
205
|
+
for server_name, session in self._sessions.items():
|
|
206
|
+
try:
|
|
207
|
+
if not hasattr(session, "connector") or not hasattr(
|
|
208
|
+
session.connector, "client_session"
|
|
209
|
+
):
|
|
210
|
+
continue
|
|
211
|
+
if session.connector.client_session is None:
|
|
212
|
+
continue
|
|
213
|
+
# Prefer standard method name if available
|
|
214
|
+
if hasattr(session.connector.client_session, "list_resources"):
|
|
215
|
+
resources = await session.connector.client_session.list_resources()
|
|
216
|
+
else:
|
|
217
|
+
# If the client doesn't support resource listing, skip
|
|
218
|
+
continue
|
|
219
|
+
return resources.resources
|
|
220
|
+
except Exception as e:
|
|
221
|
+
if self.verbose:
|
|
222
|
+
logger.debug("Could not list resources from server '%s': %s", server_name, e)
|
|
223
|
+
continue
|
|
224
|
+
return []
|
|
225
|
+
|
|
226
|
+
async def read_resource(self, uri: str | AnyUrl) -> types.ReadResourceResult | None:
|
|
227
|
+
"""Read a resource by URI from any server that provides it."""
|
|
228
|
+
if self._client is None or not self._sessions:
|
|
229
|
+
raise ValueError("Client is not connected, call initialize() first")
|
|
230
|
+
|
|
231
|
+
for server_name, session in self._sessions.items():
|
|
232
|
+
try:
|
|
233
|
+
if not hasattr(session, "connector") or not hasattr(
|
|
234
|
+
session.connector, "client_session"
|
|
235
|
+
):
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
if session.connector.client_session is None:
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
# Convert str to AnyUrl if needed
|
|
242
|
+
resource_uri = AnyUrl(uri) if isinstance(uri, str) else uri
|
|
243
|
+
# Prefer read_resource; fall back to list_resources if needed
|
|
244
|
+
if hasattr(session.connector.client_session, "read_resource"):
|
|
245
|
+
result = await session.connector.client_session.read_resource(resource_uri)
|
|
246
|
+
else:
|
|
247
|
+
# Fallback path for older clients: not supported in strict typing
|
|
248
|
+
raise AttributeError("read_resource not available")
|
|
249
|
+
|
|
250
|
+
if self.verbose:
|
|
251
|
+
logger.debug(
|
|
252
|
+
"Successfully read resource '%s' from server '%s'", uri, server_name
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
return result
|
|
256
|
+
|
|
257
|
+
except McpError as e:
|
|
258
|
+
# McpError is expected for unsupported resources
|
|
259
|
+
if "telemetry://" in str(uri):
|
|
260
|
+
logger.debug(
|
|
261
|
+
"Telemetry resource not supported by server '%s': %s", server_name, e
|
|
262
|
+
)
|
|
263
|
+
elif self.verbose:
|
|
264
|
+
logger.debug(
|
|
265
|
+
"MCP resource error for '%s' from server '%s': %s", uri, server_name, e
|
|
266
|
+
)
|
|
267
|
+
continue
|
|
268
|
+
except Exception as e:
|
|
269
|
+
# Other errors might be more serious
|
|
270
|
+
if "telemetry://" in str(uri):
|
|
271
|
+
logger.debug("Failed to fetch telemetry from server '%s': %s", server_name, e)
|
|
272
|
+
else:
|
|
273
|
+
logger.warning(
|
|
274
|
+
"Unexpected error reading resource '%s' from server '%s': %s",
|
|
275
|
+
uri,
|
|
276
|
+
server_name,
|
|
277
|
+
e,
|
|
278
|
+
)
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
async def _disconnect(self) -> None:
|
|
284
|
+
"""Close all active sessions."""
|
|
285
|
+
if self._client is None:
|
|
286
|
+
logger.warning("Client is not connected, cannot close")
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
await self._client.close_all_sessions()
|
|
290
|
+
self._sessions = {}
|
|
291
|
+
self._tool_map = {}
|
|
292
|
+
self._initialized = False
|
|
293
|
+
logger.debug("MCP-use client disconnected")
|
|
294
|
+
|
|
295
|
+
# Legacy compatibility methods (limited; tests should not rely on these)
|
|
296
|
+
def get_sessions(self) -> dict[str, Any]:
|
|
297
|
+
"""Get active MCP sessions."""
|
|
298
|
+
return self._sessions
|
hud/clients/tests/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"""Tests for HUD MCP clients."""
|
|
1
|
+
"""Tests for HUD MCP clients."""
|