hud-python 0.4.11__py3-none-any.whl → 0.4.12__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 (63) hide show
  1. hud/__main__.py +8 -0
  2. hud/agents/base.py +7 -8
  3. hud/agents/langchain.py +2 -2
  4. hud/agents/tests/test_openai.py +3 -1
  5. hud/cli/__init__.py +106 -51
  6. hud/cli/build.py +121 -71
  7. hud/cli/debug.py +2 -2
  8. hud/cli/{mcp_server.py → dev.py} +60 -25
  9. hud/cli/eval.py +148 -68
  10. hud/cli/init.py +0 -1
  11. hud/cli/list_func.py +72 -71
  12. hud/cli/pull.py +1 -2
  13. hud/cli/push.py +35 -23
  14. hud/cli/remove.py +35 -41
  15. hud/cli/tests/test_analyze.py +2 -1
  16. hud/cli/tests/test_analyze_metadata.py +42 -49
  17. hud/cli/tests/test_build.py +28 -52
  18. hud/cli/tests/test_cursor.py +1 -1
  19. hud/cli/tests/test_debug.py +1 -1
  20. hud/cli/tests/test_list_func.py +75 -64
  21. hud/cli/tests/test_main_module.py +30 -0
  22. hud/cli/tests/test_mcp_server.py +3 -3
  23. hud/cli/tests/test_pull.py +30 -61
  24. hud/cli/tests/test_push.py +70 -89
  25. hud/cli/tests/test_registry.py +36 -38
  26. hud/cli/tests/test_utils.py +1 -1
  27. hud/cli/utils/__init__.py +1 -0
  28. hud/cli/{docker_utils.py → utils/docker.py} +36 -0
  29. hud/cli/{env_utils.py → utils/environment.py} +7 -7
  30. hud/cli/{interactive.py → utils/interactive.py} +91 -19
  31. hud/cli/{analyze_metadata.py → utils/metadata.py} +12 -8
  32. hud/cli/{registry.py → utils/registry.py} +28 -30
  33. hud/cli/{remote_runner.py → utils/remote_runner.py} +1 -1
  34. hud/cli/utils/runner.py +134 -0
  35. hud/cli/utils/server.py +250 -0
  36. hud/clients/base.py +1 -1
  37. hud/clients/fastmcp.py +7 -5
  38. hud/clients/mcp_use.py +8 -6
  39. hud/server/server.py +34 -4
  40. hud/shared/exceptions.py +11 -0
  41. hud/shared/tests/test_exceptions.py +22 -0
  42. hud/telemetry/tests/__init__.py +0 -0
  43. hud/telemetry/tests/test_replay.py +40 -0
  44. hud/telemetry/tests/test_trace.py +63 -0
  45. hud/tools/base.py +20 -3
  46. hud/tools/computer/hud.py +15 -6
  47. hud/tools/executors/tests/test_base_executor.py +27 -0
  48. hud/tools/response.py +12 -8
  49. hud/tools/tests/test_response.py +60 -0
  50. hud/tools/tests/test_tools_init.py +49 -0
  51. hud/utils/design.py +19 -8
  52. hud/utils/mcp.py +17 -5
  53. hud/utils/tests/test_mcp.py +112 -0
  54. hud/utils/tests/test_version.py +1 -1
  55. hud/version.py +1 -1
  56. {hud_python-0.4.11.dist-info → hud_python-0.4.12.dist-info}/METADATA +14 -10
  57. {hud_python-0.4.11.dist-info → hud_python-0.4.12.dist-info}/RECORD +62 -52
  58. hud/cli/runner.py +0 -160
  59. /hud/cli/{cursor.py → utils/cursor.py} +0 -0
  60. /hud/cli/{utils.py → utils/logging.py} +0 -0
  61. {hud_python-0.4.11.dist-info → hud_python-0.4.12.dist-info}/WHEEL +0 -0
  62. {hud_python-0.4.11.dist-info → hud_python-0.4.12.dist-info}/entry_points.txt +0 -0
  63. {hud_python-0.4.11.dist-info → hud_python-0.4.12.dist-info}/licenses/LICENSE +0 -0
hud/cli/build.py CHANGED
@@ -17,18 +17,18 @@ from hud.clients import MCPClient
17
17
  from hud.utils.design import HUDDesign
18
18
  from hud.version import __version__ as hud_version
19
19
 
20
- from .registry import save_to_registry
20
+ from .utils.registry import save_to_registry
21
21
 
22
22
 
23
23
  def parse_version(version_str: str) -> tuple[int, int, int]:
24
24
  """Parse version string like '1.0.0' or '1.0' into tuple of integers."""
25
25
  # Remove 'v' prefix if present
26
- version_str = version_str.lstrip('v')
27
-
26
+ version_str = version_str.lstrip("v")
27
+
28
28
  # Split by dots and pad with zeros if needed
29
- parts = version_str.split('.')
30
- parts.extend(['0'] * (3 - len(parts))) # Ensure we have at least 3 parts
31
-
29
+ parts = version_str.split(".")
30
+ parts.extend(["0"] * (3 - len(parts))) # Ensure we have at least 3 parts
31
+
32
32
  try:
33
33
  return (int(parts[0]), int(parts[1]), int(parts[2]))
34
34
  except (ValueError, IndexError):
@@ -39,7 +39,7 @@ def parse_version(version_str: str) -> tuple[int, int, int]:
39
39
  def increment_version(version_str: str, increment_type: str = "patch") -> str:
40
40
  """Increment version string. increment_type can be 'major', 'minor', or 'patch'."""
41
41
  major, minor, patch = parse_version(version_str)
42
-
42
+
43
43
  if increment_type == "major":
44
44
  return f"{major + 1}.0.0"
45
45
  elif increment_type == "minor":
@@ -52,11 +52,11 @@ def get_existing_version(lock_path: Path) -> str | None:
52
52
  """Get the internal version from existing lock file if it exists."""
53
53
  if not lock_path.exists():
54
54
  return None
55
-
55
+
56
56
  try:
57
57
  with open(lock_path) as f:
58
58
  lock_data = yaml.safe_load(f)
59
-
59
+
60
60
  # Look for internal version in build metadata
61
61
  build_data = lock_data.get("build", {})
62
62
  return build_data.get("version", None)
@@ -81,7 +81,7 @@ def get_docker_image_digest(image: str) -> str | None:
81
81
  if digest_list:
82
82
  # Return full image reference with digest
83
83
  return digest_list[0]
84
- except Exception:
84
+ except Exception: # noqa: S110
85
85
  # Don't print error here, let calling code handle it
86
86
  pass
87
87
  return None
@@ -113,30 +113,60 @@ def extract_env_vars_from_dockerfile(dockerfile_path: Path) -> tuple[list[str],
113
113
  if not dockerfile_path.exists():
114
114
  return required, optional
115
115
 
116
- # Simple parsing - look for ENV directives
117
- # This is a basic implementation - could be enhanced
116
+ # Parse both ENV and ARG directives
118
117
  content = dockerfile_path.read_text()
118
+ arg_vars = set() # Track ARG variables
119
+
119
120
  for line in content.splitlines():
120
121
  line = line.strip()
121
- if line.startswith("ENV "):
122
- # Basic parsing - this could be more sophisticated
122
+
123
+ # Look for ARG directives (build-time variables)
124
+ if line.startswith("ARG "):
123
125
  parts = line[4:].strip().split("=", 1)
124
- if len(parts) == 2 and not parts[1].strip():
126
+ var_name = parts[0].strip()
127
+ if len(parts) == 1 or not parts[1].strip():
125
128
  # No default value = required
126
- required.append(parts[0])
129
+ arg_vars.add(var_name)
130
+ if var_name not in required:
131
+ required.append(var_name)
132
+
133
+ # Look for ENV directives (runtime variables)
134
+ elif line.startswith("ENV "):
135
+ parts = line[4:].strip().split("=", 1)
136
+ var_name = parts[0].strip()
137
+
138
+ # Check if it references an ARG variable (e.g., ENV MY_VAR=$MY_VAR)
139
+ if len(parts) == 2 and parts[1].strip().startswith("$"):
140
+ ref_var = parts[1].strip()[1:]
141
+ if ref_var in arg_vars and var_name not in required:
142
+ required.append(var_name)
143
+ elif len(parts) == 2 and not parts[1].strip():
144
+ # No default value = required
145
+ if var_name not in required:
146
+ required.append(var_name)
127
147
  elif len(parts) == 1:
128
148
  # No equals sign = required
129
- required.append(parts[0])
149
+ if var_name not in required:
150
+ required.append(var_name)
130
151
 
131
152
  return required, optional
132
153
 
133
154
 
134
- async def analyze_mcp_environment(image: str, verbose: bool = False) -> dict[str, Any]:
155
+ async def analyze_mcp_environment(
156
+ image: str, verbose: bool = False, env_vars: dict[str, str] | None = None
157
+ ) -> dict[str, Any]:
135
158
  """Analyze an MCP environment to extract metadata."""
136
159
  design = HUDDesign()
160
+ env_vars = env_vars or {}
137
161
 
138
162
  # Build Docker command to run the image
139
- docker_cmd = ["docker", "run", "--rm", "-i", image]
163
+ docker_cmd = ["docker", "run", "--rm", "-i"]
164
+
165
+ # Add environment variables
166
+ for key, value in env_vars.items():
167
+ docker_cmd.extend(["-e", f"{key}={value}"])
168
+
169
+ docker_cmd.append(image)
140
170
 
141
171
  # Create MCP config
142
172
  config = {
@@ -209,10 +239,15 @@ async def analyze_mcp_environment(image: str, verbose: bool = False) -> dict[str
209
239
 
210
240
 
211
241
  def build_docker_image(
212
- directory: Path, tag: str, no_cache: bool = False, verbose: bool = False
242
+ directory: Path,
243
+ tag: str,
244
+ no_cache: bool = False,
245
+ verbose: bool = False,
246
+ build_args: dict[str, str] | None = None,
213
247
  ) -> bool:
214
248
  """Build a Docker image from a directory."""
215
249
  design = HUDDesign()
250
+ build_args = build_args or {}
216
251
 
217
252
  # Check if Dockerfile exists
218
253
  dockerfile = directory / "Dockerfile"
@@ -224,39 +259,35 @@ def build_docker_image(
224
259
  cmd = ["docker", "build", "-t", tag]
225
260
  if no_cache:
226
261
  cmd.append("--no-cache")
262
+
263
+ # Add build args
264
+ for key, value in build_args.items():
265
+ cmd.extend(["--build-arg", f"{key}={value}"])
266
+
227
267
  cmd.append(str(directory))
228
268
 
229
269
  # Always show build output
230
270
  design.info(f"Running: {' '.join(cmd)}")
231
271
 
232
272
  try:
233
- # Run with real-time output, handling encoding issues on Windows
234
- process = subprocess.Popen( # noqa: S603
235
- cmd,
236
- stdout=subprocess.PIPE,
237
- stderr=subprocess.STDOUT,
238
- text=True,
239
- encoding="utf-8",
240
- errors="replace", # Replace invalid chars instead of failing
241
- )
242
-
243
- # Stream output
244
- for line in process.stdout or []:
245
- design.info(line.rstrip())
246
-
247
- process.wait()
248
-
249
- return process.returncode == 0
273
+ # Use Docker's native output formatting - no capture, let Docker handle display
274
+ result = subprocess.run(cmd, check=False) # noqa: S603
275
+ return result.returncode == 0
250
276
  except Exception as e:
251
277
  design.error(f"Build error: {e}")
252
278
  return False
253
279
 
254
280
 
255
281
  def build_environment(
256
- directory: str = ".", tag: str | None = None, no_cache: bool = False, verbose: bool = False
282
+ directory: str = ".",
283
+ tag: str | None = None,
284
+ no_cache: bool = False,
285
+ verbose: bool = False,
286
+ env_vars: dict[str, str] | None = None,
257
287
  ) -> None:
258
288
  """Build a HUD environment and generate lock file."""
259
289
  design = HUDDesign()
290
+ env_vars = env_vars or {}
260
291
  design.header("HUD Environment Build")
261
292
 
262
293
  # Resolve directory
@@ -292,7 +323,7 @@ def build_environment(
292
323
 
293
324
  design.progress_message(f"Building Docker image: {temp_tag}")
294
325
 
295
- # Build the image
326
+ # Build the image (env vars are for runtime, not build time)
296
327
  if not build_docker_image(env_dir, temp_tag, no_cache, verbose):
297
328
  design.error("Docker build failed")
298
329
  raise typer.Exit(1)
@@ -305,7 +336,7 @@ def build_environment(
305
336
  loop = asyncio.new_event_loop()
306
337
  asyncio.set_event_loop(loop)
307
338
  try:
308
- analysis = loop.run_until_complete(analyze_mcp_environment(temp_tag, verbose))
339
+ analysis = loop.run_until_complete(analyze_mcp_environment(temp_tag, verbose, env_vars))
309
340
  finally:
310
341
  loop.close()
311
342
 
@@ -316,9 +347,9 @@ def build_environment(
316
347
 
317
348
  # Provide helpful debugging tips
318
349
  design.section_title("Debugging Tips")
319
- design.info("1. Test your server directly:")
320
- design.command_example(f"docker run --rm -it {temp_tag}")
321
- design.dim_info(" Expected output", "MCP initialization messages")
350
+ design.info("1. Debug your environment build:")
351
+ design.command_example("hud debug . --build")
352
+ design.dim_info(" This will", "test MCP server connection and show detailed logs")
322
353
  design.info("")
323
354
  design.info("2. Check for common issues:")
324
355
  design.info(" - Server crashes on startup")
@@ -336,10 +367,28 @@ def build_environment(
336
367
  dockerfile_path = env_dir / "Dockerfile"
337
368
  required_env, optional_env = extract_env_vars_from_dockerfile(dockerfile_path)
338
369
 
370
+ # Merge user-provided env vars with detected ones
371
+ provided_env_vars = {}
372
+ missing_required = []
373
+ if env_vars:
374
+ provided_env_vars = env_vars.copy()
375
+ # Track which required vars are still missing
376
+ missing_required = [e for e in required_env if e not in env_vars]
377
+
378
+ # Show what env vars were provided
379
+ design.success(f"Using provided environment variables: {', '.join(env_vars.keys())}")
380
+ else:
381
+ missing_required = required_env[:]
382
+
383
+ # Warn about missing required variables
384
+ if missing_required:
385
+ design.warning(f"Missing required environment variables: {', '.join(missing_required)}")
386
+ design.info("These can be added to the lock file after build or provided with -e flags")
387
+
339
388
  # Check for existing version and increment
340
389
  lock_path = env_dir / "hud.lock.yaml"
341
390
  existing_version = get_existing_version(lock_path)
342
-
391
+
343
392
  if existing_version:
344
393
  # Increment existing version
345
394
  new_version = increment_version(existing_version)
@@ -365,11 +414,20 @@ def build_environment(
365
414
  },
366
415
  }
367
416
 
368
- # Only add environment variables if they exist
369
- if required_env or optional_env:
417
+ # Add environment variables section if any exist
418
+ if missing_required or optional_env or provided_env_vars:
370
419
  lock_content["environment"]["variables"] = {}
371
- if required_env:
372
- lock_content["environment"]["variables"]["required"] = required_env
420
+
421
+ # Add note about editing environment variables
422
+ lock_content["environment"]["variables"]["_note"] = (
423
+ "You can edit this section to add or modify environment variables. "
424
+ "Provided variables will be used when running the environment."
425
+ )
426
+
427
+ if provided_env_vars:
428
+ lock_content["environment"]["variables"]["provided"] = provided_env_vars
429
+ if missing_required:
430
+ lock_content["environment"]["variables"]["required"] = missing_required
373
431
  if optional_env:
374
432
  lock_content["environment"]["variables"]["optional"] = optional_env
375
433
 
@@ -399,7 +457,7 @@ def build_environment(
399
457
  # Also tag with version
400
458
  base_name = tag.split(":")[0] if tag and ":" in tag else tag
401
459
  version_tag = f"{base_name}:{new_version}"
402
-
460
+
403
461
  label_cmd = [
404
462
  "docker",
405
463
  "build",
@@ -411,30 +469,21 @@ def build_environment(
411
469
  tag,
412
470
  "-t",
413
471
  version_tag,
414
- str(env_dir),
415
472
  ]
416
473
 
417
- # Run rebuild with proper encoding
418
- process = subprocess.Popen( # noqa: S603
419
- label_cmd,
420
- stdout=subprocess.PIPE,
421
- stderr=subprocess.STDOUT,
422
- text=True,
423
- encoding="utf-8",
424
- errors="replace",
425
- )
426
-
427
- # Stream output if verbose
474
+ label_cmd.append(str(env_dir))
475
+
476
+ # Run rebuild using Docker's native output formatting
428
477
  if verbose:
429
- for line in process.stdout or []:
430
- design.info(line.rstrip())
478
+ # Show Docker's native output when verbose
479
+ result = subprocess.run(label_cmd, check=False) # noqa: S603
431
480
  else:
432
- # Just consume output to avoid blocking
433
- process.stdout.read() # type: ignore
434
-
435
- process.wait()
481
+ # Hide output when not verbose
482
+ result = subprocess.run( # noqa: S603
483
+ label_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False
484
+ )
436
485
 
437
- if process.returncode != 0:
486
+ if result.returncode != 0:
438
487
  design.error("Failed to rebuild with label")
439
488
  raise typer.Exit(1)
440
489
 
@@ -475,8 +524,8 @@ def build_environment(
475
524
  design.status_item("Also tagged", tag)
476
525
  design.status_item("Version", new_version)
477
526
  design.status_item("Lock file", "hud.lock.yaml")
478
- design.status_item("Tools found", str(analysis['toolCount']))
479
-
527
+ design.status_item("Tools found", str(analysis["toolCount"]))
528
+
480
529
  # Show the digest info separately if we have it
481
530
  if image_id:
482
531
  design.dim_info("\nImage digest", image_id)
@@ -500,6 +549,7 @@ def build_command(
500
549
  ),
501
550
  no_cache: bool = typer.Option(False, "--no-cache", help="Build without Docker cache"),
502
551
  verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
552
+ env_vars: dict[str, str] | None = None,
503
553
  ) -> None:
504
554
  """Build a HUD environment and generate lock file."""
505
- build_environment(directory, tag, no_cache, verbose)
555
+ build_environment(directory, tag, no_cache, verbose, env_vars)
hud/cli/debug.py CHANGED
@@ -14,7 +14,7 @@ from rich.console import Console
14
14
  from hud.clients import MCPClient
15
15
  from hud.utils.design import HUDDesign
16
16
 
17
- from .utils import CaptureLogger, Colors, analyze_error_for_hints
17
+ from .utils.logging import CaptureLogger, Colors, analyze_error_for_hints
18
18
 
19
19
  console = Console()
20
20
 
@@ -167,7 +167,7 @@ async def debug_mcp_stdio(command: list[str], logger: CaptureLogger, max_phase:
167
167
  break
168
168
  except Exception as e:
169
169
  logger.error(f"Failed to parse MCP response: {e}")
170
- logger.error(f"Raw output that caused the error: {repr(line)}")
170
+ logger.error(f"Raw output that caused the error: {line!r}")
171
171
  logger.hint("This usually means non-JSON output is being sent to STDOUT")
172
172
  logger.hint("Common causes:")
173
173
  logger.hint(" - Print statements in your server code")
@@ -9,12 +9,17 @@ import subprocess
9
9
  from pathlib import Path
10
10
 
11
11
  import click
12
- import toml
13
12
  from fastmcp import FastMCP
14
13
 
15
14
  from hud.utils.design import HUDDesign
16
- from .docker_utils import get_docker_cmd, inject_supervisor
17
- from .env_utils import get_image_name, update_pyproject_toml, build_environment, image_exists
15
+
16
+ from .utils.docker import get_docker_cmd, inject_supervisor
17
+ from .utils.environment import (
18
+ build_environment,
19
+ get_image_name,
20
+ image_exists,
21
+ update_pyproject_toml,
22
+ )
18
23
 
19
24
  # Global design instance
20
25
  design = HUDDesign()
@@ -97,6 +102,12 @@ def create_proxy_server(
97
102
  design.info("Container will run without hot-reload")
98
103
  design.command_example(f"docker logs -f {container_name}", "View container logs")
99
104
 
105
+ # Show the full Docker command if there are environment variables
106
+ if docker_args and any(arg == "-e" or arg.startswith("--env") for arg in docker_args):
107
+ design.info("")
108
+ design.info("Docker command with environment variables:")
109
+ design.info(" ".join(docker_cmd))
110
+
100
111
  # Create the HTTP proxy server using config
101
112
  try:
102
113
  proxy = FastMCP.as_proxy(config, name=f"HUD Dev Proxy - {image_name}")
@@ -127,10 +138,9 @@ async def start_mcp_proxy(
127
138
  import asyncio
128
139
  import logging
129
140
  import os
130
- import subprocess
131
141
  import sys
132
142
 
133
- from .utils import find_free_port
143
+ from .utils.logging import find_free_port
134
144
 
135
145
  # Always disable the banner - we have our own output
136
146
  os.environ["FASTMCP_DISABLE_BANNER"] = "1"
@@ -207,15 +217,15 @@ async def start_mcp_proxy(
207
217
 
208
218
  # Remove any existing container with the same name (silently)
209
219
  # Note: The proxy creates containers on-demand when clients connect
210
- try:
220
+ try: # noqa: SIM105
211
221
  subprocess.run( # noqa: S603, ASYNC221
212
222
  ["docker", "rm", "-f", container_name], # noqa: S607
213
223
  stdout=subprocess.DEVNULL,
214
224
  stderr=subprocess.DEVNULL,
215
225
  check=False, # Don't raise error if container doesn't exist
216
226
  )
217
- except Exception:
218
- pass # Silent failure, container might not exist
227
+ except Exception: # noqa: S110
228
+ pass
219
229
 
220
230
  if transport == "stdio":
221
231
  if verbose:
@@ -281,13 +291,11 @@ async def start_mcp_proxy(
281
291
  design.error("Failed to launch inspector")
282
292
 
283
293
  # Launch inspector asynchronously so it doesn't block
284
- asyncio.create_task(launch_inspector())
294
+ asyncio.create_task(launch_inspector()) # noqa: RUF006
285
295
 
286
296
  # Launch interactive mode if requested
287
297
  if interactive:
288
298
  if transport != "http":
289
- from hud.utils.design import HUDDesign
290
-
291
299
  design.warning("Interactive mode only works with HTTP transport")
292
300
  else:
293
301
  server_url = f"http://localhost:{actual_port}/mcp"
@@ -306,7 +314,7 @@ async def start_mcp_proxy(
306
314
  design.info("Press Ctrl+C in the interactive session to exit")
307
315
 
308
316
  # Import and run interactive mode in a new event loop
309
- from .interactive import run_interactive_mode
317
+ from .utils.interactive import run_interactive_mode
310
318
 
311
319
  # Create a new event loop for the thread
312
320
  loop = asyncio.new_event_loop()
@@ -329,7 +337,11 @@ async def start_mcp_proxy(
329
337
 
330
338
  # Function to stream Docker logs
331
339
  async def stream_docker_logs() -> None:
332
- """Stream Docker container logs asynchronously."""
340
+ """Stream Docker container logs asynchronously.
341
+
342
+ Note: The Docker container is created on-demand when the first client connects.
343
+ Any environment variables passed via -e flags are included when the container starts.
344
+ """
333
345
  log_design = design
334
346
 
335
347
  # Always show waiting message
@@ -505,9 +517,7 @@ async def start_mcp_proxy(
505
517
  design.info("")
506
518
  design.info("Then you can:")
507
519
  design.info(" • Test locally: [cyan]hud run <image>[/cyan]")
508
- design.info(
509
- " • Push to registry: [cyan]hud push --image <registry/name>[/cyan]"
510
- )
520
+ design.info(" • Push to registry: [cyan]hud push --image <registry/name>[/cyan]")
511
521
  except Exception as e:
512
522
  # Suppress the graceful shutdown error and other FastMCP/uvicorn internal errors
513
523
  error_msg = str(e)
@@ -527,7 +537,7 @@ async def start_mcp_proxy(
527
537
  try:
528
538
  await log_task
529
539
  except asyncio.CancelledError:
530
- pass # Log streaming cancelled, normal shutdown
540
+ contextlib.suppress(asyncio.CancelledError)
531
541
 
532
542
 
533
543
  def run_mcp_dev_server(
@@ -588,7 +598,7 @@ def run_mcp_dev_server(
588
598
  # For HTTP transport, find available port first
589
599
  actual_port = port
590
600
  if transport == "http":
591
- from .utils import find_free_port
601
+ from .utils.logging import find_free_port
592
602
 
593
603
  actual_port = find_free_port(port)
594
604
  if actual_port is None:
@@ -600,8 +610,13 @@ def run_mcp_dev_server(
600
610
  # Create config
601
611
  if transport == "stdio":
602
612
  server_config = {"command": "hud", "args": ["dev", directory, "--transport", "stdio"]}
613
+ # For stdio, include docker args in the command
614
+ if docker_args:
615
+ server_config["args"].extend(docker_args)
603
616
  else:
604
617
  server_config = {"url": f"http://localhost:{actual_port}/mcp"}
618
+ # Note: Environment variables are passed to the Docker container via the proxy,
619
+ # not included in the client configuration
605
620
 
606
621
  # For the deeplink, we only need the server config
607
622
  server_config_json = json.dumps(server_config, indent=2)
@@ -627,7 +642,27 @@ def run_mcp_dev_server(
627
642
  else:
628
643
  design.info(f"🐳 {resolved_image}")
629
644
 
630
- design.progress_message(f"❗ If any issues arise, run `hud debug {resolved_image}` to debug the container")
645
+ design.progress_message(
646
+ f"❗ If any issues arise, run `hud debug {resolved_image}` to debug the container"
647
+ )
648
+
649
+ # Show environment variables if provided
650
+ if docker_args and any(arg == "-e" or arg.startswith("--env") for arg in docker_args):
651
+ design.section_title("Environment Variables")
652
+ design.info("The following environment variables will be passed to the Docker container:")
653
+ i = 0
654
+ while i < len(docker_args):
655
+ if docker_args[i] == "-e" and i + 1 < len(docker_args):
656
+ design.info(f" • {docker_args[i + 1]}")
657
+ i += 2
658
+ elif docker_args[i].startswith("--env="):
659
+ design.info(f" • {docker_args[i][6:]}")
660
+ i += 1
661
+ elif docker_args[i] == "--env" and i + 1 < len(docker_args):
662
+ design.info(f" • {docker_args[i + 1]}")
663
+ i += 2
664
+ else:
665
+ i += 1
631
666
 
632
667
  # Show hints about inspector and interactive mode
633
668
  if transport == "http":
@@ -679,10 +714,10 @@ def run_mcp_dev_server(
679
714
  )
680
715
  )
681
716
  except Exception as e:
682
- d.error(f"Failed to start MCP server: {e}")
683
- d.info("")
684
- d.info("💡 Tip: Run the following command to debug the container:")
685
- d.info(f" hud debug {resolved_image}")
686
- d.info("")
687
- d.info("This will help identify connection issues or initialization failures.")
717
+ design.error(f"Failed to start MCP server: {e}")
718
+ design.info("")
719
+ design.info("💡 Tip: Run the following command to debug the container:")
720
+ design.info(f" hud debug {resolved_image}")
721
+ design.info("")
722
+ design.info("This will help identify connection issues or initialization failures.")
688
723
  raise