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.

Files changed (130) hide show
  1. hud/__init__.py +22 -22
  2. hud/agents/__init__.py +13 -15
  3. hud/agents/base.py +599 -599
  4. hud/agents/claude.py +373 -373
  5. hud/agents/langchain.py +261 -250
  6. hud/agents/misc/__init__.py +7 -7
  7. hud/agents/misc/response_agent.py +82 -80
  8. hud/agents/openai.py +352 -352
  9. hud/agents/openai_chat_generic.py +154 -154
  10. hud/agents/tests/__init__.py +1 -1
  11. hud/agents/tests/test_base.py +742 -742
  12. hud/agents/tests/test_claude.py +324 -324
  13. hud/agents/tests/test_client.py +363 -363
  14. hud/agents/tests/test_openai.py +237 -237
  15. hud/cli/__init__.py +617 -617
  16. hud/cli/__main__.py +8 -8
  17. hud/cli/analyze.py +371 -371
  18. hud/cli/analyze_metadata.py +230 -230
  19. hud/cli/build.py +498 -427
  20. hud/cli/clone.py +185 -185
  21. hud/cli/cursor.py +92 -92
  22. hud/cli/debug.py +392 -392
  23. hud/cli/docker_utils.py +83 -83
  24. hud/cli/init.py +280 -281
  25. hud/cli/interactive.py +353 -353
  26. hud/cli/mcp_server.py +764 -756
  27. hud/cli/pull.py +330 -336
  28. hud/cli/push.py +404 -370
  29. hud/cli/remote_runner.py +311 -311
  30. hud/cli/runner.py +160 -160
  31. hud/cli/tests/__init__.py +3 -3
  32. hud/cli/tests/test_analyze.py +284 -284
  33. hud/cli/tests/test_cli_init.py +265 -265
  34. hud/cli/tests/test_cli_main.py +27 -27
  35. hud/cli/tests/test_clone.py +142 -142
  36. hud/cli/tests/test_cursor.py +253 -253
  37. hud/cli/tests/test_debug.py +453 -453
  38. hud/cli/tests/test_mcp_server.py +139 -139
  39. hud/cli/tests/test_utils.py +388 -388
  40. hud/cli/utils.py +263 -263
  41. hud/clients/README.md +143 -143
  42. hud/clients/__init__.py +16 -16
  43. hud/clients/base.py +378 -379
  44. hud/clients/fastmcp.py +222 -222
  45. hud/clients/mcp_use.py +298 -278
  46. hud/clients/tests/__init__.py +1 -1
  47. hud/clients/tests/test_client_integration.py +111 -111
  48. hud/clients/tests/test_fastmcp.py +342 -342
  49. hud/clients/tests/test_protocol.py +188 -188
  50. hud/clients/utils/__init__.py +1 -1
  51. hud/clients/utils/retry_transport.py +160 -160
  52. hud/datasets.py +327 -322
  53. hud/misc/__init__.py +1 -1
  54. hud/misc/claude_plays_pokemon.py +292 -292
  55. hud/otel/__init__.py +35 -35
  56. hud/otel/collector.py +142 -142
  57. hud/otel/config.py +164 -164
  58. hud/otel/context.py +536 -536
  59. hud/otel/exporters.py +366 -366
  60. hud/otel/instrumentation.py +97 -97
  61. hud/otel/processors.py +118 -118
  62. hud/otel/tests/__init__.py +1 -1
  63. hud/otel/tests/test_processors.py +197 -197
  64. hud/server/__init__.py +5 -5
  65. hud/server/context.py +114 -114
  66. hud/server/helper/__init__.py +5 -5
  67. hud/server/low_level.py +132 -132
  68. hud/server/server.py +170 -166
  69. hud/server/tests/__init__.py +3 -3
  70. hud/settings.py +73 -73
  71. hud/shared/__init__.py +5 -5
  72. hud/shared/exceptions.py +180 -180
  73. hud/shared/requests.py +264 -264
  74. hud/shared/tests/test_exceptions.py +157 -157
  75. hud/shared/tests/test_requests.py +275 -275
  76. hud/telemetry/__init__.py +25 -25
  77. hud/telemetry/instrument.py +379 -379
  78. hud/telemetry/job.py +309 -309
  79. hud/telemetry/replay.py +74 -74
  80. hud/telemetry/trace.py +83 -83
  81. hud/tools/__init__.py +33 -33
  82. hud/tools/base.py +365 -365
  83. hud/tools/bash.py +161 -161
  84. hud/tools/computer/__init__.py +15 -15
  85. hud/tools/computer/anthropic.py +437 -437
  86. hud/tools/computer/hud.py +376 -376
  87. hud/tools/computer/openai.py +295 -295
  88. hud/tools/computer/settings.py +82 -82
  89. hud/tools/edit.py +314 -314
  90. hud/tools/executors/__init__.py +30 -30
  91. hud/tools/executors/base.py +539 -539
  92. hud/tools/executors/pyautogui.py +621 -621
  93. hud/tools/executors/tests/__init__.py +1 -1
  94. hud/tools/executors/tests/test_base_executor.py +338 -338
  95. hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
  96. hud/tools/executors/xdo.py +511 -511
  97. hud/tools/playwright.py +412 -412
  98. hud/tools/tests/__init__.py +3 -3
  99. hud/tools/tests/test_base.py +282 -282
  100. hud/tools/tests/test_bash.py +158 -158
  101. hud/tools/tests/test_bash_extended.py +197 -197
  102. hud/tools/tests/test_computer.py +425 -425
  103. hud/tools/tests/test_computer_actions.py +34 -34
  104. hud/tools/tests/test_edit.py +259 -259
  105. hud/tools/tests/test_init.py +27 -27
  106. hud/tools/tests/test_playwright_tool.py +183 -183
  107. hud/tools/tests/test_tools.py +145 -145
  108. hud/tools/tests/test_utils.py +156 -156
  109. hud/tools/types.py +72 -72
  110. hud/tools/utils.py +50 -50
  111. hud/types.py +136 -136
  112. hud/utils/__init__.py +10 -10
  113. hud/utils/async_utils.py +65 -65
  114. hud/utils/design.py +236 -168
  115. hud/utils/mcp.py +55 -55
  116. hud/utils/progress.py +149 -149
  117. hud/utils/telemetry.py +66 -66
  118. hud/utils/tests/test_async_utils.py +173 -173
  119. hud/utils/tests/test_init.py +17 -17
  120. hud/utils/tests/test_progress.py +261 -261
  121. hud/utils/tests/test_telemetry.py +82 -82
  122. hud/utils/tests/test_version.py +8 -8
  123. hud/version.py +7 -7
  124. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/METADATA +10 -8
  125. hud_python-0.4.3.dist-info/RECORD +131 -0
  126. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/licenses/LICENSE +21 -21
  127. hud/agents/art.py +0 -101
  128. hud_python-0.4.1.dist-info/RECORD +0 -132
  129. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/WHEEL +0 -0
  130. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/entry_points.txt +0 -0
hud/cli/remote_runner.py CHANGED
@@ -1,311 +1,311 @@
1
- """Remote runner for HUD MCP servers."""
2
-
3
- from __future__ import annotations
4
-
5
- import asyncio
6
- import os
7
- import sys
8
-
9
- import click
10
- from fastmcp import FastMCP
11
-
12
- from hud.settings import settings
13
-
14
-
15
- def parse_headers(header_args: list[str]) -> dict[str, str]:
16
- """Parse header arguments into a dictionary.
17
-
18
- Args:
19
- header_args: List of header strings in format "Key:Value" or "Key=Value"
20
-
21
- Returns:
22
- Dictionary of headers
23
- """
24
- headers = {}
25
- for header in header_args:
26
- if ":" in header:
27
- key, value = header.split(":", 1)
28
- elif "=" in header:
29
- key, value = header.split("=", 1)
30
- else:
31
- click.echo(f"āš ļø Invalid header format: {header} (use Key:Value or Key=Value)")
32
- continue
33
-
34
- headers[key.strip()] = value.strip()
35
-
36
- return headers
37
-
38
-
39
- def parse_env_vars(env_args: list[str]) -> dict[str, str]:
40
- """Parse environment variable arguments into headers.
41
-
42
- Args:
43
- env_args: List of env var strings in format "KEY=VALUE"
44
-
45
- Returns:
46
- Dictionary of headers with Env- prefix
47
- """
48
- env_headers = {}
49
- for env in env_args:
50
- if "=" not in env:
51
- click.echo(f"āš ļø Invalid env format: {env} (use KEY=VALUE)")
52
- continue
53
-
54
- key, value = env.split("=", 1)
55
- # Convert KEY_NAME to Env-Key-Name header format
56
- # e.g., API_KEY=xxx becomes Env-Api-Key: xxx
57
- # e.g., OPENAI_API_KEY=xxx becomes Env-Openai-Api-Key: xxx
58
- header_parts = key.split("_")
59
- header_key = f"Env-{'-'.join(part.capitalize() for part in header_parts)}"
60
- env_headers[header_key] = value
61
-
62
- return env_headers
63
-
64
-
65
- def build_remote_headers(
66
- image: str,
67
- env_args: list[str],
68
- header_args: list[str],
69
- api_key: str | None = None,
70
- run_id: str | None = None,
71
- ) -> dict[str, str]:
72
- """Build headers for remote MCP server.
73
-
74
- Args:
75
- image: Docker image name
76
- env_args: Environment variable arguments
77
- header_args: Additional header arguments
78
- api_key: API key (from env or arg)
79
- run_id: Run ID (optional)
80
-
81
- Returns:
82
- Complete headers dictionary
83
- """
84
- headers = {}
85
-
86
- # Required headers
87
- headers["Mcp-Image"] = image
88
-
89
- # API key
90
- if api_key:
91
- headers["Authorization"] = f"Bearer {api_key}"
92
-
93
- # Run ID if provided
94
- if run_id:
95
- headers["Run-Id"] = run_id
96
-
97
- # Environment variables as headers
98
- env_headers = parse_env_vars(env_args)
99
- headers.update(env_headers)
100
-
101
- # Additional headers
102
- extra_headers = parse_headers(header_args)
103
- headers.update(extra_headers)
104
-
105
- return headers
106
-
107
-
108
- def run_remote_stdio(
109
- url: str,
110
- headers: dict[str, str],
111
- verbose: bool = False,
112
- ) -> None:
113
- """Run remote MCP server with stdio transport."""
114
- # CRITICAL: Configure ALL output to go to stderr to keep stdout clean for MCP protocol
115
- import logging
116
- import warnings
117
-
118
- # Force all output to stderr
119
- sys.stdout = sys.stderr
120
-
121
- # Always disable FastMCP banner for stdio
122
- os.environ["FASTMCP_DISABLE_BANNER"] = "1"
123
-
124
- # Configure root logger to use stderr
125
- root_logger = logging.getLogger()
126
- root_logger.handlers.clear()
127
-
128
- if not verbose:
129
- # Suppress all logs and warnings for clean stdio
130
- stderr_handler = logging.StreamHandler(sys.stderr)
131
- stderr_handler.setLevel(logging.CRITICAL)
132
- root_logger.addHandler(stderr_handler)
133
- root_logger.setLevel(logging.CRITICAL)
134
-
135
- # Set all known loggers to CRITICAL
136
- for logger_name in ["fastmcp", "mcp", "httpx", "httpcore", "anyio", "asyncio", "uvicorn"]:
137
- logging.getLogger(logger_name).setLevel(logging.CRITICAL)
138
-
139
- # Suppress warnings
140
- warnings.filterwarnings("ignore")
141
- else:
142
- # Only show important logs to stderr
143
- stderr_handler = logging.StreamHandler(sys.stderr)
144
- stderr_handler.setFormatter(logging.Formatter("%(name)s: %(message)s"))
145
- root_logger.addHandler(stderr_handler)
146
- root_logger.setLevel(logging.INFO)
147
-
148
- async def run() -> None:
149
- # Save the real stdout before we redirected it
150
- real_stdout = sys.__stdout__
151
-
152
- if verbose:
153
- click.echo(f"šŸ”— Connecting to: {url}", err=True)
154
- click.echo(f"šŸ“¦ Image: {headers.get('Mcp-Image', 'unknown')}", err=True)
155
- click.echo(f"šŸ”‘ Headers: {list(headers.keys())}", err=True)
156
-
157
- # Create proxy configuration
158
- proxy_config = {
159
- "mcpServers": {
160
- "remote": {"transport": "streamable-http", "url": url, "headers": headers}
161
- }
162
- }
163
-
164
- try:
165
- # Restore stdout for the proxy to use
166
- sys.stdout = real_stdout
167
-
168
- # Create proxy that forwards remote HTTP to local stdio
169
- proxy = FastMCP.as_proxy(proxy_config, name="HUD Remote Proxy")
170
-
171
- # Run with stdio transport - this will handle stdin/stdout properly
172
- await proxy.run_async(transport="stdio", show_banner=False)
173
- except Exception as e:
174
- # Ensure errors go to stderr
175
- sys.stdout = sys.stderr
176
- if verbose:
177
- import traceback
178
-
179
- click.echo(f"āŒ Proxy error: {e}", err=True)
180
- click.echo(traceback.format_exc(), err=True)
181
- raise
182
-
183
- try:
184
- asyncio.run(run())
185
- except KeyboardInterrupt:
186
- if verbose:
187
- click.echo("\nāœ… Remote proxy stopped", err=True)
188
- sys.exit(0)
189
- except Exception as e:
190
- if verbose:
191
- click.echo(f"āŒ Error: {e}", err=True)
192
- sys.exit(1)
193
-
194
-
195
- async def run_remote_http(
196
- url: str,
197
- headers: dict[str, str],
198
- port: int,
199
- verbose: bool = False,
200
- ) -> None:
201
- """Run remote MCP server with HTTP transport."""
202
- from .utils import find_free_port
203
-
204
- # Find available port
205
- actual_port = find_free_port(port)
206
- if actual_port is None:
207
- click.echo(f"āŒ No available ports found starting from {port}")
208
- return
209
-
210
- if actual_port != port:
211
- click.echo(f"āš ļø Port {port} in use, using port {actual_port} instead")
212
-
213
- # Suppress logs unless verbose
214
- if not verbose:
215
- import logging
216
- import os
217
-
218
- os.environ["FASTMCP_DISABLE_BANNER"] = "1"
219
- logging.getLogger("fastmcp").setLevel(logging.ERROR)
220
- logging.getLogger("mcp").setLevel(logging.ERROR)
221
- logging.getLogger("uvicorn").setLevel(logging.ERROR)
222
- logging.getLogger("uvicorn.access").setLevel(logging.ERROR)
223
- logging.getLogger("uvicorn.error").setLevel(logging.ERROR)
224
- logging.getLogger("httpx").setLevel(logging.ERROR)
225
- logging.getLogger("httpcore").setLevel(logging.ERROR)
226
-
227
- # Create the MCP config for the proxy
228
- config = {"remote": {"transport": "streamable-http", "url": url, "headers": headers}}
229
-
230
- # Create proxy that forwards remote HTTP to local HTTP
231
- proxy = FastMCP.as_proxy(config, name="HUD Remote Proxy")
232
-
233
- click.echo(f"🌐 Starting HTTP proxy on port {actual_port}")
234
- click.echo(f"šŸ”— Server URL: http://localhost:{actual_port}/mcp")
235
- click.echo(f"ā˜ļø Proxying to: {url}")
236
- click.echo("ā¹ļø Press Ctrl+C to stop")
237
-
238
- try:
239
- # Run with HTTP transport
240
- await proxy.run_async(
241
- transport="http",
242
- host="0.0.0.0", # noqa: S104
243
- port=actual_port,
244
- path="/mcp",
245
- log_level="error" if not verbose else "info",
246
- show_banner=False,
247
- )
248
- except KeyboardInterrupt:
249
- click.echo("\nšŸ‘‹ Shutting down...")
250
-
251
-
252
- def run_remote_server(
253
- image: str,
254
- docker_args: list[str],
255
- transport: str,
256
- port: int,
257
- url: str,
258
- api_key: str | None,
259
- run_id: str | None,
260
- verbose: bool,
261
- ) -> None:
262
- """Run remote MCP server via proxy.
263
-
264
- Args:
265
- image: Docker image name
266
- docker_args: Docker-style arguments (-e, -h)
267
- transport: Output transport (stdio or http)
268
- port: Port for HTTP transport
269
- url: Remote MCP server URL
270
- api_key: API key for authentication
271
- run_id: Optional run ID
272
- verbose: Show detailed logs
273
- """
274
- # Parse docker args into env vars and headers
275
- env_args = []
276
- header_args = []
277
-
278
- i = 0
279
- while i < len(docker_args):
280
- arg = docker_args[i]
281
-
282
- if arg == "-e" and i + 1 < len(docker_args):
283
- env_args.append(docker_args[i + 1])
284
- i += 2
285
- elif arg == "-h" and i + 1 < len(docker_args):
286
- header_args.append(docker_args[i + 1])
287
- i += 2
288
- else:
289
- click.echo(f"āš ļø Unknown argument: {arg}", err=True)
290
- i += 1
291
-
292
- # Get API key from env if not provided
293
- if not api_key:
294
- api_key = settings.api_key
295
- if not api_key:
296
- click.echo("āŒ API key required. Set HUD_API_KEY env var or use --api-key", err=True)
297
- sys.exit(1)
298
-
299
- # Build headers
300
- headers = build_remote_headers(image, env_args, header_args, api_key, run_id)
301
-
302
- if verbose:
303
- click.echo(f"šŸ”§ Remote URL: {url}", err=True)
304
- click.echo(f"šŸ“¦ Image: {image}", err=True)
305
- click.echo(f"šŸ”‘ Headers: {list(headers.keys())}", err=True)
306
-
307
- # Run based on transport
308
- if transport == "stdio":
309
- run_remote_stdio(url, headers, verbose)
310
- else:
311
- asyncio.run(run_remote_http(url, headers, port, verbose))
1
+ """Remote runner for HUD MCP servers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ import sys
8
+
9
+ import click
10
+ from fastmcp import FastMCP
11
+
12
+ from hud.settings import settings
13
+
14
+
15
+ def parse_headers(header_args: list[str]) -> dict[str, str]:
16
+ """Parse header arguments into a dictionary.
17
+
18
+ Args:
19
+ header_args: List of header strings in format "Key:Value" or "Key=Value"
20
+
21
+ Returns:
22
+ Dictionary of headers
23
+ """
24
+ headers = {}
25
+ for header in header_args:
26
+ if ":" in header:
27
+ key, value = header.split(":", 1)
28
+ elif "=" in header:
29
+ key, value = header.split("=", 1)
30
+ else:
31
+ click.echo(f"āš ļø Invalid header format: {header} (use Key:Value or Key=Value)")
32
+ continue
33
+
34
+ headers[key.strip()] = value.strip()
35
+
36
+ return headers
37
+
38
+
39
+ def parse_env_vars(env_args: list[str]) -> dict[str, str]:
40
+ """Parse environment variable arguments into headers.
41
+
42
+ Args:
43
+ env_args: List of env var strings in format "KEY=VALUE"
44
+
45
+ Returns:
46
+ Dictionary of headers with Env- prefix
47
+ """
48
+ env_headers = {}
49
+ for env in env_args:
50
+ if "=" not in env:
51
+ click.echo(f"āš ļø Invalid env format: {env} (use KEY=VALUE)")
52
+ continue
53
+
54
+ key, value = env.split("=", 1)
55
+ # Convert KEY_NAME to Env-Key-Name header format
56
+ # e.g., API_KEY=xxx becomes Env-Api-Key: xxx
57
+ # e.g., OPENAI_API_KEY=xxx becomes Env-Openai-Api-Key: xxx
58
+ header_parts = key.split("_")
59
+ header_key = f"Env-{'-'.join(part.capitalize() for part in header_parts)}"
60
+ env_headers[header_key] = value
61
+
62
+ return env_headers
63
+
64
+
65
+ def build_remote_headers(
66
+ image: str,
67
+ env_args: list[str],
68
+ header_args: list[str],
69
+ api_key: str | None = None,
70
+ run_id: str | None = None,
71
+ ) -> dict[str, str]:
72
+ """Build headers for remote MCP server.
73
+
74
+ Args:
75
+ image: Docker image name
76
+ env_args: Environment variable arguments
77
+ header_args: Additional header arguments
78
+ api_key: API key (from env or arg)
79
+ run_id: Run ID (optional)
80
+
81
+ Returns:
82
+ Complete headers dictionary
83
+ """
84
+ headers = {}
85
+
86
+ # Required headers
87
+ headers["Mcp-Image"] = image
88
+
89
+ # API key
90
+ if api_key:
91
+ headers["Authorization"] = f"Bearer {api_key}"
92
+
93
+ # Run ID if provided
94
+ if run_id:
95
+ headers["Run-Id"] = run_id
96
+
97
+ # Environment variables as headers
98
+ env_headers = parse_env_vars(env_args)
99
+ headers.update(env_headers)
100
+
101
+ # Additional headers
102
+ extra_headers = parse_headers(header_args)
103
+ headers.update(extra_headers)
104
+
105
+ return headers
106
+
107
+
108
+ def run_remote_stdio(
109
+ url: str,
110
+ headers: dict[str, str],
111
+ verbose: bool = False,
112
+ ) -> None:
113
+ """Run remote MCP server with stdio transport."""
114
+ # CRITICAL: Configure ALL output to go to stderr to keep stdout clean for MCP protocol
115
+ import logging
116
+ import warnings
117
+
118
+ # Force all output to stderr
119
+ sys.stdout = sys.stderr
120
+
121
+ # Always disable FastMCP banner for stdio
122
+ os.environ["FASTMCP_DISABLE_BANNER"] = "1"
123
+
124
+ # Configure root logger to use stderr
125
+ root_logger = logging.getLogger()
126
+ root_logger.handlers.clear()
127
+
128
+ if not verbose:
129
+ # Suppress all logs and warnings for clean stdio
130
+ stderr_handler = logging.StreamHandler(sys.stderr)
131
+ stderr_handler.setLevel(logging.CRITICAL)
132
+ root_logger.addHandler(stderr_handler)
133
+ root_logger.setLevel(logging.CRITICAL)
134
+
135
+ # Set all known loggers to CRITICAL
136
+ for logger_name in ["fastmcp", "mcp", "httpx", "httpcore", "anyio", "asyncio", "uvicorn"]:
137
+ logging.getLogger(logger_name).setLevel(logging.CRITICAL)
138
+
139
+ # Suppress warnings
140
+ warnings.filterwarnings("ignore")
141
+ else:
142
+ # Only show important logs to stderr
143
+ stderr_handler = logging.StreamHandler(sys.stderr)
144
+ stderr_handler.setFormatter(logging.Formatter("%(name)s: %(message)s"))
145
+ root_logger.addHandler(stderr_handler)
146
+ root_logger.setLevel(logging.INFO)
147
+
148
+ async def run() -> None:
149
+ # Save the real stdout before we redirected it
150
+ real_stdout = sys.__stdout__
151
+
152
+ if verbose:
153
+ click.echo(f"šŸ”— Connecting to: {url}", err=True)
154
+ click.echo(f"šŸ“¦ Image: {headers.get('Mcp-Image', 'unknown')}", err=True)
155
+ click.echo(f"šŸ”‘ Headers: {list(headers.keys())}", err=True)
156
+
157
+ # Create proxy configuration
158
+ proxy_config = {
159
+ "mcpServers": {
160
+ "remote": {"transport": "streamable-http", "url": url, "headers": headers}
161
+ }
162
+ }
163
+
164
+ try:
165
+ # Restore stdout for the proxy to use
166
+ sys.stdout = real_stdout
167
+
168
+ # Create proxy that forwards remote HTTP to local stdio
169
+ proxy = FastMCP.as_proxy(proxy_config, name="HUD Remote Proxy")
170
+
171
+ # Run with stdio transport - this will handle stdin/stdout properly
172
+ await proxy.run_async(transport="stdio", show_banner=False)
173
+ except Exception as e:
174
+ # Ensure errors go to stderr
175
+ sys.stdout = sys.stderr
176
+ if verbose:
177
+ import traceback
178
+
179
+ click.echo(f"āŒ Proxy error: {e}", err=True)
180
+ click.echo(traceback.format_exc(), err=True)
181
+ raise
182
+
183
+ try:
184
+ asyncio.run(run())
185
+ except KeyboardInterrupt:
186
+ if verbose:
187
+ click.echo("\nāœ… Remote proxy stopped", err=True)
188
+ sys.exit(0)
189
+ except Exception as e:
190
+ if verbose:
191
+ click.echo(f"āŒ Error: {e}", err=True)
192
+ sys.exit(1)
193
+
194
+
195
+ async def run_remote_http(
196
+ url: str,
197
+ headers: dict[str, str],
198
+ port: int,
199
+ verbose: bool = False,
200
+ ) -> None:
201
+ """Run remote MCP server with HTTP transport."""
202
+ from .utils import find_free_port
203
+
204
+ # Find available port
205
+ actual_port = find_free_port(port)
206
+ if actual_port is None:
207
+ click.echo(f"āŒ No available ports found starting from {port}")
208
+ return
209
+
210
+ if actual_port != port:
211
+ click.echo(f"āš ļø Port {port} in use, using port {actual_port} instead")
212
+
213
+ # Suppress logs unless verbose
214
+ if not verbose:
215
+ import logging
216
+ import os
217
+
218
+ os.environ["FASTMCP_DISABLE_BANNER"] = "1"
219
+ logging.getLogger("fastmcp").setLevel(logging.ERROR)
220
+ logging.getLogger("mcp").setLevel(logging.ERROR)
221
+ logging.getLogger("uvicorn").setLevel(logging.ERROR)
222
+ logging.getLogger("uvicorn.access").setLevel(logging.ERROR)
223
+ logging.getLogger("uvicorn.error").setLevel(logging.ERROR)
224
+ logging.getLogger("httpx").setLevel(logging.ERROR)
225
+ logging.getLogger("httpcore").setLevel(logging.ERROR)
226
+
227
+ # Create the MCP config for the proxy
228
+ config = {"remote": {"transport": "streamable-http", "url": url, "headers": headers}}
229
+
230
+ # Create proxy that forwards remote HTTP to local HTTP
231
+ proxy = FastMCP.as_proxy(config, name="HUD Remote Proxy")
232
+
233
+ click.echo(f"🌐 Starting HTTP proxy on port {actual_port}")
234
+ click.echo(f"šŸ”— Server URL: http://localhost:{actual_port}/mcp")
235
+ click.echo(f"ā˜ļø Proxying to: {url}")
236
+ click.echo("ā¹ļø Press Ctrl+C to stop")
237
+
238
+ try:
239
+ # Run with HTTP transport
240
+ await proxy.run_async(
241
+ transport="http",
242
+ host="0.0.0.0", # noqa: S104
243
+ port=actual_port,
244
+ path="/mcp",
245
+ log_level="error" if not verbose else "info",
246
+ show_banner=False,
247
+ )
248
+ except KeyboardInterrupt:
249
+ click.echo("\nšŸ‘‹ Shutting down...")
250
+
251
+
252
+ def run_remote_server(
253
+ image: str,
254
+ docker_args: list[str],
255
+ transport: str,
256
+ port: int,
257
+ url: str,
258
+ api_key: str | None,
259
+ run_id: str | None,
260
+ verbose: bool,
261
+ ) -> None:
262
+ """Run remote MCP server via proxy.
263
+
264
+ Args:
265
+ image: Docker image name
266
+ docker_args: Docker-style arguments (-e, -h)
267
+ transport: Output transport (stdio or http)
268
+ port: Port for HTTP transport
269
+ url: Remote MCP server URL
270
+ api_key: API key for authentication
271
+ run_id: Optional run ID
272
+ verbose: Show detailed logs
273
+ """
274
+ # Parse docker args into env vars and headers
275
+ env_args = []
276
+ header_args = []
277
+
278
+ i = 0
279
+ while i < len(docker_args):
280
+ arg = docker_args[i]
281
+
282
+ if arg == "-e" and i + 1 < len(docker_args):
283
+ env_args.append(docker_args[i + 1])
284
+ i += 2
285
+ elif arg == "-h" and i + 1 < len(docker_args):
286
+ header_args.append(docker_args[i + 1])
287
+ i += 2
288
+ else:
289
+ click.echo(f"āš ļø Unknown argument: {arg}", err=True)
290
+ i += 1
291
+
292
+ # Get API key from env if not provided
293
+ if not api_key:
294
+ api_key = settings.api_key
295
+ if not api_key:
296
+ click.echo("āŒ API key required. Set HUD_API_KEY env var or use --api-key", err=True)
297
+ sys.exit(1)
298
+
299
+ # Build headers
300
+ headers = build_remote_headers(image, env_args, header_args, api_key, run_id)
301
+
302
+ if verbose:
303
+ click.echo(f"šŸ”§ Remote URL: {url}", err=True)
304
+ click.echo(f"šŸ“¦ Image: {image}", err=True)
305
+ click.echo(f"šŸ”‘ Headers: {list(headers.keys())}", err=True)
306
+
307
+ # Run based on transport
308
+ if transport == "stdio":
309
+ run_remote_stdio(url, headers, verbose)
310
+ else:
311
+ asyncio.run(run_remote_http(url, headers, port, verbose))