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.
- hud/__init__.py +27 -7
- hud/agents/__init__.py +70 -5
- hud/agents/base.py +238 -500
- hud/agents/claude.py +236 -247
- hud/agents/gateway.py +42 -0
- hud/agents/gemini.py +264 -0
- hud/agents/gemini_cua.py +324 -0
- hud/agents/grounded_openai.py +98 -100
- hud/agents/misc/integration_test_agent.py +51 -20
- hud/agents/misc/response_agent.py +48 -36
- hud/agents/openai.py +282 -296
- hud/agents/{openai_chat_generic.py → openai_chat.py} +63 -33
- hud/agents/operator.py +199 -0
- hud/agents/resolver.py +70 -0
- hud/agents/tests/conftest.py +133 -0
- hud/agents/tests/test_base.py +300 -622
- hud/agents/tests/test_base_runtime.py +233 -0
- hud/agents/tests/test_claude.py +381 -214
- hud/agents/tests/test_client.py +9 -10
- hud/agents/tests/test_gemini.py +369 -0
- hud/agents/tests/test_grounded_openai_agent.py +65 -50
- hud/agents/tests/test_openai.py +377 -140
- hud/agents/tests/test_operator.py +362 -0
- hud/agents/tests/test_resolver.py +192 -0
- hud/agents/tests/test_run_eval.py +179 -0
- hud/agents/types.py +148 -0
- hud/cli/__init__.py +493 -546
- hud/cli/analyze.py +43 -5
- hud/cli/build.py +699 -113
- hud/cli/debug.py +8 -5
- hud/cli/dev.py +889 -732
- hud/cli/eval.py +793 -667
- hud/cli/flows/dev.py +167 -0
- hud/cli/flows/init.py +191 -0
- hud/cli/flows/tasks.py +153 -56
- hud/cli/flows/templates.py +151 -0
- hud/cli/flows/tests/__init__.py +1 -0
- hud/cli/flows/tests/test_dev.py +126 -0
- hud/cli/init.py +60 -58
- hud/cli/pull.py +1 -1
- hud/cli/push.py +38 -13
- hud/cli/rft.py +311 -0
- hud/cli/rft_status.py +145 -0
- hud/cli/tests/test_analyze.py +5 -5
- hud/cli/tests/test_analyze_metadata.py +3 -2
- hud/cli/tests/test_analyze_module.py +120 -0
- hud/cli/tests/test_build.py +110 -8
- hud/cli/tests/test_build_failure.py +41 -0
- hud/cli/tests/test_build_module.py +50 -0
- hud/cli/tests/test_cli_init.py +6 -1
- hud/cli/tests/test_cli_more_wrappers.py +30 -0
- hud/cli/tests/test_cli_root.py +140 -0
- hud/cli/tests/test_convert.py +361 -0
- hud/cli/tests/test_debug.py +12 -10
- hud/cli/tests/test_dev.py +197 -0
- hud/cli/tests/test_eval.py +251 -0
- hud/cli/tests/test_eval_bedrock.py +51 -0
- hud/cli/tests/test_init.py +124 -0
- hud/cli/tests/test_main_module.py +11 -5
- hud/cli/tests/test_mcp_server.py +12 -100
- hud/cli/tests/test_push.py +1 -1
- hud/cli/tests/test_push_happy.py +74 -0
- hud/cli/tests/test_push_wrapper.py +23 -0
- hud/cli/tests/test_registry.py +1 -1
- hud/cli/tests/test_utils.py +1 -1
- hud/cli/{rl → utils}/celebrate.py +14 -12
- hud/cli/utils/config.py +18 -1
- hud/cli/utils/docker.py +130 -4
- hud/cli/utils/env_check.py +9 -9
- hud/cli/utils/git.py +136 -0
- hud/cli/utils/interactive.py +39 -5
- hud/cli/utils/metadata.py +70 -1
- hud/cli/utils/runner.py +1 -1
- hud/cli/utils/server.py +2 -2
- hud/cli/utils/source_hash.py +3 -3
- hud/cli/utils/tasks.py +4 -1
- hud/cli/utils/tests/__init__.py +0 -0
- hud/cli/utils/tests/test_config.py +58 -0
- hud/cli/utils/tests/test_docker.py +93 -0
- hud/cli/utils/tests/test_docker_hints.py +71 -0
- hud/cli/utils/tests/test_env_check.py +74 -0
- hud/cli/utils/tests/test_environment.py +42 -0
- hud/cli/utils/tests/test_git.py +142 -0
- hud/cli/utils/tests/test_interactive_module.py +60 -0
- hud/cli/utils/tests/test_local_runner.py +50 -0
- hud/cli/utils/tests/test_logging_utils.py +23 -0
- hud/cli/utils/tests/test_metadata.py +49 -0
- hud/cli/utils/tests/test_package_runner.py +35 -0
- hud/cli/utils/tests/test_registry_utils.py +49 -0
- hud/cli/utils/tests/test_remote_runner.py +25 -0
- hud/cli/utils/tests/test_runner_modules.py +52 -0
- hud/cli/utils/tests/test_source_hash.py +36 -0
- hud/cli/utils/tests/test_tasks.py +80 -0
- hud/cli/utils/version_check.py +258 -0
- hud/cli/{rl → utils}/viewer.py +2 -2
- hud/clients/README.md +12 -11
- hud/clients/__init__.py +4 -3
- hud/clients/base.py +166 -26
- hud/clients/environment.py +51 -0
- hud/clients/fastmcp.py +13 -6
- hud/clients/mcp_use.py +45 -15
- hud/clients/tests/test_analyze_scenarios.py +206 -0
- hud/clients/tests/test_protocol.py +9 -3
- hud/datasets/__init__.py +23 -20
- hud/datasets/loader.py +326 -0
- hud/datasets/runner.py +198 -105
- hud/datasets/tests/__init__.py +0 -0
- hud/datasets/tests/test_loader.py +221 -0
- hud/datasets/tests/test_utils.py +315 -0
- hud/datasets/utils.py +270 -90
- hud/environment/__init__.py +52 -0
- hud/environment/connection.py +258 -0
- hud/environment/connectors/__init__.py +33 -0
- hud/environment/connectors/base.py +68 -0
- hud/environment/connectors/local.py +177 -0
- hud/environment/connectors/mcp_config.py +137 -0
- hud/environment/connectors/openai.py +101 -0
- hud/environment/connectors/remote.py +172 -0
- hud/environment/environment.py +835 -0
- hud/environment/integrations/__init__.py +45 -0
- hud/environment/integrations/adk.py +67 -0
- hud/environment/integrations/anthropic.py +196 -0
- hud/environment/integrations/gemini.py +92 -0
- hud/environment/integrations/langchain.py +82 -0
- hud/environment/integrations/llamaindex.py +68 -0
- hud/environment/integrations/openai.py +238 -0
- hud/environment/mock.py +306 -0
- hud/environment/router.py +263 -0
- hud/environment/scenarios.py +620 -0
- hud/environment/tests/__init__.py +1 -0
- hud/environment/tests/test_connection.py +317 -0
- hud/environment/tests/test_connectors.py +205 -0
- hud/environment/tests/test_environment.py +593 -0
- hud/environment/tests/test_integrations.py +257 -0
- hud/environment/tests/test_local_connectors.py +242 -0
- hud/environment/tests/test_scenarios.py +1086 -0
- hud/environment/tests/test_tools.py +208 -0
- hud/environment/types.py +23 -0
- hud/environment/utils/__init__.py +35 -0
- hud/environment/utils/formats.py +215 -0
- hud/environment/utils/schema.py +171 -0
- hud/environment/utils/tool_wrappers.py +113 -0
- hud/eval/__init__.py +67 -0
- hud/eval/context.py +727 -0
- hud/eval/display.py +299 -0
- hud/eval/instrument.py +187 -0
- hud/eval/manager.py +533 -0
- hud/eval/parallel.py +268 -0
- hud/eval/task.py +372 -0
- hud/eval/tests/__init__.py +1 -0
- hud/eval/tests/test_context.py +178 -0
- hud/eval/tests/test_eval.py +210 -0
- hud/eval/tests/test_manager.py +152 -0
- hud/eval/tests/test_parallel.py +168 -0
- hud/eval/tests/test_task.py +291 -0
- hud/eval/types.py +65 -0
- hud/eval/utils.py +194 -0
- hud/patches/__init__.py +19 -0
- hud/patches/mcp_patches.py +308 -0
- hud/patches/warnings.py +54 -0
- hud/samples/browser.py +4 -4
- hud/server/__init__.py +2 -1
- hud/server/low_level.py +2 -1
- hud/server/router.py +164 -0
- hud/server/server.py +567 -80
- hud/server/tests/test_mcp_server_integration.py +11 -11
- hud/server/tests/test_mcp_server_more.py +1 -1
- hud/server/tests/test_server_extra.py +2 -0
- hud/settings.py +45 -3
- hud/shared/exceptions.py +36 -10
- hud/shared/hints.py +26 -1
- hud/shared/requests.py +15 -3
- hud/shared/tests/test_exceptions.py +40 -31
- hud/shared/tests/test_hints.py +167 -0
- hud/telemetry/__init__.py +20 -19
- hud/telemetry/exporter.py +201 -0
- hud/telemetry/instrument.py +165 -253
- hud/telemetry/tests/test_eval_telemetry.py +356 -0
- hud/telemetry/tests/test_exporter.py +258 -0
- hud/telemetry/tests/test_instrument.py +401 -0
- hud/tools/__init__.py +18 -2
- hud/tools/agent.py +223 -0
- hud/tools/apply_patch.py +639 -0
- hud/tools/base.py +54 -4
- hud/tools/bash.py +2 -2
- hud/tools/computer/__init__.py +36 -3
- hud/tools/computer/anthropic.py +2 -2
- hud/tools/computer/gemini.py +385 -0
- hud/tools/computer/hud.py +23 -6
- hud/tools/computer/openai.py +20 -21
- hud/tools/computer/qwen.py +434 -0
- hud/tools/computer/settings.py +37 -0
- hud/tools/edit.py +3 -7
- hud/tools/executors/base.py +4 -2
- hud/tools/executors/pyautogui.py +1 -1
- hud/tools/grounding/grounded_tool.py +13 -18
- hud/tools/grounding/grounder.py +10 -31
- hud/tools/grounding/tests/test_grounded_tool.py +26 -44
- hud/tools/jupyter.py +330 -0
- hud/tools/playwright.py +18 -3
- hud/tools/shell.py +308 -0
- hud/tools/tests/test_agent_tool.py +355 -0
- hud/tools/tests/test_apply_patch.py +718 -0
- hud/tools/tests/test_computer.py +4 -9
- hud/tools/tests/test_computer_actions.py +24 -2
- hud/tools/tests/test_jupyter_tool.py +181 -0
- hud/tools/tests/test_shell.py +596 -0
- hud/tools/tests/test_submit.py +85 -0
- hud/tools/tests/test_types.py +193 -0
- hud/tools/types.py +21 -1
- hud/types.py +194 -56
- hud/utils/__init__.py +2 -0
- hud/utils/env.py +67 -0
- hud/utils/hud_console.py +89 -18
- hud/utils/mcp.py +15 -58
- hud/utils/strict_schema.py +162 -0
- hud/utils/tests/test_init.py +1 -2
- hud/utils/tests/test_mcp.py +1 -28
- hud/utils/tests/test_pretty_errors.py +186 -0
- hud/utils/tests/test_tool_shorthand.py +154 -0
- hud/utils/tests/test_version.py +1 -1
- hud/utils/types.py +20 -0
- hud/version.py +1 -1
- hud_python-0.5.13.dist-info/METADATA +264 -0
- hud_python-0.5.13.dist-info/RECORD +305 -0
- {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/WHEEL +1 -1
- hud/agents/langchain.py +0 -261
- hud/agents/lite_llm.py +0 -72
- hud/cli/rl/__init__.py +0 -180
- hud/cli/rl/config.py +0 -101
- hud/cli/rl/display.py +0 -133
- hud/cli/rl/gpu.py +0 -63
- hud/cli/rl/gpu_utils.py +0 -321
- hud/cli/rl/local_runner.py +0 -595
- hud/cli/rl/presets.py +0 -96
- hud/cli/rl/remote_runner.py +0 -463
- hud/cli/rl/rl_api.py +0 -150
- hud/cli/rl/vllm.py +0 -177
- hud/cli/rl/wait_utils.py +0 -89
- hud/datasets/parallel.py +0 -687
- hud/misc/__init__.py +0 -1
- hud/misc/claude_plays_pokemon.py +0 -292
- hud/otel/__init__.py +0 -35
- hud/otel/collector.py +0 -142
- hud/otel/config.py +0 -181
- hud/otel/context.py +0 -570
- hud/otel/exporters.py +0 -369
- hud/otel/instrumentation.py +0 -135
- hud/otel/processors.py +0 -121
- hud/otel/tests/__init__.py +0 -1
- hud/otel/tests/test_processors.py +0 -197
- hud/rl/README.md +0 -30
- hud/rl/__init__.py +0 -1
- hud/rl/actor.py +0 -176
- hud/rl/buffer.py +0 -405
- hud/rl/chat_template.jinja +0 -101
- hud/rl/config.py +0 -192
- hud/rl/distributed.py +0 -132
- hud/rl/learner.py +0 -637
- hud/rl/tests/__init__.py +0 -1
- hud/rl/tests/test_learner.py +0 -186
- hud/rl/train.py +0 -382
- hud/rl/types.py +0 -101
- hud/rl/utils/start_vllm_server.sh +0 -30
- hud/rl/utils.py +0 -524
- hud/rl/vllm_adapter.py +0 -143
- hud/telemetry/job.py +0 -352
- hud/telemetry/replay.py +0 -74
- hud/telemetry/tests/test_replay.py +0 -40
- hud/telemetry/tests/test_trace.py +0 -63
- hud/telemetry/trace.py +0 -158
- hud/utils/agent_factories.py +0 -86
- hud/utils/async_utils.py +0 -65
- hud/utils/group_eval.py +0 -223
- hud/utils/progress.py +0 -149
- hud/utils/tasks.py +0 -127
- hud/utils/tests/test_async_utils.py +0 -173
- hud/utils/tests/test_progress.py +0 -261
- hud_python-0.4.45.dist-info/METADATA +0 -552
- hud_python-0.4.45.dist-info/RECORD +0 -228
- {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/entry_points.txt +0 -0
- {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
|
+
)
|