hud-python 0.4.45__py3-none-any.whl → 0.5.13__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 (282) hide show
  1. hud/__init__.py +27 -7
  2. hud/agents/__init__.py +70 -5
  3. hud/agents/base.py +238 -500
  4. hud/agents/claude.py +236 -247
  5. hud/agents/gateway.py +42 -0
  6. hud/agents/gemini.py +264 -0
  7. hud/agents/gemini_cua.py +324 -0
  8. hud/agents/grounded_openai.py +98 -100
  9. hud/agents/misc/integration_test_agent.py +51 -20
  10. hud/agents/misc/response_agent.py +48 -36
  11. hud/agents/openai.py +282 -296
  12. hud/agents/{openai_chat_generic.py → openai_chat.py} +63 -33
  13. hud/agents/operator.py +199 -0
  14. hud/agents/resolver.py +70 -0
  15. hud/agents/tests/conftest.py +133 -0
  16. hud/agents/tests/test_base.py +300 -622
  17. hud/agents/tests/test_base_runtime.py +233 -0
  18. hud/agents/tests/test_claude.py +381 -214
  19. hud/agents/tests/test_client.py +9 -10
  20. hud/agents/tests/test_gemini.py +369 -0
  21. hud/agents/tests/test_grounded_openai_agent.py +65 -50
  22. hud/agents/tests/test_openai.py +377 -140
  23. hud/agents/tests/test_operator.py +362 -0
  24. hud/agents/tests/test_resolver.py +192 -0
  25. hud/agents/tests/test_run_eval.py +179 -0
  26. hud/agents/types.py +148 -0
  27. hud/cli/__init__.py +493 -546
  28. hud/cli/analyze.py +43 -5
  29. hud/cli/build.py +699 -113
  30. hud/cli/debug.py +8 -5
  31. hud/cli/dev.py +889 -732
  32. hud/cli/eval.py +793 -667
  33. hud/cli/flows/dev.py +167 -0
  34. hud/cli/flows/init.py +191 -0
  35. hud/cli/flows/tasks.py +153 -56
  36. hud/cli/flows/templates.py +151 -0
  37. hud/cli/flows/tests/__init__.py +1 -0
  38. hud/cli/flows/tests/test_dev.py +126 -0
  39. hud/cli/init.py +60 -58
  40. hud/cli/pull.py +1 -1
  41. hud/cli/push.py +38 -13
  42. hud/cli/rft.py +311 -0
  43. hud/cli/rft_status.py +145 -0
  44. hud/cli/tests/test_analyze.py +5 -5
  45. hud/cli/tests/test_analyze_metadata.py +3 -2
  46. hud/cli/tests/test_analyze_module.py +120 -0
  47. hud/cli/tests/test_build.py +110 -8
  48. hud/cli/tests/test_build_failure.py +41 -0
  49. hud/cli/tests/test_build_module.py +50 -0
  50. hud/cli/tests/test_cli_init.py +6 -1
  51. hud/cli/tests/test_cli_more_wrappers.py +30 -0
  52. hud/cli/tests/test_cli_root.py +140 -0
  53. hud/cli/tests/test_convert.py +361 -0
  54. hud/cli/tests/test_debug.py +12 -10
  55. hud/cli/tests/test_dev.py +197 -0
  56. hud/cli/tests/test_eval.py +251 -0
  57. hud/cli/tests/test_eval_bedrock.py +51 -0
  58. hud/cli/tests/test_init.py +124 -0
  59. hud/cli/tests/test_main_module.py +11 -5
  60. hud/cli/tests/test_mcp_server.py +12 -100
  61. hud/cli/tests/test_push.py +1 -1
  62. hud/cli/tests/test_push_happy.py +74 -0
  63. hud/cli/tests/test_push_wrapper.py +23 -0
  64. hud/cli/tests/test_registry.py +1 -1
  65. hud/cli/tests/test_utils.py +1 -1
  66. hud/cli/{rl → utils}/celebrate.py +14 -12
  67. hud/cli/utils/config.py +18 -1
  68. hud/cli/utils/docker.py +130 -4
  69. hud/cli/utils/env_check.py +9 -9
  70. hud/cli/utils/git.py +136 -0
  71. hud/cli/utils/interactive.py +39 -5
  72. hud/cli/utils/metadata.py +70 -1
  73. hud/cli/utils/runner.py +1 -1
  74. hud/cli/utils/server.py +2 -2
  75. hud/cli/utils/source_hash.py +3 -3
  76. hud/cli/utils/tasks.py +4 -1
  77. hud/cli/utils/tests/__init__.py +0 -0
  78. hud/cli/utils/tests/test_config.py +58 -0
  79. hud/cli/utils/tests/test_docker.py +93 -0
  80. hud/cli/utils/tests/test_docker_hints.py +71 -0
  81. hud/cli/utils/tests/test_env_check.py +74 -0
  82. hud/cli/utils/tests/test_environment.py +42 -0
  83. hud/cli/utils/tests/test_git.py +142 -0
  84. hud/cli/utils/tests/test_interactive_module.py +60 -0
  85. hud/cli/utils/tests/test_local_runner.py +50 -0
  86. hud/cli/utils/tests/test_logging_utils.py +23 -0
  87. hud/cli/utils/tests/test_metadata.py +49 -0
  88. hud/cli/utils/tests/test_package_runner.py +35 -0
  89. hud/cli/utils/tests/test_registry_utils.py +49 -0
  90. hud/cli/utils/tests/test_remote_runner.py +25 -0
  91. hud/cli/utils/tests/test_runner_modules.py +52 -0
  92. hud/cli/utils/tests/test_source_hash.py +36 -0
  93. hud/cli/utils/tests/test_tasks.py +80 -0
  94. hud/cli/utils/version_check.py +258 -0
  95. hud/cli/{rl → utils}/viewer.py +2 -2
  96. hud/clients/README.md +12 -11
  97. hud/clients/__init__.py +4 -3
  98. hud/clients/base.py +166 -26
  99. hud/clients/environment.py +51 -0
  100. hud/clients/fastmcp.py +13 -6
  101. hud/clients/mcp_use.py +45 -15
  102. hud/clients/tests/test_analyze_scenarios.py +206 -0
  103. hud/clients/tests/test_protocol.py +9 -3
  104. hud/datasets/__init__.py +23 -20
  105. hud/datasets/loader.py +326 -0
  106. hud/datasets/runner.py +198 -105
  107. hud/datasets/tests/__init__.py +0 -0
  108. hud/datasets/tests/test_loader.py +221 -0
  109. hud/datasets/tests/test_utils.py +315 -0
  110. hud/datasets/utils.py +270 -90
  111. hud/environment/__init__.py +52 -0
  112. hud/environment/connection.py +258 -0
  113. hud/environment/connectors/__init__.py +33 -0
  114. hud/environment/connectors/base.py +68 -0
  115. hud/environment/connectors/local.py +177 -0
  116. hud/environment/connectors/mcp_config.py +137 -0
  117. hud/environment/connectors/openai.py +101 -0
  118. hud/environment/connectors/remote.py +172 -0
  119. hud/environment/environment.py +835 -0
  120. hud/environment/integrations/__init__.py +45 -0
  121. hud/environment/integrations/adk.py +67 -0
  122. hud/environment/integrations/anthropic.py +196 -0
  123. hud/environment/integrations/gemini.py +92 -0
  124. hud/environment/integrations/langchain.py +82 -0
  125. hud/environment/integrations/llamaindex.py +68 -0
  126. hud/environment/integrations/openai.py +238 -0
  127. hud/environment/mock.py +306 -0
  128. hud/environment/router.py +263 -0
  129. hud/environment/scenarios.py +620 -0
  130. hud/environment/tests/__init__.py +1 -0
  131. hud/environment/tests/test_connection.py +317 -0
  132. hud/environment/tests/test_connectors.py +205 -0
  133. hud/environment/tests/test_environment.py +593 -0
  134. hud/environment/tests/test_integrations.py +257 -0
  135. hud/environment/tests/test_local_connectors.py +242 -0
  136. hud/environment/tests/test_scenarios.py +1086 -0
  137. hud/environment/tests/test_tools.py +208 -0
  138. hud/environment/types.py +23 -0
  139. hud/environment/utils/__init__.py +35 -0
  140. hud/environment/utils/formats.py +215 -0
  141. hud/environment/utils/schema.py +171 -0
  142. hud/environment/utils/tool_wrappers.py +113 -0
  143. hud/eval/__init__.py +67 -0
  144. hud/eval/context.py +727 -0
  145. hud/eval/display.py +299 -0
  146. hud/eval/instrument.py +187 -0
  147. hud/eval/manager.py +533 -0
  148. hud/eval/parallel.py +268 -0
  149. hud/eval/task.py +372 -0
  150. hud/eval/tests/__init__.py +1 -0
  151. hud/eval/tests/test_context.py +178 -0
  152. hud/eval/tests/test_eval.py +210 -0
  153. hud/eval/tests/test_manager.py +152 -0
  154. hud/eval/tests/test_parallel.py +168 -0
  155. hud/eval/tests/test_task.py +291 -0
  156. hud/eval/types.py +65 -0
  157. hud/eval/utils.py +194 -0
  158. hud/patches/__init__.py +19 -0
  159. hud/patches/mcp_patches.py +308 -0
  160. hud/patches/warnings.py +54 -0
  161. hud/samples/browser.py +4 -4
  162. hud/server/__init__.py +2 -1
  163. hud/server/low_level.py +2 -1
  164. hud/server/router.py +164 -0
  165. hud/server/server.py +567 -80
  166. hud/server/tests/test_mcp_server_integration.py +11 -11
  167. hud/server/tests/test_mcp_server_more.py +1 -1
  168. hud/server/tests/test_server_extra.py +2 -0
  169. hud/settings.py +45 -3
  170. hud/shared/exceptions.py +36 -10
  171. hud/shared/hints.py +26 -1
  172. hud/shared/requests.py +15 -3
  173. hud/shared/tests/test_exceptions.py +40 -31
  174. hud/shared/tests/test_hints.py +167 -0
  175. hud/telemetry/__init__.py +20 -19
  176. hud/telemetry/exporter.py +201 -0
  177. hud/telemetry/instrument.py +165 -253
  178. hud/telemetry/tests/test_eval_telemetry.py +356 -0
  179. hud/telemetry/tests/test_exporter.py +258 -0
  180. hud/telemetry/tests/test_instrument.py +401 -0
  181. hud/tools/__init__.py +18 -2
  182. hud/tools/agent.py +223 -0
  183. hud/tools/apply_patch.py +639 -0
  184. hud/tools/base.py +54 -4
  185. hud/tools/bash.py +2 -2
  186. hud/tools/computer/__init__.py +36 -3
  187. hud/tools/computer/anthropic.py +2 -2
  188. hud/tools/computer/gemini.py +385 -0
  189. hud/tools/computer/hud.py +23 -6
  190. hud/tools/computer/openai.py +20 -21
  191. hud/tools/computer/qwen.py +434 -0
  192. hud/tools/computer/settings.py +37 -0
  193. hud/tools/edit.py +3 -7
  194. hud/tools/executors/base.py +4 -2
  195. hud/tools/executors/pyautogui.py +1 -1
  196. hud/tools/grounding/grounded_tool.py +13 -18
  197. hud/tools/grounding/grounder.py +10 -31
  198. hud/tools/grounding/tests/test_grounded_tool.py +26 -44
  199. hud/tools/jupyter.py +330 -0
  200. hud/tools/playwright.py +18 -3
  201. hud/tools/shell.py +308 -0
  202. hud/tools/tests/test_agent_tool.py +355 -0
  203. hud/tools/tests/test_apply_patch.py +718 -0
  204. hud/tools/tests/test_computer.py +4 -9
  205. hud/tools/tests/test_computer_actions.py +24 -2
  206. hud/tools/tests/test_jupyter_tool.py +181 -0
  207. hud/tools/tests/test_shell.py +596 -0
  208. hud/tools/tests/test_submit.py +85 -0
  209. hud/tools/tests/test_types.py +193 -0
  210. hud/tools/types.py +21 -1
  211. hud/types.py +194 -56
  212. hud/utils/__init__.py +2 -0
  213. hud/utils/env.py +67 -0
  214. hud/utils/hud_console.py +89 -18
  215. hud/utils/mcp.py +15 -58
  216. hud/utils/strict_schema.py +162 -0
  217. hud/utils/tests/test_init.py +1 -2
  218. hud/utils/tests/test_mcp.py +1 -28
  219. hud/utils/tests/test_pretty_errors.py +186 -0
  220. hud/utils/tests/test_tool_shorthand.py +154 -0
  221. hud/utils/tests/test_version.py +1 -1
  222. hud/utils/types.py +20 -0
  223. hud/version.py +1 -1
  224. hud_python-0.5.13.dist-info/METADATA +264 -0
  225. hud_python-0.5.13.dist-info/RECORD +305 -0
  226. {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/WHEEL +1 -1
  227. hud/agents/langchain.py +0 -261
  228. hud/agents/lite_llm.py +0 -72
  229. hud/cli/rl/__init__.py +0 -180
  230. hud/cli/rl/config.py +0 -101
  231. hud/cli/rl/display.py +0 -133
  232. hud/cli/rl/gpu.py +0 -63
  233. hud/cli/rl/gpu_utils.py +0 -321
  234. hud/cli/rl/local_runner.py +0 -595
  235. hud/cli/rl/presets.py +0 -96
  236. hud/cli/rl/remote_runner.py +0 -463
  237. hud/cli/rl/rl_api.py +0 -150
  238. hud/cli/rl/vllm.py +0 -177
  239. hud/cli/rl/wait_utils.py +0 -89
  240. hud/datasets/parallel.py +0 -687
  241. hud/misc/__init__.py +0 -1
  242. hud/misc/claude_plays_pokemon.py +0 -292
  243. hud/otel/__init__.py +0 -35
  244. hud/otel/collector.py +0 -142
  245. hud/otel/config.py +0 -181
  246. hud/otel/context.py +0 -570
  247. hud/otel/exporters.py +0 -369
  248. hud/otel/instrumentation.py +0 -135
  249. hud/otel/processors.py +0 -121
  250. hud/otel/tests/__init__.py +0 -1
  251. hud/otel/tests/test_processors.py +0 -197
  252. hud/rl/README.md +0 -30
  253. hud/rl/__init__.py +0 -1
  254. hud/rl/actor.py +0 -176
  255. hud/rl/buffer.py +0 -405
  256. hud/rl/chat_template.jinja +0 -101
  257. hud/rl/config.py +0 -192
  258. hud/rl/distributed.py +0 -132
  259. hud/rl/learner.py +0 -637
  260. hud/rl/tests/__init__.py +0 -1
  261. hud/rl/tests/test_learner.py +0 -186
  262. hud/rl/train.py +0 -382
  263. hud/rl/types.py +0 -101
  264. hud/rl/utils/start_vllm_server.sh +0 -30
  265. hud/rl/utils.py +0 -524
  266. hud/rl/vllm_adapter.py +0 -143
  267. hud/telemetry/job.py +0 -352
  268. hud/telemetry/replay.py +0 -74
  269. hud/telemetry/tests/test_replay.py +0 -40
  270. hud/telemetry/tests/test_trace.py +0 -63
  271. hud/telemetry/trace.py +0 -158
  272. hud/utils/agent_factories.py +0 -86
  273. hud/utils/async_utils.py +0 -65
  274. hud/utils/group_eval.py +0 -223
  275. hud/utils/progress.py +0 -149
  276. hud/utils/tasks.py +0 -127
  277. hud/utils/tests/test_async_utils.py +0 -173
  278. hud/utils/tests/test_progress.py +0 -261
  279. hud_python-0.4.45.dist-info/METADATA +0 -552
  280. hud_python-0.4.45.dist-info/RECORD +0 -228
  281. {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/entry_points.txt +0 -0
  282. {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,258 @@
1
+ """Connection management for MCP servers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from enum import Enum
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ import mcp.types as mcp_types
10
+
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Callable
13
+
14
+ from fastmcp.client import Client as FastMCPClient
15
+ from fastmcp.tools.tool import Tool
16
+
17
+ __all__ = ["ConnectionConfig", "ConnectionType", "Connector"]
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class ConnectionType(str, Enum):
23
+ """Type of connection - determines parallelization capability."""
24
+
25
+ LOCAL = "local" # Stdio/Docker - single instance, not parallelizable
26
+ REMOTE = "remote" # HTTP/URL - can spawn multiple instances
27
+
28
+
29
+ class ConnectionConfig:
30
+ """Configuration for filtering/transforming tools from a remote connection."""
31
+
32
+ def __init__(
33
+ self,
34
+ *,
35
+ prefix: str | None = None,
36
+ include: list[str] | None = None,
37
+ exclude: list[str] | None = None,
38
+ transform: Callable[[Tool], Tool | None] | None = None,
39
+ ) -> None:
40
+ self.prefix = prefix
41
+ self.include = include
42
+ self.exclude = exclude
43
+ self.transform = transform
44
+
45
+
46
+ class Connector:
47
+ """Manages a connection to an MCP server with tool caching.
48
+
49
+ Client creation is deferred to connect() so that:
50
+ 1. Each parallel trace gets fresh client instances
51
+ 2. Connection happens inside trace context (for header injection)
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ transport: Any,
57
+ config: ConnectionConfig,
58
+ name: str,
59
+ connection_type: ConnectionType,
60
+ *,
61
+ auth: str | None = None,
62
+ ) -> None:
63
+ # Store transport config - client created in connect()
64
+ self._transport = transport
65
+ self._auth = auth
66
+ self.config = config
67
+ self.name = name
68
+ self.connection_type = connection_type
69
+ self.client: FastMCPClient[Any] | None = None
70
+ self._tools_cache: list[mcp_types.Tool] | None = None
71
+ self._prompts_cache: list[mcp_types.Prompt] | None = None
72
+ self._resources_cache: list[mcp_types.Resource] | None = None
73
+
74
+ def copy(self) -> Connector:
75
+ """Create a copy of this connector with fresh (unconnected) state.
76
+
77
+ The copy shares transport config but has its own client instance,
78
+ allowing parallel execution without conflicts.
79
+ """
80
+ return Connector(
81
+ transport=self._transport,
82
+ config=self.config,
83
+ name=self.name,
84
+ connection_type=self.connection_type,
85
+ auth=self._auth,
86
+ )
87
+
88
+ @property
89
+ def is_local(self) -> bool:
90
+ """True if this is a local (non-parallelizable) connection."""
91
+ return self.connection_type == ConnectionType.LOCAL
92
+
93
+ @property
94
+ def is_remote(self) -> bool:
95
+ """True if this is a remote (parallelizable) connection."""
96
+ return self.connection_type == ConnectionType.REMOTE
97
+
98
+ @property
99
+ def is_connected(self) -> bool:
100
+ return self.client is not None and self.client.is_connected()
101
+
102
+ @property
103
+ def cached_tools(self) -> list[mcp_types.Tool]:
104
+ return self._tools_cache or []
105
+
106
+ @property
107
+ def cached_prompts(self) -> list[mcp_types.Prompt]:
108
+ return self._prompts_cache or []
109
+
110
+ @property
111
+ def cached_resources(self) -> list[mcp_types.Resource]:
112
+ return self._resources_cache or []
113
+
114
+ async def connect(self) -> None:
115
+ """Create FastMCP client and connect.
116
+
117
+ Client is created here (not in __init__) so that:
118
+ 1. Each parallel trace gets fresh client instances
119
+ 2. httpx auto-instrumentation can inject trace headers
120
+ """
121
+ from fastmcp.client import Client as FastMCPClient
122
+
123
+ self.client = FastMCPClient(
124
+ transport=self._transport,
125
+ auth=self._auth,
126
+ )
127
+ await self.client.__aenter__()
128
+
129
+ async def disconnect(self) -> None:
130
+ """Disconnect and clear all caches."""
131
+ if self.client is not None and self.is_connected:
132
+ await self.client.__aexit__(None, None, None)
133
+ self.client = None
134
+ self._tools_cache = None
135
+ self._prompts_cache = None
136
+ self._resources_cache = None
137
+
138
+ async def list_tools(self) -> list[mcp_types.Tool]:
139
+ """Fetch tools from server, apply filters/transforms/prefix, and cache.
140
+
141
+ Always fetches fresh data from the server (no caching check).
142
+ The result is cached for use by router.build() via cached_tools property.
143
+ """
144
+ if self.client is None:
145
+ raise RuntimeError("Not connected - call connect() first")
146
+ tools = await self.client.list_tools()
147
+
148
+ result: list[mcp_types.Tool] = []
149
+ for tool in tools:
150
+ # Apply include/exclude filter
151
+ if self.config.include is not None and tool.name not in self.config.include:
152
+ continue
153
+ if self.config.exclude is not None and tool.name in self.config.exclude:
154
+ continue
155
+
156
+ # Apply transform
157
+ if self.config.transform is not None:
158
+ from fastmcp.tools.tool import Tool as FastMCPTool
159
+
160
+ fastmcp_tool = FastMCPTool.model_construct(
161
+ name=tool.name,
162
+ description=tool.description or "",
163
+ parameters=tool.inputSchema,
164
+ )
165
+ transformed = self.config.transform(fastmcp_tool)
166
+ if transformed is None:
167
+ continue
168
+ tool = mcp_types.Tool(
169
+ name=transformed.name,
170
+ description=transformed.description,
171
+ inputSchema=transformed.parameters,
172
+ )
173
+
174
+ # Apply prefix
175
+ name = f"{self.config.prefix}_{tool.name}" if self.config.prefix else tool.name
176
+ result.append(
177
+ mcp_types.Tool(
178
+ name=name,
179
+ description=tool.description,
180
+ inputSchema=tool.inputSchema,
181
+ )
182
+ )
183
+
184
+ self._tools_cache = result
185
+ return result
186
+
187
+ async def call_tool(
188
+ self, name: str, arguments: dict[str, Any] | None = None
189
+ ) -> mcp_types.CallToolResult:
190
+ """Call a tool, stripping prefix if needed."""
191
+ if self.client is None:
192
+ raise RuntimeError("Not connected - call connect() first")
193
+ # Strip prefix when calling remote
194
+ if self.config.prefix and name.startswith(f"{self.config.prefix}_"):
195
+ name = name[len(self.config.prefix) + 1 :]
196
+ return await self.client.call_tool_mcp(name, arguments or {})
197
+
198
+ async def list_resources(self) -> list[mcp_types.Resource]:
199
+ """Fetch resources from server and cache.
200
+
201
+ Always fetches fresh data from the server (no caching check).
202
+ The result is cached for use by router.build_resources() via cached_resources property.
203
+
204
+ Note: resources/list is optional in the MCP spec. If the server doesn't
205
+ implement it, we return an empty list gracefully.
206
+ """
207
+ if self.client is None:
208
+ raise RuntimeError("Not connected - call connect() first")
209
+ try:
210
+ self._resources_cache = await self.client.list_resources()
211
+ except Exception as e:
212
+ # Handle servers that don't implement resources/list (optional in MCP spec)
213
+ if "Method not found" in str(e):
214
+ logger.debug("Server %s does not support resources/list", self.name)
215
+ self._resources_cache = []
216
+ else:
217
+ raise
218
+ return self._resources_cache
219
+
220
+ async def list_prompts(self) -> list[mcp_types.Prompt]:
221
+ """Fetch prompts from server and cache.
222
+
223
+ Always fetches fresh data from the server (no caching check).
224
+ The result is cached for use by router.build_prompts() via cached_prompts property.
225
+
226
+ Note: prompts/list is optional in the MCP spec. If the server doesn't
227
+ implement it, we return an empty list gracefully.
228
+ """
229
+ if self.client is None:
230
+ raise RuntimeError("Not connected - call connect() first")
231
+ try:
232
+ self._prompts_cache = await self.client.list_prompts()
233
+ except Exception as e:
234
+ # Handle servers that don't implement prompts/list (optional in MCP spec)
235
+ if "Method not found" in str(e):
236
+ logger.debug("Server %s does not support prompts/list", self.name)
237
+ self._prompts_cache = []
238
+ else:
239
+ raise
240
+ return self._prompts_cache
241
+
242
+ async def read_resource(
243
+ self, uri: str
244
+ ) -> list[mcp_types.TextResourceContents | mcp_types.BlobResourceContents]:
245
+ if self.client is None:
246
+ raise RuntimeError("Not connected - call connect() first")
247
+ return await self.client.read_resource(uri)
248
+
249
+ async def get_prompt(
250
+ self, name: str, arguments: dict[str, Any] | None = None
251
+ ) -> mcp_types.GetPromptResult:
252
+ if self.client is None:
253
+ raise RuntimeError("Not connected - call connect() first")
254
+ return await self.client.get_prompt(name, arguments)
255
+
256
+ def __repr__(self) -> str:
257
+ t = self.connection_type.value
258
+ return f"Connector({self.name!r}, {t}, connected={self.is_connected})"
@@ -0,0 +1,33 @@
1
+ """Connection connectors - methods for connecting to various sources."""
2
+
3
+ from hud.environment.connectors.local import LocalConnectorMixin
4
+ from hud.environment.connectors.openai import OpenAIConnectorMixin
5
+ from hud.environment.connectors.remote import RemoteConnectorMixin
6
+
7
+ __all__ = ["ConnectorsMixin"]
8
+
9
+
10
+ class ConnectorsMixin(
11
+ RemoteConnectorMixin,
12
+ LocalConnectorMixin,
13
+ OpenAIConnectorMixin,
14
+ ):
15
+ """Combined connector mixin providing all connection methods.
16
+
17
+ Remote connections:
18
+ connect_hub(slug) - HUD Hub environment
19
+ connect_url(url) - MCP server via URL
20
+ connect_openapi(spec) - Mount OpenAPI spec as MCP server
21
+
22
+ Local connections (in-process):
23
+ connect_image(image) - Docker image via stdio
24
+ connect_fastapi(app) - Mount FastAPI app as MCP server
25
+ connect_server(server) - Mount MCPServer/FastMCP directly
26
+
27
+ MCP config:
28
+ connect_mcp(config) - Single mcp_config server (auto-detects local/remote)
29
+ connect_mcp_config(mcp_config) - Multiple mcp_config servers
30
+
31
+ Framework imports:
32
+ connect_function_tools(tools) - Import OpenAI Agents SDK FunctionTools
33
+ """
@@ -0,0 +1,68 @@
1
+ """Base connector mixin with shared helper."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ if TYPE_CHECKING:
8
+ from collections.abc import Callable
9
+
10
+ from fastmcp.tools.tool import Tool
11
+
12
+ from hud.environment.connection import ConnectionType, Connector
13
+
14
+ __all__ = ["BaseConnectorMixin"]
15
+
16
+
17
+ class BaseConnectorMixin:
18
+ """Base mixin providing connection helper.
19
+
20
+ Requires:
21
+ _connections: dict[str, Connector]
22
+ """
23
+
24
+ _connections: dict[str, Connector]
25
+
26
+ def _add_connection(
27
+ self,
28
+ name: str,
29
+ transport: Any,
30
+ *,
31
+ connection_type: ConnectionType,
32
+ auth: str | None = None,
33
+ prefix: str | None = None,
34
+ include: list[str] | None = None,
35
+ exclude: list[str] | None = None,
36
+ transform: Callable[[Tool], Tool | None] | None = None,
37
+ ) -> Any:
38
+ """Add a connection to the environment.
39
+
40
+ Args:
41
+ name: Connection name/alias.
42
+ transport: FastMCP transport (URL, config dict, etc.).
43
+ connection_type: LOCAL or REMOTE - determines parallelization.
44
+ auth: Authorization header value.
45
+ prefix: Prefix for tool names.
46
+ include: Only include these tools.
47
+ exclude: Exclude these tools.
48
+ transform: Transform function for tools.
49
+
50
+ Returns:
51
+ self for chaining.
52
+ """
53
+ from hud.environment.connection import ConnectionConfig, Connector
54
+
55
+ config = ConnectionConfig(
56
+ prefix=prefix,
57
+ include=include,
58
+ exclude=exclude,
59
+ transform=transform,
60
+ )
61
+ self._connections[name] = Connector(
62
+ transport,
63
+ config,
64
+ name,
65
+ connection_type=connection_type,
66
+ auth=auth,
67
+ )
68
+ return self
@@ -0,0 +1,177 @@
1
+ """Local connection connectors - Docker image, FastAPI, MCPServer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from hud.environment.connectors.mcp_config import MCPConfigConnectorMixin
8
+
9
+ if TYPE_CHECKING:
10
+ from collections.abc import Callable
11
+
12
+ from fastmcp.tools.tool import Tool
13
+
14
+ __all__ = ["LocalConnectorMixin"]
15
+
16
+
17
+ class LocalConnectorMixin(MCPConfigConnectorMixin):
18
+ """Mixin providing local connection methods.
19
+
20
+ Methods:
21
+ connect_image(image) - Run Docker image via stdio
22
+ connect_fastapi(app) - Mount FastAPI app as MCP server
23
+ connect_server(server) - Mount any MCPServer/FastMCP directly
24
+
25
+ Inherits connect_mcp() from MCPConfigConnectorMixin.
26
+
27
+ Note: include_router() is inherited from MCPServer (via FastMCP).
28
+ """
29
+
30
+ def connect_image(
31
+ self,
32
+ image: str,
33
+ *,
34
+ alias: str | None = None,
35
+ docker_args: list[str] | None = None,
36
+ env_vars: dict[str, str] | None = None,
37
+ prefix: str | None = None,
38
+ include: list[str] | None = None,
39
+ exclude: list[str] | None = None,
40
+ transform: Callable[[Tool], Tool | None] | None = None,
41
+ ) -> Any:
42
+ """Connect to a Docker image via stdio.
43
+
44
+ Creates an MCP config that runs: docker run -i --rm {image}
45
+ Environment variables from `.env` files are auto-injected.
46
+
47
+ Example:
48
+ ```python
49
+ env = Environment("my-env")
50
+ env.connect_image("mcp/fetch")
51
+
52
+ async with env:
53
+ result = await env.call_tool("fetch", url="https://example.com")
54
+ ```
55
+ """
56
+ from hud.cli.utils.docker import create_docker_run_command
57
+
58
+ cmd = create_docker_run_command(
59
+ image=image,
60
+ docker_args=docker_args,
61
+ extra_env=env_vars,
62
+ interactive=True,
63
+ remove=True,
64
+ )
65
+
66
+ name = alias or image
67
+ mcp_config = {
68
+ name: {
69
+ "command": cmd[0],
70
+ "args": cmd[1:],
71
+ }
72
+ }
73
+ return self.connect_mcp(
74
+ mcp_config,
75
+ alias=name,
76
+ prefix=prefix,
77
+ include=include,
78
+ exclude=exclude,
79
+ transform=transform,
80
+ )
81
+
82
+ def connect_fastapi(
83
+ self,
84
+ app: Any,
85
+ *,
86
+ name: str | None = None,
87
+ prefix: str | None = None,
88
+ include_hidden: bool = True,
89
+ ) -> Any:
90
+ """Import a FastAPI application's routes as MCP tools.
91
+
92
+ Uses FastMCP's from_fastapi() to convert FastAPI endpoints to MCP tools,
93
+ then imports them synchronously so they're available immediately.
94
+
95
+ Args:
96
+ app: FastAPI application instance
97
+ name: Custom name for the server (defaults to app.title)
98
+ prefix: Optional prefix for tool names
99
+ include_hidden: If True (default), includes routes with include_in_schema=False
100
+
101
+ Example:
102
+ ```python
103
+ from fastapi import FastAPI
104
+
105
+ api = FastAPI()
106
+
107
+
108
+ @api.get("/users/{user_id}", operation_id="get_user")
109
+ def get_user(user_id: int):
110
+ return {"id": user_id, "name": "Alice"}
111
+
112
+
113
+ env = Environment("my-env")
114
+ env.connect_fastapi(api)
115
+
116
+ async with env:
117
+ result = await env.call_tool("get_user", user_id=1)
118
+ ```
119
+
120
+ Tip: Use operation_id in FastAPI decorators for cleaner tool names.
121
+ """
122
+ from fastmcp import FastMCP
123
+
124
+ # Temporarily enable hidden routes for OpenAPI generation
125
+ hidden_routes: list[Any] = []
126
+ if include_hidden:
127
+ for route in getattr(app, "routes", []):
128
+ if hasattr(route, "include_in_schema") and not route.include_in_schema:
129
+ hidden_routes.append(route)
130
+ route.include_in_schema = True
131
+ # Clear cached openapi schema so it regenerates
132
+ if hasattr(app, "openapi_schema"):
133
+ app.openapi_schema = None
134
+
135
+ try:
136
+ server_name = name or getattr(app, "title", None) or "fastapi"
137
+ mcp_server = FastMCP.from_fastapi(app=app, name=server_name)
138
+ # Use include_router for synchronous import (tools available immediately)
139
+ self.include_router(mcp_server, prefix=prefix) # type: ignore
140
+ finally:
141
+ # Restore original states
142
+ for route in hidden_routes:
143
+ route.include_in_schema = False
144
+ if hidden_routes and hasattr(app, "openapi_schema"):
145
+ app.openapi_schema = None # Clear cache again
146
+
147
+ return self
148
+
149
+ def connect_server(
150
+ self,
151
+ server: Any,
152
+ *,
153
+ prefix: str | None = None,
154
+ ) -> Any:
155
+ """Import an MCPServer or FastMCP instance's tools directly.
156
+
157
+ Example:
158
+ ```python
159
+ from fastmcp import FastMCP
160
+
161
+ tools = FastMCP("tools")
162
+
163
+
164
+ @tools.tool
165
+ def greet(name: str) -> str:
166
+ return f"Hello, {name}!"
167
+
168
+
169
+ env = Environment("my-env")
170
+ env.connect_server(tools)
171
+
172
+ async with env:
173
+ result = await env.call_tool("greet", name="World")
174
+ ```
175
+ """
176
+ self.include_router(server, prefix=prefix) # type: ignore
177
+ return self
@@ -0,0 +1,137 @@
1
+ """MCP config connection connectors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from hud.environment.connectors.base import BaseConnectorMixin
8
+
9
+ if TYPE_CHECKING:
10
+ from collections.abc import Callable
11
+
12
+ from fastmcp.tools.tool import Tool
13
+
14
+ __all__ = ["MCPConfigConnectorMixin"]
15
+
16
+
17
+ class MCPConfigConnectorMixin(BaseConnectorMixin):
18
+ """Mixin providing mcp_config connection methods."""
19
+
20
+ def connect_mcp(
21
+ self,
22
+ config: dict[str, dict[str, Any]],
23
+ *,
24
+ alias: str | None = None,
25
+ prefix: str | None = None,
26
+ include: list[str] | None = None,
27
+ exclude: list[str] | None = None,
28
+ transform: Callable[[Tool], Tool | None] | None = None,
29
+ ) -> Any:
30
+ """Connect using an mcp_config dictionary (single server).
31
+
32
+ Auto-detects LOCAL (stdio) vs REMOTE (URL) based on config.
33
+
34
+ Example:
35
+ ```python
36
+ env = Environment("my-env")
37
+
38
+ # Stdio server
39
+ env.connect_mcp(
40
+ {
41
+ "filesystem": {
42
+ "command": "npx",
43
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
44
+ }
45
+ }
46
+ )
47
+
48
+ async with env:
49
+ await env.call_tool("read_file", path="/tmp/test.txt")
50
+ ```
51
+ """
52
+ from hud.environment.connection import ConnectionType
53
+ from hud.settings import settings
54
+
55
+ name = alias or next(iter(config.keys()), "mcp")
56
+ server_config = next(iter(config.values()), {})
57
+
58
+ is_local = "command" in server_config or "args" in server_config
59
+ conn_type = ConnectionType.LOCAL if is_local else ConnectionType.REMOTE
60
+
61
+ transport: Any = config
62
+ if not is_local and "url" in server_config:
63
+ max_request_timeout = 840
64
+ server_config.setdefault(
65
+ "sse_read_timeout",
66
+ min(settings.client_timeout, max_request_timeout)
67
+ if settings.client_timeout > 0
68
+ else max_request_timeout,
69
+ )
70
+ transport = _build_transport(server_config)
71
+
72
+ return self._add_connection(
73
+ name,
74
+ transport,
75
+ connection_type=conn_type,
76
+ prefix=prefix,
77
+ include=include,
78
+ exclude=exclude,
79
+ transform=transform,
80
+ )
81
+
82
+ def connect_mcp_config(
83
+ self,
84
+ mcp_config: dict[str, dict[str, Any]],
85
+ **kwargs: Any,
86
+ ) -> Any:
87
+ """Connect multiple servers from an mcp_config dictionary.
88
+
89
+ Example:
90
+ ```python
91
+ env = Environment("my-env")
92
+
93
+ # Claude Desktop style config
94
+ env.connect_mcp_config(
95
+ {
96
+ "filesystem": {
97
+ "command": "npx",
98
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
99
+ },
100
+ "github": {
101
+ "command": "npx",
102
+ "args": ["-y", "@modelcontextprotocol/server-github"],
103
+ "env": {"GITHUB_TOKEN": "..."},
104
+ },
105
+ }
106
+ )
107
+
108
+ async with env:
109
+ await env.call_tool("read_file", path="/tmp/test.txt")
110
+ await env.call_tool("search_repositories", query="mcp")
111
+ ```
112
+ """
113
+ # Store mcp_config for serialization (v4 format)
114
+ # Merge with existing if called multiple times
115
+ if not hasattr(self, "_mcp_config") or self._mcp_config is None:
116
+ self._mcp_config = {}
117
+ self._mcp_config.update(mcp_config)
118
+
119
+ for server_name, server_config in mcp_config.items():
120
+ self.connect_mcp({server_name: server_config}, alias=server_name, **kwargs)
121
+ return self
122
+
123
+
124
+ def _build_transport(server_config: dict[str, Any]) -> Any:
125
+ from fastmcp.client.transports import SSETransport, StreamableHttpTransport
126
+ from fastmcp.mcp_config import infer_transport_type_from_url
127
+
128
+ url = server_config["url"]
129
+ transport_type = server_config.get("transport") or infer_transport_type_from_url(url)
130
+ transport_cls = SSETransport if transport_type == "sse" else StreamableHttpTransport
131
+
132
+ return transport_cls(
133
+ url=url,
134
+ headers=server_config.get("headers"),
135
+ auth=server_config.get("auth"),
136
+ sse_read_timeout=server_config.get("sse_read_timeout"),
137
+ )