hud-python 0.4.1__py3-none-any.whl → 0.4.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of hud-python might be problematic. Click here for more details.
- hud/__init__.py +22 -22
- hud/agents/__init__.py +13 -15
- hud/agents/base.py +599 -599
- hud/agents/claude.py +373 -373
- hud/agents/langchain.py +261 -250
- hud/agents/misc/__init__.py +7 -7
- hud/agents/misc/response_agent.py +82 -80
- hud/agents/openai.py +352 -352
- hud/agents/openai_chat_generic.py +154 -154
- hud/agents/tests/__init__.py +1 -1
- hud/agents/tests/test_base.py +742 -742
- hud/agents/tests/test_claude.py +324 -324
- hud/agents/tests/test_client.py +363 -363
- hud/agents/tests/test_openai.py +237 -237
- hud/cli/__init__.py +617 -617
- hud/cli/__main__.py +8 -8
- hud/cli/analyze.py +371 -371
- hud/cli/analyze_metadata.py +230 -230
- hud/cli/build.py +498 -427
- hud/cli/clone.py +185 -185
- hud/cli/cursor.py +92 -92
- hud/cli/debug.py +392 -392
- hud/cli/docker_utils.py +83 -83
- hud/cli/init.py +280 -281
- hud/cli/interactive.py +353 -353
- hud/cli/mcp_server.py +764 -756
- hud/cli/pull.py +330 -336
- hud/cli/push.py +404 -370
- hud/cli/remote_runner.py +311 -311
- hud/cli/runner.py +160 -160
- hud/cli/tests/__init__.py +3 -3
- hud/cli/tests/test_analyze.py +284 -284
- hud/cli/tests/test_cli_init.py +265 -265
- hud/cli/tests/test_cli_main.py +27 -27
- hud/cli/tests/test_clone.py +142 -142
- hud/cli/tests/test_cursor.py +253 -253
- hud/cli/tests/test_debug.py +453 -453
- hud/cli/tests/test_mcp_server.py +139 -139
- hud/cli/tests/test_utils.py +388 -388
- hud/cli/utils.py +263 -263
- hud/clients/README.md +143 -143
- hud/clients/__init__.py +16 -16
- hud/clients/base.py +378 -379
- hud/clients/fastmcp.py +222 -222
- hud/clients/mcp_use.py +298 -278
- hud/clients/tests/__init__.py +1 -1
- hud/clients/tests/test_client_integration.py +111 -111
- hud/clients/tests/test_fastmcp.py +342 -342
- hud/clients/tests/test_protocol.py +188 -188
- hud/clients/utils/__init__.py +1 -1
- hud/clients/utils/retry_transport.py +160 -160
- hud/datasets.py +327 -322
- hud/misc/__init__.py +1 -1
- hud/misc/claude_plays_pokemon.py +292 -292
- hud/otel/__init__.py +35 -35
- hud/otel/collector.py +142 -142
- hud/otel/config.py +164 -164
- hud/otel/context.py +536 -536
- hud/otel/exporters.py +366 -366
- hud/otel/instrumentation.py +97 -97
- hud/otel/processors.py +118 -118
- hud/otel/tests/__init__.py +1 -1
- hud/otel/tests/test_processors.py +197 -197
- hud/server/__init__.py +5 -5
- hud/server/context.py +114 -114
- hud/server/helper/__init__.py +5 -5
- hud/server/low_level.py +132 -132
- hud/server/server.py +170 -166
- hud/server/tests/__init__.py +3 -3
- hud/settings.py +73 -73
- hud/shared/__init__.py +5 -5
- hud/shared/exceptions.py +180 -180
- hud/shared/requests.py +264 -264
- hud/shared/tests/test_exceptions.py +157 -157
- hud/shared/tests/test_requests.py +275 -275
- hud/telemetry/__init__.py +25 -25
- hud/telemetry/instrument.py +379 -379
- hud/telemetry/job.py +309 -309
- hud/telemetry/replay.py +74 -74
- hud/telemetry/trace.py +83 -83
- hud/tools/__init__.py +33 -33
- hud/tools/base.py +365 -365
- hud/tools/bash.py +161 -161
- hud/tools/computer/__init__.py +15 -15
- hud/tools/computer/anthropic.py +437 -437
- hud/tools/computer/hud.py +376 -376
- hud/tools/computer/openai.py +295 -295
- hud/tools/computer/settings.py +82 -82
- hud/tools/edit.py +314 -314
- hud/tools/executors/__init__.py +30 -30
- hud/tools/executors/base.py +539 -539
- hud/tools/executors/pyautogui.py +621 -621
- hud/tools/executors/tests/__init__.py +1 -1
- hud/tools/executors/tests/test_base_executor.py +338 -338
- hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
- hud/tools/executors/xdo.py +511 -511
- hud/tools/playwright.py +412 -412
- hud/tools/tests/__init__.py +3 -3
- hud/tools/tests/test_base.py +282 -282
- hud/tools/tests/test_bash.py +158 -158
- hud/tools/tests/test_bash_extended.py +197 -197
- hud/tools/tests/test_computer.py +425 -425
- hud/tools/tests/test_computer_actions.py +34 -34
- hud/tools/tests/test_edit.py +259 -259
- hud/tools/tests/test_init.py +27 -27
- hud/tools/tests/test_playwright_tool.py +183 -183
- hud/tools/tests/test_tools.py +145 -145
- hud/tools/tests/test_utils.py +156 -156
- hud/tools/types.py +72 -72
- hud/tools/utils.py +50 -50
- hud/types.py +136 -136
- hud/utils/__init__.py +10 -10
- hud/utils/async_utils.py +65 -65
- hud/utils/design.py +236 -168
- hud/utils/mcp.py +55 -55
- hud/utils/progress.py +149 -149
- hud/utils/telemetry.py +66 -66
- hud/utils/tests/test_async_utils.py +173 -173
- hud/utils/tests/test_init.py +17 -17
- hud/utils/tests/test_progress.py +261 -261
- hud/utils/tests/test_telemetry.py +82 -82
- hud/utils/tests/test_version.py +8 -8
- hud/version.py +7 -7
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/METADATA +10 -8
- hud_python-0.4.3.dist-info/RECORD +131 -0
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/licenses/LICENSE +21 -21
- hud/agents/art.py +0 -101
- hud_python-0.4.1.dist-info/RECORD +0 -132
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/WHEEL +0 -0
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/entry_points.txt +0 -0
hud/cli/__init__.py
CHANGED
|
@@ -1,617 +1,617 @@
|
|
|
1
|
-
"""HUD CLI - Command-line interface for MCP environment analysis and debugging."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import asyncio
|
|
6
|
-
import json
|
|
7
|
-
import os
|
|
8
|
-
import sys
|
|
9
|
-
from pathlib import Path # noqa: TC003
|
|
10
|
-
|
|
11
|
-
import typer
|
|
12
|
-
from rich.console import Console
|
|
13
|
-
from rich.panel import Panel
|
|
14
|
-
from rich.table import Table
|
|
15
|
-
|
|
16
|
-
from .analyze import (
|
|
17
|
-
analyze_environment,
|
|
18
|
-
analyze_environment_from_config,
|
|
19
|
-
analyze_environment_from_mcp_config,
|
|
20
|
-
)
|
|
21
|
-
from .build import build_command
|
|
22
|
-
from .clone import clone_repository, get_clone_message, print_error, print_tutorial
|
|
23
|
-
from .cursor import get_cursor_config_path, list_cursor_servers, parse_cursor_config
|
|
24
|
-
from .debug import debug_mcp_stdio
|
|
25
|
-
from .init import create_environment
|
|
26
|
-
from .mcp_server import run_mcp_dev_server
|
|
27
|
-
from .pull import pull_command
|
|
28
|
-
from .push import push_command
|
|
29
|
-
from .utils import CaptureLogger
|
|
30
|
-
|
|
31
|
-
# Create the main Typer app
|
|
32
|
-
app = typer.Typer(
|
|
33
|
-
name="hud",
|
|
34
|
-
help="🚀 HUD CLI for MCP environment analysis and debugging",
|
|
35
|
-
add_completion=False,
|
|
36
|
-
rich_markup_mode="rich",
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
console = Console()
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
# Capture IMAGE and any following Docker args as a single variadic argument list.
|
|
43
|
-
@app.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
|
|
44
|
-
def analyze(
|
|
45
|
-
params: list[str] = typer.Argument( # type: ignore[arg-type] # noqa: B008
|
|
46
|
-
None, # Optional positional arguments
|
|
47
|
-
help="Docker image followed by optional Docker run arguments (e.g., 'hud-image:latest -e KEY=value')", # noqa: E501
|
|
48
|
-
),
|
|
49
|
-
config: Path = typer.Option( # noqa: B008
|
|
50
|
-
None,
|
|
51
|
-
"--config",
|
|
52
|
-
"-c",
|
|
53
|
-
help="JSON config file with MCP configuration",
|
|
54
|
-
exists=True,
|
|
55
|
-
file_okay=True,
|
|
56
|
-
dir_okay=False,
|
|
57
|
-
),
|
|
58
|
-
cursor: str | None = typer.Option(
|
|
59
|
-
None,
|
|
60
|
-
"--cursor",
|
|
61
|
-
help="Analyze a server from Cursor config",
|
|
62
|
-
),
|
|
63
|
-
output_format: str = typer.Option(
|
|
64
|
-
"interactive",
|
|
65
|
-
"--format",
|
|
66
|
-
"-f",
|
|
67
|
-
help="Output format: interactive, json, markdown",
|
|
68
|
-
),
|
|
69
|
-
verbose: bool = typer.Option(
|
|
70
|
-
False,
|
|
71
|
-
"--verbose",
|
|
72
|
-
"-v",
|
|
73
|
-
help="Enable verbose output (shows tool schemas)",
|
|
74
|
-
),
|
|
75
|
-
live: bool = typer.Option(
|
|
76
|
-
False,
|
|
77
|
-
"--live",
|
|
78
|
-
help="Run container for live analysis (slower but more accurate)",
|
|
79
|
-
),
|
|
80
|
-
) -> None:
|
|
81
|
-
"""🔍 Analyze MCP environment - discover tools, resources, and capabilities.
|
|
82
|
-
|
|
83
|
-
By default, uses cached metadata for instant results.
|
|
84
|
-
Use --live to run the container for real-time analysis.
|
|
85
|
-
|
|
86
|
-
Examples:
|
|
87
|
-
hud analyze hudpython/test_init # Fast metadata inspection
|
|
88
|
-
hud analyze my-env --live # Full container analysis
|
|
89
|
-
hud analyze --config mcp-config.json # From MCP config
|
|
90
|
-
hud analyze --cursor text-2048-dev # From Cursor config
|
|
91
|
-
"""
|
|
92
|
-
if config:
|
|
93
|
-
# Load config from JSON file (always live for configs)
|
|
94
|
-
asyncio.run(analyze_environment_from_config(config, output_format, verbose))
|
|
95
|
-
elif cursor:
|
|
96
|
-
# Parse cursor config (always live for cursor)
|
|
97
|
-
command, error = parse_cursor_config(cursor)
|
|
98
|
-
if error or command is None:
|
|
99
|
-
console.print(f"[red]❌ {error or 'Failed to parse cursor config'}[/red]")
|
|
100
|
-
raise typer.Exit(1)
|
|
101
|
-
# Convert to MCP config
|
|
102
|
-
mcp_config = {
|
|
103
|
-
"local": {"command": command[0], "args": command[1:] if len(command) > 1 else []}
|
|
104
|
-
}
|
|
105
|
-
asyncio.run(analyze_environment_from_mcp_config(mcp_config, output_format, verbose))
|
|
106
|
-
elif params:
|
|
107
|
-
image, *docker_args = params
|
|
108
|
-
if live or docker_args: # If docker args provided, assume live mode
|
|
109
|
-
# Build Docker command from image and args
|
|
110
|
-
docker_cmd = ["docker", "run", "--rm", "-i", *docker_args, image]
|
|
111
|
-
asyncio.run(analyze_environment(docker_cmd, output_format, verbose))
|
|
112
|
-
else:
|
|
113
|
-
# Fast mode - analyze from metadata
|
|
114
|
-
from .analyze_metadata import analyze_from_metadata
|
|
115
|
-
|
|
116
|
-
asyncio.run(analyze_from_metadata(image, output_format, verbose))
|
|
117
|
-
else:
|
|
118
|
-
console.print("[red]Error: Must specify either a Docker image, --config, or --cursor[/red]")
|
|
119
|
-
console.print("\nExamples:")
|
|
120
|
-
console.print(" hud analyze hudpython/test_init # Fast metadata analysis")
|
|
121
|
-
console.print(" hud analyze my-env --live # Live container analysis")
|
|
122
|
-
console.print(" hud analyze --config mcp-config.json # From config file")
|
|
123
|
-
console.print(" hud analyze --cursor my-server # From Cursor")
|
|
124
|
-
raise typer.Exit(1)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
# Same variadic approach for debug.
|
|
128
|
-
@app.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
|
|
129
|
-
def debug(
|
|
130
|
-
params: list[str] = typer.Argument( # type: ignore[arg-type] # noqa: B008
|
|
131
|
-
None,
|
|
132
|
-
help="Docker image followed by optional Docker run arguments (e.g., 'hud-image:latest -e KEY=value')", # noqa: E501
|
|
133
|
-
),
|
|
134
|
-
config: Path = typer.Option( # noqa: B008
|
|
135
|
-
None,
|
|
136
|
-
"--config",
|
|
137
|
-
"-c",
|
|
138
|
-
help="JSON config file with MCP configuration",
|
|
139
|
-
exists=True,
|
|
140
|
-
file_okay=True,
|
|
141
|
-
dir_okay=False,
|
|
142
|
-
),
|
|
143
|
-
cursor: str | None = typer.Option(
|
|
144
|
-
None,
|
|
145
|
-
"--cursor",
|
|
146
|
-
help="Debug a server from Cursor config",
|
|
147
|
-
),
|
|
148
|
-
max_phase: int = typer.Option(
|
|
149
|
-
5,
|
|
150
|
-
"--max-phase",
|
|
151
|
-
"-p",
|
|
152
|
-
min=1,
|
|
153
|
-
max=5,
|
|
154
|
-
help="Maximum debug phase (1-5)",
|
|
155
|
-
),
|
|
156
|
-
) -> None:
|
|
157
|
-
"""🐛 Debug MCP environment - test initialization, tools, and readiness.
|
|
158
|
-
|
|
159
|
-
Examples:
|
|
160
|
-
hud debug hud-text-2048:latest
|
|
161
|
-
hud debug my-mcp-server:v1 -e API_KEY=xxx -p 8080:8080
|
|
162
|
-
hud debug --config mcp-config.json
|
|
163
|
-
hud debug --cursor text-2048-dev
|
|
164
|
-
hud debug hud-browser:dev --max-phase 3
|
|
165
|
-
"""
|
|
166
|
-
|
|
167
|
-
# Determine the command to run
|
|
168
|
-
command = None
|
|
169
|
-
|
|
170
|
-
if config:
|
|
171
|
-
# Load config from JSON file
|
|
172
|
-
with open(config) as f:
|
|
173
|
-
mcp_config = json.load(f)
|
|
174
|
-
|
|
175
|
-
# Extract command from first server in config
|
|
176
|
-
server_name = next(iter(mcp_config.keys()))
|
|
177
|
-
server_config = mcp_config[server_name]
|
|
178
|
-
command = [server_config["command"], *server_config.get("args", [])]
|
|
179
|
-
elif cursor:
|
|
180
|
-
# Parse cursor config
|
|
181
|
-
command, error = parse_cursor_config(cursor)
|
|
182
|
-
if error or command is None:
|
|
183
|
-
console.print(f"[red]❌ {error or 'Failed to parse cursor config'}[/red]")
|
|
184
|
-
raise typer.Exit(1)
|
|
185
|
-
elif params:
|
|
186
|
-
image, *docker_args = params
|
|
187
|
-
# Build Docker command
|
|
188
|
-
command = ["docker", "run", "--rm", "-i", *docker_args, image]
|
|
189
|
-
else:
|
|
190
|
-
console.print("[red]Error: Must specify either a Docker image, --config, or --cursor[/red]")
|
|
191
|
-
console.print("\nExamples:")
|
|
192
|
-
console.print(" hud debug hud-text-2048:latest")
|
|
193
|
-
console.print(" hud debug --config mcp-config.json")
|
|
194
|
-
console.print(" hud debug --cursor my-server")
|
|
195
|
-
raise typer.Exit(1)
|
|
196
|
-
|
|
197
|
-
# Create logger and run debug
|
|
198
|
-
logger = CaptureLogger(print_output=True)
|
|
199
|
-
phases_completed = asyncio.run(debug_mcp_stdio(command, logger, max_phase=max_phase))
|
|
200
|
-
|
|
201
|
-
# Show summary using design system
|
|
202
|
-
from hud.utils.design import HUDDesign
|
|
203
|
-
|
|
204
|
-
design = HUDDesign()
|
|
205
|
-
|
|
206
|
-
design.info("") # Empty line
|
|
207
|
-
design.section_title("Debug Summary")
|
|
208
|
-
|
|
209
|
-
if phases_completed == max_phase:
|
|
210
|
-
design.success(f"All {max_phase} phases completed successfully!")
|
|
211
|
-
if max_phase == 5:
|
|
212
|
-
design.info("Your MCP server is fully functional and ready for production use.")
|
|
213
|
-
else:
|
|
214
|
-
design.warning(f"Completed {phases_completed} out of {max_phase} phases")
|
|
215
|
-
design.info("Check the errors above for troubleshooting.")
|
|
216
|
-
|
|
217
|
-
# Exit with appropriate code
|
|
218
|
-
if phases_completed < max_phase:
|
|
219
|
-
raise typer.Exit(1)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
@app.command()
|
|
223
|
-
def cursor_list() -> None:
|
|
224
|
-
"""📋 List all MCP servers configured in Cursor."""
|
|
225
|
-
console.print(Panel.fit("📋 [bold cyan]Cursor MCP Servers[/bold cyan]", border_style="cyan"))
|
|
226
|
-
|
|
227
|
-
servers, error = list_cursor_servers()
|
|
228
|
-
|
|
229
|
-
if error:
|
|
230
|
-
console.print(f"[red]❌ {error}[/red]")
|
|
231
|
-
raise typer.Exit(1)
|
|
232
|
-
|
|
233
|
-
if not servers:
|
|
234
|
-
console.print("[yellow]No servers found in Cursor config[/yellow]")
|
|
235
|
-
return
|
|
236
|
-
|
|
237
|
-
# Display servers in a table
|
|
238
|
-
table = Table(title="Available Servers")
|
|
239
|
-
table.add_column("Server Name", style="cyan")
|
|
240
|
-
table.add_column("Command Preview", style="dim")
|
|
241
|
-
|
|
242
|
-
config_path = get_cursor_config_path()
|
|
243
|
-
if config_path.exists():
|
|
244
|
-
with open(config_path) as f:
|
|
245
|
-
config = json.load(f)
|
|
246
|
-
mcp_servers = config.get("mcpServers", {})
|
|
247
|
-
|
|
248
|
-
for server_name in servers:
|
|
249
|
-
server_config = mcp_servers.get(server_name, {})
|
|
250
|
-
command = server_config.get("command", "")
|
|
251
|
-
args = server_config.get("args", [])
|
|
252
|
-
|
|
253
|
-
# Create command preview
|
|
254
|
-
if args:
|
|
255
|
-
preview = f"{command} {' '.join(args[:2])}"
|
|
256
|
-
if len(args) > 2:
|
|
257
|
-
preview += " ..."
|
|
258
|
-
else:
|
|
259
|
-
preview = command
|
|
260
|
-
|
|
261
|
-
table.add_row(server_name, preview)
|
|
262
|
-
|
|
263
|
-
console.print(table)
|
|
264
|
-
console.print(f"\n[dim]Config location: {config_path}[/dim]")
|
|
265
|
-
console.print(
|
|
266
|
-
"\n[green]Tip:[/green] Use [cyan]hud debug --cursor <server-name>[/cyan] to debug a server"
|
|
267
|
-
)
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
@app.command()
|
|
271
|
-
def version() -> None:
|
|
272
|
-
"""Show HUD CLI version."""
|
|
273
|
-
try:
|
|
274
|
-
from hud import __version__
|
|
275
|
-
|
|
276
|
-
console.print(f"HUD CLI version: [cyan]{__version__}[/cyan]")
|
|
277
|
-
except ImportError:
|
|
278
|
-
console.print("HUD CLI version: [cyan]unknown[/cyan]")
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
@app.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
|
|
282
|
-
def dev(
|
|
283
|
-
params: list[str] = typer.Argument( # type: ignore[arg-type] # noqa: B008
|
|
284
|
-
None,
|
|
285
|
-
help="Environment directory followed by optional Docker arguments (e.g., '. -e KEY=value')",
|
|
286
|
-
),
|
|
287
|
-
image: str | None = typer.Option(
|
|
288
|
-
None, "--image", "-i", help="Docker image name (overrides auto-detection)"
|
|
289
|
-
),
|
|
290
|
-
build: bool = typer.Option(False, "--build", "-b", help="Build image before starting"),
|
|
291
|
-
no_cache: bool = typer.Option(False, "--no-cache", help="Force rebuild without cache"),
|
|
292
|
-
transport: str = typer.Option(
|
|
293
|
-
"http", "--transport", "-t", help="Transport protocol: http (default) or stdio"
|
|
294
|
-
),
|
|
295
|
-
port: int = typer.Option(8765, "--port", "-p", help="HTTP server port (ignored for stdio)"),
|
|
296
|
-
no_reload: bool = typer.Option(False, "--no-reload", help="Disable hot-reload"),
|
|
297
|
-
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show server logs"),
|
|
298
|
-
inspector: bool = typer.Option(
|
|
299
|
-
False, "--inspector", help="Launch MCP Inspector (HTTP mode only)"
|
|
300
|
-
),
|
|
301
|
-
no_logs: bool = typer.Option(False, "--no-logs", help="Disable streaming Docker logs"),
|
|
302
|
-
interactive: bool = typer.Option(
|
|
303
|
-
False, "--interactive", help="Launch interactive testing mode (HTTP mode only)"
|
|
304
|
-
),
|
|
305
|
-
) -> None:
|
|
306
|
-
"""🔥 Development mode with hot-reload.
|
|
307
|
-
|
|
308
|
-
Runs your MCP environment in Docker with automatic restart on file changes.
|
|
309
|
-
|
|
310
|
-
The container's last command (typically the MCP server) will be wrapped
|
|
311
|
-
with watchfiles for hot-reload functionality.
|
|
312
|
-
|
|
313
|
-
Examples:
|
|
314
|
-
hud dev # Auto-detect in current directory
|
|
315
|
-
hud dev environments/browser # Specific directory
|
|
316
|
-
hud dev . --build # Build image first
|
|
317
|
-
hud dev . --image custom:tag # Use specific image
|
|
318
|
-
hud dev . --no-cache # Force clean rebuild
|
|
319
|
-
hud dev . --verbose # Show detailed logs
|
|
320
|
-
hud dev . --transport stdio # Use stdio proxy for multiple connections
|
|
321
|
-
hud dev . --inspector # Launch MCP Inspector (HTTP mode only)
|
|
322
|
-
hud dev . --interactive # Launch interactive testing mode (HTTP mode only)
|
|
323
|
-
hud dev . --no-logs # Disable Docker log streaming
|
|
324
|
-
|
|
325
|
-
# With Docker arguments (after all options):
|
|
326
|
-
hud dev . -e BROWSER_PROVIDER=anchorbrowser -e ANCHOR_API_KEY=xxx
|
|
327
|
-
hud dev . -e API_KEY=secret -v /tmp/data:/data --network host
|
|
328
|
-
hud dev . --build -e DEBUG=true --memory 2g
|
|
329
|
-
"""
|
|
330
|
-
# Parse directory and Docker arguments
|
|
331
|
-
if params:
|
|
332
|
-
directory = params[0]
|
|
333
|
-
docker_args = params[1:] if len(params) > 1 else []
|
|
334
|
-
else:
|
|
335
|
-
directory = "."
|
|
336
|
-
docker_args = []
|
|
337
|
-
|
|
338
|
-
run_mcp_dev_server(
|
|
339
|
-
directory,
|
|
340
|
-
image,
|
|
341
|
-
build,
|
|
342
|
-
no_cache,
|
|
343
|
-
transport,
|
|
344
|
-
port,
|
|
345
|
-
no_reload,
|
|
346
|
-
verbose,
|
|
347
|
-
inspector,
|
|
348
|
-
no_logs,
|
|
349
|
-
interactive,
|
|
350
|
-
docker_args,
|
|
351
|
-
)
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
@app.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
|
|
355
|
-
def run(
|
|
356
|
-
params: list[str] = typer.Argument( # type: ignore[arg-type] # noqa: B008
|
|
357
|
-
None,
|
|
358
|
-
help="Docker image followed by optional arguments (e.g., 'hud-image:latest -e KEY=value')",
|
|
359
|
-
),
|
|
360
|
-
local: bool = typer.Option(
|
|
361
|
-
False,
|
|
362
|
-
"--local",
|
|
363
|
-
help="Run locally with Docker (default: remote via mcp.hud.so)",
|
|
364
|
-
),
|
|
365
|
-
remote: bool = typer.Option(
|
|
366
|
-
False,
|
|
367
|
-
"--remote",
|
|
368
|
-
help="Run remotely via mcp.hud.so (default)",
|
|
369
|
-
),
|
|
370
|
-
transport: str = typer.Option(
|
|
371
|
-
"stdio",
|
|
372
|
-
"--transport",
|
|
373
|
-
"-t",
|
|
374
|
-
help="Transport protocol: stdio (default) or http",
|
|
375
|
-
),
|
|
376
|
-
port: int = typer.Option(
|
|
377
|
-
8765,
|
|
378
|
-
"--port",
|
|
379
|
-
"-p",
|
|
380
|
-
help="Port for HTTP transport (ignored for stdio)",
|
|
381
|
-
),
|
|
382
|
-
url: str = typer.Option(
|
|
383
|
-
None,
|
|
384
|
-
"--url",
|
|
385
|
-
help="Remote MCP server URL (default: HUD_MCP_URL or mcp.hud.so)",
|
|
386
|
-
),
|
|
387
|
-
api_key: str | None = typer.Option(
|
|
388
|
-
None,
|
|
389
|
-
"--api-key",
|
|
390
|
-
help="API key for remote server (default: HUD_API_KEY env var)",
|
|
391
|
-
),
|
|
392
|
-
run_id: str | None = typer.Option(
|
|
393
|
-
None,
|
|
394
|
-
"--run-id",
|
|
395
|
-
help="Run ID for tracking (remote only)",
|
|
396
|
-
),
|
|
397
|
-
verbose: bool = typer.Option(
|
|
398
|
-
False,
|
|
399
|
-
"--verbose",
|
|
400
|
-
"-v",
|
|
401
|
-
help="Show detailed output",
|
|
402
|
-
),
|
|
403
|
-
) -> None:
|
|
404
|
-
"""🚀 Run MCP server locally or remotely.
|
|
405
|
-
|
|
406
|
-
By default, runs remotely via mcp.hud.so. Use --local for Docker.
|
|
407
|
-
|
|
408
|
-
Remote Examples:
|
|
409
|
-
hud run hud-text-2048:latest
|
|
410
|
-
hud run my-server:v1 -e API_KEY=xxx -h Run-Id:abc123
|
|
411
|
-
hud run my-server:v1 --transport http --port 9000
|
|
412
|
-
|
|
413
|
-
Local Examples:
|
|
414
|
-
hud run --local hud-text-2048:latest
|
|
415
|
-
hud run --local my-server:v1 -e API_KEY=xxx
|
|
416
|
-
hud run --local my-server:v1 --transport http
|
|
417
|
-
"""
|
|
418
|
-
if not params:
|
|
419
|
-
typer.echo("❌ Docker image is required")
|
|
420
|
-
raise typer.Exit(1)
|
|
421
|
-
|
|
422
|
-
# Parse image and args
|
|
423
|
-
image = params[0]
|
|
424
|
-
docker_args = params[1:] if len(params) > 1 else []
|
|
425
|
-
|
|
426
|
-
# Handle conflicting flags
|
|
427
|
-
if local and remote:
|
|
428
|
-
typer.echo("❌ Cannot use both --local and --remote")
|
|
429
|
-
raise typer.Exit(1)
|
|
430
|
-
|
|
431
|
-
# Default to remote if not explicitly local
|
|
432
|
-
is_local = local and not remote
|
|
433
|
-
|
|
434
|
-
if is_local:
|
|
435
|
-
# Local Docker execution
|
|
436
|
-
from .runner import run_mcp_server
|
|
437
|
-
|
|
438
|
-
run_mcp_server(image, docker_args, transport, port, verbose)
|
|
439
|
-
else:
|
|
440
|
-
# Remote execution via proxy
|
|
441
|
-
from .remote_runner import run_remote_server
|
|
442
|
-
|
|
443
|
-
# Get URL from options or environment
|
|
444
|
-
if not url:
|
|
445
|
-
url = os.getenv("HUD_MCP_URL", "https://mcp.hud.so/v3/mcp")
|
|
446
|
-
|
|
447
|
-
run_remote_server(image, docker_args, transport, port, url, api_key, run_id, verbose)
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
@app.command()
|
|
451
|
-
def clone(
|
|
452
|
-
url: str = typer.Argument(
|
|
453
|
-
...,
|
|
454
|
-
help="Git repository URL to clone",
|
|
455
|
-
),
|
|
456
|
-
) -> None:
|
|
457
|
-
"""🚀 Clone a git repository quietly with a pretty output.
|
|
458
|
-
|
|
459
|
-
This command wraps 'git clone' with the --quiet flag and displays
|
|
460
|
-
a rich formatted success message. If the repository contains a clone
|
|
461
|
-
message in pyproject.toml, it will be displayed as a tutorial.
|
|
462
|
-
|
|
463
|
-
Configure clone messages in your repository's pyproject.toml:
|
|
464
|
-
|
|
465
|
-
[tool.hud.clone]
|
|
466
|
-
title = "🚀 My Project"
|
|
467
|
-
message = "Thanks for cloning! Run 'pip install -e .' to get started."
|
|
468
|
-
|
|
469
|
-
# Or use markdown format:
|
|
470
|
-
# markdown = "## Welcome!\\n\\nHere's how to get started..."
|
|
471
|
-
# style = "cyan"
|
|
472
|
-
|
|
473
|
-
Examples:
|
|
474
|
-
hud clone https://github.com/user/repo.git
|
|
475
|
-
"""
|
|
476
|
-
# Run the clone
|
|
477
|
-
success, result = clone_repository(url)
|
|
478
|
-
|
|
479
|
-
if success:
|
|
480
|
-
# Look for clone message configuration
|
|
481
|
-
clone_config = get_clone_message(result)
|
|
482
|
-
print_tutorial(clone_config)
|
|
483
|
-
else:
|
|
484
|
-
print_error(result)
|
|
485
|
-
raise typer.Exit(1)
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
@app.command()
|
|
489
|
-
def build(
|
|
490
|
-
directory: str = typer.Argument(".", help="Environment directory to build"),
|
|
491
|
-
tag: str | None = typer.Option(
|
|
492
|
-
None, "--tag", "-t", help="Docker image tag (default: from pyproject.toml)"
|
|
493
|
-
),
|
|
494
|
-
no_cache: bool = typer.Option(False, "--no-cache", help="Build without Docker cache"),
|
|
495
|
-
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
|
|
496
|
-
) -> None:
|
|
497
|
-
"""🏗️ Build a HUD environment and generate lock file.
|
|
498
|
-
|
|
499
|
-
This command:
|
|
500
|
-
- Builds a Docker image from your environment
|
|
501
|
-
- Analyzes the MCP server to extract metadata
|
|
502
|
-
- Generates a hud.lock.yaml file for reproducibility
|
|
503
|
-
|
|
504
|
-
Examples:
|
|
505
|
-
hud build # Build current directory
|
|
506
|
-
hud build environments/text_2048
|
|
507
|
-
hud build . --tag my-env:v1.0
|
|
508
|
-
hud build . --no-cache # Force rebuild
|
|
509
|
-
"""
|
|
510
|
-
build_command(directory, tag, no_cache, verbose)
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
@app.command()
|
|
514
|
-
def push(
|
|
515
|
-
directory: str = typer.Argument(".", help="Environment directory containing hud.lock.yaml"),
|
|
516
|
-
image: str | None = typer.Option(None, "--image", "-i", help="Override registry image name"),
|
|
517
|
-
tag: str | None = typer.Option(
|
|
518
|
-
None, "--tag", "-t", help="Override tag (e.g., 'v1.0', 'latest')"
|
|
519
|
-
),
|
|
520
|
-
sign: bool = typer.Option(
|
|
521
|
-
False, "--sign", help="Sign the image with cosign (not yet implemented)"
|
|
522
|
-
),
|
|
523
|
-
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompts"),
|
|
524
|
-
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
|
|
525
|
-
) -> None:
|
|
526
|
-
"""📤 Push HUD environment to registry.
|
|
527
|
-
|
|
528
|
-
Reads hud.lock.yaml from the directory and pushes to registry.
|
|
529
|
-
Auto-detects your Docker username if --image not specified.
|
|
530
|
-
|
|
531
|
-
Examples:
|
|
532
|
-
hud push # Push with auto-detected name
|
|
533
|
-
hud push --tag v1.0 # Push with specific tag
|
|
534
|
-
hud push . --image myuser/myenv:v1.0
|
|
535
|
-
hud push --yes # Skip confirmation
|
|
536
|
-
"""
|
|
537
|
-
push_command(directory, image, tag, sign, yes, verbose)
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
@app.command()
|
|
541
|
-
def pull(
|
|
542
|
-
target: str = typer.Argument(..., help="Image reference or lock file to pull"),
|
|
543
|
-
lock_file: str | None = typer.Option(
|
|
544
|
-
None, "--lock", "-l", help="Path to lock file (if target is image ref)"
|
|
545
|
-
),
|
|
546
|
-
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
547
|
-
verify_only: bool = typer.Option(
|
|
548
|
-
False, "--verify-only", help="Only verify metadata without pulling"
|
|
549
|
-
),
|
|
550
|
-
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
|
|
551
|
-
) -> None:
|
|
552
|
-
"""📥 Pull HUD environment from registry with metadata preview.
|
|
553
|
-
|
|
554
|
-
Shows environment details before downloading.
|
|
555
|
-
|
|
556
|
-
Examples:
|
|
557
|
-
hud pull hud.lock.yaml # Pull from lock file
|
|
558
|
-
hud pull myuser/myenv:latest # Pull by image reference
|
|
559
|
-
hud pull myuser/myenv --verify-only # Check metadata only
|
|
560
|
-
"""
|
|
561
|
-
pull_command(target, lock_file, yes, verify_only, verbose)
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
@app.command()
|
|
565
|
-
def init(
|
|
566
|
-
name: str = typer.Argument(None, help="Environment name (default: current directory name)"),
|
|
567
|
-
directory: str = typer.Option(".", "--dir", "-d", help="Target directory"),
|
|
568
|
-
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files"),
|
|
569
|
-
) -> None:
|
|
570
|
-
"""🚀 Initialize a new HUD environment with minimal boilerplate.
|
|
571
|
-
|
|
572
|
-
Creates a working MCP environment with:
|
|
573
|
-
- Dockerfile for containerization
|
|
574
|
-
- pyproject.toml for dependencies
|
|
575
|
-
- Minimal MCP server with context
|
|
576
|
-
- Required setup/evaluate tools
|
|
577
|
-
|
|
578
|
-
Examples:
|
|
579
|
-
hud init # Use current directory name
|
|
580
|
-
hud init my-env # Create in ./my-env/
|
|
581
|
-
hud init my-env --dir /tmp # Create in /tmp/my-env/
|
|
582
|
-
"""
|
|
583
|
-
create_environment(name, directory, force)
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
@app.command()
|
|
587
|
-
def quickstart() -> None:
|
|
588
|
-
"""
|
|
589
|
-
Quickstart with evaluating an agent!
|
|
590
|
-
"""
|
|
591
|
-
# Just call the clone command with the quickstart URL
|
|
592
|
-
clone("https://github.com/hud-evals/quickstart.git")
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
def main() -> None:
|
|
596
|
-
"""Main entry point for the CLI."""
|
|
597
|
-
# Show header for main help
|
|
598
|
-
if len(sys.argv) == 1 or (len(sys.argv) == 2 and sys.argv[1] in ["--help", "-h"]):
|
|
599
|
-
console.print(
|
|
600
|
-
Panel.fit(
|
|
601
|
-
"[bold cyan]🚀 HUD CLI[/bold cyan]\nMCP Environment Analysis & Debugging",
|
|
602
|
-
border_style="cyan",
|
|
603
|
-
)
|
|
604
|
-
)
|
|
605
|
-
console.print("\n[yellow]Quick Start:[/yellow]")
|
|
606
|
-
console.print(" 1. Create a new environment: [cyan]hud init my-env && cd my-env[/cyan]")
|
|
607
|
-
console.print(" 2. Develop with hot-reload: [cyan]hud dev --interactive[/cyan]")
|
|
608
|
-
console.print(" 3. Build for production: [cyan]hud build[/cyan]")
|
|
609
|
-
console.print(" 4. Share your environment: [cyan]hud push[/cyan]")
|
|
610
|
-
console.print(" 5. Get shared environments: [cyan]hud pull <org/name:tag>[/cyan]")
|
|
611
|
-
console.print(" 6. Run and test: [cyan]hud run <image>[/cyan]\n")
|
|
612
|
-
|
|
613
|
-
app()
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
if __name__ == "__main__":
|
|
617
|
-
main()
|
|
1
|
+
"""HUD CLI - Command-line interface for MCP environment analysis and debugging."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path # noqa: TC003
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
|
|
16
|
+
from .analyze import (
|
|
17
|
+
analyze_environment,
|
|
18
|
+
analyze_environment_from_config,
|
|
19
|
+
analyze_environment_from_mcp_config,
|
|
20
|
+
)
|
|
21
|
+
from .build import build_command
|
|
22
|
+
from .clone import clone_repository, get_clone_message, print_error, print_tutorial
|
|
23
|
+
from .cursor import get_cursor_config_path, list_cursor_servers, parse_cursor_config
|
|
24
|
+
from .debug import debug_mcp_stdio
|
|
25
|
+
from .init import create_environment
|
|
26
|
+
from .mcp_server import run_mcp_dev_server
|
|
27
|
+
from .pull import pull_command
|
|
28
|
+
from .push import push_command
|
|
29
|
+
from .utils import CaptureLogger
|
|
30
|
+
|
|
31
|
+
# Create the main Typer app
|
|
32
|
+
app = typer.Typer(
|
|
33
|
+
name="hud",
|
|
34
|
+
help="🚀 HUD CLI for MCP environment analysis and debugging",
|
|
35
|
+
add_completion=False,
|
|
36
|
+
rich_markup_mode="rich",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
console = Console()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Capture IMAGE and any following Docker args as a single variadic argument list.
|
|
43
|
+
@app.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
|
|
44
|
+
def analyze(
|
|
45
|
+
params: list[str] = typer.Argument( # type: ignore[arg-type] # noqa: B008
|
|
46
|
+
None, # Optional positional arguments
|
|
47
|
+
help="Docker image followed by optional Docker run arguments (e.g., 'hud-image:latest -e KEY=value')", # noqa: E501
|
|
48
|
+
),
|
|
49
|
+
config: Path = typer.Option( # noqa: B008
|
|
50
|
+
None,
|
|
51
|
+
"--config",
|
|
52
|
+
"-c",
|
|
53
|
+
help="JSON config file with MCP configuration",
|
|
54
|
+
exists=True,
|
|
55
|
+
file_okay=True,
|
|
56
|
+
dir_okay=False,
|
|
57
|
+
),
|
|
58
|
+
cursor: str | None = typer.Option(
|
|
59
|
+
None,
|
|
60
|
+
"--cursor",
|
|
61
|
+
help="Analyze a server from Cursor config",
|
|
62
|
+
),
|
|
63
|
+
output_format: str = typer.Option(
|
|
64
|
+
"interactive",
|
|
65
|
+
"--format",
|
|
66
|
+
"-f",
|
|
67
|
+
help="Output format: interactive, json, markdown",
|
|
68
|
+
),
|
|
69
|
+
verbose: bool = typer.Option(
|
|
70
|
+
False,
|
|
71
|
+
"--verbose",
|
|
72
|
+
"-v",
|
|
73
|
+
help="Enable verbose output (shows tool schemas)",
|
|
74
|
+
),
|
|
75
|
+
live: bool = typer.Option(
|
|
76
|
+
False,
|
|
77
|
+
"--live",
|
|
78
|
+
help="Run container for live analysis (slower but more accurate)",
|
|
79
|
+
),
|
|
80
|
+
) -> None:
|
|
81
|
+
"""🔍 Analyze MCP environment - discover tools, resources, and capabilities.
|
|
82
|
+
|
|
83
|
+
By default, uses cached metadata for instant results.
|
|
84
|
+
Use --live to run the container for real-time analysis.
|
|
85
|
+
|
|
86
|
+
Examples:
|
|
87
|
+
hud analyze hudpython/test_init # Fast metadata inspection
|
|
88
|
+
hud analyze my-env --live # Full container analysis
|
|
89
|
+
hud analyze --config mcp-config.json # From MCP config
|
|
90
|
+
hud analyze --cursor text-2048-dev # From Cursor config
|
|
91
|
+
"""
|
|
92
|
+
if config:
|
|
93
|
+
# Load config from JSON file (always live for configs)
|
|
94
|
+
asyncio.run(analyze_environment_from_config(config, output_format, verbose))
|
|
95
|
+
elif cursor:
|
|
96
|
+
# Parse cursor config (always live for cursor)
|
|
97
|
+
command, error = parse_cursor_config(cursor)
|
|
98
|
+
if error or command is None:
|
|
99
|
+
console.print(f"[red]❌ {error or 'Failed to parse cursor config'}[/red]")
|
|
100
|
+
raise typer.Exit(1)
|
|
101
|
+
# Convert to MCP config
|
|
102
|
+
mcp_config = {
|
|
103
|
+
"local": {"command": command[0], "args": command[1:] if len(command) > 1 else []}
|
|
104
|
+
}
|
|
105
|
+
asyncio.run(analyze_environment_from_mcp_config(mcp_config, output_format, verbose))
|
|
106
|
+
elif params:
|
|
107
|
+
image, *docker_args = params
|
|
108
|
+
if live or docker_args: # If docker args provided, assume live mode
|
|
109
|
+
# Build Docker command from image and args
|
|
110
|
+
docker_cmd = ["docker", "run", "--rm", "-i", *docker_args, image]
|
|
111
|
+
asyncio.run(analyze_environment(docker_cmd, output_format, verbose))
|
|
112
|
+
else:
|
|
113
|
+
# Fast mode - analyze from metadata
|
|
114
|
+
from .analyze_metadata import analyze_from_metadata
|
|
115
|
+
|
|
116
|
+
asyncio.run(analyze_from_metadata(image, output_format, verbose))
|
|
117
|
+
else:
|
|
118
|
+
console.print("[red]Error: Must specify either a Docker image, --config, or --cursor[/red]")
|
|
119
|
+
console.print("\nExamples:")
|
|
120
|
+
console.print(" hud analyze hudpython/test_init # Fast metadata analysis")
|
|
121
|
+
console.print(" hud analyze my-env --live # Live container analysis")
|
|
122
|
+
console.print(" hud analyze --config mcp-config.json # From config file")
|
|
123
|
+
console.print(" hud analyze --cursor my-server # From Cursor")
|
|
124
|
+
raise typer.Exit(1)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# Same variadic approach for debug.
|
|
128
|
+
@app.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
|
|
129
|
+
def debug(
|
|
130
|
+
params: list[str] = typer.Argument( # type: ignore[arg-type] # noqa: B008
|
|
131
|
+
None,
|
|
132
|
+
help="Docker image followed by optional Docker run arguments (e.g., 'hud-image:latest -e KEY=value')", # noqa: E501
|
|
133
|
+
),
|
|
134
|
+
config: Path = typer.Option( # noqa: B008
|
|
135
|
+
None,
|
|
136
|
+
"--config",
|
|
137
|
+
"-c",
|
|
138
|
+
help="JSON config file with MCP configuration",
|
|
139
|
+
exists=True,
|
|
140
|
+
file_okay=True,
|
|
141
|
+
dir_okay=False,
|
|
142
|
+
),
|
|
143
|
+
cursor: str | None = typer.Option(
|
|
144
|
+
None,
|
|
145
|
+
"--cursor",
|
|
146
|
+
help="Debug a server from Cursor config",
|
|
147
|
+
),
|
|
148
|
+
max_phase: int = typer.Option(
|
|
149
|
+
5,
|
|
150
|
+
"--max-phase",
|
|
151
|
+
"-p",
|
|
152
|
+
min=1,
|
|
153
|
+
max=5,
|
|
154
|
+
help="Maximum debug phase (1-5)",
|
|
155
|
+
),
|
|
156
|
+
) -> None:
|
|
157
|
+
"""🐛 Debug MCP environment - test initialization, tools, and readiness.
|
|
158
|
+
|
|
159
|
+
Examples:
|
|
160
|
+
hud debug hud-text-2048:latest
|
|
161
|
+
hud debug my-mcp-server:v1 -e API_KEY=xxx -p 8080:8080
|
|
162
|
+
hud debug --config mcp-config.json
|
|
163
|
+
hud debug --cursor text-2048-dev
|
|
164
|
+
hud debug hud-browser:dev --max-phase 3
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
# Determine the command to run
|
|
168
|
+
command = None
|
|
169
|
+
|
|
170
|
+
if config:
|
|
171
|
+
# Load config from JSON file
|
|
172
|
+
with open(config) as f:
|
|
173
|
+
mcp_config = json.load(f)
|
|
174
|
+
|
|
175
|
+
# Extract command from first server in config
|
|
176
|
+
server_name = next(iter(mcp_config.keys()))
|
|
177
|
+
server_config = mcp_config[server_name]
|
|
178
|
+
command = [server_config["command"], *server_config.get("args", [])]
|
|
179
|
+
elif cursor:
|
|
180
|
+
# Parse cursor config
|
|
181
|
+
command, error = parse_cursor_config(cursor)
|
|
182
|
+
if error or command is None:
|
|
183
|
+
console.print(f"[red]❌ {error or 'Failed to parse cursor config'}[/red]")
|
|
184
|
+
raise typer.Exit(1)
|
|
185
|
+
elif params:
|
|
186
|
+
image, *docker_args = params
|
|
187
|
+
# Build Docker command
|
|
188
|
+
command = ["docker", "run", "--rm", "-i", *docker_args, image]
|
|
189
|
+
else:
|
|
190
|
+
console.print("[red]Error: Must specify either a Docker image, --config, or --cursor[/red]")
|
|
191
|
+
console.print("\nExamples:")
|
|
192
|
+
console.print(" hud debug hud-text-2048:latest")
|
|
193
|
+
console.print(" hud debug --config mcp-config.json")
|
|
194
|
+
console.print(" hud debug --cursor my-server")
|
|
195
|
+
raise typer.Exit(1)
|
|
196
|
+
|
|
197
|
+
# Create logger and run debug
|
|
198
|
+
logger = CaptureLogger(print_output=True)
|
|
199
|
+
phases_completed = asyncio.run(debug_mcp_stdio(command, logger, max_phase=max_phase))
|
|
200
|
+
|
|
201
|
+
# Show summary using design system
|
|
202
|
+
from hud.utils.design import HUDDesign
|
|
203
|
+
|
|
204
|
+
design = HUDDesign()
|
|
205
|
+
|
|
206
|
+
design.info("") # Empty line
|
|
207
|
+
design.section_title("Debug Summary")
|
|
208
|
+
|
|
209
|
+
if phases_completed == max_phase:
|
|
210
|
+
design.success(f"All {max_phase} phases completed successfully!")
|
|
211
|
+
if max_phase == 5:
|
|
212
|
+
design.info("Your MCP server is fully functional and ready for production use.")
|
|
213
|
+
else:
|
|
214
|
+
design.warning(f"Completed {phases_completed} out of {max_phase} phases")
|
|
215
|
+
design.info("Check the errors above for troubleshooting.")
|
|
216
|
+
|
|
217
|
+
# Exit with appropriate code
|
|
218
|
+
if phases_completed < max_phase:
|
|
219
|
+
raise typer.Exit(1)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@app.command()
|
|
223
|
+
def cursor_list() -> None:
|
|
224
|
+
"""📋 List all MCP servers configured in Cursor."""
|
|
225
|
+
console.print(Panel.fit("📋 [bold cyan]Cursor MCP Servers[/bold cyan]", border_style="cyan"))
|
|
226
|
+
|
|
227
|
+
servers, error = list_cursor_servers()
|
|
228
|
+
|
|
229
|
+
if error:
|
|
230
|
+
console.print(f"[red]❌ {error}[/red]")
|
|
231
|
+
raise typer.Exit(1)
|
|
232
|
+
|
|
233
|
+
if not servers:
|
|
234
|
+
console.print("[yellow]No servers found in Cursor config[/yellow]")
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
# Display servers in a table
|
|
238
|
+
table = Table(title="Available Servers")
|
|
239
|
+
table.add_column("Server Name", style="cyan")
|
|
240
|
+
table.add_column("Command Preview", style="dim")
|
|
241
|
+
|
|
242
|
+
config_path = get_cursor_config_path()
|
|
243
|
+
if config_path.exists():
|
|
244
|
+
with open(config_path) as f:
|
|
245
|
+
config = json.load(f)
|
|
246
|
+
mcp_servers = config.get("mcpServers", {})
|
|
247
|
+
|
|
248
|
+
for server_name in servers:
|
|
249
|
+
server_config = mcp_servers.get(server_name, {})
|
|
250
|
+
command = server_config.get("command", "")
|
|
251
|
+
args = server_config.get("args", [])
|
|
252
|
+
|
|
253
|
+
# Create command preview
|
|
254
|
+
if args:
|
|
255
|
+
preview = f"{command} {' '.join(args[:2])}"
|
|
256
|
+
if len(args) > 2:
|
|
257
|
+
preview += " ..."
|
|
258
|
+
else:
|
|
259
|
+
preview = command
|
|
260
|
+
|
|
261
|
+
table.add_row(server_name, preview)
|
|
262
|
+
|
|
263
|
+
console.print(table)
|
|
264
|
+
console.print(f"\n[dim]Config location: {config_path}[/dim]")
|
|
265
|
+
console.print(
|
|
266
|
+
"\n[green]Tip:[/green] Use [cyan]hud debug --cursor <server-name>[/cyan] to debug a server"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@app.command()
|
|
271
|
+
def version() -> None:
|
|
272
|
+
"""Show HUD CLI version."""
|
|
273
|
+
try:
|
|
274
|
+
from hud import __version__
|
|
275
|
+
|
|
276
|
+
console.print(f"HUD CLI version: [cyan]{__version__}[/cyan]")
|
|
277
|
+
except ImportError:
|
|
278
|
+
console.print("HUD CLI version: [cyan]unknown[/cyan]")
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@app.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
|
|
282
|
+
def dev(
|
|
283
|
+
params: list[str] = typer.Argument( # type: ignore[arg-type] # noqa: B008
|
|
284
|
+
None,
|
|
285
|
+
help="Environment directory followed by optional Docker arguments (e.g., '. -e KEY=value')",
|
|
286
|
+
),
|
|
287
|
+
image: str | None = typer.Option(
|
|
288
|
+
None, "--image", "-i", help="Docker image name (overrides auto-detection)"
|
|
289
|
+
),
|
|
290
|
+
build: bool = typer.Option(False, "--build", "-b", help="Build image before starting"),
|
|
291
|
+
no_cache: bool = typer.Option(False, "--no-cache", help="Force rebuild without cache"),
|
|
292
|
+
transport: str = typer.Option(
|
|
293
|
+
"http", "--transport", "-t", help="Transport protocol: http (default) or stdio"
|
|
294
|
+
),
|
|
295
|
+
port: int = typer.Option(8765, "--port", "-p", help="HTTP server port (ignored for stdio)"),
|
|
296
|
+
no_reload: bool = typer.Option(False, "--no-reload", help="Disable hot-reload"),
|
|
297
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show server logs"),
|
|
298
|
+
inspector: bool = typer.Option(
|
|
299
|
+
False, "--inspector", help="Launch MCP Inspector (HTTP mode only)"
|
|
300
|
+
),
|
|
301
|
+
no_logs: bool = typer.Option(False, "--no-logs", help="Disable streaming Docker logs"),
|
|
302
|
+
interactive: bool = typer.Option(
|
|
303
|
+
False, "--interactive", help="Launch interactive testing mode (HTTP mode only)"
|
|
304
|
+
),
|
|
305
|
+
) -> None:
|
|
306
|
+
"""🔥 Development mode with hot-reload.
|
|
307
|
+
|
|
308
|
+
Runs your MCP environment in Docker with automatic restart on file changes.
|
|
309
|
+
|
|
310
|
+
The container's last command (typically the MCP server) will be wrapped
|
|
311
|
+
with watchfiles for hot-reload functionality.
|
|
312
|
+
|
|
313
|
+
Examples:
|
|
314
|
+
hud dev # Auto-detect in current directory
|
|
315
|
+
hud dev environments/browser # Specific directory
|
|
316
|
+
hud dev . --build # Build image first
|
|
317
|
+
hud dev . --image custom:tag # Use specific image
|
|
318
|
+
hud dev . --no-cache # Force clean rebuild
|
|
319
|
+
hud dev . --verbose # Show detailed logs
|
|
320
|
+
hud dev . --transport stdio # Use stdio proxy for multiple connections
|
|
321
|
+
hud dev . --inspector # Launch MCP Inspector (HTTP mode only)
|
|
322
|
+
hud dev . --interactive # Launch interactive testing mode (HTTP mode only)
|
|
323
|
+
hud dev . --no-logs # Disable Docker log streaming
|
|
324
|
+
|
|
325
|
+
# With Docker arguments (after all options):
|
|
326
|
+
hud dev . -e BROWSER_PROVIDER=anchorbrowser -e ANCHOR_API_KEY=xxx
|
|
327
|
+
hud dev . -e API_KEY=secret -v /tmp/data:/data --network host
|
|
328
|
+
hud dev . --build -e DEBUG=true --memory 2g
|
|
329
|
+
"""
|
|
330
|
+
# Parse directory and Docker arguments
|
|
331
|
+
if params:
|
|
332
|
+
directory = params[0]
|
|
333
|
+
docker_args = params[1:] if len(params) > 1 else []
|
|
334
|
+
else:
|
|
335
|
+
directory = "."
|
|
336
|
+
docker_args = []
|
|
337
|
+
|
|
338
|
+
run_mcp_dev_server(
|
|
339
|
+
directory,
|
|
340
|
+
image,
|
|
341
|
+
build,
|
|
342
|
+
no_cache,
|
|
343
|
+
transport,
|
|
344
|
+
port,
|
|
345
|
+
no_reload,
|
|
346
|
+
verbose,
|
|
347
|
+
inspector,
|
|
348
|
+
no_logs,
|
|
349
|
+
interactive,
|
|
350
|
+
docker_args,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@app.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
|
|
355
|
+
def run(
|
|
356
|
+
params: list[str] = typer.Argument( # type: ignore[arg-type] # noqa: B008
|
|
357
|
+
None,
|
|
358
|
+
help="Docker image followed by optional arguments (e.g., 'hud-image:latest -e KEY=value')",
|
|
359
|
+
),
|
|
360
|
+
local: bool = typer.Option(
|
|
361
|
+
False,
|
|
362
|
+
"--local",
|
|
363
|
+
help="Run locally with Docker (default: remote via mcp.hud.so)",
|
|
364
|
+
),
|
|
365
|
+
remote: bool = typer.Option(
|
|
366
|
+
False,
|
|
367
|
+
"--remote",
|
|
368
|
+
help="Run remotely via mcp.hud.so (default)",
|
|
369
|
+
),
|
|
370
|
+
transport: str = typer.Option(
|
|
371
|
+
"stdio",
|
|
372
|
+
"--transport",
|
|
373
|
+
"-t",
|
|
374
|
+
help="Transport protocol: stdio (default) or http",
|
|
375
|
+
),
|
|
376
|
+
port: int = typer.Option(
|
|
377
|
+
8765,
|
|
378
|
+
"--port",
|
|
379
|
+
"-p",
|
|
380
|
+
help="Port for HTTP transport (ignored for stdio)",
|
|
381
|
+
),
|
|
382
|
+
url: str = typer.Option(
|
|
383
|
+
None,
|
|
384
|
+
"--url",
|
|
385
|
+
help="Remote MCP server URL (default: HUD_MCP_URL or mcp.hud.so)",
|
|
386
|
+
),
|
|
387
|
+
api_key: str | None = typer.Option(
|
|
388
|
+
None,
|
|
389
|
+
"--api-key",
|
|
390
|
+
help="API key for remote server (default: HUD_API_KEY env var)",
|
|
391
|
+
),
|
|
392
|
+
run_id: str | None = typer.Option(
|
|
393
|
+
None,
|
|
394
|
+
"--run-id",
|
|
395
|
+
help="Run ID for tracking (remote only)",
|
|
396
|
+
),
|
|
397
|
+
verbose: bool = typer.Option(
|
|
398
|
+
False,
|
|
399
|
+
"--verbose",
|
|
400
|
+
"-v",
|
|
401
|
+
help="Show detailed output",
|
|
402
|
+
),
|
|
403
|
+
) -> None:
|
|
404
|
+
"""🚀 Run MCP server locally or remotely.
|
|
405
|
+
|
|
406
|
+
By default, runs remotely via mcp.hud.so. Use --local for Docker.
|
|
407
|
+
|
|
408
|
+
Remote Examples:
|
|
409
|
+
hud run hud-text-2048:latest
|
|
410
|
+
hud run my-server:v1 -e API_KEY=xxx -h Run-Id:abc123
|
|
411
|
+
hud run my-server:v1 --transport http --port 9000
|
|
412
|
+
|
|
413
|
+
Local Examples:
|
|
414
|
+
hud run --local hud-text-2048:latest
|
|
415
|
+
hud run --local my-server:v1 -e API_KEY=xxx
|
|
416
|
+
hud run --local my-server:v1 --transport http
|
|
417
|
+
"""
|
|
418
|
+
if not params:
|
|
419
|
+
typer.echo("❌ Docker image is required")
|
|
420
|
+
raise typer.Exit(1)
|
|
421
|
+
|
|
422
|
+
# Parse image and args
|
|
423
|
+
image = params[0]
|
|
424
|
+
docker_args = params[1:] if len(params) > 1 else []
|
|
425
|
+
|
|
426
|
+
# Handle conflicting flags
|
|
427
|
+
if local and remote:
|
|
428
|
+
typer.echo("❌ Cannot use both --local and --remote")
|
|
429
|
+
raise typer.Exit(1)
|
|
430
|
+
|
|
431
|
+
# Default to remote if not explicitly local
|
|
432
|
+
is_local = local and not remote
|
|
433
|
+
|
|
434
|
+
if is_local:
|
|
435
|
+
# Local Docker execution
|
|
436
|
+
from .runner import run_mcp_server
|
|
437
|
+
|
|
438
|
+
run_mcp_server(image, docker_args, transport, port, verbose)
|
|
439
|
+
else:
|
|
440
|
+
# Remote execution via proxy
|
|
441
|
+
from .remote_runner import run_remote_server
|
|
442
|
+
|
|
443
|
+
# Get URL from options or environment
|
|
444
|
+
if not url:
|
|
445
|
+
url = os.getenv("HUD_MCP_URL", "https://mcp.hud.so/v3/mcp")
|
|
446
|
+
|
|
447
|
+
run_remote_server(image, docker_args, transport, port, url, api_key, run_id, verbose)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
@app.command()
|
|
451
|
+
def clone(
|
|
452
|
+
url: str = typer.Argument(
|
|
453
|
+
...,
|
|
454
|
+
help="Git repository URL to clone",
|
|
455
|
+
),
|
|
456
|
+
) -> None:
|
|
457
|
+
"""🚀 Clone a git repository quietly with a pretty output.
|
|
458
|
+
|
|
459
|
+
This command wraps 'git clone' with the --quiet flag and displays
|
|
460
|
+
a rich formatted success message. If the repository contains a clone
|
|
461
|
+
message in pyproject.toml, it will be displayed as a tutorial.
|
|
462
|
+
|
|
463
|
+
Configure clone messages in your repository's pyproject.toml:
|
|
464
|
+
|
|
465
|
+
[tool.hud.clone]
|
|
466
|
+
title = "🚀 My Project"
|
|
467
|
+
message = "Thanks for cloning! Run 'pip install -e .' to get started."
|
|
468
|
+
|
|
469
|
+
# Or use markdown format:
|
|
470
|
+
# markdown = "## Welcome!\\n\\nHere's how to get started..."
|
|
471
|
+
# style = "cyan"
|
|
472
|
+
|
|
473
|
+
Examples:
|
|
474
|
+
hud clone https://github.com/user/repo.git
|
|
475
|
+
"""
|
|
476
|
+
# Run the clone
|
|
477
|
+
success, result = clone_repository(url)
|
|
478
|
+
|
|
479
|
+
if success:
|
|
480
|
+
# Look for clone message configuration
|
|
481
|
+
clone_config = get_clone_message(result)
|
|
482
|
+
print_tutorial(clone_config)
|
|
483
|
+
else:
|
|
484
|
+
print_error(result)
|
|
485
|
+
raise typer.Exit(1)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
@app.command()
|
|
489
|
+
def build(
|
|
490
|
+
directory: str = typer.Argument(".", help="Environment directory to build"),
|
|
491
|
+
tag: str | None = typer.Option(
|
|
492
|
+
None, "--tag", "-t", help="Docker image tag (default: from pyproject.toml)"
|
|
493
|
+
),
|
|
494
|
+
no_cache: bool = typer.Option(False, "--no-cache", help="Build without Docker cache"),
|
|
495
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
|
|
496
|
+
) -> None:
|
|
497
|
+
"""🏗️ Build a HUD environment and generate lock file.
|
|
498
|
+
|
|
499
|
+
This command:
|
|
500
|
+
- Builds a Docker image from your environment
|
|
501
|
+
- Analyzes the MCP server to extract metadata
|
|
502
|
+
- Generates a hud.lock.yaml file for reproducibility
|
|
503
|
+
|
|
504
|
+
Examples:
|
|
505
|
+
hud build # Build current directory
|
|
506
|
+
hud build environments/text_2048
|
|
507
|
+
hud build . --tag my-env:v1.0
|
|
508
|
+
hud build . --no-cache # Force rebuild
|
|
509
|
+
"""
|
|
510
|
+
build_command(directory, tag, no_cache, verbose)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
@app.command()
|
|
514
|
+
def push(
|
|
515
|
+
directory: str = typer.Argument(".", help="Environment directory containing hud.lock.yaml"),
|
|
516
|
+
image: str | None = typer.Option(None, "--image", "-i", help="Override registry image name"),
|
|
517
|
+
tag: str | None = typer.Option(
|
|
518
|
+
None, "--tag", "-t", help="Override tag (e.g., 'v1.0', 'latest')"
|
|
519
|
+
),
|
|
520
|
+
sign: bool = typer.Option(
|
|
521
|
+
False, "--sign", help="Sign the image with cosign (not yet implemented)"
|
|
522
|
+
),
|
|
523
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompts"),
|
|
524
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
|
|
525
|
+
) -> None:
|
|
526
|
+
"""📤 Push HUD environment to registry.
|
|
527
|
+
|
|
528
|
+
Reads hud.lock.yaml from the directory and pushes to registry.
|
|
529
|
+
Auto-detects your Docker username if --image not specified.
|
|
530
|
+
|
|
531
|
+
Examples:
|
|
532
|
+
hud push # Push with auto-detected name
|
|
533
|
+
hud push --tag v1.0 # Push with specific tag
|
|
534
|
+
hud push . --image myuser/myenv:v1.0
|
|
535
|
+
hud push --yes # Skip confirmation
|
|
536
|
+
"""
|
|
537
|
+
push_command(directory, image, tag, sign, yes, verbose)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
@app.command()
|
|
541
|
+
def pull(
|
|
542
|
+
target: str = typer.Argument(..., help="Image reference or lock file to pull"),
|
|
543
|
+
lock_file: str | None = typer.Option(
|
|
544
|
+
None, "--lock", "-l", help="Path to lock file (if target is image ref)"
|
|
545
|
+
),
|
|
546
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
547
|
+
verify_only: bool = typer.Option(
|
|
548
|
+
False, "--verify-only", help="Only verify metadata without pulling"
|
|
549
|
+
),
|
|
550
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
|
|
551
|
+
) -> None:
|
|
552
|
+
"""📥 Pull HUD environment from registry with metadata preview.
|
|
553
|
+
|
|
554
|
+
Shows environment details before downloading.
|
|
555
|
+
|
|
556
|
+
Examples:
|
|
557
|
+
hud pull hud.lock.yaml # Pull from lock file
|
|
558
|
+
hud pull myuser/myenv:latest # Pull by image reference
|
|
559
|
+
hud pull myuser/myenv --verify-only # Check metadata only
|
|
560
|
+
"""
|
|
561
|
+
pull_command(target, lock_file, yes, verify_only, verbose)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
@app.command()
|
|
565
|
+
def init(
|
|
566
|
+
name: str = typer.Argument(None, help="Environment name (default: current directory name)"),
|
|
567
|
+
directory: str = typer.Option(".", "--dir", "-d", help="Target directory"),
|
|
568
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files"),
|
|
569
|
+
) -> None:
|
|
570
|
+
"""🚀 Initialize a new HUD environment with minimal boilerplate.
|
|
571
|
+
|
|
572
|
+
Creates a working MCP environment with:
|
|
573
|
+
- Dockerfile for containerization
|
|
574
|
+
- pyproject.toml for dependencies
|
|
575
|
+
- Minimal MCP server with context
|
|
576
|
+
- Required setup/evaluate tools
|
|
577
|
+
|
|
578
|
+
Examples:
|
|
579
|
+
hud init # Use current directory name
|
|
580
|
+
hud init my-env # Create in ./my-env/
|
|
581
|
+
hud init my-env --dir /tmp # Create in /tmp/my-env/
|
|
582
|
+
"""
|
|
583
|
+
create_environment(name, directory, force)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
@app.command()
|
|
587
|
+
def quickstart() -> None:
|
|
588
|
+
"""
|
|
589
|
+
Quickstart with evaluating an agent!
|
|
590
|
+
"""
|
|
591
|
+
# Just call the clone command with the quickstart URL
|
|
592
|
+
clone("https://github.com/hud-evals/quickstart.git")
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def main() -> None:
|
|
596
|
+
"""Main entry point for the CLI."""
|
|
597
|
+
# Show header for main help
|
|
598
|
+
if len(sys.argv) == 1 or (len(sys.argv) == 2 and sys.argv[1] in ["--help", "-h"]):
|
|
599
|
+
console.print(
|
|
600
|
+
Panel.fit(
|
|
601
|
+
"[bold cyan]🚀 HUD CLI[/bold cyan]\nMCP Environment Analysis & Debugging",
|
|
602
|
+
border_style="cyan",
|
|
603
|
+
)
|
|
604
|
+
)
|
|
605
|
+
console.print("\n[yellow]Quick Start:[/yellow]")
|
|
606
|
+
console.print(" 1. Create a new environment: [cyan]hud init my-env && cd my-env[/cyan]")
|
|
607
|
+
console.print(" 2. Develop with hot-reload: [cyan]hud dev --interactive[/cyan]")
|
|
608
|
+
console.print(" 3. Build for production: [cyan]hud build[/cyan]")
|
|
609
|
+
console.print(" 4. Share your environment: [cyan]hud push[/cyan]")
|
|
610
|
+
console.print(" 5. Get shared environments: [cyan]hud pull <org/name:tag>[/cyan]")
|
|
611
|
+
console.print(" 6. Run and test: [cyan]hud run <image>[/cyan]\n")
|
|
612
|
+
|
|
613
|
+
app()
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
if __name__ == "__main__":
|
|
617
|
+
main()
|