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.
- hud/__main__.py +8 -0
- hud/agents/base.py +7 -8
- hud/agents/langchain.py +2 -2
- hud/agents/tests/test_openai.py +3 -1
- hud/cli/__init__.py +106 -51
- hud/cli/build.py +121 -71
- hud/cli/debug.py +2 -2
- hud/cli/{mcp_server.py → dev.py} +60 -25
- hud/cli/eval.py +148 -68
- hud/cli/init.py +0 -1
- hud/cli/list_func.py +72 -71
- hud/cli/pull.py +1 -2
- hud/cli/push.py +35 -23
- hud/cli/remove.py +35 -41
- hud/cli/tests/test_analyze.py +2 -1
- hud/cli/tests/test_analyze_metadata.py +42 -49
- hud/cli/tests/test_build.py +28 -52
- hud/cli/tests/test_cursor.py +1 -1
- hud/cli/tests/test_debug.py +1 -1
- hud/cli/tests/test_list_func.py +75 -64
- hud/cli/tests/test_main_module.py +30 -0
- hud/cli/tests/test_mcp_server.py +3 -3
- hud/cli/tests/test_pull.py +30 -61
- hud/cli/tests/test_push.py +70 -89
- hud/cli/tests/test_registry.py +36 -38
- hud/cli/tests/test_utils.py +1 -1
- hud/cli/utils/__init__.py +1 -0
- hud/cli/{docker_utils.py → utils/docker.py} +36 -0
- hud/cli/{env_utils.py → utils/environment.py} +7 -7
- hud/cli/{interactive.py → utils/interactive.py} +91 -19
- hud/cli/{analyze_metadata.py → utils/metadata.py} +12 -8
- hud/cli/{registry.py → utils/registry.py} +28 -30
- hud/cli/{remote_runner.py → utils/remote_runner.py} +1 -1
- hud/cli/utils/runner.py +134 -0
- hud/cli/utils/server.py +250 -0
- hud/clients/base.py +1 -1
- hud/clients/fastmcp.py +7 -5
- hud/clients/mcp_use.py +8 -6
- hud/server/server.py +34 -4
- hud/shared/exceptions.py +11 -0
- hud/shared/tests/test_exceptions.py +22 -0
- hud/telemetry/tests/__init__.py +0 -0
- hud/telemetry/tests/test_replay.py +40 -0
- hud/telemetry/tests/test_trace.py +63 -0
- hud/tools/base.py +20 -3
- hud/tools/computer/hud.py +15 -6
- hud/tools/executors/tests/test_base_executor.py +27 -0
- hud/tools/response.py +12 -8
- hud/tools/tests/test_response.py +60 -0
- hud/tools/tests/test_tools_init.py +49 -0
- hud/utils/design.py +19 -8
- hud/utils/mcp.py +17 -5
- hud/utils/tests/test_mcp.py +112 -0
- hud/utils/tests/test_version.py +1 -1
- hud/version.py +1 -1
- {hud_python-0.4.11.dist-info → hud_python-0.4.12.dist-info}/METADATA +14 -10
- {hud_python-0.4.11.dist-info → hud_python-0.4.12.dist-info}/RECORD +62 -52
- hud/cli/runner.py +0 -160
- /hud/cli/{cursor.py → utils/cursor.py} +0 -0
- /hud/cli/{utils.py → utils/logging.py} +0 -0
- {hud_python-0.4.11.dist-info → hud_python-0.4.12.dist-info}/WHEEL +0 -0
- {hud_python-0.4.11.dist-info → hud_python-0.4.12.dist-info}/entry_points.txt +0 -0
- {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(
|
|
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([
|
|
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
|
-
#
|
|
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
|
-
|
|
122
|
-
|
|
122
|
+
|
|
123
|
+
# Look for ARG directives (build-time variables)
|
|
124
|
+
if line.startswith("ARG "):
|
|
123
125
|
parts = line[4:].strip().split("=", 1)
|
|
124
|
-
|
|
126
|
+
var_name = parts[0].strip()
|
|
127
|
+
if len(parts) == 1 or not parts[1].strip():
|
|
125
128
|
# No default value = required
|
|
126
|
-
|
|
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
|
|
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(
|
|
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"
|
|
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,
|
|
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
|
-
#
|
|
234
|
-
|
|
235
|
-
|
|
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 = ".",
|
|
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.
|
|
320
|
-
design.command_example(
|
|
321
|
-
design.dim_info("
|
|
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
|
-
#
|
|
369
|
-
if
|
|
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
|
-
|
|
372
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
430
|
-
|
|
478
|
+
# Show Docker's native output when verbose
|
|
479
|
+
result = subprocess.run(label_cmd, check=False) # noqa: S603
|
|
431
480
|
else:
|
|
432
|
-
#
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
|
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[
|
|
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: {
|
|
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")
|
hud/cli/{mcp_server.py → dev.py}
RENAMED
|
@@ -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
|
-
|
|
17
|
-
from .
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|