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/analyze.py
CHANGED
|
@@ -1,371 +1,371 @@
|
|
|
1
|
-
"""Analyze command implementation for MCP environments."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
from typing import TYPE_CHECKING, Any
|
|
7
|
-
|
|
8
|
-
from rich.console import Console
|
|
9
|
-
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
10
|
-
from rich.syntax import Syntax
|
|
11
|
-
from rich.table import Table
|
|
12
|
-
from rich.tree import Tree
|
|
13
|
-
|
|
14
|
-
from hud.clients import MCPClient
|
|
15
|
-
from hud.utils.design import HUDDesign
|
|
16
|
-
|
|
17
|
-
if TYPE_CHECKING:
|
|
18
|
-
from pathlib import Path
|
|
19
|
-
|
|
20
|
-
console = Console()
|
|
21
|
-
design = HUDDesign()
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def parse_docker_command(docker_cmd: list[str]) -> dict:
|
|
25
|
-
"""Convert Docker command to MCP config."""
|
|
26
|
-
return {
|
|
27
|
-
"local": {"command": docker_cmd[0], "args": docker_cmd[1:] if len(docker_cmd) > 1 else []}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
async def analyze_environment(docker_cmd: list[str], output_format: str, verbose: bool) -> None:
|
|
32
|
-
"""Analyze MCP environment and display results."""
|
|
33
|
-
design.header("MCP Environment Analysis", icon="🔍")
|
|
34
|
-
|
|
35
|
-
# Convert Docker command to MCP config
|
|
36
|
-
mcp_config = parse_docker_command(docker_cmd)
|
|
37
|
-
|
|
38
|
-
# Display command being analyzed
|
|
39
|
-
design.dim_info("Command:", " ".join(docker_cmd))
|
|
40
|
-
design.info("") # Empty line
|
|
41
|
-
|
|
42
|
-
# Create client
|
|
43
|
-
with Progress(
|
|
44
|
-
SpinnerColumn(),
|
|
45
|
-
TextColumn("[progress.description]{task.description}"),
|
|
46
|
-
console=console,
|
|
47
|
-
) as progress:
|
|
48
|
-
task = progress.add_task("Initializing MCP client...", total=None)
|
|
49
|
-
|
|
50
|
-
client = MCPClient(mcp_config=mcp_config, verbose=verbose, auto_trace=False)
|
|
51
|
-
|
|
52
|
-
try:
|
|
53
|
-
await client.initialize()
|
|
54
|
-
progress.update(task, description="[green]✓ Client initialized[/green]")
|
|
55
|
-
|
|
56
|
-
# Analyze environment
|
|
57
|
-
progress.update(task, description="Analyzing environment...")
|
|
58
|
-
analysis = await client.analyze_environment()
|
|
59
|
-
progress.update(task, description="[green]✓ Analysis complete[/green]")
|
|
60
|
-
|
|
61
|
-
except Exception as e:
|
|
62
|
-
progress.update(task, description=f"[red]✗ Failed: {e}[/red]")
|
|
63
|
-
|
|
64
|
-
# On Windows, Docker stderr might not propagate properly
|
|
65
|
-
import platform
|
|
66
|
-
|
|
67
|
-
if platform.system() == "Windows" and "docker" in docker_cmd[0].lower():
|
|
68
|
-
console.print("\n[yellow]💡 Tip: Docker logs may not show on Windows.[/yellow]")
|
|
69
|
-
console.print(f"[yellow] Try: hud debug {' '.join(docker_cmd[3:])}[/yellow]")
|
|
70
|
-
console.print("[yellow] This will show more detailed error information.[/yellow]")
|
|
71
|
-
elif verbose:
|
|
72
|
-
console.print("\n[dim]For more details, try running with 'hud debug'[/dim]")
|
|
73
|
-
|
|
74
|
-
return
|
|
75
|
-
finally:
|
|
76
|
-
await client.shutdown()
|
|
77
|
-
|
|
78
|
-
# Display results based on format
|
|
79
|
-
if output_format == "json":
|
|
80
|
-
console.print_json(json.dumps(analysis, indent=2))
|
|
81
|
-
elif output_format == "markdown":
|
|
82
|
-
display_markdown(analysis)
|
|
83
|
-
else: # interactive
|
|
84
|
-
display_interactive(analysis)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def display_interactive(analysis: dict) -> None:
|
|
88
|
-
"""Display analysis results in interactive format."""
|
|
89
|
-
# Server metadata
|
|
90
|
-
design.section_title("📊 Environment Overview")
|
|
91
|
-
meta_table = Table(show_header=False, box=None)
|
|
92
|
-
meta_table.add_column("Property", style="dim")
|
|
93
|
-
meta_table.add_column("Value")
|
|
94
|
-
|
|
95
|
-
# Check if this is a live analysis (has metadata) or metadata-only analysis
|
|
96
|
-
if "metadata" in analysis:
|
|
97
|
-
# Live analysis format
|
|
98
|
-
for server in analysis["metadata"]["servers"]:
|
|
99
|
-
meta_table.add_row("Server", f"[green]{server}[/green]")
|
|
100
|
-
meta_table.add_row(
|
|
101
|
-
"Initialized",
|
|
102
|
-
"[green]✓[/green]" if analysis["metadata"]["initialized"] else "[red]✗[/red]",
|
|
103
|
-
)
|
|
104
|
-
else:
|
|
105
|
-
# Metadata-only format
|
|
106
|
-
if "image" in analysis:
|
|
107
|
-
# Show simple name in table
|
|
108
|
-
image = analysis["image"]
|
|
109
|
-
display_ref = image.split("@")[0] if ":" in image and "@" in image else image
|
|
110
|
-
meta_table.add_row("Image", f"[green]{display_ref}[/green]")
|
|
111
|
-
|
|
112
|
-
if "status" in analysis:
|
|
113
|
-
meta_table.add_row("Source", analysis.get("source", analysis["status"]).title())
|
|
114
|
-
|
|
115
|
-
if "build_info" in analysis:
|
|
116
|
-
meta_table.add_row("Built", analysis["build_info"].get("generatedAt", "Unknown"))
|
|
117
|
-
meta_table.add_row("HUD Version", analysis["build_info"].get("hudVersion", "Unknown"))
|
|
118
|
-
|
|
119
|
-
if "push_info" in analysis:
|
|
120
|
-
meta_table.add_row("Pushed", analysis["push_info"].get("pushedAt", "Unknown"))
|
|
121
|
-
|
|
122
|
-
if "init_time" in analysis:
|
|
123
|
-
meta_table.add_row("Init Time", f"{analysis['init_time']} ms")
|
|
124
|
-
|
|
125
|
-
if "tool_count" in analysis:
|
|
126
|
-
meta_table.add_row("Tools", str(analysis["tool_count"]))
|
|
127
|
-
|
|
128
|
-
console.print(meta_table)
|
|
129
|
-
|
|
130
|
-
# Tools
|
|
131
|
-
design.section_title("🔧 Available Tools")
|
|
132
|
-
tools_tree = Tree("Tools")
|
|
133
|
-
|
|
134
|
-
# Check if we have hub_tools info (live analysis) or not (metadata-only)
|
|
135
|
-
if "hub_tools" in analysis:
|
|
136
|
-
# Live analysis format - separate regular and hub tools
|
|
137
|
-
# Regular tools
|
|
138
|
-
regular_tools = tools_tree.add("Regular Tools")
|
|
139
|
-
for tool in analysis["tools"]:
|
|
140
|
-
if tool["name"] not in analysis["hub_tools"]:
|
|
141
|
-
tool_node = regular_tools.add(f"[default]{tool['name']}[/default]")
|
|
142
|
-
if tool["description"]:
|
|
143
|
-
tool_node.add(f"[dim]{tool['description']}[/dim]")
|
|
144
|
-
|
|
145
|
-
# Show input schema if verbose
|
|
146
|
-
if analysis.get("verbose") and tool.get("input_schema"):
|
|
147
|
-
schema_str = json.dumps(tool["input_schema"], indent=2)
|
|
148
|
-
syntax = Syntax(schema_str, "json", theme="monokai", line_numbers=False)
|
|
149
|
-
tool_node.add(syntax)
|
|
150
|
-
|
|
151
|
-
# Hub tools
|
|
152
|
-
if analysis["hub_tools"]:
|
|
153
|
-
hub_tools = tools_tree.add("Hub Tools")
|
|
154
|
-
for hub_name, functions in analysis["hub_tools"].items():
|
|
155
|
-
hub_node = hub_tools.add(f"[yellow]{hub_name}[/yellow]")
|
|
156
|
-
for func in functions:
|
|
157
|
-
hub_node.add(f"[default]{func}[/default]")
|
|
158
|
-
else:
|
|
159
|
-
# Metadata-only format - just list all tools
|
|
160
|
-
for tool in analysis["tools"]:
|
|
161
|
-
tool_node = tools_tree.add(f"[default]{tool['name']}[/default]")
|
|
162
|
-
if tool.get("description"):
|
|
163
|
-
tool_node.add(f"[dim]{tool['description']}[/dim]")
|
|
164
|
-
|
|
165
|
-
# Show input schema if verbose
|
|
166
|
-
if tool.get("inputSchema"):
|
|
167
|
-
schema_str = json.dumps(tool["inputSchema"], indent=2)
|
|
168
|
-
syntax = Syntax(schema_str, "json", theme="monokai", line_numbers=False)
|
|
169
|
-
tool_node.add(syntax)
|
|
170
|
-
|
|
171
|
-
console.print(tools_tree)
|
|
172
|
-
|
|
173
|
-
# Resources
|
|
174
|
-
if analysis["resources"]:
|
|
175
|
-
design.section_title("📚 Available Resources")
|
|
176
|
-
resources_table = Table()
|
|
177
|
-
resources_table.add_column("URI", style="default")
|
|
178
|
-
resources_table.add_column("Name", style="white")
|
|
179
|
-
resources_table.add_column("Type", style="dim")
|
|
180
|
-
|
|
181
|
-
for resource in analysis["resources"][:10]:
|
|
182
|
-
resources_table.add_row(
|
|
183
|
-
resource["uri"], resource.get("name", ""), resource.get("mime_type", "")
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
console.print(resources_table)
|
|
187
|
-
|
|
188
|
-
if len(analysis["resources"]) > 10:
|
|
189
|
-
console.print(f"[dim]... and {len(analysis['resources']) - 10} more resources[/dim]")
|
|
190
|
-
|
|
191
|
-
# Telemetry (only for live analysis)
|
|
192
|
-
if analysis.get("telemetry"):
|
|
193
|
-
design.section_title("📡 Telemetry Data")
|
|
194
|
-
telemetry_table = Table(show_header=False, box=None)
|
|
195
|
-
telemetry_table.add_column("Key", style="dim")
|
|
196
|
-
telemetry_table.add_column("Value")
|
|
197
|
-
|
|
198
|
-
if "live_url" in analysis["telemetry"]:
|
|
199
|
-
telemetry_table.add_row("Live URL", f"[link]{analysis['telemetry']['live_url']}[/link]")
|
|
200
|
-
if "status" in analysis["telemetry"]:
|
|
201
|
-
telemetry_table.add_row("Status", f"[green]{analysis['telemetry']['status']}[/green]")
|
|
202
|
-
if "services" in analysis["telemetry"]:
|
|
203
|
-
services = analysis["telemetry"]["services"]
|
|
204
|
-
running = sum(1 for s in services.values() if s == "running")
|
|
205
|
-
telemetry_table.add_row("Services", f"{running}/{len(services)} running")
|
|
206
|
-
|
|
207
|
-
console.print(telemetry_table)
|
|
208
|
-
|
|
209
|
-
# Environment variables (for metadata-only analysis)
|
|
210
|
-
if analysis.get("env_vars"):
|
|
211
|
-
design.section_title("🔑 Environment Variables")
|
|
212
|
-
env_table = Table(show_header=False, box=None)
|
|
213
|
-
env_table.add_column("Type", style="dim")
|
|
214
|
-
env_table.add_column("Variables")
|
|
215
|
-
|
|
216
|
-
if analysis["env_vars"].get("required"):
|
|
217
|
-
env_table.add_row("Required", ", ".join(analysis["env_vars"]["required"]))
|
|
218
|
-
if analysis["env_vars"].get("optional"):
|
|
219
|
-
env_table.add_row("Optional", ", ".join(analysis["env_vars"]["optional"]))
|
|
220
|
-
|
|
221
|
-
console.print(env_table)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
def display_markdown(analysis: dict) -> None:
|
|
225
|
-
"""Display analysis results in markdown format."""
|
|
226
|
-
md = []
|
|
227
|
-
md.append("# MCP Environment Analysis\n")
|
|
228
|
-
|
|
229
|
-
# Metadata
|
|
230
|
-
md.append("## Environment Overview")
|
|
231
|
-
|
|
232
|
-
# Check if this is live analysis or metadata-only
|
|
233
|
-
if "metadata" in analysis:
|
|
234
|
-
md.append(f"- **Servers**: {', '.join(analysis['metadata']['servers'])}")
|
|
235
|
-
md.append(f"- **Initialized**: {'✓' if analysis['metadata']['initialized'] else '✗'}")
|
|
236
|
-
else:
|
|
237
|
-
# Metadata-only format
|
|
238
|
-
if "image" in analysis:
|
|
239
|
-
md.append(f"- **Image**: {analysis['image']}")
|
|
240
|
-
if "source" in analysis:
|
|
241
|
-
md.append(f"- **Source**: {analysis['source']}")
|
|
242
|
-
if "build_info" in analysis:
|
|
243
|
-
md.append(f"- **Built**: {analysis['build_info'].get('generatedAt', 'Unknown')}")
|
|
244
|
-
if "tool_count" in analysis:
|
|
245
|
-
md.append(f"- **Tools**: {analysis['tool_count']}")
|
|
246
|
-
|
|
247
|
-
md.append("")
|
|
248
|
-
|
|
249
|
-
# Tools
|
|
250
|
-
md.append("## Available Tools\n")
|
|
251
|
-
|
|
252
|
-
# Check if we have hub_tools info (live analysis) or not (metadata-only)
|
|
253
|
-
if "hub_tools" in analysis:
|
|
254
|
-
# Regular tools
|
|
255
|
-
md.append("### Regular Tools")
|
|
256
|
-
for tool in analysis["tools"]:
|
|
257
|
-
if tool["name"] not in analysis["hub_tools"]:
|
|
258
|
-
md.extend([f"- **{tool['name']}**: {tool.get('description', 'No description')}"])
|
|
259
|
-
md.append("")
|
|
260
|
-
|
|
261
|
-
# Hub tools
|
|
262
|
-
if analysis["hub_tools"]:
|
|
263
|
-
md.append("### Hub Tools")
|
|
264
|
-
for hub_name, functions in analysis["hub_tools"].items():
|
|
265
|
-
md.extend([f"- **{hub_name}**"])
|
|
266
|
-
for func in functions:
|
|
267
|
-
md.extend([f" - {func}"])
|
|
268
|
-
md.append("")
|
|
269
|
-
else:
|
|
270
|
-
# Metadata-only format - just list all tools
|
|
271
|
-
for tool in analysis["tools"]:
|
|
272
|
-
md.extend([f"- **{tool['name']}**: {tool.get('description', 'No description')}"])
|
|
273
|
-
md.append("")
|
|
274
|
-
|
|
275
|
-
# Resources
|
|
276
|
-
if analysis["resources"]:
|
|
277
|
-
md.append("## Available Resources\n")
|
|
278
|
-
md.append("| URI | Name | Type |")
|
|
279
|
-
md.append("|-----|------|------|")
|
|
280
|
-
for resource in analysis["resources"]:
|
|
281
|
-
uri = resource["uri"]
|
|
282
|
-
name = resource.get("name", "")
|
|
283
|
-
mime_type = resource.get("mime_type", "")
|
|
284
|
-
md.extend([f"| {uri} | {name} | {mime_type} |"])
|
|
285
|
-
md.append("")
|
|
286
|
-
|
|
287
|
-
# Telemetry (only for live analysis)
|
|
288
|
-
if analysis.get("telemetry"):
|
|
289
|
-
md.append("## Telemetry")
|
|
290
|
-
if "live_url" in analysis["telemetry"]:
|
|
291
|
-
md.extend([f"- **Live URL**: {analysis['telemetry']['live_url']}"])
|
|
292
|
-
if "status" in analysis["telemetry"]:
|
|
293
|
-
md.extend([f"- **Status**: {analysis['telemetry']['status']}"])
|
|
294
|
-
if "services" in analysis["telemetry"]:
|
|
295
|
-
md.extend([f"- **Services**: {analysis['telemetry']['services']}"])
|
|
296
|
-
md.append("")
|
|
297
|
-
|
|
298
|
-
# Environment variables (for metadata-only analysis)
|
|
299
|
-
if analysis.get("env_vars"):
|
|
300
|
-
md.append("## Environment Variables")
|
|
301
|
-
if analysis["env_vars"].get("required"):
|
|
302
|
-
md.extend([f"- **Required**: {', '.join(analysis['env_vars']['required'])}"])
|
|
303
|
-
if analysis["env_vars"].get("optional"):
|
|
304
|
-
md.extend([f"- **Optional**: {', '.join(analysis['env_vars']['optional'])}"])
|
|
305
|
-
md.append("")
|
|
306
|
-
|
|
307
|
-
console.print("\n".join(md))
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
async def analyze_environment_from_config(
|
|
311
|
-
config_path: Path, output_format: str, verbose: bool
|
|
312
|
-
) -> None:
|
|
313
|
-
"""Analyze MCP environment from a JSON config file."""
|
|
314
|
-
design.header("MCP Environment Analysis", icon="🔍")
|
|
315
|
-
|
|
316
|
-
# Load config from file
|
|
317
|
-
try:
|
|
318
|
-
with open(config_path) as f: # noqa: ASYNC230
|
|
319
|
-
mcp_config = json.load(f)
|
|
320
|
-
console.print(f"[dim]Config: {config_path}[/dim]\n")
|
|
321
|
-
except Exception as e:
|
|
322
|
-
console.print(f"[red]Error loading config: {e}[/red]")
|
|
323
|
-
return
|
|
324
|
-
|
|
325
|
-
await _analyze_with_config(mcp_config, output_format, verbose)
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
async def analyze_environment_from_mcp_config(
|
|
329
|
-
mcp_config: dict[str, Any], output_format: str, verbose: bool
|
|
330
|
-
) -> None:
|
|
331
|
-
"""Analyze MCP environment from MCP config dict."""
|
|
332
|
-
design.header("MCP Environment Analysis", icon="🔍")
|
|
333
|
-
await _analyze_with_config(mcp_config, output_format, verbose)
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
async def _analyze_with_config(
|
|
337
|
-
mcp_config: dict[str, Any], output_format: str, verbose: bool
|
|
338
|
-
) -> None:
|
|
339
|
-
"""Internal helper to analyze with MCP config."""
|
|
340
|
-
# Create client
|
|
341
|
-
with Progress(
|
|
342
|
-
SpinnerColumn(),
|
|
343
|
-
TextColumn("[progress.description]{task.description}"),
|
|
344
|
-
console=console,
|
|
345
|
-
) as progress:
|
|
346
|
-
task = progress.add_task("Initializing MCP client...", total=None)
|
|
347
|
-
|
|
348
|
-
client = MCPClient(mcp_config=mcp_config, verbose=verbose)
|
|
349
|
-
|
|
350
|
-
try:
|
|
351
|
-
await client.initialize()
|
|
352
|
-
progress.update(task, description="[green]✓ Client initialized[/green]")
|
|
353
|
-
|
|
354
|
-
# Analyze environment
|
|
355
|
-
progress.update(task, description="Analyzing environment...")
|
|
356
|
-
analysis = await client.analyze_environment()
|
|
357
|
-
progress.update(task, description="[green]✓ Analysis complete[/green]")
|
|
358
|
-
|
|
359
|
-
except Exception as e:
|
|
360
|
-
progress.update(task, description=f"[red]✗ Failed: {e}[/red]")
|
|
361
|
-
return
|
|
362
|
-
finally:
|
|
363
|
-
await client.shutdown()
|
|
364
|
-
|
|
365
|
-
# Display results based on format
|
|
366
|
-
if output_format == "json":
|
|
367
|
-
console.print_json(json.dumps(analysis, indent=2))
|
|
368
|
-
elif output_format == "markdown":
|
|
369
|
-
display_markdown(analysis)
|
|
370
|
-
else: # interactive
|
|
371
|
-
display_interactive(analysis)
|
|
1
|
+
"""Analyze command implementation for MCP environments."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
10
|
+
from rich.syntax import Syntax
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
from rich.tree import Tree
|
|
13
|
+
|
|
14
|
+
from hud.clients import MCPClient
|
|
15
|
+
from hud.utils.design import HUDDesign
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
design = HUDDesign()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_docker_command(docker_cmd: list[str]) -> dict:
|
|
25
|
+
"""Convert Docker command to MCP config."""
|
|
26
|
+
return {
|
|
27
|
+
"local": {"command": docker_cmd[0], "args": docker_cmd[1:] if len(docker_cmd) > 1 else []}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def analyze_environment(docker_cmd: list[str], output_format: str, verbose: bool) -> None:
|
|
32
|
+
"""Analyze MCP environment and display results."""
|
|
33
|
+
design.header("MCP Environment Analysis", icon="🔍")
|
|
34
|
+
|
|
35
|
+
# Convert Docker command to MCP config
|
|
36
|
+
mcp_config = parse_docker_command(docker_cmd)
|
|
37
|
+
|
|
38
|
+
# Display command being analyzed
|
|
39
|
+
design.dim_info("Command:", " ".join(docker_cmd))
|
|
40
|
+
design.info("") # Empty line
|
|
41
|
+
|
|
42
|
+
# Create client
|
|
43
|
+
with Progress(
|
|
44
|
+
SpinnerColumn(),
|
|
45
|
+
TextColumn("[progress.description]{task.description}"),
|
|
46
|
+
console=console,
|
|
47
|
+
) as progress:
|
|
48
|
+
task = progress.add_task("Initializing MCP client...", total=None)
|
|
49
|
+
|
|
50
|
+
client = MCPClient(mcp_config=mcp_config, verbose=verbose, auto_trace=False)
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
await client.initialize()
|
|
54
|
+
progress.update(task, description="[green]✓ Client initialized[/green]")
|
|
55
|
+
|
|
56
|
+
# Analyze environment
|
|
57
|
+
progress.update(task, description="Analyzing environment...")
|
|
58
|
+
analysis = await client.analyze_environment()
|
|
59
|
+
progress.update(task, description="[green]✓ Analysis complete[/green]")
|
|
60
|
+
|
|
61
|
+
except Exception as e:
|
|
62
|
+
progress.update(task, description=f"[red]✗ Failed: {e}[/red]")
|
|
63
|
+
|
|
64
|
+
# On Windows, Docker stderr might not propagate properly
|
|
65
|
+
import platform
|
|
66
|
+
|
|
67
|
+
if platform.system() == "Windows" and "docker" in docker_cmd[0].lower():
|
|
68
|
+
console.print("\n[yellow]💡 Tip: Docker logs may not show on Windows.[/yellow]")
|
|
69
|
+
console.print(f"[yellow] Try: hud debug {' '.join(docker_cmd[3:])}[/yellow]")
|
|
70
|
+
console.print("[yellow] This will show more detailed error information.[/yellow]")
|
|
71
|
+
elif verbose:
|
|
72
|
+
console.print("\n[dim]For more details, try running with 'hud debug'[/dim]")
|
|
73
|
+
|
|
74
|
+
return
|
|
75
|
+
finally:
|
|
76
|
+
await client.shutdown()
|
|
77
|
+
|
|
78
|
+
# Display results based on format
|
|
79
|
+
if output_format == "json":
|
|
80
|
+
console.print_json(json.dumps(analysis, indent=2))
|
|
81
|
+
elif output_format == "markdown":
|
|
82
|
+
display_markdown(analysis)
|
|
83
|
+
else: # interactive
|
|
84
|
+
display_interactive(analysis)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def display_interactive(analysis: dict) -> None:
|
|
88
|
+
"""Display analysis results in interactive format."""
|
|
89
|
+
# Server metadata
|
|
90
|
+
design.section_title("📊 Environment Overview")
|
|
91
|
+
meta_table = Table(show_header=False, box=None)
|
|
92
|
+
meta_table.add_column("Property", style="dim")
|
|
93
|
+
meta_table.add_column("Value")
|
|
94
|
+
|
|
95
|
+
# Check if this is a live analysis (has metadata) or metadata-only analysis
|
|
96
|
+
if "metadata" in analysis:
|
|
97
|
+
# Live analysis format
|
|
98
|
+
for server in analysis["metadata"]["servers"]:
|
|
99
|
+
meta_table.add_row("Server", f"[green]{server}[/green]")
|
|
100
|
+
meta_table.add_row(
|
|
101
|
+
"Initialized",
|
|
102
|
+
"[green]✓[/green]" if analysis["metadata"]["initialized"] else "[red]✗[/red]",
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
# Metadata-only format
|
|
106
|
+
if "image" in analysis:
|
|
107
|
+
# Show simple name in table
|
|
108
|
+
image = analysis["image"]
|
|
109
|
+
display_ref = image.split("@")[0] if ":" in image and "@" in image else image
|
|
110
|
+
meta_table.add_row("Image", f"[green]{display_ref}[/green]")
|
|
111
|
+
|
|
112
|
+
if "status" in analysis:
|
|
113
|
+
meta_table.add_row("Source", analysis.get("source", analysis["status"]).title())
|
|
114
|
+
|
|
115
|
+
if "build_info" in analysis:
|
|
116
|
+
meta_table.add_row("Built", analysis["build_info"].get("generatedAt", "Unknown"))
|
|
117
|
+
meta_table.add_row("HUD Version", analysis["build_info"].get("hudVersion", "Unknown"))
|
|
118
|
+
|
|
119
|
+
if "push_info" in analysis:
|
|
120
|
+
meta_table.add_row("Pushed", analysis["push_info"].get("pushedAt", "Unknown"))
|
|
121
|
+
|
|
122
|
+
if "init_time" in analysis:
|
|
123
|
+
meta_table.add_row("Init Time", f"{analysis['init_time']} ms")
|
|
124
|
+
|
|
125
|
+
if "tool_count" in analysis:
|
|
126
|
+
meta_table.add_row("Tools", str(analysis["tool_count"]))
|
|
127
|
+
|
|
128
|
+
console.print(meta_table)
|
|
129
|
+
|
|
130
|
+
# Tools
|
|
131
|
+
design.section_title("🔧 Available Tools")
|
|
132
|
+
tools_tree = Tree("Tools")
|
|
133
|
+
|
|
134
|
+
# Check if we have hub_tools info (live analysis) or not (metadata-only)
|
|
135
|
+
if "hub_tools" in analysis:
|
|
136
|
+
# Live analysis format - separate regular and hub tools
|
|
137
|
+
# Regular tools
|
|
138
|
+
regular_tools = tools_tree.add("Regular Tools")
|
|
139
|
+
for tool in analysis["tools"]:
|
|
140
|
+
if tool["name"] not in analysis["hub_tools"]:
|
|
141
|
+
tool_node = regular_tools.add(f"[default]{tool['name']}[/default]")
|
|
142
|
+
if tool["description"]:
|
|
143
|
+
tool_node.add(f"[dim]{tool['description']}[/dim]")
|
|
144
|
+
|
|
145
|
+
# Show input schema if verbose
|
|
146
|
+
if analysis.get("verbose") and tool.get("input_schema"):
|
|
147
|
+
schema_str = json.dumps(tool["input_schema"], indent=2)
|
|
148
|
+
syntax = Syntax(schema_str, "json", theme="monokai", line_numbers=False)
|
|
149
|
+
tool_node.add(syntax)
|
|
150
|
+
|
|
151
|
+
# Hub tools
|
|
152
|
+
if analysis["hub_tools"]:
|
|
153
|
+
hub_tools = tools_tree.add("Hub Tools")
|
|
154
|
+
for hub_name, functions in analysis["hub_tools"].items():
|
|
155
|
+
hub_node = hub_tools.add(f"[yellow]{hub_name}[/yellow]")
|
|
156
|
+
for func in functions:
|
|
157
|
+
hub_node.add(f"[default]{func}[/default]")
|
|
158
|
+
else:
|
|
159
|
+
# Metadata-only format - just list all tools
|
|
160
|
+
for tool in analysis["tools"]:
|
|
161
|
+
tool_node = tools_tree.add(f"[default]{tool['name']}[/default]")
|
|
162
|
+
if tool.get("description"):
|
|
163
|
+
tool_node.add(f"[dim]{tool['description']}[/dim]")
|
|
164
|
+
|
|
165
|
+
# Show input schema if verbose
|
|
166
|
+
if tool.get("inputSchema"):
|
|
167
|
+
schema_str = json.dumps(tool["inputSchema"], indent=2)
|
|
168
|
+
syntax = Syntax(schema_str, "json", theme="monokai", line_numbers=False)
|
|
169
|
+
tool_node.add(syntax)
|
|
170
|
+
|
|
171
|
+
console.print(tools_tree)
|
|
172
|
+
|
|
173
|
+
# Resources
|
|
174
|
+
if analysis["resources"]:
|
|
175
|
+
design.section_title("📚 Available Resources")
|
|
176
|
+
resources_table = Table()
|
|
177
|
+
resources_table.add_column("URI", style="default")
|
|
178
|
+
resources_table.add_column("Name", style="white")
|
|
179
|
+
resources_table.add_column("Type", style="dim")
|
|
180
|
+
|
|
181
|
+
for resource in analysis["resources"][:10]:
|
|
182
|
+
resources_table.add_row(
|
|
183
|
+
resource["uri"], resource.get("name", ""), resource.get("mime_type", "")
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
console.print(resources_table)
|
|
187
|
+
|
|
188
|
+
if len(analysis["resources"]) > 10:
|
|
189
|
+
console.print(f"[dim]... and {len(analysis['resources']) - 10} more resources[/dim]")
|
|
190
|
+
|
|
191
|
+
# Telemetry (only for live analysis)
|
|
192
|
+
if analysis.get("telemetry"):
|
|
193
|
+
design.section_title("📡 Telemetry Data")
|
|
194
|
+
telemetry_table = Table(show_header=False, box=None)
|
|
195
|
+
telemetry_table.add_column("Key", style="dim")
|
|
196
|
+
telemetry_table.add_column("Value")
|
|
197
|
+
|
|
198
|
+
if "live_url" in analysis["telemetry"]:
|
|
199
|
+
telemetry_table.add_row("Live URL", f"[link]{analysis['telemetry']['live_url']}[/link]")
|
|
200
|
+
if "status" in analysis["telemetry"]:
|
|
201
|
+
telemetry_table.add_row("Status", f"[green]{analysis['telemetry']['status']}[/green]")
|
|
202
|
+
if "services" in analysis["telemetry"]:
|
|
203
|
+
services = analysis["telemetry"]["services"]
|
|
204
|
+
running = sum(1 for s in services.values() if s == "running")
|
|
205
|
+
telemetry_table.add_row("Services", f"{running}/{len(services)} running")
|
|
206
|
+
|
|
207
|
+
console.print(telemetry_table)
|
|
208
|
+
|
|
209
|
+
# Environment variables (for metadata-only analysis)
|
|
210
|
+
if analysis.get("env_vars"):
|
|
211
|
+
design.section_title("🔑 Environment Variables")
|
|
212
|
+
env_table = Table(show_header=False, box=None)
|
|
213
|
+
env_table.add_column("Type", style="dim")
|
|
214
|
+
env_table.add_column("Variables")
|
|
215
|
+
|
|
216
|
+
if analysis["env_vars"].get("required"):
|
|
217
|
+
env_table.add_row("Required", ", ".join(analysis["env_vars"]["required"]))
|
|
218
|
+
if analysis["env_vars"].get("optional"):
|
|
219
|
+
env_table.add_row("Optional", ", ".join(analysis["env_vars"]["optional"]))
|
|
220
|
+
|
|
221
|
+
console.print(env_table)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def display_markdown(analysis: dict) -> None:
|
|
225
|
+
"""Display analysis results in markdown format."""
|
|
226
|
+
md = []
|
|
227
|
+
md.append("# MCP Environment Analysis\n")
|
|
228
|
+
|
|
229
|
+
# Metadata
|
|
230
|
+
md.append("## Environment Overview")
|
|
231
|
+
|
|
232
|
+
# Check if this is live analysis or metadata-only
|
|
233
|
+
if "metadata" in analysis:
|
|
234
|
+
md.append(f"- **Servers**: {', '.join(analysis['metadata']['servers'])}")
|
|
235
|
+
md.append(f"- **Initialized**: {'✓' if analysis['metadata']['initialized'] else '✗'}")
|
|
236
|
+
else:
|
|
237
|
+
# Metadata-only format
|
|
238
|
+
if "image" in analysis:
|
|
239
|
+
md.append(f"- **Image**: {analysis['image']}")
|
|
240
|
+
if "source" in analysis:
|
|
241
|
+
md.append(f"- **Source**: {analysis['source']}")
|
|
242
|
+
if "build_info" in analysis:
|
|
243
|
+
md.append(f"- **Built**: {analysis['build_info'].get('generatedAt', 'Unknown')}")
|
|
244
|
+
if "tool_count" in analysis:
|
|
245
|
+
md.append(f"- **Tools**: {analysis['tool_count']}")
|
|
246
|
+
|
|
247
|
+
md.append("")
|
|
248
|
+
|
|
249
|
+
# Tools
|
|
250
|
+
md.append("## Available Tools\n")
|
|
251
|
+
|
|
252
|
+
# Check if we have hub_tools info (live analysis) or not (metadata-only)
|
|
253
|
+
if "hub_tools" in analysis:
|
|
254
|
+
# Regular tools
|
|
255
|
+
md.append("### Regular Tools")
|
|
256
|
+
for tool in analysis["tools"]:
|
|
257
|
+
if tool["name"] not in analysis["hub_tools"]:
|
|
258
|
+
md.extend([f"- **{tool['name']}**: {tool.get('description', 'No description')}"])
|
|
259
|
+
md.append("")
|
|
260
|
+
|
|
261
|
+
# Hub tools
|
|
262
|
+
if analysis["hub_tools"]:
|
|
263
|
+
md.append("### Hub Tools")
|
|
264
|
+
for hub_name, functions in analysis["hub_tools"].items():
|
|
265
|
+
md.extend([f"- **{hub_name}**"])
|
|
266
|
+
for func in functions:
|
|
267
|
+
md.extend([f" - {func}"])
|
|
268
|
+
md.append("")
|
|
269
|
+
else:
|
|
270
|
+
# Metadata-only format - just list all tools
|
|
271
|
+
for tool in analysis["tools"]:
|
|
272
|
+
md.extend([f"- **{tool['name']}**: {tool.get('description', 'No description')}"])
|
|
273
|
+
md.append("")
|
|
274
|
+
|
|
275
|
+
# Resources
|
|
276
|
+
if analysis["resources"]:
|
|
277
|
+
md.append("## Available Resources\n")
|
|
278
|
+
md.append("| URI | Name | Type |")
|
|
279
|
+
md.append("|-----|------|------|")
|
|
280
|
+
for resource in analysis["resources"]:
|
|
281
|
+
uri = resource["uri"]
|
|
282
|
+
name = resource.get("name", "")
|
|
283
|
+
mime_type = resource.get("mime_type", "")
|
|
284
|
+
md.extend([f"| {uri} | {name} | {mime_type} |"])
|
|
285
|
+
md.append("")
|
|
286
|
+
|
|
287
|
+
# Telemetry (only for live analysis)
|
|
288
|
+
if analysis.get("telemetry"):
|
|
289
|
+
md.append("## Telemetry")
|
|
290
|
+
if "live_url" in analysis["telemetry"]:
|
|
291
|
+
md.extend([f"- **Live URL**: {analysis['telemetry']['live_url']}"])
|
|
292
|
+
if "status" in analysis["telemetry"]:
|
|
293
|
+
md.extend([f"- **Status**: {analysis['telemetry']['status']}"])
|
|
294
|
+
if "services" in analysis["telemetry"]:
|
|
295
|
+
md.extend([f"- **Services**: {analysis['telemetry']['services']}"])
|
|
296
|
+
md.append("")
|
|
297
|
+
|
|
298
|
+
# Environment variables (for metadata-only analysis)
|
|
299
|
+
if analysis.get("env_vars"):
|
|
300
|
+
md.append("## Environment Variables")
|
|
301
|
+
if analysis["env_vars"].get("required"):
|
|
302
|
+
md.extend([f"- **Required**: {', '.join(analysis['env_vars']['required'])}"])
|
|
303
|
+
if analysis["env_vars"].get("optional"):
|
|
304
|
+
md.extend([f"- **Optional**: {', '.join(analysis['env_vars']['optional'])}"])
|
|
305
|
+
md.append("")
|
|
306
|
+
|
|
307
|
+
console.print("\n".join(md))
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
async def analyze_environment_from_config(
|
|
311
|
+
config_path: Path, output_format: str, verbose: bool
|
|
312
|
+
) -> None:
|
|
313
|
+
"""Analyze MCP environment from a JSON config file."""
|
|
314
|
+
design.header("MCP Environment Analysis", icon="🔍")
|
|
315
|
+
|
|
316
|
+
# Load config from file
|
|
317
|
+
try:
|
|
318
|
+
with open(config_path) as f: # noqa: ASYNC230
|
|
319
|
+
mcp_config = json.load(f)
|
|
320
|
+
console.print(f"[dim]Config: {config_path}[/dim]\n")
|
|
321
|
+
except Exception as e:
|
|
322
|
+
console.print(f"[red]Error loading config: {e}[/red]")
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
await _analyze_with_config(mcp_config, output_format, verbose)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
async def analyze_environment_from_mcp_config(
|
|
329
|
+
mcp_config: dict[str, Any], output_format: str, verbose: bool
|
|
330
|
+
) -> None:
|
|
331
|
+
"""Analyze MCP environment from MCP config dict."""
|
|
332
|
+
design.header("MCP Environment Analysis", icon="🔍")
|
|
333
|
+
await _analyze_with_config(mcp_config, output_format, verbose)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
async def _analyze_with_config(
|
|
337
|
+
mcp_config: dict[str, Any], output_format: str, verbose: bool
|
|
338
|
+
) -> None:
|
|
339
|
+
"""Internal helper to analyze with MCP config."""
|
|
340
|
+
# Create client
|
|
341
|
+
with Progress(
|
|
342
|
+
SpinnerColumn(),
|
|
343
|
+
TextColumn("[progress.description]{task.description}"),
|
|
344
|
+
console=console,
|
|
345
|
+
) as progress:
|
|
346
|
+
task = progress.add_task("Initializing MCP client...", total=None)
|
|
347
|
+
|
|
348
|
+
client = MCPClient(mcp_config=mcp_config, verbose=verbose)
|
|
349
|
+
|
|
350
|
+
try:
|
|
351
|
+
await client.initialize()
|
|
352
|
+
progress.update(task, description="[green]✓ Client initialized[/green]")
|
|
353
|
+
|
|
354
|
+
# Analyze environment
|
|
355
|
+
progress.update(task, description="Analyzing environment...")
|
|
356
|
+
analysis = await client.analyze_environment()
|
|
357
|
+
progress.update(task, description="[green]✓ Analysis complete[/green]")
|
|
358
|
+
|
|
359
|
+
except Exception as e:
|
|
360
|
+
progress.update(task, description=f"[red]✗ Failed: {e}[/red]")
|
|
361
|
+
return
|
|
362
|
+
finally:
|
|
363
|
+
await client.shutdown()
|
|
364
|
+
|
|
365
|
+
# Display results based on format
|
|
366
|
+
if output_format == "json":
|
|
367
|
+
console.print_json(json.dumps(analysis, indent=2))
|
|
368
|
+
elif output_format == "markdown":
|
|
369
|
+
display_markdown(analysis)
|
|
370
|
+
else: # interactive
|
|
371
|
+
display_interactive(analysis)
|