hud-python 0.4.45__py3-none-any.whl → 0.5.1__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.
Files changed (274) hide show
  1. hud/__init__.py +27 -7
  2. hud/agents/__init__.py +11 -5
  3. hud/agents/base.py +220 -500
  4. hud/agents/claude.py +200 -240
  5. hud/agents/gemini.py +275 -0
  6. hud/agents/gemini_cua.py +335 -0
  7. hud/agents/grounded_openai.py +98 -100
  8. hud/agents/misc/integration_test_agent.py +51 -20
  9. hud/agents/misc/response_agent.py +41 -36
  10. hud/agents/openai.py +291 -292
  11. hud/agents/{openai_chat_generic.py → openai_chat.py} +80 -34
  12. hud/agents/operator.py +211 -0
  13. hud/agents/tests/conftest.py +133 -0
  14. hud/agents/tests/test_base.py +300 -622
  15. hud/agents/tests/test_base_runtime.py +233 -0
  16. hud/agents/tests/test_claude.py +379 -210
  17. hud/agents/tests/test_client.py +9 -10
  18. hud/agents/tests/test_gemini.py +369 -0
  19. hud/agents/tests/test_grounded_openai_agent.py +65 -50
  20. hud/agents/tests/test_openai.py +376 -140
  21. hud/agents/tests/test_operator.py +362 -0
  22. hud/agents/tests/test_run_eval.py +179 -0
  23. hud/cli/__init__.py +461 -545
  24. hud/cli/analyze.py +43 -5
  25. hud/cli/build.py +664 -110
  26. hud/cli/debug.py +8 -5
  27. hud/cli/dev.py +882 -734
  28. hud/cli/eval.py +782 -668
  29. hud/cli/flows/dev.py +167 -0
  30. hud/cli/flows/init.py +191 -0
  31. hud/cli/flows/tasks.py +153 -56
  32. hud/cli/flows/templates.py +151 -0
  33. hud/cli/flows/tests/__init__.py +1 -0
  34. hud/cli/flows/tests/test_dev.py +126 -0
  35. hud/cli/init.py +60 -58
  36. hud/cli/push.py +29 -11
  37. hud/cli/rft.py +311 -0
  38. hud/cli/rft_status.py +145 -0
  39. hud/cli/tests/test_analyze.py +5 -5
  40. hud/cli/tests/test_analyze_metadata.py +3 -2
  41. hud/cli/tests/test_analyze_module.py +120 -0
  42. hud/cli/tests/test_build.py +108 -6
  43. hud/cli/tests/test_build_failure.py +41 -0
  44. hud/cli/tests/test_build_module.py +50 -0
  45. hud/cli/tests/test_cli_init.py +6 -1
  46. hud/cli/tests/test_cli_more_wrappers.py +30 -0
  47. hud/cli/tests/test_cli_root.py +140 -0
  48. hud/cli/tests/test_convert.py +361 -0
  49. hud/cli/tests/test_debug.py +12 -10
  50. hud/cli/tests/test_dev.py +197 -0
  51. hud/cli/tests/test_eval.py +251 -0
  52. hud/cli/tests/test_eval_bedrock.py +51 -0
  53. hud/cli/tests/test_init.py +124 -0
  54. hud/cli/tests/test_main_module.py +11 -5
  55. hud/cli/tests/test_mcp_server.py +12 -100
  56. hud/cli/tests/test_push_happy.py +74 -0
  57. hud/cli/tests/test_push_wrapper.py +23 -0
  58. hud/cli/tests/test_registry.py +1 -1
  59. hud/cli/tests/test_utils.py +1 -1
  60. hud/cli/{rl → utils}/celebrate.py +14 -12
  61. hud/cli/utils/config.py +18 -1
  62. hud/cli/utils/docker.py +130 -4
  63. hud/cli/utils/env_check.py +9 -9
  64. hud/cli/utils/git.py +136 -0
  65. hud/cli/utils/interactive.py +39 -5
  66. hud/cli/utils/metadata.py +69 -0
  67. hud/cli/utils/runner.py +1 -1
  68. hud/cli/utils/server.py +2 -2
  69. hud/cli/utils/source_hash.py +3 -3
  70. hud/cli/utils/tasks.py +4 -1
  71. hud/cli/utils/tests/__init__.py +0 -0
  72. hud/cli/utils/tests/test_config.py +58 -0
  73. hud/cli/utils/tests/test_docker.py +93 -0
  74. hud/cli/utils/tests/test_docker_hints.py +71 -0
  75. hud/cli/utils/tests/test_env_check.py +74 -0
  76. hud/cli/utils/tests/test_environment.py +42 -0
  77. hud/cli/utils/tests/test_git.py +142 -0
  78. hud/cli/utils/tests/test_interactive_module.py +60 -0
  79. hud/cli/utils/tests/test_local_runner.py +50 -0
  80. hud/cli/utils/tests/test_logging_utils.py +23 -0
  81. hud/cli/utils/tests/test_metadata.py +49 -0
  82. hud/cli/utils/tests/test_package_runner.py +35 -0
  83. hud/cli/utils/tests/test_registry_utils.py +49 -0
  84. hud/cli/utils/tests/test_remote_runner.py +25 -0
  85. hud/cli/utils/tests/test_runner_modules.py +52 -0
  86. hud/cli/utils/tests/test_source_hash.py +36 -0
  87. hud/cli/utils/tests/test_tasks.py +80 -0
  88. hud/cli/utils/version_check.py +258 -0
  89. hud/cli/{rl → utils}/viewer.py +2 -2
  90. hud/clients/README.md +12 -11
  91. hud/clients/__init__.py +4 -3
  92. hud/clients/base.py +166 -26
  93. hud/clients/environment.py +51 -0
  94. hud/clients/fastmcp.py +13 -6
  95. hud/clients/mcp_use.py +40 -15
  96. hud/clients/tests/test_analyze_scenarios.py +206 -0
  97. hud/clients/tests/test_protocol.py +9 -3
  98. hud/datasets/__init__.py +23 -20
  99. hud/datasets/loader.py +327 -0
  100. hud/datasets/runner.py +192 -105
  101. hud/datasets/tests/__init__.py +0 -0
  102. hud/datasets/tests/test_loader.py +221 -0
  103. hud/datasets/tests/test_utils.py +315 -0
  104. hud/datasets/utils.py +270 -90
  105. hud/environment/__init__.py +50 -0
  106. hud/environment/connection.py +206 -0
  107. hud/environment/connectors/__init__.py +33 -0
  108. hud/environment/connectors/base.py +68 -0
  109. hud/environment/connectors/local.py +177 -0
  110. hud/environment/connectors/mcp_config.py +109 -0
  111. hud/environment/connectors/openai.py +101 -0
  112. hud/environment/connectors/remote.py +172 -0
  113. hud/environment/environment.py +694 -0
  114. hud/environment/integrations/__init__.py +45 -0
  115. hud/environment/integrations/adk.py +67 -0
  116. hud/environment/integrations/anthropic.py +196 -0
  117. hud/environment/integrations/gemini.py +92 -0
  118. hud/environment/integrations/langchain.py +82 -0
  119. hud/environment/integrations/llamaindex.py +68 -0
  120. hud/environment/integrations/openai.py +238 -0
  121. hud/environment/mock.py +306 -0
  122. hud/environment/router.py +112 -0
  123. hud/environment/scenarios.py +493 -0
  124. hud/environment/tests/__init__.py +1 -0
  125. hud/environment/tests/test_connection.py +317 -0
  126. hud/environment/tests/test_connectors.py +218 -0
  127. hud/environment/tests/test_environment.py +161 -0
  128. hud/environment/tests/test_integrations.py +257 -0
  129. hud/environment/tests/test_local_connectors.py +201 -0
  130. hud/environment/tests/test_scenarios.py +280 -0
  131. hud/environment/tests/test_tools.py +208 -0
  132. hud/environment/types.py +23 -0
  133. hud/environment/utils/__init__.py +35 -0
  134. hud/environment/utils/formats.py +215 -0
  135. hud/environment/utils/schema.py +171 -0
  136. hud/environment/utils/tool_wrappers.py +113 -0
  137. hud/eval/__init__.py +67 -0
  138. hud/eval/context.py +674 -0
  139. hud/eval/display.py +299 -0
  140. hud/eval/instrument.py +185 -0
  141. hud/eval/manager.py +466 -0
  142. hud/eval/parallel.py +268 -0
  143. hud/eval/task.py +340 -0
  144. hud/eval/tests/__init__.py +1 -0
  145. hud/eval/tests/test_context.py +178 -0
  146. hud/eval/tests/test_eval.py +210 -0
  147. hud/eval/tests/test_manager.py +152 -0
  148. hud/eval/tests/test_parallel.py +168 -0
  149. hud/eval/tests/test_task.py +145 -0
  150. hud/eval/types.py +63 -0
  151. hud/eval/utils.py +183 -0
  152. hud/patches/__init__.py +19 -0
  153. hud/patches/mcp_patches.py +151 -0
  154. hud/patches/warnings.py +54 -0
  155. hud/samples/browser.py +4 -4
  156. hud/server/__init__.py +2 -1
  157. hud/server/low_level.py +2 -1
  158. hud/server/router.py +164 -0
  159. hud/server/server.py +567 -80
  160. hud/server/tests/test_mcp_server_integration.py +11 -11
  161. hud/server/tests/test_mcp_server_more.py +1 -1
  162. hud/server/tests/test_server_extra.py +2 -0
  163. hud/settings.py +45 -3
  164. hud/shared/exceptions.py +36 -10
  165. hud/shared/hints.py +26 -1
  166. hud/shared/requests.py +15 -3
  167. hud/shared/tests/test_exceptions.py +40 -31
  168. hud/shared/tests/test_hints.py +167 -0
  169. hud/telemetry/__init__.py +20 -19
  170. hud/telemetry/exporter.py +201 -0
  171. hud/telemetry/instrument.py +158 -253
  172. hud/telemetry/tests/test_eval_telemetry.py +356 -0
  173. hud/telemetry/tests/test_exporter.py +258 -0
  174. hud/telemetry/tests/test_instrument.py +401 -0
  175. hud/tools/__init__.py +16 -2
  176. hud/tools/apply_patch.py +639 -0
  177. hud/tools/base.py +54 -4
  178. hud/tools/bash.py +2 -2
  179. hud/tools/computer/__init__.py +4 -0
  180. hud/tools/computer/anthropic.py +2 -2
  181. hud/tools/computer/gemini.py +385 -0
  182. hud/tools/computer/hud.py +23 -6
  183. hud/tools/computer/openai.py +20 -21
  184. hud/tools/computer/qwen.py +434 -0
  185. hud/tools/computer/settings.py +37 -0
  186. hud/tools/edit.py +3 -7
  187. hud/tools/executors/base.py +4 -2
  188. hud/tools/executors/pyautogui.py +1 -1
  189. hud/tools/grounding/grounded_tool.py +13 -18
  190. hud/tools/grounding/grounder.py +10 -31
  191. hud/tools/grounding/tests/test_grounded_tool.py +26 -44
  192. hud/tools/jupyter.py +330 -0
  193. hud/tools/playwright.py +18 -3
  194. hud/tools/shell.py +308 -0
  195. hud/tools/tests/test_apply_patch.py +718 -0
  196. hud/tools/tests/test_computer.py +4 -9
  197. hud/tools/tests/test_computer_actions.py +24 -2
  198. hud/tools/tests/test_jupyter_tool.py +181 -0
  199. hud/tools/tests/test_shell.py +596 -0
  200. hud/tools/tests/test_submit.py +85 -0
  201. hud/tools/tests/test_types.py +193 -0
  202. hud/tools/types.py +21 -1
  203. hud/types.py +167 -57
  204. hud/utils/__init__.py +2 -0
  205. hud/utils/env.py +67 -0
  206. hud/utils/hud_console.py +61 -3
  207. hud/utils/mcp.py +15 -58
  208. hud/utils/strict_schema.py +162 -0
  209. hud/utils/tests/test_init.py +1 -2
  210. hud/utils/tests/test_mcp.py +1 -28
  211. hud/utils/tests/test_pretty_errors.py +186 -0
  212. hud/utils/tests/test_tool_shorthand.py +154 -0
  213. hud/utils/tests/test_version.py +1 -1
  214. hud/utils/types.py +20 -0
  215. hud/version.py +1 -1
  216. hud_python-0.5.1.dist-info/METADATA +264 -0
  217. hud_python-0.5.1.dist-info/RECORD +299 -0
  218. {hud_python-0.4.45.dist-info → hud_python-0.5.1.dist-info}/WHEEL +1 -1
  219. hud/agents/langchain.py +0 -261
  220. hud/agents/lite_llm.py +0 -72
  221. hud/cli/rl/__init__.py +0 -180
  222. hud/cli/rl/config.py +0 -101
  223. hud/cli/rl/display.py +0 -133
  224. hud/cli/rl/gpu.py +0 -63
  225. hud/cli/rl/gpu_utils.py +0 -321
  226. hud/cli/rl/local_runner.py +0 -595
  227. hud/cli/rl/presets.py +0 -96
  228. hud/cli/rl/remote_runner.py +0 -463
  229. hud/cli/rl/rl_api.py +0 -150
  230. hud/cli/rl/vllm.py +0 -177
  231. hud/cli/rl/wait_utils.py +0 -89
  232. hud/datasets/parallel.py +0 -687
  233. hud/misc/__init__.py +0 -1
  234. hud/misc/claude_plays_pokemon.py +0 -292
  235. hud/otel/__init__.py +0 -35
  236. hud/otel/collector.py +0 -142
  237. hud/otel/config.py +0 -181
  238. hud/otel/context.py +0 -570
  239. hud/otel/exporters.py +0 -369
  240. hud/otel/instrumentation.py +0 -135
  241. hud/otel/processors.py +0 -121
  242. hud/otel/tests/__init__.py +0 -1
  243. hud/otel/tests/test_processors.py +0 -197
  244. hud/rl/README.md +0 -30
  245. hud/rl/__init__.py +0 -1
  246. hud/rl/actor.py +0 -176
  247. hud/rl/buffer.py +0 -405
  248. hud/rl/chat_template.jinja +0 -101
  249. hud/rl/config.py +0 -192
  250. hud/rl/distributed.py +0 -132
  251. hud/rl/learner.py +0 -637
  252. hud/rl/tests/__init__.py +0 -1
  253. hud/rl/tests/test_learner.py +0 -186
  254. hud/rl/train.py +0 -382
  255. hud/rl/types.py +0 -101
  256. hud/rl/utils/start_vllm_server.sh +0 -30
  257. hud/rl/utils.py +0 -524
  258. hud/rl/vllm_adapter.py +0 -143
  259. hud/telemetry/job.py +0 -352
  260. hud/telemetry/replay.py +0 -74
  261. hud/telemetry/tests/test_replay.py +0 -40
  262. hud/telemetry/tests/test_trace.py +0 -63
  263. hud/telemetry/trace.py +0 -158
  264. hud/utils/agent_factories.py +0 -86
  265. hud/utils/async_utils.py +0 -65
  266. hud/utils/group_eval.py +0 -223
  267. hud/utils/progress.py +0 -149
  268. hud/utils/tasks.py +0 -127
  269. hud/utils/tests/test_async_utils.py +0 -173
  270. hud/utils/tests/test_progress.py +0 -261
  271. hud_python-0.4.45.dist-info/METADATA +0 -552
  272. hud_python-0.4.45.dist-info/RECORD +0 -228
  273. {hud_python-0.4.45.dist-info → hud_python-0.5.1.dist-info}/entry_points.txt +0 -0
  274. {hud_python-0.4.45.dist-info → hud_python-0.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,151 @@
1
+ """
2
+ Runtime patches for the standard mcp package.
3
+
4
+ These patches apply fixes from the HUD fork without requiring a separate package.
5
+ Import this module early (e.g., in hud/__init__.py) to apply patches.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from typing import Any
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def patch_streamable_http_error_handling() -> None:
17
+ """
18
+ Patch StreamableHTTPTransport.post_writer to handle request errors properly.
19
+
20
+ The original implementation doesn't catch errors in handle_request_async,
21
+ which can cause silent failures. This patch wraps the handler to send
22
+ errors to the read stream so clients know the request failed.
23
+ """
24
+ try:
25
+ from mcp.client.streamable_http import StreamableHTTPTransport
26
+
27
+ async def patched_post_writer(
28
+ self: Any,
29
+ client: Any,
30
+ write_stream_reader: Any,
31
+ read_stream_writer: Any,
32
+ write_stream: Any,
33
+ start_get_stream: Any,
34
+ tg: Any,
35
+ ) -> None:
36
+ """Patched post_writer with error handling for handle_request_async."""
37
+ from mcp.client.streamable_http import RequestContext
38
+ from mcp.shared.message import ClientMessageMetadata
39
+ from mcp.types import JSONRPCRequest
40
+
41
+ try:
42
+ async with write_stream_reader:
43
+ async for session_message in write_stream_reader:
44
+ message = session_message.message
45
+ metadata = (
46
+ session_message.metadata
47
+ if isinstance(session_message.metadata, ClientMessageMetadata)
48
+ else None
49
+ )
50
+
51
+ is_resumption = bool(metadata and metadata.resumption_token)
52
+
53
+ logger.debug("Sending client message: %s", message)
54
+
55
+ if self._is_initialized_notification(message):
56
+ start_get_stream()
57
+
58
+ ctx = RequestContext(
59
+ client=client,
60
+ headers=self.request_headers,
61
+ session_id=self.session_id,
62
+ session_message=session_message,
63
+ metadata=metadata,
64
+ read_stream_writer=read_stream_writer,
65
+ sse_read_timeout=self.sse_read_timeout,
66
+ )
67
+
68
+ # Patched: Accept ctx and is_resumption as params, add error handling
69
+ async def handle_request_async(
70
+ ctx: RequestContext = ctx,
71
+ is_resumption: bool = is_resumption,
72
+ ) -> None:
73
+ try:
74
+ if is_resumption:
75
+ await self._handle_resumption_request(ctx)
76
+ else:
77
+ await self._handle_post_request(ctx)
78
+ except Exception as e:
79
+ # Send error to read stream so client knows request failed
80
+ logger.error("Request handler error: %s", e)
81
+ await ctx.read_stream_writer.send(e)
82
+
83
+ if isinstance(message.root, JSONRPCRequest):
84
+ tg.start_soon(handle_request_async, ctx, is_resumption)
85
+ else:
86
+ await handle_request_async(ctx, is_resumption)
87
+
88
+ except Exception:
89
+ logger.exception("Error in post_writer")
90
+ finally:
91
+ await read_stream_writer.aclose()
92
+ await write_stream.aclose()
93
+
94
+ StreamableHTTPTransport.post_writer = patched_post_writer
95
+ logger.debug("Patched StreamableHTTPTransport.post_writer")
96
+
97
+ except ImportError:
98
+ logger.debug("mcp.client.streamable_http not available, skipping patch")
99
+ except Exception as e:
100
+ logger.warning("Failed to patch streamable_http: %s", e)
101
+
102
+
103
+ def patch_client_session_validation() -> None:
104
+ """
105
+ Patch ClientSession to skip structured output validation.
106
+
107
+ The original validation is strict and raises errors for non-conforming
108
+ but usable responses. We replace it with a no-op.
109
+ """
110
+ try:
111
+ from mcp.client.session import ClientSession
112
+
113
+ async def noop_validate(self: Any, name: str, result: Any) -> None:
114
+ """Skip structured output validation entirely."""
115
+
116
+ ClientSession._validate_tool_result = noop_validate
117
+ logger.debug("Patched ClientSession._validate_tool_result to skip validation")
118
+
119
+ except ImportError:
120
+ logger.debug("mcp.client.session not available, skipping patch")
121
+ except Exception as e:
122
+ logger.warning("Failed to patch client session: %s", e)
123
+
124
+
125
+ def suppress_fastmcp_logging(level: int = logging.WARNING) -> None:
126
+ """
127
+ Suppress verbose fastmcp logging.
128
+
129
+ FastMCP logs a lot of INFO-level messages that clutter output.
130
+ This sets all fastmcp loggers to the specified level.
131
+
132
+ Args:
133
+ level: Logging level to set (default: WARNING)
134
+ """
135
+ loggers_to_suppress = [
136
+ "fastmcp",
137
+ "fastmcp.server.server",
138
+ "fastmcp.server.openapi",
139
+ "fastmcp.tools.tool_manager",
140
+ ]
141
+ for logger_name in loggers_to_suppress:
142
+ logging.getLogger(logger_name).setLevel(level)
143
+ logger.debug("Suppressed fastmcp logging to level %s", level)
144
+
145
+
146
+ def apply_all_patches() -> None:
147
+ """Apply all MCP patches."""
148
+ patch_streamable_http_error_handling()
149
+ patch_client_session_validation()
150
+ suppress_fastmcp_logging()
151
+ logger.debug("All MCP patches applied")
@@ -0,0 +1,54 @@
1
+ """
2
+ Centralized warning filters for noisy third-party dependencies.
3
+
4
+ Keep these helpers here so the rest of the codebase can stay clean and avoid
5
+ scattering warning filters across unrelated modules.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import warnings
11
+ from contextlib import contextmanager
12
+ from typing import TYPE_CHECKING
13
+
14
+ if TYPE_CHECKING:
15
+ from collections.abc import Iterator
16
+
17
+
18
+ def apply_default_warning_filters(*, verbose: bool) -> None:
19
+ """Apply our default warning filters for non-verbose CLI/server modes."""
20
+ if verbose:
21
+ return
22
+
23
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
24
+
25
+ # Pydantic v2 emits PydanticDeprecatedSince20 for v1-style config usage in deps.
26
+ try:
27
+ from pydantic.warnings import PydanticDeprecatedSince20
28
+ except Exception:
29
+ return
30
+
31
+ warnings.filterwarnings("ignore", category=PydanticDeprecatedSince20)
32
+
33
+
34
+ @contextmanager
35
+ def suppress_mcp_use_import_warnings() -> Iterator[None]:
36
+ """Suppress known noisy warnings emitted during `mcp_use` imports."""
37
+ try:
38
+ from pydantic.warnings import PydanticDeprecatedSince20
39
+ except Exception: # pragma: no cover
40
+ PydanticDeprecatedSince20 = None # type: ignore[assignment]
41
+
42
+ with warnings.catch_warnings():
43
+ # mcp_use currently emits DeprecationWarning from its package __init__.py.
44
+ warnings.filterwarnings("ignore", category=DeprecationWarning, module=r"mcp_use(\..*)?$")
45
+
46
+ # mcp_use currently defines Pydantic v1-style `class Config` in oauth models.
47
+ if PydanticDeprecatedSince20 is not None:
48
+ warnings.filterwarnings(
49
+ "ignore",
50
+ category=PydanticDeprecatedSince20,
51
+ module=r"mcp_use\.client\.auth\.oauth$",
52
+ )
53
+
54
+ yield
hud/samples/browser.py CHANGED
@@ -7,17 +7,17 @@ from typing import Any
7
7
  from pydantic import Field
8
8
 
9
9
  from hud.settings import settings
10
- from hud.types import MCPToolCall, Task
10
+ from hud.types import LegacyTask, MCPToolCall
11
11
 
12
12
 
13
- class BrowserTask(Task):
14
- """Task subclass with browser defaults for BrowserTask(prompt=...)."""
13
+ class BrowserTask(LegacyTask):
14
+ """LegacyTask subclass with browser defaults for BrowserTask(prompt=...)."""
15
15
 
16
16
  prompt: str = "Open Google and be ready to search."
17
17
  mcp_config: dict[str, Any] = Field(
18
18
  default_factory=lambda: {
19
19
  "browser": {
20
- "url": "https://mcp.hud.so/v3/mcp",
20
+ "url": settings.hud_mcp_url,
21
21
  "headers": {
22
22
  "Authorization": f"Bearer {settings.api_key}",
23
23
  "Mcp-Image": "hudevals/hud-remote-browser:0.1.1",
hud/server/__init__.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from .router import MCPRouter
3
4
  from .server import MCPServer
4
5
 
5
- __all__ = ["MCPServer"]
6
+ __all__ = ["MCPRouter", "MCPServer"]
hud/server/low_level.py CHANGED
@@ -89,11 +89,12 @@ class LowLevelServerWithInit(_BaseLL):
89
89
 
90
90
  def __init__(
91
91
  self,
92
+ fastmcp: Any,
92
93
  *args: Any,
93
94
  init_fn: Callable[[RequestContext], Awaitable[None]] | None = None,
94
95
  **kwargs: Any,
95
96
  ) -> None:
96
- super().__init__(*args, **kwargs)
97
+ super().__init__(fastmcp, *args, **kwargs)
97
98
  self._init_fn = init_fn
98
99
 
99
100
  async def run(
hud/server/router.py ADDED
@@ -0,0 +1,164 @@
1
+ """MCP Router utilities for FastAPI-like composition patterns."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from hud.server.server import MCPServer
9
+
10
+ if TYPE_CHECKING:
11
+ from collections.abc import Callable
12
+
13
+ from fastmcp import FastMCP
14
+ from fastmcp.tools import Tool
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # MCPRouter is just an alias to FastMCP for FastAPI-like patterns
19
+ MCPRouter = MCPServer
20
+
21
+ # Prefix for internal tool names
22
+ _INTERNAL_PREFIX = "int_"
23
+
24
+
25
+ class HiddenRouter(MCPRouter):
26
+ """Wraps a FastMCP router to provide a single dispatcher tool for its sub-tools.
27
+
28
+ Instead of exposing all tools at the top level, this creates a single tool
29
+ (named after the router) that dispatches to the router's tools internally.
30
+
31
+ Useful for setup/evaluate patterns where you want:
32
+ - A single 'setup' tool that can call setup_basic(), setup_advanced(), etc.
33
+ - A single 'evaluate' tool that can call evaluate_score(), evaluate_complete(), etc.
34
+
35
+ Example:
36
+ # Create a router with multiple setup functions
37
+ setup_router = MCPRouter(name="setup")
38
+
39
+ @setup_router.tool
40
+ async def reset():
41
+ return "Environment reset"
42
+
43
+ @setup_router.tool
44
+ async def seed_data():
45
+ return "Data seeded"
46
+
47
+ # Wrap in HiddenRouter
48
+ hidden_setup = HiddenRouter(setup_router)
49
+
50
+ # Now you have one 'setup' tool that dispatches to reset/seed_data
51
+ mcp.include_router(hidden_setup)
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ router: FastMCP,
57
+ *,
58
+ title: str | None = None,
59
+ description: str | None = None,
60
+ meta: dict[str, Any] | None = None,
61
+ ) -> None:
62
+ """Wrap an existing router with a dispatcher pattern.
63
+
64
+ Args:
65
+ router: The FastMCP router to wrap
66
+ title: Optional title for the dispatcher tool (defaults to "{name} Dispatcher")
67
+ description: Optional description for the dispatcher tool
68
+ meta: Optional metadata for the dispatcher tool
69
+ """
70
+ name = router.name or "router"
71
+
72
+ # Naming scheme for hidden/internal tools
73
+ self._prefix_fn: Callable[[str], str] = lambda n: f"{_INTERNAL_PREFIX}{n}"
74
+
75
+ super().__init__(name=name)
76
+
77
+ # Set up dispatcher tool
78
+ dispatcher_title = title or f"{name.title()} Dispatcher"
79
+ dispatcher_desc = description or f"Call internal '{name}' functions"
80
+
81
+ # Register dispatcher that routes to hidden tools
82
+ async def _dispatch(
83
+ name: str,
84
+ arguments: dict | str | None = None,
85
+ ctx: Any | None = None,
86
+ ) -> Any:
87
+ """Gateway to hidden tools.
88
+
89
+ Args:
90
+ name: Internal function name (without prefix)
91
+ arguments: Arguments to forward to the internal tool (dict or JSON string)
92
+ ctx: Request context injected by FastMCP
93
+ """
94
+ # Handle JSON string inputs
95
+ if isinstance(arguments, str):
96
+ import json
97
+
98
+ try:
99
+ arguments = json.loads(arguments)
100
+ except json.JSONDecodeError:
101
+ arguments = {}
102
+
103
+ # Call the internal tool
104
+ return await self._tool_manager.call_tool(self._prefix_fn(name), arguments or {}) # type: ignore
105
+
106
+ from fastmcp.tools.tool import FunctionTool
107
+
108
+ dispatcher_tool = FunctionTool.from_function(
109
+ _dispatch,
110
+ name=name,
111
+ title=dispatcher_title,
112
+ description=dispatcher_desc,
113
+ tags=set(),
114
+ meta=meta,
115
+ )
116
+ self._tool_manager.add_tool(dispatcher_tool)
117
+
118
+ # Copy all tools from source router as hidden tools
119
+ for tool in router._tool_manager._tools.values():
120
+ tool._key = self._prefix_fn(tool.name)
121
+ self._tool_manager.add_tool(tool)
122
+
123
+ # Expose list of available functions via resource
124
+ async def _functions_catalogue() -> list[str]:
125
+ """List all internal function names without prefix."""
126
+ return [
127
+ key.removeprefix(_INTERNAL_PREFIX)
128
+ for key in self._tool_manager._tools
129
+ if key.startswith(_INTERNAL_PREFIX)
130
+ ]
131
+
132
+ from fastmcp.resources import Resource
133
+
134
+ catalogue_resource = Resource.from_function(
135
+ _functions_catalogue,
136
+ uri=f"{name}://functions",
137
+ name=f"{name.title()} Functions",
138
+ description=f"List of available {name} functions",
139
+ )
140
+ self._resource_manager.add_resource(catalogue_resource)
141
+
142
+ # Override _list_tools to hide internal tools when mounted
143
+ async def _list_tools(self, context: Any = None) -> list[Tool]:
144
+ """Override _list_tools to hide internal tools when mounted.
145
+
146
+ Args:
147
+ context: MiddlewareContext passed by FastMCP (optional for backwards compat)
148
+ """
149
+ return [
150
+ tool
151
+ for key, tool in self._tool_manager._tools.items()
152
+ if not key.startswith(_INTERNAL_PREFIX)
153
+ ]
154
+
155
+ def _sync_list_tools(self) -> dict[str, Tool]:
156
+ """Override _list_tools to hide internal tools when mounted."""
157
+ return {
158
+ key: tool
159
+ for key, tool in self._tool_manager._tools.items()
160
+ if not key.startswith(_INTERNAL_PREFIX)
161
+ }
162
+
163
+
164
+ __all__ = ["HiddenRouter", "MCPRouter"]