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