hud-python 0.4.56__py3-none-any.whl → 0.4.57__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/cli/dev.py CHANGED
@@ -237,7 +237,7 @@ async def run_mcp_module(
237
237
 
238
238
  from hud.cli.flows.dev import create_dynamic_trace
239
239
 
240
- live_trace_url = await create_dynamic_trace(
240
+ _, live_trace_url = await create_dynamic_trace(
241
241
  mcp_config=local_mcp_config,
242
242
  build_status=False,
243
243
  environment_name=mcp_server.name or "mcp-server",
@@ -608,7 +608,7 @@ def run_docker_dev_server(
608
608
  "headers": {},
609
609
  }
610
610
  }
611
- live_trace_url = _asy.run(
611
+ _, live_trace_url = _asy.run(
612
612
  create_dynamic_trace(
613
613
  mcp_config=local_mcp_config,
614
614
  build_status=True,
@@ -661,13 +661,33 @@ def run_docker_dev_server(
661
661
  # Create and run proxy with HUD helpers
662
662
  async def run_proxy() -> None:
663
663
  from fastmcp import FastMCP
664
+ from fastmcp.server.proxy import ProxyClient
664
665
 
665
- # Create FastMCP proxy to Docker stdio
666
- fastmcp_proxy = FastMCP.as_proxy(mcp_config, name="HUD Docker Dev Proxy")
666
+ # Create ProxyClient without custom log handler since we capture Docker logs directly
667
+ proxy_client = ProxyClient(mcp_config, name="HUD Docker Dev Proxy")
668
+
669
+ # Extract container name from docker args and store for logs endpoint
670
+ docker_cmd = mcp_config["docker"]["args"]
671
+ container_name = None
672
+ for i, arg in enumerate(docker_cmd):
673
+ if arg == "--name" and i + 1 < len(docker_cmd):
674
+ container_name = docker_cmd[i + 1]
675
+ break
676
+
677
+ if container_name:
678
+ # Store container name for logs endpoint to use
679
+ os.environ["_HUD_DEV_DOCKER_CONTAINER"] = container_name
680
+ hud_console.debug(f"Docker container: {container_name}")
681
+
682
+ # Create FastMCP proxy using the ProxyClient
683
+ fastmcp_proxy = FastMCP.as_proxy(proxy_client)
667
684
 
668
685
  # Wrap in MCPServer to get /docs and REST wrappers
669
686
  proxy = MCPServer(name="HUD Docker Dev Proxy")
670
687
 
688
+ # Enable logs endpoint on HTTP server
689
+ os.environ["_HUD_DEV_LOGS_PROVIDER"] = "enabled"
690
+
671
691
  # Import all tools from the FastMCP proxy
672
692
  await proxy.import_server(fastmcp_proxy)
673
693
 
hud/cli/flows/dev.py CHANGED
@@ -18,7 +18,7 @@ async def create_dynamic_trace(
18
18
  mcp_config: dict[str, dict[str, Any]],
19
19
  build_status: bool,
20
20
  environment_name: str,
21
- ) -> str | None:
21
+ ) -> tuple[str | None, str | None]:
22
22
  """
23
23
  Create a dynamic trace for HUD dev sessions when running in HTTP mode.
24
24
 
@@ -43,27 +43,16 @@ async def create_dynamic_trace(
43
43
  api_key = settings.api_key
44
44
  if not api_key:
45
45
  logger.warning("Skipping dynamic trace creation; missing HUD_API_KEY")
46
- return None
46
+ return None, None
47
47
 
48
48
  try:
49
49
  resp = await make_request("POST", url=url, json=payload, api_key=api_key)
50
50
  # New API returns an id; construct the URL as https://hud.so/trace/{id}
51
- trace_id = None
52
- if isinstance(resp, dict):
53
- trace_id = resp.get("id")
54
- if trace_id is None:
55
- data = resp.get("data", {}) or {}
56
- if isinstance(data, dict):
57
- trace_id = data.get("id")
58
- # Backcompat: if url is provided directly
59
- if not trace_id:
60
- direct_url = resp.get("url") or (resp.get("data", {}) or {}).get("url")
61
- if isinstance(direct_url, str) and direct_url:
62
- return direct_url
51
+ trace_id = resp.get("id")
63
52
 
64
53
  if isinstance(trace_id, str) and trace_id:
65
- return f"https://hud.so/trace/{trace_id}"
66
- return None
54
+ return trace_id, f"https://hud.so/trace/{trace_id}"
55
+ return None, None
67
56
  except Exception as e:
68
57
  # Do not interrupt dev flow
69
58
  try:
@@ -71,7 +60,7 @@ async def create_dynamic_trace(
71
60
  logger.warning("Failed to create dynamic dev trace: %s | payload=%s", e, preview)
72
61
  except Exception:
73
62
  logger.warning("Failed to create dynamic dev trace: %s", e)
74
- return None
63
+ return None, None
75
64
 
76
65
 
77
66
  def show_dev_ui(
@@ -0,0 +1,367 @@
1
+ """Tests for the convert command."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from unittest.mock import patch
6
+
7
+ import pytest
8
+ import typer
9
+
10
+ from hud.cli.flows.tasks import convert_tasks_to_remote
11
+ from hud.types import Task
12
+
13
+
14
+ class TestConvertCommand:
15
+ """Test the convert command functionality."""
16
+
17
+ @pytest.fixture
18
+ def temp_tasks_file(self, tmp_path):
19
+ """Create a temporary tasks file."""
20
+ tasks = [
21
+ {
22
+ "prompt": "Test task 1",
23
+ "mcp_config": {
24
+ "local": {
25
+ "command": "docker",
26
+ "args": ["run", "--rm", "-i", "test-image:latest"],
27
+ }
28
+ },
29
+ }
30
+ ]
31
+ tasks_file = tmp_path / "tasks.json"
32
+ tasks_file.write_text(json.dumps(tasks))
33
+ return tasks_file
34
+
35
+ @pytest.fixture
36
+ def mock_env_dir(self, tmp_path):
37
+ """Create a mock environment directory with lock file."""
38
+ env_dir = tmp_path / "env"
39
+ env_dir.mkdir()
40
+
41
+ # Create lock file
42
+ lock_data = {
43
+ "images": {
44
+ "remote": "registry.hud.so/test-org/test-env:v1.0.0",
45
+ "local": "test-env:latest",
46
+ }
47
+ }
48
+ lock_file = env_dir / "hud.lock.yaml"
49
+ import yaml
50
+
51
+ lock_file.write_text(yaml.dump(lock_data))
52
+
53
+ return env_dir
54
+
55
+ @patch("hud.cli.flows.tasks._derive_remote_image")
56
+ @patch("hud.cli.flows.tasks._ensure_pushed")
57
+ @patch("hud.cli.flows.tasks.find_environment_dir")
58
+ @patch("hud.cli.flows.tasks.load_tasks")
59
+ @patch("hud.settings.settings")
60
+ def test_convert_tasks_basic(
61
+ self,
62
+ mock_settings,
63
+ mock_load_tasks,
64
+ mock_find_env,
65
+ mock_ensure_pushed,
66
+ mock_derive_remote,
67
+ temp_tasks_file,
68
+ mock_env_dir,
69
+ ):
70
+ """Test basic task conversion from local to remote."""
71
+ # Setup mocks
72
+ mock_settings.api_key = "test-api-key"
73
+ mock_find_env.return_value = mock_env_dir
74
+
75
+ # Mock the push check to return updated lock data
76
+ mock_ensure_pushed.return_value = {
77
+ "images": {
78
+ "remote": "registry.hud.so/test-org/test-env:v1.0.0",
79
+ "local": "test-env:v1.0.0",
80
+ }
81
+ }
82
+
83
+ # Mock derive remote image
84
+ mock_derive_remote.return_value = "registry.hud.so/test-org/test-env:v1.0.0"
85
+
86
+ task = Task(
87
+ prompt="Test task",
88
+ mcp_config={
89
+ "local": {"command": "docker", "args": ["run", "--rm", "-i", "test-image:latest"]}
90
+ },
91
+ )
92
+ raw_task = {
93
+ "prompt": "Test task",
94
+ "mcp_config": {
95
+ "local": {"command": "docker", "args": ["run", "--rm", "-i", "test-image:latest"]}
96
+ },
97
+ }
98
+
99
+ mock_load_tasks.side_effect = [[task], [raw_task]]
100
+
101
+ # Run conversion
102
+ result_path = convert_tasks_to_remote(str(temp_tasks_file))
103
+
104
+ # Check result
105
+ assert result_path.endswith("remote_tasks.json")
106
+ assert Path(result_path).exists()
107
+
108
+ # Verify converted content
109
+ with open(result_path) as f:
110
+ converted_tasks = json.load(f)
111
+
112
+ assert len(converted_tasks) == 1
113
+ assert "hud" in converted_tasks[0]["mcp_config"]
114
+ assert converted_tasks[0]["mcp_config"]["hud"]["url"] == "https://mcp.hud.so/v3/mcp"
115
+
116
+ @patch("hud.settings.settings")
117
+ def test_convert_missing_api_key(self, mock_settings, temp_tasks_file):
118
+ """Test that conversion fails without API key."""
119
+ mock_settings.api_key = ""
120
+
121
+ with pytest.raises(typer.Exit):
122
+ convert_tasks_to_remote(str(temp_tasks_file))
123
+
124
+ @patch("hud.cli.flows.tasks.find_environment_dir")
125
+ @patch("hud.cli.flows.tasks.load_tasks")
126
+ @patch("hud.settings.settings")
127
+ def test_convert_already_remote(
128
+ self, mock_settings, mock_load_tasks, mock_find_env, temp_tasks_file
129
+ ):
130
+ """Test that already remote tasks are not converted again."""
131
+ mock_settings.api_key = "test-api-key"
132
+ mock_find_env.return_value = None # No env dir needed for remote tasks
133
+
134
+ # Create task that's already remote
135
+ task = Task(
136
+ prompt="Test task",
137
+ mcp_config={
138
+ "remote": {
139
+ "url": "https://mcp.hud.so",
140
+ "headers": {"Mcp-Image": "registry.hud.so/test/image:v1"},
141
+ }
142
+ },
143
+ )
144
+
145
+ mock_load_tasks.return_value = [task]
146
+
147
+ # Should return original path without modification
148
+ result_path = convert_tasks_to_remote(str(temp_tasks_file))
149
+ assert result_path == str(temp_tasks_file)
150
+
151
+ @patch("hud.cli.flows.tasks.find_environment_dir")
152
+ @patch("hud.cli.flows.tasks.load_tasks")
153
+ @patch("hud.settings.settings")
154
+ def test_convert_no_environment(
155
+ self, mock_settings, mock_load_tasks, mock_find_env, temp_tasks_file
156
+ ):
157
+ """Test that conversion fails when no environment is found."""
158
+ mock_settings.api_key = "test-api-key"
159
+ mock_find_env.return_value = None
160
+
161
+ task = Task(
162
+ prompt="Test task",
163
+ mcp_config={
164
+ "local": {"command": "docker", "args": ["run", "--rm", "-i", "test-image:latest"]}
165
+ },
166
+ )
167
+
168
+ mock_load_tasks.return_value = [task]
169
+
170
+ with pytest.raises(typer.Exit):
171
+ convert_tasks_to_remote(str(temp_tasks_file))
172
+
173
+ @patch("hud.utils.hud_console.hud_console.confirm")
174
+ @patch("hud.cli.flows.tasks._derive_remote_image")
175
+ @patch("hud.cli.flows.tasks._ensure_pushed")
176
+ @patch("hud.cli.flows.tasks.find_environment_dir")
177
+ @patch("hud.cli.flows.tasks.load_tasks")
178
+ @patch("hud.settings.settings")
179
+ def test_convert_with_env_vars(
180
+ self,
181
+ mock_settings,
182
+ mock_load_tasks,
183
+ mock_find_env,
184
+ mock_ensure_pushed,
185
+ mock_derive_remote,
186
+ mock_confirm,
187
+ temp_tasks_file,
188
+ mock_env_dir,
189
+ ):
190
+ """Test conversion includes environment variables as headers."""
191
+ mock_settings.api_key = "test-api-key"
192
+ mock_find_env.return_value = mock_env_dir
193
+ mock_confirm.return_value = True # Always confirm in tests
194
+
195
+ # Mock the push check to return updated lock data
196
+ mock_ensure_pushed.return_value = {
197
+ "images": {
198
+ "remote": "registry.hud.so/test-org/test-env:v1.0.0",
199
+ "local": "test-env:v1.0.0",
200
+ }
201
+ }
202
+
203
+ # Mock derive remote image
204
+ mock_derive_remote.return_value = "registry.hud.so/test-org/test-env:v1.0.0"
205
+
206
+ # Add .env file with API keys
207
+ env_file = mock_env_dir / ".env"
208
+ env_file.write_text("OPENAI_API_KEY=sk-test123\nANTHROPIC_API_KEY=sk-ant456")
209
+
210
+ task = Task(
211
+ prompt="Test task",
212
+ mcp_config={
213
+ "local": {
214
+ "command": "docker",
215
+ "args": ["run", "--rm", "-i", "-e", "OPENAI_API_KEY", "test-image:latest"],
216
+ }
217
+ },
218
+ )
219
+ raw_task = task.model_dump()
220
+
221
+ mock_load_tasks.side_effect = [[task], [raw_task]]
222
+
223
+ # Run conversion
224
+ result_path = convert_tasks_to_remote(str(temp_tasks_file))
225
+
226
+ # Verify headers include env vars
227
+ with open(result_path) as f:
228
+ converted_tasks = json.load(f)
229
+
230
+ headers = converted_tasks[0]["mcp_config"]["hud"]["headers"]
231
+ assert "Env-Openai-Api-Key" in headers
232
+ assert headers["Env-Openai-Api-Key"] == "${OPENAI_API_KEY}"
233
+
234
+
235
+ class TestConvertHelperFunctions:
236
+ """Test helper functions used by convert command."""
237
+
238
+ def test_env_var_to_header_key(self):
239
+ """Test environment variable name conversion to header format."""
240
+ from hud.cli.flows.tasks import _env_var_to_header_key
241
+
242
+ assert _env_var_to_header_key("OPENAI_API_KEY") == "Env-Openai-Api-Key"
243
+ assert _env_var_to_header_key("ANTHROPIC_API_KEY") == "Env-Anthropic-Api-Key"
244
+ assert _env_var_to_header_key("SIMPLE") == "Env-Simple"
245
+ assert _env_var_to_header_key("MULTIPLE_WORD_VAR") == "Env-Multiple-Word-Var"
246
+
247
+ def test_extract_dotenv_api_key_vars(self):
248
+ """Test extraction of API-like variables from .env file."""
249
+ # Create test env directory with .env file
250
+ import tempfile
251
+
252
+ from hud.cli.flows.tasks import _extract_dotenv_api_key_vars
253
+
254
+ with tempfile.TemporaryDirectory() as tmpdir:
255
+ env_dir = Path(tmpdir)
256
+ env_file = env_dir / ".env"
257
+ env_file.write_text("""
258
+ # Test .env file
259
+ OPENAI_API_KEY=sk-test123
260
+ ANTHROPIC_API_KEY=sk-ant456
261
+ SOME_TOKEN=abc123
262
+ CLIENT_SECRET=secret789
263
+ USER_PASSWORD=pass123
264
+ REGULAR_VAR=not_included
265
+ HUD_API_URL=https://api.hud.so
266
+ """)
267
+
268
+ result = _extract_dotenv_api_key_vars(env_dir)
269
+
270
+ # Should include only API-like variables
271
+ assert "OPENAI_API_KEY" in result
272
+ assert "ANTHROPIC_API_KEY" in result
273
+ assert "SOME_TOKEN" in result
274
+ assert "CLIENT_SECRET" in result
275
+ assert "USER_PASSWORD" in result
276
+ assert "REGULAR_VAR" not in result
277
+ assert "HUD_API_URL" in result # API in name, so it's included
278
+
279
+ def test_is_remote_url(self):
280
+ """Test remote URL detection."""
281
+ from hud.cli.flows.tasks import _is_remote_url
282
+
283
+ # This function matches URLs with domain names (not localhost or IPs)
284
+ assert _is_remote_url("https://mcp.hud.so")
285
+ assert _is_remote_url("http://mcp.hud.so")
286
+ assert _is_remote_url("https://mcp.hud.so/some/path")
287
+ assert _is_remote_url("https://example.com") # Also matches
288
+ assert not _is_remote_url("http://localhost:8000") # localhost doesn't match
289
+ assert not _is_remote_url("file:///path/to/file") # file:// doesn't match
290
+
291
+ def test_extract_env_vars_from_docker_args(self):
292
+ """Test extraction of environment variables from docker arguments."""
293
+ from hud.cli.flows.tasks import _extract_env_vars_from_docker_args
294
+
295
+ # Test with various docker arg formats
296
+ args = [
297
+ "run",
298
+ "--rm",
299
+ "-i",
300
+ "-e",
301
+ "VAR1",
302
+ "-e",
303
+ "VAR2=value",
304
+ "--env",
305
+ "VAR3",
306
+ "--env=VAR4",
307
+ # Note: -eFOO compact form is not supported by the implementation
308
+ "--env-file",
309
+ ".env",
310
+ "-p",
311
+ "8080:80",
312
+ ]
313
+
314
+ result = _extract_env_vars_from_docker_args(args)
315
+
316
+ assert "VAR1" in result
317
+ assert "VAR2" in result
318
+ assert "VAR3" in result
319
+ assert "VAR4" in result
320
+ # FOO is not extracted because -eFOO compact form is not supported
321
+ assert len(result) == 4
322
+
323
+ def test_derive_remote_image(self):
324
+ """Test deriving remote image from lock data."""
325
+ from hud.cli.flows.tasks import _derive_remote_image
326
+
327
+ # The function derives remote image from images.local, not images.remote
328
+ lock_data = {"images": {"local": "test-env:v1.0.0"}}
329
+ result = _derive_remote_image(lock_data)
330
+ assert result == "test-env:v1.0.0"
331
+
332
+ # Test fallback to legacy format
333
+ lock_data = {
334
+ "image": "test-org/test-env:v1.0.0",
335
+ }
336
+ result = _derive_remote_image(lock_data)
337
+ assert result == "test-org/test-env:v1.0.0"
338
+
339
+ def test_extract_vars_from_task_configs(self):
340
+ """Test extraction of env vars from task configurations."""
341
+ from hud.cli.flows.tasks import _extract_vars_from_task_configs
342
+
343
+ raw_tasks = [
344
+ {
345
+ "prompt": "Task 1",
346
+ "mcp_config": {
347
+ "local": {"command": "docker", "args": ["run", "-e", "API_KEY1", "image1"]}
348
+ },
349
+ },
350
+ {
351
+ "prompt": "Task 2",
352
+ "mcp_config": {
353
+ "local": {
354
+ "command": "docker",
355
+ "args": ["run", "-e", "API_KEY2", "--env", "API_KEY3", "image2"],
356
+ }
357
+ },
358
+ },
359
+ {"prompt": "Task 3", "mcp_config": {"remote": {"url": "https://mcp.hud.so"}}},
360
+ ]
361
+
362
+ result = _extract_vars_from_task_configs(raw_tasks)
363
+
364
+ assert "API_KEY1" in result
365
+ assert "API_KEY2" in result
366
+ assert "API_KEY3" in result
367
+ assert len(result) == 3
@@ -17,6 +17,7 @@ from __future__ import annotations
17
17
 
18
18
  import contextlib
19
19
  import json
20
+ import logging
20
21
  import os
21
22
  import time
22
23
  from pathlib import Path
@@ -27,6 +28,9 @@ from packaging import version
27
28
 
28
29
  from hud.utils.hud_console import HUDConsole
29
30
 
31
+ # Logger for version checking
32
+ logger = logging.getLogger(__name__)
33
+
30
34
  # Cache location for version check data
31
35
  CACHE_DIR = Path.home() / ".hud" / ".cache"
32
36
  VERSION_CACHE_FILE = CACHE_DIR / "version_check.json"
@@ -218,7 +222,7 @@ def display_update_prompt(console: HUDConsole | None = None) -> None:
218
222
  console: HUDConsole instance for output. If None, creates a new one.
219
223
  """
220
224
  if console is None:
221
- console = HUDConsole()
225
+ console = HUDConsole(logger=logger)
222
226
 
223
227
  try:
224
228
  info = check_for_updates()
@@ -231,11 +235,8 @@ def display_update_prompt(console: HUDConsole | None = None) -> None:
231
235
  f" Run: [bold yellow]uv tool upgrade hud-python[/bold yellow] to update"
232
236
  )
233
237
 
234
- # Display as a subtle but noticeable panel
235
- console._stdout_console.print(
236
- f"\n[yellow]{update_msg}[/yellow]\n",
237
- highlight=False,
238
- )
238
+ # Display using console info
239
+ console.info(f"[yellow]{update_msg}[/yellow]")
239
240
  except Exception: # noqa: S110
240
241
  # Never let version checking disrupt the user's workflow
241
242
  pass
hud/clients/base.py CHANGED
@@ -104,6 +104,7 @@ class BaseHUDClient(AgentMCPClient):
104
104
 
105
105
  self._initialized = False
106
106
  self._telemetry_data = {} # Initialize telemetry data
107
+ self._cached_resources: list[types.Resource] = [] # Cache for resources
107
108
 
108
109
  if self.verbose:
109
110
  self._setup_verbose_logging()
@@ -170,6 +171,7 @@ class BaseHUDClient(AgentMCPClient):
170
171
  if self._initialized:
171
172
  await self._disconnect()
172
173
  self._initialized = False
174
+ self._cached_resources.clear()
173
175
  hud_console.info("Environment Shutdown completed")
174
176
  else:
175
177
  hud_console.debug("Client was not initialized, skipping disconnect")
@@ -211,9 +213,22 @@ class BaseHUDClient(AgentMCPClient):
211
213
  """List all available tools."""
212
214
  raise NotImplementedError
213
215
 
214
- @abstractmethod
215
216
  async def list_resources(self) -> list[types.Resource]:
216
- """List all available resources."""
217
+ """List all available resources.
218
+
219
+ Uses cached resources if available, otherwise fetches from the server.
220
+
221
+ Returns:
222
+ List of available resources.
223
+ """
224
+ # If cache is empty, populate it
225
+ if not self._cached_resources:
226
+ self._cached_resources = await self._list_resources_impl()
227
+ return self._cached_resources
228
+
229
+ @abstractmethod
230
+ async def _list_resources_impl(self) -> list[types.Resource]:
231
+ """Implementation-specific resource listing. Subclasses must implement this."""
217
232
  raise NotImplementedError
218
233
 
219
234
  @abstractmethod
@@ -270,6 +285,17 @@ class BaseHUDClient(AgentMCPClient):
270
285
  async def _fetch_telemetry(self) -> None:
271
286
  """Common telemetry fetching for all hud clients."""
272
287
  try:
288
+ # Get resources (will use cache if available, otherwise fetch)
289
+ resources = await self.list_resources()
290
+ telemetry_available = any(
291
+ str(resource.uri) == "telemetry://live" for resource in resources
292
+ )
293
+
294
+ if not telemetry_available:
295
+ if self.verbose:
296
+ hud_console.debug("Telemetry resource not available from server")
297
+ return
298
+
273
299
  # Try to read telemetry resource directly
274
300
  result = await self.read_resource("telemetry://live")
275
301
  if result and result.contents:
hud/clients/fastmcp.py CHANGED
@@ -143,8 +143,8 @@ class FastMCPHUDClient(BaseHUDClient):
143
143
  structuredContent=result.structured_content,
144
144
  )
145
145
 
146
- async def list_resources(self) -> list[types.Resource]:
147
- """List all available resources."""
146
+ async def _list_resources_impl(self) -> list[types.Resource]:
147
+ """Implementation of resource listing for FastMCP client."""
148
148
  if self._client is None:
149
149
  raise ValueError("Client is not connected, call initialize() first")
150
150
  return await self._client.list_resources()
hud/clients/mcp_use.py CHANGED
@@ -243,8 +243,8 @@ class MCPUseHUDClient(BaseHUDClient):
243
243
  structuredContent=result.structuredContent,
244
244
  )
245
245
 
246
- async def list_resources(self) -> list[types.Resource]:
247
- """List all available resources."""
246
+ async def _list_resources_impl(self) -> list[types.Resource]:
247
+ """Implementation of resource listing for MCP-use client."""
248
248
  if self._client is None or not self._sessions:
249
249
  raise ValueError("Client is not connected, call initialize() first")
250
250
 
@@ -35,9 +35,15 @@ class MockClient(BaseHUDClient):
35
35
  raise RuntimeError("Not connected")
36
36
  return self._mock_tools
37
37
 
38
- async def list_resources(self) -> list[types.Resource]:
39
- """Minimal list_resources for protocol satisfaction in tests."""
40
- return []
38
+ async def _list_resources_impl(self) -> list[types.Resource]:
39
+ """Minimal resource listing implementation for tests."""
40
+ from pydantic import AnyUrl
41
+
42
+ return [
43
+ types.Resource(
44
+ uri=AnyUrl("telemetry://live"), name="telemetry", description="Live telemetry data"
45
+ )
46
+ ]
41
47
 
42
48
  async def _call_tool(self, tool_call: MCPToolCall) -> MCPToolResult:
43
49
  if tool_call.name == "test_tool":
hud/server/server.py CHANGED
@@ -13,6 +13,7 @@ from typing import TYPE_CHECKING, Any
13
13
 
14
14
  import anyio
15
15
  from fastmcp.server.server import FastMCP, Transport
16
+ from starlette.requests import Request
16
17
  from starlette.responses import JSONResponse, Response
17
18
 
18
19
  from hud.server.low_level import LowLevelServerWithInit
@@ -492,6 +493,7 @@ class MCPServer(FastMCP):
492
493
  - GET /docs - Interactive documentation and tool testing
493
494
  - POST /api/tools/{name} - REST wrappers for MCP tools
494
495
  - GET /openapi.json - OpenAPI spec for REST endpoints
496
+ - GET /logs - Development log endpoint (when provided by dev runtime)
495
497
  """
496
498
 
497
499
  # Register REST wrapper for each tool
@@ -542,6 +544,63 @@ class MCPServer(FastMCP):
542
544
  endpoint = create_tool_endpoint(tool_key)
543
545
  self.custom_route(f"/api/tools/{tool_key}", methods=["POST"])(endpoint)
544
546
 
547
+ # Development log endpoint - only if dev runtime set a provider
548
+ provider = os.environ.get("_HUD_DEV_LOGS_PROVIDER")
549
+ if provider == "enabled":
550
+
551
+ @self.custom_route("/logs", methods=["GET"])
552
+ async def get_logs(request: Request) -> Response:
553
+ """Return Docker container logs on demand.
554
+
555
+ Query params:
556
+ - limit: max number of lines to return (default 100)
557
+ - tail: number of lines from end to return (default 100)
558
+ """
559
+ import subprocess
560
+
561
+ # Get container name from environment
562
+ container_name = os.environ.get("_HUD_DEV_DOCKER_CONTAINER")
563
+ if not container_name:
564
+ return JSONResponse({"items": [], "next": None})
565
+
566
+ # Get query params
567
+ params = request.query_params
568
+ tail = params.get("tail", "100")
569
+
570
+ try:
571
+ # Run docker logs to get recent output
572
+ result = subprocess.run( # noqa: S603, ASYNC221
573
+ ["docker", "logs", "--tail", tail, container_name], # noqa: S607
574
+ stdout=subprocess.PIPE,
575
+ stderr=subprocess.STDOUT,
576
+ text=True,
577
+ encoding="utf-8",
578
+ errors="replace",
579
+ timeout=5,
580
+ )
581
+
582
+ # Parse logs into items
583
+ items = []
584
+ lines = result.stdout.strip().split("\n") if result.stdout else []
585
+
586
+ for i, line in enumerate(lines):
587
+ if line.strip():
588
+ items.append(
589
+ {
590
+ "id": i,
591
+ "stream": "mixed",
592
+ "log": line,
593
+ "container_name": container_name,
594
+ }
595
+ )
596
+
597
+ return JSONResponse({"items": items, "next": len(items) - 1 if items else None})
598
+
599
+ except subprocess.TimeoutExpired:
600
+ return JSONResponse({"error": "Docker logs timeout"}, status_code=500)
601
+ except Exception as e:
602
+ return JSONResponse({"error": f"Failed to get logs: {e!s}"}, status_code=500)
603
+
545
604
  @self.custom_route("/openapi.json", methods=["GET"])
546
605
  async def openapi_spec(request: Request) -> Response:
547
606
  """Generate OpenAPI spec from MCP tools."""
@@ -5,4 +5,4 @@ def test_import():
5
5
  """Test that the package can be imported."""
6
6
  import hud
7
7
 
8
- assert hud.__version__ == "0.4.56"
8
+ assert hud.__version__ == "0.4.57"
hud/version.py CHANGED
@@ -4,4 +4,4 @@ Version information for the HUD SDK.
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
- __version__ = "0.4.56"
7
+ __version__ = "0.4.57"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hud-python
3
- Version: 0.4.56
3
+ Version: 0.4.57
4
4
  Summary: SDK for the HUD platform.
5
5
  Project-URL: Homepage, https://github.com/hud-evals/hud-python
6
6
  Project-URL: Bug Tracker, https://github.com/hud-evals/hud-python/issues
@@ -2,7 +2,7 @@ hud/__init__.py,sha256=0LQ9PyuU6yZx7Fxu8YJXKC1i3TTHjg3UrInThh759QE,653
2
2
  hud/__main__.py,sha256=YR8Dq8OhINOsVfQ55PmRXXg4fEK84Rt_-rMtJ5rvhWo,145
3
3
  hud/settings.py,sha256=9SPEZcsZGqplw_LuV8RvJisDRdlsbQgxBqG8ifmjGNc,4117
4
4
  hud/types.py,sha256=jeC1npyPlno5CYNWBWH06Kxw7VMJld9YLEFBFNtjwoI,11608
5
- hud/version.py,sha256=5r7gUjXhMnfdXi42tFi9IN4DXbsCYx9soTL0r3ID9IE,105
5
+ hud/version.py,sha256=5oonlS-3cemOdbkDddf3Eg6Wr5IkBtjvigjh9Y3A664,105
6
6
  hud/agents/__init__.py,sha256=UoIkljWdbq4bM0LD-mSaw6w826EqdEjOk7r6glNYwYQ,286
7
7
  hud/agents/base.py,sha256=nRRnK_kA_PZDT0fepMMTz3QDTHj0jpqvbTqdgRPKSlg,32514
8
8
  hud/agents/claude.py,sha256=Lf7p_h5H4sd2w16ZNc2ru3xuxNP7IDeFayrcdePOLSE,16030
@@ -28,7 +28,7 @@ hud/cli/analyze.py,sha256=4u5oYfJMquOjT9PzzRTYVcTZDxDi0ilNP_g532_hpOU,14716
28
28
  hud/cli/build.py,sha256=EV6PsJ08p3mZZkfeUFjZ687ithxKGlUZ66yAF9lvtaI,28780
29
29
  hud/cli/clone.py,sha256=AwVDIuhr8mHb1oT2Af2HrD25SiTdwATpE6zd93vzLgA,6099
30
30
  hud/cli/debug.py,sha256=jtFW8J5F_3rhq1Hf1_SkJ7aLS3wjnyIs_LsC8k5cnzc,14200
31
- hud/cli/dev.py,sha256=WXxvhqQ2l5gpplJFWtMKnJYycpLyzNAt6x3y3oLa5mk,25379
31
+ hud/cli/dev.py,sha256=dGA8p5DvKFUgcYA9awXbbIfJ8OV04ew_jd0fElfJRoU,26255
32
32
  hud/cli/eval.py,sha256=1guW_pNQqFLpF1RylZltBKdXt6iFd_jBNV3Rxy36QRE,29024
33
33
  hud/cli/get.py,sha256=sksKrdzBGZa7ZuSoQkc0haj-CvOGVSSikoVXeaUd3N4,6274
34
34
  hud/cli/init.py,sha256=D1NS4PBHxNqsl9hz2VsMTj6HSB_teJgCqsyY77T5lLs,10042
@@ -37,7 +37,7 @@ hud/cli/pull.py,sha256=XGEZ8n60tbzLQP_8d9h7XYmzyCW0e2-Rkr3_tLG7jvw,12449
37
37
  hud/cli/push.py,sha256=rWJIqHebvp3DchK-00L6G0olD3-klsobLutRW4PP_ts,19488
38
38
  hud/cli/remove.py,sha256=8vGQyXDqgtjz85_vtusoIG8zurH4RHz6z8UMevQRYM4,6861
39
39
  hud/cli/flows/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
- hud/cli/flows/dev.py,sha256=Q6vz9KDfeLHvZNs3Kga7PUTMgt_QEm5J98XP9jRKuhM,5220
40
+ hud/cli/flows/dev.py,sha256=7-Dki2C8MbdcFcHewKhmRfkfjVdo6E4CGmItKVMaUSI,4760
41
41
  hud/cli/flows/tasks.py,sha256=SAQzS53g6vBPMvuEaMRsmkYOJMwvy28RQ-miVmVnP_8,18028
42
42
  hud/cli/rl/__init__.py,sha256=pGx4WGaL-yGdogJNzgEE7BtjFtT4I9CTI_UdCm49h98,5376
43
43
  hud/cli/rl/celebrate.py,sha256=trGEJn3xebexlHwFVKPJKhRujVVV8sy7TQTJvRd2p9A,5947
@@ -64,6 +64,7 @@ hud/cli/tests/test_cli_main.py,sha256=0wMho9p9NcGjp0jLiUtCQh_FYdbMaCJtSY3sBbSgPw
64
64
  hud/cli/tests/test_cli_more_wrappers.py,sha256=MdcsGXOwPQOSjDhSm4s7FjiVy1ru8YeRiZxIGVLAgnA,847
65
65
  hud/cli/tests/test_cli_root.py,sha256=ifHjb1k9zSRTB3dgOxApmRYulzK_D8cKjI4GPQXNQQI,3972
66
66
  hud/cli/tests/test_clone.py,sha256=oC2mf-41QQVc7ODJkjrWbVPNMB2fDW3nZ6jY6w93gvQ,4458
67
+ hud/cli/tests/test_convert.py,sha256=_-W8wLEADUsS9LjaveERJ1i0v8PLP0Oj5Vu7lNp6a_0,12510
67
68
  hud/cli/tests/test_cursor.py,sha256=ZfxAFKJesJ3UV1JBoASSRlv6BXbpvVEk_pjxUg1jnf4,9821
68
69
  hud/cli/tests/test_debug.py,sha256=bQ76d_0HJfthHBSECmGNv499ZE57CIOKsanMlNfNHGk,18036
69
70
  hud/cli/tests/test_eval.py,sha256=2ExXbxnIP8yamQidDAQBQDzF6Y4ZbYGyeOJ3cx84TQI,22380
@@ -93,7 +94,7 @@ hud/cli/utils/runner.py,sha256=16_dXkSZTvT2JQKWbZBCoIz6iiLMhjqgFXvcWr1WS1M,4439
93
94
  hud/cli/utils/server.py,sha256=EE5DJ0RAmXCEjMcZycpAsAxxCj6sOdIsXqPh38kK2ew,7416
94
95
  hud/cli/utils/source_hash.py,sha256=EDD3KC4pLGBVoSL5UTv1GvF2TAs2ThNHNOhP_5Sbub4,2979
95
96
  hud/cli/utils/tasks.py,sha256=lX9SeM5XekrTBx2HWKMA0BQ7R3Q8jqGgi4G5vIHsnJM,994
96
- hud/cli/utils/version_check.py,sha256=30VgZQqwUb36Fm4dAzKn5HuT0We6OnA-rd0eIqjd5Tc,7271
97
+ hud/cli/utils/version_check.py,sha256=TdsPh7mpPw5giWahiQgpLZVvT6Lex2z2tsMOGCvm0Dc,7268
97
98
  hud/cli/utils/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
98
99
  hud/cli/utils/tests/test_config.py,sha256=dZugvnjXiFNAvcx0nrIp578zmReMBEdupXfxpqHWGAk,1422
99
100
  hud/cli/utils/tests/test_docker.py,sha256=WHYg_la0WfIr47Wu7Fmy8BtQrSyiWUdTYccv_DECBvw,2556
@@ -112,14 +113,14 @@ hud/cli/utils/tests/test_source_hash.py,sha256=GGFklT9xrVPMEpPwgiUzr94An66R-nhNn
112
113
  hud/cli/utils/tests/test_tasks.py,sha256=FT4smFGUHaJpZGIqj-yevBH8l9ARCu03RoedGxiLc2M,2624
113
114
  hud/clients/README.md,sha256=XNE3mch95ozDgVqfwCGcrhlHY9CwT1GKfNANNboowto,3826
114
115
  hud/clients/__init__.py,sha256=N5M_gZv4nP7dLRwpAiaqqaxyaLieGW6397FszeG7JGw,364
115
- hud/clients/base.py,sha256=uvBIjU1K5VZjPtDTG4wk1cV6ysV5gRIAydd5Nke7aqc,14212
116
- hud/clients/fastmcp.py,sha256=1xaAg7DwMcwt_GRx2n3OsZaX-UMEQCZCaLDK4cr2HhQ,9178
117
- hud/clients/mcp_use.py,sha256=2Pb8bhAEmD9TJYKb-2JbxmyIsh4ttE_pP-kldrt6AH0,14750
116
+ hud/clients/base.py,sha256=QPFAjWKxczJWQEcsMGULQ9zj_xEMjMK52fWlTZ8rv9c,15274
117
+ hud/clients/fastmcp.py,sha256=_dk1Iczvv_hKhti2z64KLE3cIY93W3vm3cvS8Og3C6w,9209
118
+ hud/clients/mcp_use.py,sha256=Zq_NBFPAMb81qoQa_adCbdTm-yUFjDFCMFG7m8tOlr4,14781
118
119
  hud/clients/tests/__init__.py,sha256=sKOtJFFa4mDIXh1U6O8ZUHjigE8CiRMQ2PzJTIBZuVE,33
119
120
  hud/clients/tests/test_client_integration.py,sha256=kohU6jfCNfwSnAushHeB1_CmDlRfQc7VBL0GEdJYSeI,4198
120
121
  hud/clients/tests/test_fastmcp.py,sha256=4q3TzDjuieTZa89taiNJIrzbUncNkYOG4MaubypA21k,13030
121
122
  hud/clients/tests/test_mcp_use_retry.py,sha256=9FxLAz4L5Vv3OTtj4wdhRY23wDYALUpE12TYWl7fbJA,13299
122
- hud/clients/tests/test_protocol.py,sha256=aK4CS4g3j1D5jPo83ykzZuHUvcZFAulYtIq9T9Hb_fQ,6640
123
+ hud/clients/tests/test_protocol.py,sha256=1JD9Ka-OLiQI6e9xBvoCaWsHXJ_iZQdGliWH5HJsaKc,6826
123
124
  hud/clients/utils/__init__.py,sha256=-zZjcKIWGj2tXbVDOW45UgoGghhLJzFQVZ6miKenuA4,595
124
125
  hud/clients/utils/mcp_use_retry.py,sha256=knsgOTR3YFXshmPFfPQE6K6C5GpR1ZBJe2J7ozEMikA,6675
125
126
  hud/clients/utils/retry.py,sha256=mMs2T_mAlb8AYhSqMR4AmCw7838gqCC4mdG3zjMAYM4,5744
@@ -169,7 +170,7 @@ hud/server/__init__.py,sha256=ZTxwhR7tMtSE14i1sONTz5UaMXURW1AYoFZMbWGBviU,134
169
170
  hud/server/context.py,sha256=6bCdSzv1FGyItu9472HbbYef279H7QuMGJDR8EtYg5Y,3210
170
171
  hud/server/low_level.py,sha256=XYs2pOJ9kN4OcJ6ahDmXM5mWkzq5wJLpKFInUYrWEok,4701
171
172
  hud/server/router.py,sha256=T9hLe08-W8SGvO14RfwY3j2zfcvB3lKWS5sR9h_4awM,5343
172
- hud/server/server.py,sha256=1IRrHar4OUF4llVsqkwwLvIH8EpgpXTN52XbyG-bbBg,28367
173
+ hud/server/server.py,sha256=zj2FGv1SO8mR_3pjrBzhzN8LT1KpCls7_LCYwSjJ8Nw,30944
173
174
  hud/server/helper/__init__.py,sha256=ZxO8VP3RZEBBp-q65VixuhzQgqEPSVzW0hEY9J9QqDA,116
174
175
  hud/server/tests/__init__.py,sha256=eEYYkxX5Hz9woXVOBJ2H2_CQoEih0vH6nRt3sH2Z8v8,49
175
176
  hud/server/tests/test_add_tool.py,sha256=9Y59LJpow3BQ31Jg7fowhV7nAeyqude9Tap9tEs_vBE,1863
@@ -265,10 +266,10 @@ hud/utils/tests/test_progress.py,sha256=QSF7Kpi03Ff_l3mAeqW9qs1nhK50j9vBiSobZq7T
265
266
  hud/utils/tests/test_tasks.py,sha256=Rbbm51vZxygyWlhjunFq4IfFPefVB3qevM9_CZSt5w4,5774
266
267
  hud/utils/tests/test_telemetry.py,sha256=5jl7bEx8C8b-FfFUko5pf4UY-mPOR-9HaeL98dGtVHM,2781
267
268
  hud/utils/tests/test_tool_shorthand.py,sha256=1p3j3D0G93OXHqnUXbvTs3G4A8awrPvwhPpLi6YPeOM,5458
268
- hud/utils/tests/test_version.py,sha256=QHrgv-B02epzVPg5ZHHzZOOjcN1W9Ke_8G_eAuDIOGQ,160
269
+ hud/utils/tests/test_version.py,sha256=hsHAkfILDrqA97RyX0l2JapaS40Gi1kJnbThs2QFZHg,160
269
270
  hud/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
270
- hud_python-0.4.56.dist-info/METADATA,sha256=TaKPkp8KGeSYMBAJC85GdqT9liQdn3dcUDHNtl105u4,22338
271
- hud_python-0.4.56.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
272
- hud_python-0.4.56.dist-info/entry_points.txt,sha256=jJbodNFg1m0-CDofe5AHvB4zKBq7sSdP97-ohaQ3ae4,63
273
- hud_python-0.4.56.dist-info/licenses/LICENSE,sha256=yIzBheVUf86FC1bztAcr7RYWWNxyd3B-UJQ3uddg1HA,1078
274
- hud_python-0.4.56.dist-info/RECORD,,
271
+ hud_python-0.4.57.dist-info/METADATA,sha256=sHE5ewR3U5SRv-JbKoxZth7mPbXzqn50W6ZR3vl_g6g,22338
272
+ hud_python-0.4.57.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
273
+ hud_python-0.4.57.dist-info/entry_points.txt,sha256=jJbodNFg1m0-CDofe5AHvB4zKBq7sSdP97-ohaQ3ae4,63
274
+ hud_python-0.4.57.dist-info/licenses/LICENSE,sha256=yIzBheVUf86FC1bztAcr7RYWWNxyd3B-UJQ3uddg1HA,1078
275
+ hud_python-0.4.57.dist-info/RECORD,,