hud-python 0.3.4__py3-none-any.whl → 0.4.0__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 -89
- hud/agents/__init__.py +17 -0
- hud/agents/art.py +101 -0
- hud/agents/base.py +599 -0
- hud/{mcp → agents}/claude.py +373 -321
- hud/{mcp → agents}/langchain.py +250 -250
- hud/agents/misc/__init__.py +7 -0
- hud/{agent → agents}/misc/response_agent.py +80 -80
- hud/{mcp → agents}/openai.py +352 -334
- hud/agents/openai_chat_generic.py +154 -0
- hud/{mcp → agents}/tests/__init__.py +1 -1
- hud/agents/tests/test_base.py +742 -0
- hud/agents/tests/test_claude.py +324 -0
- hud/{mcp → agents}/tests/test_client.py +363 -324
- hud/{mcp → agents}/tests/test_openai.py +237 -238
- hud/cli/__init__.py +617 -0
- hud/cli/__main__.py +8 -0
- hud/cli/analyze.py +371 -0
- hud/cli/analyze_metadata.py +230 -0
- hud/cli/build.py +427 -0
- hud/cli/clone.py +185 -0
- hud/cli/cursor.py +92 -0
- hud/cli/debug.py +392 -0
- hud/cli/docker_utils.py +83 -0
- hud/cli/init.py +281 -0
- hud/cli/interactive.py +353 -0
- hud/cli/mcp_server.py +756 -0
- hud/cli/pull.py +336 -0
- hud/cli/push.py +379 -0
- hud/cli/remote_runner.py +311 -0
- hud/cli/runner.py +160 -0
- hud/cli/tests/__init__.py +3 -0
- hud/cli/tests/test_analyze.py +284 -0
- hud/cli/tests/test_cli_init.py +265 -0
- hud/cli/tests/test_cli_main.py +27 -0
- hud/cli/tests/test_clone.py +142 -0
- hud/cli/tests/test_cursor.py +253 -0
- hud/cli/tests/test_debug.py +453 -0
- hud/cli/tests/test_mcp_server.py +139 -0
- hud/cli/tests/test_utils.py +388 -0
- hud/cli/utils.py +263 -0
- hud/clients/README.md +143 -0
- hud/clients/__init__.py +16 -0
- hud/clients/base.py +354 -0
- hud/clients/fastmcp.py +202 -0
- hud/clients/mcp_use.py +278 -0
- hud/clients/tests/__init__.py +1 -0
- hud/clients/tests/test_client_integration.py +111 -0
- hud/clients/tests/test_fastmcp.py +342 -0
- hud/clients/tests/test_protocol.py +188 -0
- hud/clients/utils/__init__.py +1 -0
- hud/clients/utils/retry_transport.py +160 -0
- hud/datasets.py +322 -192
- hud/misc/__init__.py +1 -0
- hud/{agent → misc}/claude_plays_pokemon.py +292 -283
- hud/otel/__init__.py +35 -0
- hud/otel/collector.py +142 -0
- hud/otel/config.py +164 -0
- hud/otel/context.py +536 -0
- hud/otel/exporters.py +366 -0
- hud/otel/instrumentation.py +97 -0
- hud/otel/processors.py +118 -0
- hud/otel/tests/__init__.py +1 -0
- hud/otel/tests/test_processors.py +197 -0
- hud/server/__init__.py +5 -5
- hud/server/context.py +114 -0
- hud/server/helper/__init__.py +5 -0
- hud/server/low_level.py +132 -0
- hud/server/server.py +166 -0
- hud/server/tests/__init__.py +3 -0
- hud/settings.py +73 -79
- hud/shared/__init__.py +5 -0
- hud/{exceptions.py → shared/exceptions.py} +180 -180
- hud/{server → shared}/requests.py +264 -264
- hud/shared/tests/test_exceptions.py +157 -0
- hud/{server → shared}/tests/test_requests.py +275 -275
- hud/telemetry/__init__.py +25 -30
- hud/telemetry/instrument.py +379 -0
- hud/telemetry/job.py +309 -141
- hud/telemetry/replay.py +74 -0
- hud/telemetry/trace.py +83 -0
- hud/tools/__init__.py +33 -34
- hud/tools/base.py +365 -65
- hud/tools/bash.py +161 -137
- hud/tools/computer/__init__.py +15 -13
- hud/tools/computer/anthropic.py +437 -414
- hud/tools/computer/hud.py +376 -328
- hud/tools/computer/openai.py +295 -286
- hud/tools/computer/settings.py +82 -0
- hud/tools/edit.py +314 -290
- hud/tools/executors/__init__.py +30 -30
- hud/tools/executors/base.py +539 -532
- hud/tools/executors/pyautogui.py +621 -619
- 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 -503
- hud/tools/{playwright_tool.py → playwright.py} +412 -379
- hud/tools/tests/__init__.py +3 -3
- hud/tools/tests/test_base.py +282 -0
- hud/tools/tests/test_bash.py +158 -152
- hud/tools/tests/test_bash_extended.py +197 -0
- hud/tools/tests/test_computer.py +425 -52
- hud/tools/tests/test_computer_actions.py +34 -34
- hud/tools/tests/test_edit.py +259 -240
- hud/tools/tests/test_init.py +27 -27
- hud/tools/tests/test_playwright_tool.py +183 -183
- hud/tools/tests/test_tools.py +145 -157
- hud/tools/tests/test_utils.py +156 -156
- hud/tools/types.py +72 -0
- hud/tools/utils.py +50 -50
- hud/types.py +136 -89
- hud/utils/__init__.py +10 -16
- hud/utils/async_utils.py +65 -0
- hud/utils/design.py +168 -0
- hud/utils/mcp.py +55 -0
- hud/utils/progress.py +149 -149
- hud/utils/telemetry.py +66 -66
- hud/utils/tests/test_async_utils.py +173 -0
- hud/utils/tests/test_init.py +17 -21
- hud/utils/tests/test_progress.py +261 -225
- hud/utils/tests/test_telemetry.py +82 -37
- hud/utils/tests/test_version.py +8 -8
- hud/version.py +7 -7
- hud_python-0.4.0.dist-info/METADATA +474 -0
- hud_python-0.4.0.dist-info/RECORD +132 -0
- hud_python-0.4.0.dist-info/entry_points.txt +3 -0
- {hud_python-0.3.4.dist-info → hud_python-0.4.0.dist-info}/licenses/LICENSE +21 -21
- hud/adapters/__init__.py +0 -8
- hud/adapters/claude/__init__.py +0 -5
- hud/adapters/claude/adapter.py +0 -180
- hud/adapters/claude/tests/__init__.py +0 -1
- hud/adapters/claude/tests/test_adapter.py +0 -519
- hud/adapters/common/__init__.py +0 -6
- hud/adapters/common/adapter.py +0 -178
- hud/adapters/common/tests/test_adapter.py +0 -289
- hud/adapters/common/types.py +0 -446
- hud/adapters/operator/__init__.py +0 -5
- hud/adapters/operator/adapter.py +0 -108
- hud/adapters/operator/tests/__init__.py +0 -1
- hud/adapters/operator/tests/test_adapter.py +0 -370
- hud/agent/__init__.py +0 -19
- hud/agent/base.py +0 -126
- hud/agent/claude.py +0 -271
- hud/agent/langchain.py +0 -215
- hud/agent/misc/__init__.py +0 -3
- hud/agent/operator.py +0 -268
- hud/agent/tests/__init__.py +0 -1
- hud/agent/tests/test_base.py +0 -202
- hud/env/__init__.py +0 -11
- hud/env/client.py +0 -35
- hud/env/docker_client.py +0 -349
- hud/env/environment.py +0 -446
- hud/env/local_docker_client.py +0 -358
- hud/env/remote_client.py +0 -212
- hud/env/remote_docker_client.py +0 -292
- hud/gym.py +0 -130
- hud/job.py +0 -773
- hud/mcp/__init__.py +0 -17
- hud/mcp/base.py +0 -631
- hud/mcp/client.py +0 -312
- hud/mcp/tests/test_base.py +0 -512
- hud/mcp/tests/test_claude.py +0 -294
- hud/task.py +0 -149
- hud/taskset.py +0 -237
- hud/telemetry/_trace.py +0 -347
- hud/telemetry/context.py +0 -230
- hud/telemetry/exporter.py +0 -575
- hud/telemetry/instrumentation/__init__.py +0 -3
- hud/telemetry/instrumentation/mcp.py +0 -259
- hud/telemetry/instrumentation/registry.py +0 -59
- hud/telemetry/mcp_models.py +0 -270
- hud/telemetry/tests/__init__.py +0 -1
- hud/telemetry/tests/test_context.py +0 -210
- hud/telemetry/tests/test_trace.py +0 -312
- hud/tools/helper/README.md +0 -56
- hud/tools/helper/__init__.py +0 -9
- hud/tools/helper/mcp_server.py +0 -78
- hud/tools/helper/server_initialization.py +0 -115
- hud/tools/helper/utils.py +0 -58
- hud/trajectory.py +0 -94
- hud/utils/agent.py +0 -37
- hud/utils/common.py +0 -256
- hud/utils/config.py +0 -120
- hud/utils/deprecation.py +0 -115
- hud/utils/misc.py +0 -53
- hud/utils/tests/test_common.py +0 -277
- hud/utils/tests/test_config.py +0 -129
- hud_python-0.3.4.dist-info/METADATA +0 -284
- hud_python-0.3.4.dist-info/RECORD +0 -120
- /hud/{adapters/common → shared}/tests/__init__.py +0 -0
- {hud_python-0.3.4.dist-info → hud_python-0.4.0.dist-info}/WHEEL +0 -0
hud/cli/init.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""Initialize new HUD environments with minimal templates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.syntax import Syntax
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
# Embedded templates
|
|
15
|
+
DOCKERFILE_TEMPLATE = """FROM python:3.11-slim
|
|
16
|
+
|
|
17
|
+
WORKDIR /app
|
|
18
|
+
|
|
19
|
+
# Install git for hud-python dependency
|
|
20
|
+
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
|
|
21
|
+
|
|
22
|
+
# Copy and install dependencies
|
|
23
|
+
COPY pyproject.toml ./
|
|
24
|
+
COPY src/ ./src/
|
|
25
|
+
RUN pip install --no-cache-dir -e .
|
|
26
|
+
|
|
27
|
+
# Set logging to stderr
|
|
28
|
+
ENV HUD_LOG_STREAM=stderr
|
|
29
|
+
|
|
30
|
+
# Start context server in background, then MCP server
|
|
31
|
+
CMD ["sh", "-c", "python -m hud_controller.context & sleep 1 && exec python -m hud_controller.server"]
|
|
32
|
+
""" # noqa: E501
|
|
33
|
+
|
|
34
|
+
PYPROJECT_TEMPLATE = """[project]
|
|
35
|
+
name = "{name}"
|
|
36
|
+
version = "0.1.0"
|
|
37
|
+
description = "A minimal HUD environment"
|
|
38
|
+
requires-python = ">=3.11"
|
|
39
|
+
dependencies = [
|
|
40
|
+
"hud-python @ git+https://github.com/hud-evals/hud-python.git",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["hatchling"]
|
|
45
|
+
build-backend = "hatchling.build"
|
|
46
|
+
|
|
47
|
+
[tool.hud]
|
|
48
|
+
image = "{name}:dev"
|
|
49
|
+
|
|
50
|
+
[tool.hatch.metadata]
|
|
51
|
+
allow-direct-references = true
|
|
52
|
+
|
|
53
|
+
[tool.hatch.build.targets.wheel]
|
|
54
|
+
packages = ["src/hud_controller"]
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
CONTEXT_TEMPLATE = '''"""Minimal context that persists across hot-reloads."""
|
|
58
|
+
from hud.server.context import run_context_server
|
|
59
|
+
import asyncio
|
|
60
|
+
|
|
61
|
+
class Context:
|
|
62
|
+
def __init__(self):
|
|
63
|
+
self.count = 0
|
|
64
|
+
|
|
65
|
+
def act(self):
|
|
66
|
+
self.count += 1
|
|
67
|
+
return self.count
|
|
68
|
+
|
|
69
|
+
def get_count(self):
|
|
70
|
+
return self.count
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
asyncio.run(run_context_server(Context()))
|
|
74
|
+
'''
|
|
75
|
+
|
|
76
|
+
SERVER_TEMPLATE = '''"""Minimal MCP server for HUD."""
|
|
77
|
+
from hud.server import MCPServer
|
|
78
|
+
from hud.server.context import attach_context
|
|
79
|
+
|
|
80
|
+
mcp = MCPServer(name="{name}")
|
|
81
|
+
ctx = None
|
|
82
|
+
|
|
83
|
+
@mcp.initialize
|
|
84
|
+
async def init(init_ctx):
|
|
85
|
+
global ctx
|
|
86
|
+
ctx = attach_context("/tmp/hud_ctx.sock")
|
|
87
|
+
|
|
88
|
+
@mcp.shutdown
|
|
89
|
+
async def cleanup():
|
|
90
|
+
global ctx
|
|
91
|
+
ctx = None
|
|
92
|
+
|
|
93
|
+
@mcp.tool()
|
|
94
|
+
async def act() -> str:
|
|
95
|
+
"""Perform an action."""
|
|
96
|
+
return f"Action #{{ctx.act()}}"
|
|
97
|
+
|
|
98
|
+
@mcp.tool()
|
|
99
|
+
async def setup() -> str:
|
|
100
|
+
"""Required for HUD environments."""
|
|
101
|
+
return "Ready"
|
|
102
|
+
|
|
103
|
+
@mcp.tool()
|
|
104
|
+
async def evaluate() -> dict:
|
|
105
|
+
"""Required for HUD environments."""
|
|
106
|
+
return {{"count": ctx.get_count()}}
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
mcp.run()
|
|
110
|
+
'''
|
|
111
|
+
|
|
112
|
+
README_TEMPLATE = '''# {title}
|
|
113
|
+
|
|
114
|
+
A minimal HUD environment created with `hud init`.
|
|
115
|
+
|
|
116
|
+
## Quick Start
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# Build and run locally
|
|
120
|
+
hud dev
|
|
121
|
+
|
|
122
|
+
# Or build first
|
|
123
|
+
docker build -t {name}:dev .
|
|
124
|
+
hud dev --image {name}:dev
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Structure
|
|
128
|
+
|
|
129
|
+
- `src/hud_controller/server.py` - MCP server with tools
|
|
130
|
+
- `src/hud_controller/context.py` - Persistent state across hot-reloads
|
|
131
|
+
- `Dockerfile` - Container configuration
|
|
132
|
+
- `pyproject.toml` - Python dependencies
|
|
133
|
+
|
|
134
|
+
## Adding Tools
|
|
135
|
+
|
|
136
|
+
Add new tools to `server.py`:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
@mcp.tool()
|
|
140
|
+
async def my_tool(param: str) -> str:
|
|
141
|
+
"""Tool description."""
|
|
142
|
+
return f"Result: {{param}}"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Adding State
|
|
146
|
+
|
|
147
|
+
Extend the `Context` class in `context.py`:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
class Context:
|
|
151
|
+
def __init__(self):
|
|
152
|
+
self.count = 0
|
|
153
|
+
self.data = {{}} # Add your state
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Learn More
|
|
157
|
+
|
|
158
|
+
- [HUD Documentation](https://docs.hud.so)
|
|
159
|
+
- [MCP Specification](https://modelcontextprotocol.io)
|
|
160
|
+
'''
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def sanitize_name(name: str) -> str:
|
|
164
|
+
"""Convert a name to a valid Python package name."""
|
|
165
|
+
# Replace spaces and hyphens with underscores
|
|
166
|
+
name = name.replace(" ", "_").replace("-", "_")
|
|
167
|
+
# Remove any non-alphanumeric characters except underscores
|
|
168
|
+
name = "".join(c for c in name if c.isalnum() or c == "_")
|
|
169
|
+
# Ensure it doesn't start with a number
|
|
170
|
+
if name and name[0].isdigit():
|
|
171
|
+
name = f"env_{name}"
|
|
172
|
+
return name.lower()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def create_environment(name: str | None, directory: str, force: bool) -> None:
|
|
176
|
+
"""Create a new HUD environment from templates."""
|
|
177
|
+
from hud.utils.design import HUDDesign
|
|
178
|
+
|
|
179
|
+
design = HUDDesign()
|
|
180
|
+
|
|
181
|
+
# Determine environment name
|
|
182
|
+
if name is None:
|
|
183
|
+
# Use current directory name
|
|
184
|
+
current_dir = Path.cwd()
|
|
185
|
+
name = current_dir.name
|
|
186
|
+
target_dir = current_dir
|
|
187
|
+
design.info(f"Using current directory name: {name}")
|
|
188
|
+
else:
|
|
189
|
+
# Create new directory
|
|
190
|
+
target_dir = Path(directory) / name
|
|
191
|
+
|
|
192
|
+
# Sanitize name for Python package
|
|
193
|
+
package_name = sanitize_name(name)
|
|
194
|
+
if package_name != name:
|
|
195
|
+
design.warning(f"Package name adjusted for Python: {name} → {package_name}")
|
|
196
|
+
|
|
197
|
+
# Check if directory exists
|
|
198
|
+
if target_dir.exists() and any(target_dir.iterdir()):
|
|
199
|
+
if not force:
|
|
200
|
+
design.error(f"Directory {target_dir} already exists and is not empty")
|
|
201
|
+
design.info("Use --force to overwrite existing files")
|
|
202
|
+
raise typer.Exit(1)
|
|
203
|
+
else:
|
|
204
|
+
design.warning(f"Overwriting existing files in {target_dir}")
|
|
205
|
+
|
|
206
|
+
# Create directory structure
|
|
207
|
+
src_dir = target_dir / "src" / "hud_controller"
|
|
208
|
+
src_dir.mkdir(parents=True, exist_ok=True)
|
|
209
|
+
|
|
210
|
+
# Write files with proper formatting
|
|
211
|
+
files_created = []
|
|
212
|
+
|
|
213
|
+
# Dockerfile
|
|
214
|
+
dockerfile_path = target_dir / "Dockerfile"
|
|
215
|
+
dockerfile_path.write_text(DOCKERFILE_TEMPLATE.strip() + "\n")
|
|
216
|
+
files_created.append("Dockerfile")
|
|
217
|
+
|
|
218
|
+
# pyproject.toml
|
|
219
|
+
pyproject_path = target_dir / "pyproject.toml"
|
|
220
|
+
pyproject_content = PYPROJECT_TEMPLATE.format(name=package_name).strip() + "\n"
|
|
221
|
+
pyproject_path.write_text(pyproject_content)
|
|
222
|
+
files_created.append("pyproject.toml")
|
|
223
|
+
|
|
224
|
+
# README.md
|
|
225
|
+
readme_path = target_dir / "README.md"
|
|
226
|
+
readme_content = README_TEMPLATE.format(name=package_name, title=name).strip() + "\n"
|
|
227
|
+
readme_path.write_text(readme_content)
|
|
228
|
+
files_created.append("README.md")
|
|
229
|
+
|
|
230
|
+
# Python files
|
|
231
|
+
# __init__.py
|
|
232
|
+
init_path = src_dir / "__init__.py"
|
|
233
|
+
init_path.write_text('"""HUD Controller Package"""\n')
|
|
234
|
+
files_created.append("src/hud_controller/__init__.py")
|
|
235
|
+
|
|
236
|
+
# context.py
|
|
237
|
+
context_path = src_dir / "context.py"
|
|
238
|
+
context_path.write_text(CONTEXT_TEMPLATE.strip() + "\n")
|
|
239
|
+
files_created.append("src/hud_controller/context.py")
|
|
240
|
+
|
|
241
|
+
# server.py (need to escape the double braces for .format())
|
|
242
|
+
server_path = src_dir / "server.py"
|
|
243
|
+
server_content = SERVER_TEMPLATE.format(name=package_name).strip() + "\n"
|
|
244
|
+
server_path.write_text(server_content)
|
|
245
|
+
files_created.append("src/hud_controller/server.py")
|
|
246
|
+
|
|
247
|
+
# Success message
|
|
248
|
+
design.header(f"Created HUD Environment: {name}")
|
|
249
|
+
|
|
250
|
+
design.section_title("Files created")
|
|
251
|
+
for file in files_created:
|
|
252
|
+
console.print(f" ✓ {file}")
|
|
253
|
+
|
|
254
|
+
design.section_title("Next steps")
|
|
255
|
+
|
|
256
|
+
# Show commands based on where we created the environment
|
|
257
|
+
if target_dir == Path.cwd():
|
|
258
|
+
console.print("1. Start development server:")
|
|
259
|
+
console.print(" [cyan]hud dev[/cyan]")
|
|
260
|
+
else:
|
|
261
|
+
console.print("1. Enter the directory:")
|
|
262
|
+
console.print(f" [cyan]cd {target_dir.relative_to(Path.cwd())}[/cyan]")
|
|
263
|
+
console.print("\n2. Start development server:")
|
|
264
|
+
console.print(" [cyan]hud dev[/cyan]")
|
|
265
|
+
|
|
266
|
+
console.print("\n3. Connect from Cursor:")
|
|
267
|
+
console.print(" Follow the instructions shown by [cyan]hud dev[/cyan]")
|
|
268
|
+
|
|
269
|
+
console.print("\n4. Customize your environment:")
|
|
270
|
+
console.print(" - Add tools to [cyan]src/hud_controller/server.py[/cyan]")
|
|
271
|
+
console.print(" - Add state to [cyan]src/hud_controller/context.py[/cyan]")
|
|
272
|
+
|
|
273
|
+
# Show a sample of the server code
|
|
274
|
+
design.section_title("Your MCP server")
|
|
275
|
+
sample_code = '''@mcp.tool()
|
|
276
|
+
async def act() -> str:
|
|
277
|
+
"""Perform an action."""
|
|
278
|
+
return f"Action #{ctx.act()}"'''
|
|
279
|
+
|
|
280
|
+
syntax = Syntax(sample_code, "python", theme="monokai", line_numbers=False)
|
|
281
|
+
console.print(Panel(syntax, border_style="dim"))
|
hud/cli/interactive.py
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""Interactive mode for testing MCP environments."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import questionary
|
|
9
|
+
from mcp.types import TextContent
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.prompt import Prompt
|
|
13
|
+
from rich.syntax import Syntax
|
|
14
|
+
from rich.tree import Tree
|
|
15
|
+
|
|
16
|
+
from hud.clients import MCPClient
|
|
17
|
+
from hud.utils.design import HUDDesign
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class InteractiveMCPTester:
|
|
23
|
+
"""Interactive MCP environment tester."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, server_url: str, verbose: bool = False) -> None:
|
|
26
|
+
"""Initialize the interactive tester.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
server_url: URL of the MCP server (e.g., http://localhost:8765/mcp)
|
|
30
|
+
verbose: Enable verbose output
|
|
31
|
+
"""
|
|
32
|
+
self.server_url = server_url
|
|
33
|
+
self.verbose = verbose
|
|
34
|
+
self.client: MCPClient | None = None
|
|
35
|
+
self.tools: list[Any] = []
|
|
36
|
+
self.design = HUDDesign()
|
|
37
|
+
|
|
38
|
+
async def connect(self) -> bool:
|
|
39
|
+
"""Connect to the MCP server."""
|
|
40
|
+
try:
|
|
41
|
+
# Create MCP config for HTTP transport
|
|
42
|
+
config = {"server": {"url": self.server_url}}
|
|
43
|
+
|
|
44
|
+
self.client = MCPClient(
|
|
45
|
+
mcp_config=config,
|
|
46
|
+
verbose=self.verbose,
|
|
47
|
+
auto_trace=False, # Disable telemetry for interactive testing
|
|
48
|
+
)
|
|
49
|
+
await self.client.initialize()
|
|
50
|
+
|
|
51
|
+
# Fetch available tools
|
|
52
|
+
self.tools = await self.client.list_tools()
|
|
53
|
+
|
|
54
|
+
return True
|
|
55
|
+
except Exception as e:
|
|
56
|
+
self.design.error(f"Failed to connect: {e}")
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
async def disconnect(self) -> None:
|
|
60
|
+
"""Disconnect from the MCP server."""
|
|
61
|
+
if self.client:
|
|
62
|
+
await self.client.shutdown()
|
|
63
|
+
self.client = None
|
|
64
|
+
|
|
65
|
+
def display_tools(self) -> None:
|
|
66
|
+
"""Display available tools in a nice format."""
|
|
67
|
+
if not self.tools:
|
|
68
|
+
console.print("[yellow]No tools available[/yellow]")
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
# Group tools by hub
|
|
72
|
+
regular_tools = []
|
|
73
|
+
hub_tools = {}
|
|
74
|
+
|
|
75
|
+
for tool in self.tools:
|
|
76
|
+
if "/" in tool.name:
|
|
77
|
+
hub, name = tool.name.split("/", 1)
|
|
78
|
+
if hub not in hub_tools:
|
|
79
|
+
hub_tools[hub] = []
|
|
80
|
+
hub_tools[hub].append(tool)
|
|
81
|
+
else:
|
|
82
|
+
regular_tools.append(tool)
|
|
83
|
+
|
|
84
|
+
# Display tools tree
|
|
85
|
+
tree = Tree("🔧 Available Tools")
|
|
86
|
+
|
|
87
|
+
if regular_tools:
|
|
88
|
+
regular_node = tree.add("[cyan]Regular Tools[/cyan]")
|
|
89
|
+
for i, tool in enumerate(regular_tools, 1):
|
|
90
|
+
tool_node = regular_node.add(f"{i}. [white]{tool.name}[/white]")
|
|
91
|
+
if tool.description:
|
|
92
|
+
tool_node.add(f"[dim]{tool.description}[/dim]")
|
|
93
|
+
|
|
94
|
+
# Add hub tools
|
|
95
|
+
tool_index = len(regular_tools) + 1
|
|
96
|
+
for hub_name, tools in hub_tools.items():
|
|
97
|
+
hub_node = tree.add(f"[yellow]{hub_name} Hub[/yellow]")
|
|
98
|
+
for tool in tools:
|
|
99
|
+
tool_node = hub_node.add(f"{tool_index}. [white]{tool.name}[/white]")
|
|
100
|
+
if tool.description:
|
|
101
|
+
tool_node.add(f"[dim]{tool.description}[/dim]")
|
|
102
|
+
tool_index += 1
|
|
103
|
+
|
|
104
|
+
console.print(tree)
|
|
105
|
+
|
|
106
|
+
async def select_tool(self) -> Any | None:
|
|
107
|
+
"""Let user select a tool."""
|
|
108
|
+
if not self.tools:
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
# Build choices list
|
|
112
|
+
choices = []
|
|
113
|
+
tool_map = {}
|
|
114
|
+
|
|
115
|
+
for _, tool in enumerate(self.tools):
|
|
116
|
+
# Create display name
|
|
117
|
+
if "/" in tool.name:
|
|
118
|
+
hub, name = tool.name.split("/", 1)
|
|
119
|
+
display = f"[{hub}] {name}"
|
|
120
|
+
else:
|
|
121
|
+
display = tool.name
|
|
122
|
+
|
|
123
|
+
# Add description if available
|
|
124
|
+
if tool.description:
|
|
125
|
+
display += f" - {tool.description}"
|
|
126
|
+
|
|
127
|
+
choices.append(display)
|
|
128
|
+
tool_map[display] = tool
|
|
129
|
+
|
|
130
|
+
# Add quit option
|
|
131
|
+
choices.append("❌ Quit")
|
|
132
|
+
|
|
133
|
+
# Show selection menu with arrow keys
|
|
134
|
+
console.print("\n[cyan]Select a tool (use arrow keys):[/cyan]")
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
# Use questionary's async select with custom styling
|
|
138
|
+
selected = await questionary.select(
|
|
139
|
+
"",
|
|
140
|
+
choices=choices,
|
|
141
|
+
style=questionary.Style(
|
|
142
|
+
[
|
|
143
|
+
("question", ""),
|
|
144
|
+
("pointer", "fg:#ff9d00 bold"),
|
|
145
|
+
("highlighted", "fg:#ff9d00 bold"),
|
|
146
|
+
("selected", "fg:#cc5454"),
|
|
147
|
+
("separator", "fg:#6c6c6c"),
|
|
148
|
+
("instruction", "fg:#858585 italic"),
|
|
149
|
+
]
|
|
150
|
+
),
|
|
151
|
+
).unsafe_ask_async()
|
|
152
|
+
|
|
153
|
+
if selected is None:
|
|
154
|
+
console.print("[yellow]No selection made (ESC or Ctrl+C pressed)[/yellow]")
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
if selected == "❌ Quit":
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
return tool_map[selected]
|
|
161
|
+
|
|
162
|
+
except KeyboardInterrupt:
|
|
163
|
+
console.print("[yellow]Interrupted by user[/yellow]")
|
|
164
|
+
return None
|
|
165
|
+
except Exception as e:
|
|
166
|
+
console.print(f"[red]Error in tool selection: {e}[/red]")
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
async def get_tool_arguments(self, tool: Any) -> dict[str, Any] | None:
|
|
170
|
+
"""Prompt user for tool arguments."""
|
|
171
|
+
if not hasattr(tool, "inputSchema") or not tool.inputSchema:
|
|
172
|
+
return {}
|
|
173
|
+
|
|
174
|
+
schema = tool.inputSchema
|
|
175
|
+
|
|
176
|
+
# Show schema
|
|
177
|
+
console.print("\n[yellow]Tool Parameters:[/yellow]")
|
|
178
|
+
schema_str = json.dumps(schema, indent=2)
|
|
179
|
+
syntax = Syntax(schema_str, "json", theme="monokai", line_numbers=False)
|
|
180
|
+
console.print(Panel(syntax, title=f"{tool.name} Schema", border_style="dim"))
|
|
181
|
+
|
|
182
|
+
# Handle different schema types
|
|
183
|
+
if schema.get("type") == "object":
|
|
184
|
+
properties = schema.get("properties", {})
|
|
185
|
+
required = schema.get("required", [])
|
|
186
|
+
|
|
187
|
+
if not properties:
|
|
188
|
+
return {}
|
|
189
|
+
|
|
190
|
+
# Prompt for each property
|
|
191
|
+
args = {}
|
|
192
|
+
for prop_name, prop_schema in properties.items():
|
|
193
|
+
prop_type = prop_schema.get("type", "string")
|
|
194
|
+
description = prop_schema.get("description", "")
|
|
195
|
+
is_required = prop_name in required
|
|
196
|
+
|
|
197
|
+
# Build prompt
|
|
198
|
+
prompt = f"{prop_name}"
|
|
199
|
+
if description:
|
|
200
|
+
prompt += f" ({description})"
|
|
201
|
+
if not is_required:
|
|
202
|
+
prompt += " [optional]"
|
|
203
|
+
|
|
204
|
+
# Get value based on type
|
|
205
|
+
if prop_type == "boolean":
|
|
206
|
+
if is_required:
|
|
207
|
+
value = await questionary.confirm(prompt).unsafe_ask_async()
|
|
208
|
+
else:
|
|
209
|
+
# For optional booleans, offer a choice
|
|
210
|
+
choice = await questionary.select(
|
|
211
|
+
prompt, choices=["true", "false", "skip (leave unset)"]
|
|
212
|
+
).unsafe_ask_async()
|
|
213
|
+
if choice == "skip (leave unset)":
|
|
214
|
+
continue
|
|
215
|
+
value = choice == "true"
|
|
216
|
+
elif prop_type == "number" or prop_type == "integer":
|
|
217
|
+
value_str = await questionary.text(
|
|
218
|
+
prompt,
|
|
219
|
+
default="",
|
|
220
|
+
validate=lambda text, pt=prop_type, req=is_required: True
|
|
221
|
+
if not text and not req
|
|
222
|
+
else (
|
|
223
|
+
text.replace("-", "").replace(".", "").isdigit()
|
|
224
|
+
if pt == "number"
|
|
225
|
+
else text.replace("-", "").isdigit()
|
|
226
|
+
)
|
|
227
|
+
or f"Please enter a valid {pt}",
|
|
228
|
+
).unsafe_ask_async()
|
|
229
|
+
if not value_str and not is_required:
|
|
230
|
+
continue
|
|
231
|
+
value = int(value_str) if prop_type == "integer" else float(value_str)
|
|
232
|
+
elif prop_type == "array":
|
|
233
|
+
value_str = await questionary.text(
|
|
234
|
+
prompt + " (comma-separated)", default=""
|
|
235
|
+
).unsafe_ask_async()
|
|
236
|
+
if not value_str and not is_required:
|
|
237
|
+
continue
|
|
238
|
+
value = [v.strip() for v in value_str.split(",")]
|
|
239
|
+
else: # string or unknown
|
|
240
|
+
value = await questionary.text(prompt, default="").unsafe_ask_async()
|
|
241
|
+
if not value and not is_required:
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
args[prop_name] = value
|
|
245
|
+
|
|
246
|
+
return args
|
|
247
|
+
else:
|
|
248
|
+
# For non-object schemas, just get a single value
|
|
249
|
+
console.print("[yellow]Enter value (or press Enter to skip):[/yellow]")
|
|
250
|
+
value = Prompt.ask("Value", default="")
|
|
251
|
+
return {"value": value} if value else {}
|
|
252
|
+
|
|
253
|
+
async def call_tool(self, tool: Any, arguments: dict[str, Any]) -> None:
|
|
254
|
+
"""Call a tool and display results."""
|
|
255
|
+
if not self.client:
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
# Show what we're calling
|
|
260
|
+
console.print(f"\n[cyan]Calling {tool.name}...[/cyan]")
|
|
261
|
+
if arguments:
|
|
262
|
+
console.print(f"[dim]Arguments: {json.dumps(arguments, indent=2)}[/dim]")
|
|
263
|
+
|
|
264
|
+
# Make the call
|
|
265
|
+
result = await self.client.call_tool(name=tool.name, arguments=arguments)
|
|
266
|
+
|
|
267
|
+
# Display results
|
|
268
|
+
console.print("\n[green]✓ Tool executed successfully[/green]")
|
|
269
|
+
|
|
270
|
+
if result.isError:
|
|
271
|
+
console.print("[red]Error result:[/red]")
|
|
272
|
+
|
|
273
|
+
# Display content blocks
|
|
274
|
+
for content in result.content:
|
|
275
|
+
if isinstance(content, TextContent):
|
|
276
|
+
console.print(
|
|
277
|
+
Panel(
|
|
278
|
+
content.text,
|
|
279
|
+
title="Result",
|
|
280
|
+
border_style="green" if not result.isError else "red",
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
else:
|
|
284
|
+
# Handle other content types
|
|
285
|
+
console.print(json.dumps(content, indent=2))
|
|
286
|
+
|
|
287
|
+
except Exception as e:
|
|
288
|
+
console.print(f"[red]✗ Tool execution failed: {e}[/red]")
|
|
289
|
+
|
|
290
|
+
async def run(self) -> None:
|
|
291
|
+
"""Run the interactive testing loop."""
|
|
292
|
+
self.design.header("Interactive MCP Tester")
|
|
293
|
+
|
|
294
|
+
# Connect to server
|
|
295
|
+
console.print(f"[cyan]Connecting to {self.server_url}...[/cyan]")
|
|
296
|
+
if not await self.connect():
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
console.print("[green]✓ Connected successfully[/green]")
|
|
300
|
+
console.print(f"[dim]Found {len(self.tools)} tools[/dim]\n")
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
while True:
|
|
304
|
+
# Select tool
|
|
305
|
+
tool = await self.select_tool()
|
|
306
|
+
if not tool:
|
|
307
|
+
break
|
|
308
|
+
|
|
309
|
+
# Get arguments
|
|
310
|
+
console.print(f"\n[cyan]Selected: {tool.name}[/cyan]")
|
|
311
|
+
arguments = await self.get_tool_arguments(tool)
|
|
312
|
+
if arguments is None:
|
|
313
|
+
console.print("[yellow]Skipping tool call[/yellow]")
|
|
314
|
+
continue
|
|
315
|
+
|
|
316
|
+
# Call tool
|
|
317
|
+
await self.call_tool(tool, arguments)
|
|
318
|
+
|
|
319
|
+
# Just add a separator and continue to tool selection
|
|
320
|
+
console.print("\n" + "─" * 50)
|
|
321
|
+
|
|
322
|
+
finally:
|
|
323
|
+
# Disconnect
|
|
324
|
+
console.print("\n[cyan]Disconnecting...[/cyan]")
|
|
325
|
+
await self.disconnect()
|
|
326
|
+
|
|
327
|
+
# Show next steps tutorial
|
|
328
|
+
self.design.section_title("Next Steps")
|
|
329
|
+
self.design.info("🏗️ Ready to test with real agents? Run:")
|
|
330
|
+
self.design.info(" [cyan]hud build[/cyan]")
|
|
331
|
+
self.design.info("")
|
|
332
|
+
self.design.info("This will:")
|
|
333
|
+
self.design.info(" 1. Build your environment image")
|
|
334
|
+
self.design.info(" 2. Generate a hud.lock.yaml file")
|
|
335
|
+
self.design.info(" 3. Prepare it for testing with agents")
|
|
336
|
+
self.design.info("")
|
|
337
|
+
self.design.info("Then you can:")
|
|
338
|
+
self.design.info(" • Test locally: [cyan]hud run <image>[/cyan]")
|
|
339
|
+
self.design.info(" • Push to registry: [cyan]hud push --image <registry/name>[/cyan]")
|
|
340
|
+
self.design.info(" • Use with agents via the lock file")
|
|
341
|
+
|
|
342
|
+
console.print("\n[dim]Happy testing! 🎉[/dim]")
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
async def run_interactive_mode(server_url: str, verbose: bool = False) -> None:
|
|
346
|
+
"""Run interactive MCP testing mode.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
server_url: URL of the MCP server
|
|
350
|
+
verbose: Enable verbose output
|
|
351
|
+
"""
|
|
352
|
+
tester = InteractiveMCPTester(server_url, verbose)
|
|
353
|
+
await tester.run()
|