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/mcp_server.py
CHANGED
|
@@ -1,756 +1,764 @@
|
|
|
1
|
-
"""MCP Development Proxy - Hot-reload environments with MCP over HTTP."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import asyncio
|
|
6
|
-
import base64
|
|
7
|
-
import json
|
|
8
|
-
import subprocess
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
|
|
11
|
-
import click
|
|
12
|
-
import toml
|
|
13
|
-
from fastmcp import FastMCP
|
|
14
|
-
|
|
15
|
-
from .
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
""
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
config
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
design.info("
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
design.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
"
|
|
128
|
-
"
|
|
129
|
-
"
|
|
130
|
-
"
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
"
|
|
135
|
-
"
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if interactive
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
docker_cmd.
|
|
151
|
-
docker_cmd.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
"
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if verbose
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
logging
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
"
|
|
243
|
-
"
|
|
244
|
-
"
|
|
245
|
-
"
|
|
246
|
-
"
|
|
247
|
-
"
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
#
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
#
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
interactive_thread
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
log_design
|
|
414
|
-
|
|
415
|
-
#
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
"
|
|
425
|
-
"
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
)
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
if
|
|
460
|
-
continue
|
|
461
|
-
|
|
462
|
-
#
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
"",
|
|
503
|
-
"fastmcp",
|
|
504
|
-
"
|
|
505
|
-
"
|
|
506
|
-
"
|
|
507
|
-
"
|
|
508
|
-
"
|
|
509
|
-
"
|
|
510
|
-
"
|
|
511
|
-
"
|
|
512
|
-
"
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
)
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
-
|
|
627
|
-
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
hud dev .
|
|
632
|
-
hud dev . --
|
|
633
|
-
hud dev . --
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
#
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
if
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
#
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
if actual_port
|
|
674
|
-
design.
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
)
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
design.
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
design.progress_message("
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
full_config =
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
design.
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
)
|
|
739
|
-
design.
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
1
|
+
"""MCP Development Proxy - Hot-reload environments with MCP over HTTP."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import base64
|
|
7
|
+
import json
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
import toml
|
|
13
|
+
from fastmcp import FastMCP
|
|
14
|
+
|
|
15
|
+
from hud.utils.design import HUDDesign
|
|
16
|
+
from .docker_utils import get_docker_cmd, image_exists, inject_supervisor
|
|
17
|
+
|
|
18
|
+
# Global design instance
|
|
19
|
+
design = HUDDesign()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_image_name(directory: str | Path, image_override: str | None = None) -> tuple[str, str]:
|
|
23
|
+
"""
|
|
24
|
+
Resolve image name with source tracking.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Tuple of (image_name, source) where source is "override", "cache", or "auto"
|
|
28
|
+
"""
|
|
29
|
+
if image_override:
|
|
30
|
+
return image_override, "override"
|
|
31
|
+
|
|
32
|
+
# Check pyproject.toml
|
|
33
|
+
pyproject_path = Path(directory) / "pyproject.toml"
|
|
34
|
+
if pyproject_path.exists():
|
|
35
|
+
try:
|
|
36
|
+
with open(pyproject_path) as f:
|
|
37
|
+
config = toml.load(f)
|
|
38
|
+
if config.get("tool", {}).get("hud", {}).get("image"):
|
|
39
|
+
return config["tool"]["hud"]["image"], "cache"
|
|
40
|
+
except Exception:
|
|
41
|
+
pass # Silent failure, will use auto-generated name
|
|
42
|
+
|
|
43
|
+
# Auto-generate with :dev tag
|
|
44
|
+
dir_path = Path(directory).resolve() # Get absolute path first
|
|
45
|
+
dir_name = dir_path.name
|
|
46
|
+
if not dir_name or dir_name == ".":
|
|
47
|
+
# If we're in root or have empty name, use parent directory
|
|
48
|
+
dir_name = dir_path.parent.name
|
|
49
|
+
clean_name = dir_name.replace("_", "-")
|
|
50
|
+
return f"hud-{clean_name}:dev", "auto"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def update_pyproject_toml(directory: str | Path, image_name: str, silent: bool = False) -> None:
|
|
54
|
+
"""Update pyproject.toml with image name."""
|
|
55
|
+
pyproject_path = Path(directory) / "pyproject.toml"
|
|
56
|
+
if pyproject_path.exists():
|
|
57
|
+
try:
|
|
58
|
+
with open(pyproject_path) as f:
|
|
59
|
+
config = toml.load(f)
|
|
60
|
+
|
|
61
|
+
# Ensure [tool.hud] exists
|
|
62
|
+
if "tool" not in config:
|
|
63
|
+
config["tool"] = {}
|
|
64
|
+
if "hud" not in config["tool"]:
|
|
65
|
+
config["tool"]["hud"] = {}
|
|
66
|
+
|
|
67
|
+
# Update image name
|
|
68
|
+
config["tool"]["hud"]["image"] = image_name
|
|
69
|
+
|
|
70
|
+
# Write back
|
|
71
|
+
with open(pyproject_path, "w") as f:
|
|
72
|
+
toml.dump(config, f)
|
|
73
|
+
|
|
74
|
+
if not silent:
|
|
75
|
+
design.success(f"Updated pyproject.toml with image: {image_name}")
|
|
76
|
+
except Exception as e:
|
|
77
|
+
if not silent:
|
|
78
|
+
design.warning(f"Could not update pyproject.toml: {e}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def build_and_update(directory: str | Path, image_name: str, no_cache: bool = False) -> None:
|
|
82
|
+
"""Build Docker image and update pyproject.toml."""
|
|
83
|
+
|
|
84
|
+
build_cmd = ["docker", "build", "-t", image_name]
|
|
85
|
+
if no_cache:
|
|
86
|
+
build_cmd.append("--no-cache")
|
|
87
|
+
build_cmd.append(str(directory))
|
|
88
|
+
|
|
89
|
+
design.info(f"๐จ Building image: {image_name}{' (no cache)' if no_cache else ''}")
|
|
90
|
+
design.info("") # Empty line before Docker output
|
|
91
|
+
|
|
92
|
+
# Just run Docker build directly - it has its own nice live display
|
|
93
|
+
result = subprocess.run(build_cmd) # noqa: S603
|
|
94
|
+
|
|
95
|
+
if result.returncode == 0:
|
|
96
|
+
design.info("") # Empty line after Docker output
|
|
97
|
+
design.success(f"Build successful! Image: {image_name}")
|
|
98
|
+
# Update pyproject.toml (silently since we already showed success)
|
|
99
|
+
update_pyproject_toml(directory, image_name, silent=True)
|
|
100
|
+
else:
|
|
101
|
+
design.error("Build failed!")
|
|
102
|
+
raise click.Abort
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def create_proxy_server(
|
|
106
|
+
directory: str | Path,
|
|
107
|
+
image_name: str,
|
|
108
|
+
no_reload: bool = False,
|
|
109
|
+
verbose: bool = False,
|
|
110
|
+
docker_args: list[str] | None = None,
|
|
111
|
+
interactive: bool = False,
|
|
112
|
+
) -> FastMCP:
|
|
113
|
+
"""Create an HTTP proxy server that forwards to Docker container with hot-reload."""
|
|
114
|
+
src_path = Path(directory) / "src"
|
|
115
|
+
|
|
116
|
+
# Get the original CMD from the image
|
|
117
|
+
original_cmd = get_docker_cmd(image_name)
|
|
118
|
+
if not original_cmd:
|
|
119
|
+
design.warning(f"Could not extract CMD from {image_name}, using default")
|
|
120
|
+
original_cmd = ["python", "-m", "hud_controller.server"]
|
|
121
|
+
|
|
122
|
+
# Generate container name from image
|
|
123
|
+
container_name = f"{image_name.replace(':', '-').replace('/', '-')}"
|
|
124
|
+
|
|
125
|
+
# Build the docker run command
|
|
126
|
+
docker_cmd = [
|
|
127
|
+
"docker",
|
|
128
|
+
"run",
|
|
129
|
+
"--rm",
|
|
130
|
+
"-i",
|
|
131
|
+
"--name",
|
|
132
|
+
container_name,
|
|
133
|
+
"-v",
|
|
134
|
+
f"{src_path.absolute()}:/app/src:rw",
|
|
135
|
+
"-e",
|
|
136
|
+
"PYTHONPATH=/app/src",
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
# Add user-provided Docker arguments
|
|
140
|
+
if docker_args:
|
|
141
|
+
docker_cmd.extend(docker_args)
|
|
142
|
+
|
|
143
|
+
# Disable hot-reload if interactive mode is enabled
|
|
144
|
+
if interactive:
|
|
145
|
+
no_reload = True
|
|
146
|
+
|
|
147
|
+
if not no_reload:
|
|
148
|
+
# Inject our supervisor into the CMD
|
|
149
|
+
modified_cmd = inject_supervisor(original_cmd)
|
|
150
|
+
docker_cmd.extend(["--entrypoint", modified_cmd[0]])
|
|
151
|
+
docker_cmd.append(image_name)
|
|
152
|
+
docker_cmd.extend(modified_cmd[1:])
|
|
153
|
+
else:
|
|
154
|
+
# No reload - use original CMD
|
|
155
|
+
docker_cmd.append(image_name)
|
|
156
|
+
|
|
157
|
+
# Create configuration following MCPConfig schema
|
|
158
|
+
config = {
|
|
159
|
+
"mcpServers": {
|
|
160
|
+
"default": {
|
|
161
|
+
"command": docker_cmd[0],
|
|
162
|
+
"args": docker_cmd[1:] if len(docker_cmd) > 1 else [],
|
|
163
|
+
# transport defaults to stdio
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# Debug output - only if verbose
|
|
169
|
+
if verbose:
|
|
170
|
+
if not no_reload:
|
|
171
|
+
design.info("Watching: /app/src for changes")
|
|
172
|
+
else:
|
|
173
|
+
design.info("Container will run without hot-reload")
|
|
174
|
+
design.command_example(f"docker logs -f {container_name}", "View container logs")
|
|
175
|
+
|
|
176
|
+
# Create the HTTP proxy server using config
|
|
177
|
+
try:
|
|
178
|
+
proxy = FastMCP.as_proxy(config, name=f"HUD Dev Proxy - {image_name}")
|
|
179
|
+
except Exception as e:
|
|
180
|
+
design.error(f"Failed to create proxy server: {e}")
|
|
181
|
+
design.info("")
|
|
182
|
+
design.info("๐ก Tip: Run the following command to debug the container:")
|
|
183
|
+
design.info(f" hud debug {image_name}")
|
|
184
|
+
raise
|
|
185
|
+
|
|
186
|
+
return proxy
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
async def start_mcp_proxy(
|
|
190
|
+
directory: str | Path,
|
|
191
|
+
image_name: str,
|
|
192
|
+
transport: str,
|
|
193
|
+
port: int,
|
|
194
|
+
no_reload: bool = False,
|
|
195
|
+
verbose: bool = False,
|
|
196
|
+
inspector: bool = False,
|
|
197
|
+
no_logs: bool = False,
|
|
198
|
+
interactive: bool = False,
|
|
199
|
+
docker_args: list[str] | None = None,
|
|
200
|
+
) -> None:
|
|
201
|
+
"""Start the MCP development proxy server."""
|
|
202
|
+
# Suppress FastMCP's verbose output FIRST
|
|
203
|
+
import asyncio
|
|
204
|
+
import logging
|
|
205
|
+
import os
|
|
206
|
+
import subprocess
|
|
207
|
+
import sys
|
|
208
|
+
|
|
209
|
+
from .utils import find_free_port
|
|
210
|
+
|
|
211
|
+
# Always disable the banner - we have our own output
|
|
212
|
+
os.environ["FASTMCP_DISABLE_BANNER"] = "1"
|
|
213
|
+
|
|
214
|
+
# Configure logging BEFORE creating proxy
|
|
215
|
+
if not verbose:
|
|
216
|
+
# Create a filter to block the specific "Starting MCP server" message
|
|
217
|
+
class _BlockStartingMCPFilter(logging.Filter):
|
|
218
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
219
|
+
return "Starting MCP server" not in record.getMessage()
|
|
220
|
+
|
|
221
|
+
# Set environment variable for FastMCP logging
|
|
222
|
+
os.environ["FASTMCP_LOG_LEVEL"] = "ERROR"
|
|
223
|
+
os.environ["LOG_LEVEL"] = "ERROR"
|
|
224
|
+
os.environ["UVICORN_LOG_LEVEL"] = "ERROR"
|
|
225
|
+
# Suppress uvicorn's annoying shutdown messages
|
|
226
|
+
os.environ["UVICORN_ACCESS_LOG"] = "0"
|
|
227
|
+
|
|
228
|
+
# Configure logging to suppress INFO
|
|
229
|
+
logging.basicConfig(level=logging.ERROR, force=True)
|
|
230
|
+
|
|
231
|
+
# Set root logger to ERROR to suppress all INFO messages
|
|
232
|
+
root_logger = logging.getLogger()
|
|
233
|
+
root_logger.setLevel(logging.ERROR)
|
|
234
|
+
|
|
235
|
+
# Add filter to all handlers
|
|
236
|
+
block_filter = _BlockStartingMCPFilter()
|
|
237
|
+
for handler in root_logger.handlers:
|
|
238
|
+
handler.addFilter(block_filter)
|
|
239
|
+
|
|
240
|
+
# Also specifically suppress these loggers
|
|
241
|
+
for logger_name in [
|
|
242
|
+
"fastmcp",
|
|
243
|
+
"fastmcp.server",
|
|
244
|
+
"fastmcp.server.server",
|
|
245
|
+
"FastMCP",
|
|
246
|
+
"FastMCP.fastmcp.server.server",
|
|
247
|
+
"mcp",
|
|
248
|
+
"mcp.server",
|
|
249
|
+
"mcp.server.lowlevel",
|
|
250
|
+
"mcp.server.lowlevel.server",
|
|
251
|
+
"uvicorn",
|
|
252
|
+
"uvicorn.access",
|
|
253
|
+
"uvicorn.error",
|
|
254
|
+
"hud.server",
|
|
255
|
+
"hud.server.server",
|
|
256
|
+
]:
|
|
257
|
+
logger = logging.getLogger(logger_name)
|
|
258
|
+
logger.setLevel(logging.ERROR)
|
|
259
|
+
# Add filter to this logger too
|
|
260
|
+
logger.addFilter(block_filter)
|
|
261
|
+
|
|
262
|
+
# Suppress deprecation warnings
|
|
263
|
+
import warnings
|
|
264
|
+
|
|
265
|
+
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
|
266
|
+
|
|
267
|
+
# CRITICAL: For stdio transport, ALL output must go to stderr
|
|
268
|
+
if transport == "stdio":
|
|
269
|
+
# Configure root logger to use stderr
|
|
270
|
+
root_logger = logging.getLogger()
|
|
271
|
+
root_logger.handlers.clear()
|
|
272
|
+
stderr_handler = logging.StreamHandler(sys.stderr)
|
|
273
|
+
root_logger.addHandler(stderr_handler)
|
|
274
|
+
|
|
275
|
+
# Now check for src directory
|
|
276
|
+
src_path = Path(directory) / "src"
|
|
277
|
+
if not src_path.exists():
|
|
278
|
+
design.error(f"Source directory not found: {src_path}")
|
|
279
|
+
raise click.Abort
|
|
280
|
+
|
|
281
|
+
# Extract container name from the proxy configuration
|
|
282
|
+
container_name = f"{image_name.replace(':', '-').replace('/', '-')}"
|
|
283
|
+
|
|
284
|
+
# Remove any existing container with the same name (silently)
|
|
285
|
+
# Note: The proxy creates containers on-demand when clients connect
|
|
286
|
+
try:
|
|
287
|
+
subprocess.run( # noqa: S603, ASYNC221
|
|
288
|
+
["docker", "rm", "-f", container_name], # noqa: S607
|
|
289
|
+
stdout=subprocess.DEVNULL,
|
|
290
|
+
stderr=subprocess.DEVNULL,
|
|
291
|
+
check=False, # Don't raise error if container doesn't exist
|
|
292
|
+
)
|
|
293
|
+
except Exception:
|
|
294
|
+
pass # Silent failure, container might not exist
|
|
295
|
+
|
|
296
|
+
if transport == "stdio":
|
|
297
|
+
if verbose:
|
|
298
|
+
design.info("Starting stdio proxy (each connection gets its own container)")
|
|
299
|
+
else:
|
|
300
|
+
# Find available port for HTTP
|
|
301
|
+
actual_port = find_free_port(port)
|
|
302
|
+
if actual_port is None:
|
|
303
|
+
design.error(f"No available ports found starting from {port}")
|
|
304
|
+
raise click.Abort
|
|
305
|
+
|
|
306
|
+
if actual_port != port and verbose:
|
|
307
|
+
design.warning(f"Port {port} in use, using port {actual_port} instead")
|
|
308
|
+
|
|
309
|
+
# Launch MCP Inspector if requested
|
|
310
|
+
if inspector:
|
|
311
|
+
server_url = f"http://localhost:{actual_port}/mcp"
|
|
312
|
+
|
|
313
|
+
# Function to launch inspector in background
|
|
314
|
+
async def launch_inspector() -> None:
|
|
315
|
+
"""Launch MCP Inspector and capture its output to extract the URL."""
|
|
316
|
+
# Wait for server to be ready
|
|
317
|
+
await asyncio.sleep(3)
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
import platform
|
|
321
|
+
import urllib.parse
|
|
322
|
+
|
|
323
|
+
# Build the direct URL with query params to auto-connect
|
|
324
|
+
encoded_url = urllib.parse.quote(server_url)
|
|
325
|
+
inspector_url = (
|
|
326
|
+
f"http://localhost:6274/?transport=streamable-http&serverUrl={encoded_url}"
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Print inspector info cleanly
|
|
330
|
+
design.section_title("MCP Inspector")
|
|
331
|
+
design.link(inspector_url)
|
|
332
|
+
|
|
333
|
+
# Set environment to disable auth (for development only)
|
|
334
|
+
env = os.environ.copy()
|
|
335
|
+
env["DANGEROUSLY_OMIT_AUTH"] = "true"
|
|
336
|
+
env["MCP_AUTO_OPEN_ENABLED"] = "true"
|
|
337
|
+
|
|
338
|
+
# Launch inspector
|
|
339
|
+
cmd = ["npx", "--yes", "@modelcontextprotocol/inspector"]
|
|
340
|
+
|
|
341
|
+
# Run in background, suppressing output to avoid log interference
|
|
342
|
+
if platform.system() == "Windows":
|
|
343
|
+
subprocess.Popen( # noqa: S602, ASYNC220
|
|
344
|
+
cmd,
|
|
345
|
+
env=env,
|
|
346
|
+
shell=True,
|
|
347
|
+
stdout=subprocess.DEVNULL,
|
|
348
|
+
stderr=subprocess.DEVNULL,
|
|
349
|
+
)
|
|
350
|
+
else:
|
|
351
|
+
subprocess.Popen( # noqa: S603, ASYNC220
|
|
352
|
+
cmd, env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
except (FileNotFoundError, Exception):
|
|
356
|
+
# Silently fail - inspector is optional
|
|
357
|
+
design.error("Failed to launch inspector")
|
|
358
|
+
|
|
359
|
+
# Launch inspector asynchronously so it doesn't block
|
|
360
|
+
asyncio.create_task(launch_inspector())
|
|
361
|
+
|
|
362
|
+
# Launch interactive mode if requested
|
|
363
|
+
if interactive:
|
|
364
|
+
if transport != "http":
|
|
365
|
+
from hud.utils.design import HUDDesign
|
|
366
|
+
|
|
367
|
+
design.warning("Interactive mode only works with HTTP transport")
|
|
368
|
+
else:
|
|
369
|
+
server_url = f"http://localhost:{actual_port}/mcp"
|
|
370
|
+
|
|
371
|
+
# Function to launch interactive mode in a separate thread
|
|
372
|
+
def launch_interactive_thread() -> None:
|
|
373
|
+
"""Launch interactive testing mode in a separate thread."""
|
|
374
|
+
import time
|
|
375
|
+
|
|
376
|
+
# Wait for server to be ready
|
|
377
|
+
time.sleep(3)
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
design.section_title("Interactive Mode")
|
|
381
|
+
design.info("Starting interactive testing mode...")
|
|
382
|
+
design.info("Press Ctrl+C in the interactive session to exit")
|
|
383
|
+
|
|
384
|
+
# Import and run interactive mode in a new event loop
|
|
385
|
+
from .interactive import run_interactive_mode
|
|
386
|
+
|
|
387
|
+
# Create a new event loop for the thread
|
|
388
|
+
loop = asyncio.new_event_loop()
|
|
389
|
+
asyncio.set_event_loop(loop)
|
|
390
|
+
try:
|
|
391
|
+
loop.run_until_complete(run_interactive_mode(server_url, verbose))
|
|
392
|
+
finally:
|
|
393
|
+
loop.close()
|
|
394
|
+
|
|
395
|
+
except Exception as e:
|
|
396
|
+
# Log error but don't crash the server
|
|
397
|
+
if verbose:
|
|
398
|
+
design.error(f"Interactive mode error: {e}")
|
|
399
|
+
|
|
400
|
+
# Launch interactive mode in a separate thread
|
|
401
|
+
import threading
|
|
402
|
+
|
|
403
|
+
interactive_thread = threading.Thread(target=launch_interactive_thread, daemon=True)
|
|
404
|
+
interactive_thread.start()
|
|
405
|
+
|
|
406
|
+
# Function to stream Docker logs
|
|
407
|
+
async def stream_docker_logs() -> None:
|
|
408
|
+
"""Stream Docker container logs asynchronously."""
|
|
409
|
+
log_design = design
|
|
410
|
+
|
|
411
|
+
# Always show waiting message
|
|
412
|
+
log_design.info("") # Empty line for spacing
|
|
413
|
+
log_design.progress_message("โณ Waiting for first client connection to start container...")
|
|
414
|
+
|
|
415
|
+
# Keep trying to stream logs - container is created on demand
|
|
416
|
+
has_shown_started = False
|
|
417
|
+
while True:
|
|
418
|
+
# Check if container exists first (silently)
|
|
419
|
+
check_result = await asyncio.create_subprocess_exec(
|
|
420
|
+
"docker",
|
|
421
|
+
"ps",
|
|
422
|
+
"--format",
|
|
423
|
+
"{{.Names}}",
|
|
424
|
+
"--filter",
|
|
425
|
+
f"name={container_name}",
|
|
426
|
+
stdout=asyncio.subprocess.PIPE,
|
|
427
|
+
stderr=asyncio.subprocess.DEVNULL,
|
|
428
|
+
)
|
|
429
|
+
stdout, _ = await check_result.communicate()
|
|
430
|
+
|
|
431
|
+
# If container doesn't exist, wait and retry
|
|
432
|
+
if container_name not in stdout.decode():
|
|
433
|
+
await asyncio.sleep(1)
|
|
434
|
+
continue
|
|
435
|
+
|
|
436
|
+
# Container exists! Show success if first time
|
|
437
|
+
if not has_shown_started:
|
|
438
|
+
log_design.success("Container started! Streaming logs...")
|
|
439
|
+
has_shown_started = True
|
|
440
|
+
|
|
441
|
+
# Now stream the logs
|
|
442
|
+
try:
|
|
443
|
+
process = await asyncio.create_subprocess_exec(
|
|
444
|
+
"docker",
|
|
445
|
+
"logs",
|
|
446
|
+
"-f",
|
|
447
|
+
container_name,
|
|
448
|
+
stdout=asyncio.subprocess.PIPE,
|
|
449
|
+
stderr=asyncio.subprocess.STDOUT, # Combine streams for simplicity
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
if process.stdout:
|
|
453
|
+
async for line in process.stdout:
|
|
454
|
+
decoded_line = line.decode().rstrip()
|
|
455
|
+
if not decoded_line: # Skip empty lines
|
|
456
|
+
continue
|
|
457
|
+
|
|
458
|
+
# Skip docker daemon errors (these happen when container is removed)
|
|
459
|
+
if "Error response from daemon" in decoded_line:
|
|
460
|
+
continue
|
|
461
|
+
|
|
462
|
+
# Show all logs with gold formatting like hud debug
|
|
463
|
+
# Format all logs in gold/dim style like hud debug's stderr
|
|
464
|
+
log_design.console.print(
|
|
465
|
+
f"[rgb(192,150,12)]โ [/rgb(192,150,12)] {decoded_line}", highlight=False
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
# Process ended - container might have been removed
|
|
469
|
+
await process.wait()
|
|
470
|
+
|
|
471
|
+
# Check if container still exists
|
|
472
|
+
await asyncio.sleep(1)
|
|
473
|
+
continue # Loop back to check if container exists
|
|
474
|
+
|
|
475
|
+
except Exception:
|
|
476
|
+
# Some unexpected error
|
|
477
|
+
if verbose:
|
|
478
|
+
log_design.warning("Failed to stream logs")
|
|
479
|
+
await asyncio.sleep(1)
|
|
480
|
+
|
|
481
|
+
# CRITICAL: Create proxy AFTER all logging setup to prevent it from resetting logging config
|
|
482
|
+
# This is important because FastMCP might initialize loggers during creation
|
|
483
|
+
proxy = create_proxy_server(
|
|
484
|
+
directory, image_name, no_reload, verbose, docker_args or [], interactive
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
# One more attempt to suppress the FastMCP server log
|
|
488
|
+
if not verbose:
|
|
489
|
+
# Re-apply the filter in case new handlers were created
|
|
490
|
+
class BlockStartingMCPFilter(logging.Filter):
|
|
491
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
492
|
+
return "Starting MCP server" not in record.getMessage()
|
|
493
|
+
|
|
494
|
+
block_filter = BlockStartingMCPFilter()
|
|
495
|
+
|
|
496
|
+
# Apply to all loggers again - comprehensive list
|
|
497
|
+
for logger_name in [
|
|
498
|
+
"", # root logger
|
|
499
|
+
"fastmcp",
|
|
500
|
+
"fastmcp.server",
|
|
501
|
+
"fastmcp.server.server",
|
|
502
|
+
"FastMCP",
|
|
503
|
+
"FastMCP.fastmcp.server.server",
|
|
504
|
+
"mcp",
|
|
505
|
+
"mcp.server",
|
|
506
|
+
"mcp.server.lowlevel",
|
|
507
|
+
"mcp.server.lowlevel.server",
|
|
508
|
+
"uvicorn",
|
|
509
|
+
"uvicorn.access",
|
|
510
|
+
"uvicorn.error",
|
|
511
|
+
"hud.server",
|
|
512
|
+
"hud.server.server",
|
|
513
|
+
]:
|
|
514
|
+
logger = logging.getLogger(logger_name)
|
|
515
|
+
logger.setLevel(logging.ERROR)
|
|
516
|
+
logger.addFilter(block_filter)
|
|
517
|
+
for handler in logger.handlers:
|
|
518
|
+
handler.addFilter(block_filter)
|
|
519
|
+
|
|
520
|
+
try:
|
|
521
|
+
# Start Docker logs streaming if enabled
|
|
522
|
+
log_task = None
|
|
523
|
+
if not no_logs:
|
|
524
|
+
log_task = asyncio.create_task(stream_docker_logs())
|
|
525
|
+
|
|
526
|
+
if transport == "stdio":
|
|
527
|
+
# Run with stdio transport
|
|
528
|
+
await proxy.run_async(
|
|
529
|
+
transport="stdio", log_level="ERROR" if not verbose else "INFO", show_banner=False
|
|
530
|
+
)
|
|
531
|
+
else:
|
|
532
|
+
# Run with HTTP transport
|
|
533
|
+
# Temporarily redirect stderr to suppress uvicorn shutdown messages
|
|
534
|
+
import contextlib
|
|
535
|
+
import io
|
|
536
|
+
|
|
537
|
+
if not verbose:
|
|
538
|
+
# Create a dummy file to swallow unwanted stderr output
|
|
539
|
+
with contextlib.redirect_stderr(io.StringIO()):
|
|
540
|
+
await proxy.run_async(
|
|
541
|
+
transport="http",
|
|
542
|
+
host="0.0.0.0", # noqa: S104
|
|
543
|
+
port=actual_port,
|
|
544
|
+
path="/mcp", # Serve at /mcp endpoint
|
|
545
|
+
log_level="ERROR",
|
|
546
|
+
show_banner=False,
|
|
547
|
+
)
|
|
548
|
+
else:
|
|
549
|
+
await proxy.run_async(
|
|
550
|
+
transport="http",
|
|
551
|
+
host="0.0.0.0", # noqa: S104
|
|
552
|
+
port=actual_port,
|
|
553
|
+
path="/mcp", # Serve at /mcp endpoint
|
|
554
|
+
log_level="INFO",
|
|
555
|
+
show_banner=False,
|
|
556
|
+
)
|
|
557
|
+
except (ConnectionError, OSError) as e:
|
|
558
|
+
design.error(f"Failed to connect to Docker container: {e}")
|
|
559
|
+
design.info("")
|
|
560
|
+
design.info("๐ก Tip: Run the following command to debug the container:")
|
|
561
|
+
design.info(f" hud debug {image_name}")
|
|
562
|
+
design.info("")
|
|
563
|
+
design.info("Common issues:")
|
|
564
|
+
design.info(" โข Container failed to start or crashed immediately")
|
|
565
|
+
design.info(" โข Server initialization failed")
|
|
566
|
+
design.info(" โข Port binding conflicts")
|
|
567
|
+
raise
|
|
568
|
+
except KeyboardInterrupt:
|
|
569
|
+
design.info("\n๐ Shutting down...")
|
|
570
|
+
|
|
571
|
+
# Show next steps tutorial
|
|
572
|
+
if not interactive: # Only show if not in interactive mode
|
|
573
|
+
design.section_title("Next Steps")
|
|
574
|
+
design.info("๐๏ธ Ready to test with real agents? Run:")
|
|
575
|
+
design.info(f" [cyan]hud build {directory}[/cyan]")
|
|
576
|
+
design.info("")
|
|
577
|
+
design.info("This will:")
|
|
578
|
+
design.info(" 1. Build your environment image")
|
|
579
|
+
design.info(" 2. Generate a hud.lock.yaml file")
|
|
580
|
+
design.info(" 3. Prepare it for testing with agents")
|
|
581
|
+
design.info("")
|
|
582
|
+
design.info("Then you can:")
|
|
583
|
+
design.info(" โข Test locally: [cyan]hud run <image>[/cyan]")
|
|
584
|
+
design.info(
|
|
585
|
+
" โข Push to registry: [cyan]hud push --image <registry/name>[/cyan]"
|
|
586
|
+
)
|
|
587
|
+
except Exception as e:
|
|
588
|
+
# Suppress the graceful shutdown error and other FastMCP/uvicorn internal errors
|
|
589
|
+
error_msg = str(e)
|
|
590
|
+
if not any(
|
|
591
|
+
x in error_msg
|
|
592
|
+
for x in [
|
|
593
|
+
"timeout graceful shutdown exceeded",
|
|
594
|
+
"Cancel 0 running task(s)",
|
|
595
|
+
"Application shutdown complete",
|
|
596
|
+
]
|
|
597
|
+
):
|
|
598
|
+
design.error(f"Unexpected error: {e}")
|
|
599
|
+
finally:
|
|
600
|
+
# Cancel log streaming task if it exists
|
|
601
|
+
if log_task and not log_task.done():
|
|
602
|
+
log_task.cancel()
|
|
603
|
+
try:
|
|
604
|
+
await log_task
|
|
605
|
+
except asyncio.CancelledError:
|
|
606
|
+
pass # Log streaming cancelled, normal shutdown
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def run_mcp_dev_server(
|
|
610
|
+
directory: str = ".",
|
|
611
|
+
image: str | None = None,
|
|
612
|
+
build: bool = False,
|
|
613
|
+
no_cache: bool = False,
|
|
614
|
+
transport: str = "http",
|
|
615
|
+
port: int = 8765,
|
|
616
|
+
no_reload: bool = False,
|
|
617
|
+
verbose: bool = False,
|
|
618
|
+
inspector: bool = False,
|
|
619
|
+
no_logs: bool = False,
|
|
620
|
+
interactive: bool = False,
|
|
621
|
+
docker_args: list[str] | None = None,
|
|
622
|
+
) -> None:
|
|
623
|
+
"""Run MCP development server with hot-reload.
|
|
624
|
+
|
|
625
|
+
This command starts a development proxy that:
|
|
626
|
+
- Auto-detects or builds Docker images
|
|
627
|
+
- Mounts local source code for hot-reload
|
|
628
|
+
- Exposes an HTTP endpoint for MCP clients
|
|
629
|
+
|
|
630
|
+
Examples:
|
|
631
|
+
hud dev . # Auto-detect image from directory
|
|
632
|
+
hud dev . --build # Build image first
|
|
633
|
+
hud dev . --image custom:tag # Use specific image
|
|
634
|
+
hud dev . --no-cache # Force clean rebuild
|
|
635
|
+
"""
|
|
636
|
+
# Ensure directory exists
|
|
637
|
+
if not Path(directory).exists():
|
|
638
|
+
design.error(f"Directory not found: {directory}")
|
|
639
|
+
raise click.Abort
|
|
640
|
+
|
|
641
|
+
# No external dependencies needed for hot-reload anymore!
|
|
642
|
+
|
|
643
|
+
# Resolve image name
|
|
644
|
+
resolved_image, source = get_image_name(directory, image)
|
|
645
|
+
|
|
646
|
+
# Update pyproject.toml with auto-generated name if needed
|
|
647
|
+
if source == "auto":
|
|
648
|
+
update_pyproject_toml(directory, resolved_image)
|
|
649
|
+
|
|
650
|
+
# Build if requested
|
|
651
|
+
if build or no_cache:
|
|
652
|
+
build_and_update(directory, resolved_image, no_cache)
|
|
653
|
+
|
|
654
|
+
# Check if image exists
|
|
655
|
+
if not image_exists(resolved_image) and not build:
|
|
656
|
+
if click.confirm(f"Image {resolved_image} not found. Build it now?"):
|
|
657
|
+
build_and_update(directory, resolved_image)
|
|
658
|
+
else:
|
|
659
|
+
raise click.Abort
|
|
660
|
+
|
|
661
|
+
# Generate server name from image
|
|
662
|
+
server_name = resolved_image.split(":")[0] if ":" in resolved_image else resolved_image
|
|
663
|
+
|
|
664
|
+
# For HTTP transport, find available port first
|
|
665
|
+
actual_port = port
|
|
666
|
+
if transport == "http":
|
|
667
|
+
from .utils import find_free_port
|
|
668
|
+
|
|
669
|
+
actual_port = find_free_port(port)
|
|
670
|
+
if actual_port is None:
|
|
671
|
+
design.error(f"No available ports found starting from {port}")
|
|
672
|
+
raise click.Abort
|
|
673
|
+
if actual_port != port and verbose:
|
|
674
|
+
design.warning(f"Port {port} in use, using port {actual_port}")
|
|
675
|
+
|
|
676
|
+
# Create config
|
|
677
|
+
if transport == "stdio":
|
|
678
|
+
server_config = {"command": "hud", "args": ["dev", directory, "--transport", "stdio"]}
|
|
679
|
+
else:
|
|
680
|
+
server_config = {"url": f"http://localhost:{actual_port}/mcp"}
|
|
681
|
+
|
|
682
|
+
# For the deeplink, we only need the server config
|
|
683
|
+
server_config_json = json.dumps(server_config, indent=2)
|
|
684
|
+
config_base64 = base64.b64encode(server_config_json.encode()).decode()
|
|
685
|
+
|
|
686
|
+
# Generate deeplink
|
|
687
|
+
deeplink = (
|
|
688
|
+
f"cursor://anysphere.cursor-deeplink/mcp/install?name={server_name}&config={config_base64}"
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
# Show header with gold border
|
|
692
|
+
design.info("") # Empty line before header
|
|
693
|
+
design.header("HUD Development Server")
|
|
694
|
+
|
|
695
|
+
# Always show the Docker image being used as the first thing after header
|
|
696
|
+
design.section_title("Docker Image")
|
|
697
|
+
if source == "cache":
|
|
698
|
+
design.info(f"๐ฆ {resolved_image}")
|
|
699
|
+
elif source == "auto":
|
|
700
|
+
design.info(f"๐ง {resolved_image} (auto-generated)")
|
|
701
|
+
elif source == "override":
|
|
702
|
+
design.info(f"๐ฏ {resolved_image} (specified)")
|
|
703
|
+
else:
|
|
704
|
+
design.info(f"๐ณ {resolved_image}")
|
|
705
|
+
|
|
706
|
+
design.progress_message(f"โ If any issues arise, run `hud debug {resolved_image}` to debug the container")
|
|
707
|
+
|
|
708
|
+
# Show hints about inspector and interactive mode
|
|
709
|
+
if transport == "http":
|
|
710
|
+
if not inspector and not interactive:
|
|
711
|
+
design.progress_message("๐ก Run with --inspector to launch MCP Inspector")
|
|
712
|
+
design.progress_message("๐งช Run with --interactive for interactive testing mode")
|
|
713
|
+
elif not inspector:
|
|
714
|
+
design.progress_message("๐ก Run with --inspector to launch MCP Inspector")
|
|
715
|
+
elif not interactive:
|
|
716
|
+
design.progress_message("๐งช Run with --interactive for interactive testing mode")
|
|
717
|
+
|
|
718
|
+
# Disable logs and hot-reload if interactive mode is enabled
|
|
719
|
+
if interactive:
|
|
720
|
+
if not no_logs:
|
|
721
|
+
design.warning("Docker logs disabled in interactive mode for better UI experience")
|
|
722
|
+
no_logs = True
|
|
723
|
+
if not no_reload:
|
|
724
|
+
design.warning("Hot-reload disabled in interactive mode to prevent output interference")
|
|
725
|
+
no_reload = True
|
|
726
|
+
|
|
727
|
+
# Show configuration as JSON (just the server config, not wrapped)
|
|
728
|
+
full_config = {}
|
|
729
|
+
full_config[server_name] = server_config
|
|
730
|
+
|
|
731
|
+
design.section_title("MCP Configuration (add this to any agent/client)")
|
|
732
|
+
design.json_config(json.dumps(full_config, indent=2))
|
|
733
|
+
|
|
734
|
+
# Show connection info
|
|
735
|
+
design.section_title(
|
|
736
|
+
"Connect to Cursor (be careful with multiple windows as that may interfere with the proxy)"
|
|
737
|
+
)
|
|
738
|
+
design.link(deeplink)
|
|
739
|
+
design.info("") # Empty line
|
|
740
|
+
|
|
741
|
+
# Start the proxy (pass original port, start_mcp_proxy will find actual port again)
|
|
742
|
+
try:
|
|
743
|
+
asyncio.run(
|
|
744
|
+
start_mcp_proxy(
|
|
745
|
+
directory,
|
|
746
|
+
resolved_image,
|
|
747
|
+
transport,
|
|
748
|
+
port,
|
|
749
|
+
no_reload,
|
|
750
|
+
verbose,
|
|
751
|
+
inspector,
|
|
752
|
+
no_logs,
|
|
753
|
+
interactive,
|
|
754
|
+
docker_args or [],
|
|
755
|
+
)
|
|
756
|
+
)
|
|
757
|
+
except Exception as e:
|
|
758
|
+
d.error(f"Failed to start MCP server: {e}")
|
|
759
|
+
d.info("")
|
|
760
|
+
d.info("๐ก Tip: Run the following command to debug the container:")
|
|
761
|
+
d.info(f" hud debug {resolved_image}")
|
|
762
|
+
d.info("")
|
|
763
|
+
d.info("This will help identify connection issues or initialization failures.")
|
|
764
|
+
raise
|