hud-python 0.3.5__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of hud-python might be problematic. Click here for more details.
- hud/__init__.py +22 -89
- hud/agents/__init__.py +17 -0
- hud/agents/art.py +101 -0
- hud/agents/base.py +599 -0
- hud/{mcp → agents}/claude.py +373 -321
- hud/{mcp → agents}/langchain.py +250 -250
- hud/agents/misc/__init__.py +7 -0
- hud/{agent → agents}/misc/response_agent.py +80 -80
- hud/{mcp → agents}/openai.py +352 -334
- hud/agents/openai_chat_generic.py +154 -0
- hud/{mcp → agents}/tests/__init__.py +1 -1
- hud/agents/tests/test_base.py +742 -0
- hud/agents/tests/test_claude.py +324 -0
- hud/{mcp → agents}/tests/test_client.py +363 -324
- hud/{mcp → agents}/tests/test_openai.py +237 -238
- hud/cli/__init__.py +617 -0
- hud/cli/__main__.py +8 -0
- hud/cli/analyze.py +371 -0
- hud/cli/analyze_metadata.py +230 -0
- hud/cli/build.py +427 -0
- hud/cli/clone.py +185 -0
- hud/cli/cursor.py +92 -0
- hud/cli/debug.py +392 -0
- hud/cli/docker_utils.py +83 -0
- hud/cli/init.py +281 -0
- hud/cli/interactive.py +353 -0
- hud/cli/mcp_server.py +756 -0
- hud/cli/pull.py +336 -0
- hud/cli/push.py +379 -0
- hud/cli/remote_runner.py +311 -0
- hud/cli/runner.py +160 -0
- hud/cli/tests/__init__.py +3 -0
- hud/cli/tests/test_analyze.py +284 -0
- hud/cli/tests/test_cli_init.py +265 -0
- hud/cli/tests/test_cli_main.py +27 -0
- hud/cli/tests/test_clone.py +142 -0
- hud/cli/tests/test_cursor.py +253 -0
- hud/cli/tests/test_debug.py +453 -0
- hud/cli/tests/test_mcp_server.py +139 -0
- hud/cli/tests/test_utils.py +388 -0
- hud/cli/utils.py +263 -0
- hud/clients/README.md +143 -0
- hud/clients/__init__.py +16 -0
- hud/clients/base.py +354 -0
- hud/clients/fastmcp.py +202 -0
- hud/clients/mcp_use.py +278 -0
- hud/clients/tests/__init__.py +1 -0
- hud/clients/tests/test_client_integration.py +111 -0
- hud/clients/tests/test_fastmcp.py +342 -0
- hud/clients/tests/test_protocol.py +188 -0
- hud/clients/utils/__init__.py +1 -0
- hud/clients/utils/retry_transport.py +160 -0
- hud/datasets.py +322 -192
- hud/misc/__init__.py +1 -0
- hud/{agent → misc}/claude_plays_pokemon.py +292 -283
- hud/otel/__init__.py +35 -0
- hud/otel/collector.py +142 -0
- hud/otel/config.py +164 -0
- hud/otel/context.py +536 -0
- hud/otel/exporters.py +366 -0
- hud/otel/instrumentation.py +97 -0
- hud/otel/processors.py +118 -0
- hud/otel/tests/__init__.py +1 -0
- hud/otel/tests/test_processors.py +197 -0
- hud/server/__init__.py +5 -5
- hud/server/context.py +114 -0
- hud/server/helper/__init__.py +5 -0
- hud/server/low_level.py +132 -0
- hud/server/server.py +166 -0
- hud/server/tests/__init__.py +3 -0
- hud/settings.py +73 -79
- hud/shared/__init__.py +5 -0
- hud/{exceptions.py → shared/exceptions.py} +180 -180
- hud/{server → shared}/requests.py +264 -264
- hud/shared/tests/test_exceptions.py +157 -0
- hud/{server → shared}/tests/test_requests.py +275 -275
- hud/telemetry/__init__.py +25 -30
- hud/telemetry/instrument.py +379 -0
- hud/telemetry/job.py +309 -141
- hud/telemetry/replay.py +74 -0
- hud/telemetry/trace.py +83 -0
- hud/tools/__init__.py +33 -34
- hud/tools/base.py +365 -65
- hud/tools/bash.py +161 -137
- hud/tools/computer/__init__.py +15 -13
- hud/tools/computer/anthropic.py +437 -420
- hud/tools/computer/hud.py +376 -334
- hud/tools/computer/openai.py +295 -292
- hud/tools/computer/settings.py +82 -0
- hud/tools/edit.py +314 -290
- hud/tools/executors/__init__.py +30 -30
- hud/tools/executors/base.py +539 -532
- hud/tools/executors/pyautogui.py +621 -619
- hud/tools/executors/tests/__init__.py +1 -1
- hud/tools/executors/tests/test_base_executor.py +338 -338
- hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
- hud/tools/executors/xdo.py +511 -503
- hud/tools/{playwright_tool.py → playwright.py} +412 -379
- hud/tools/tests/__init__.py +3 -3
- hud/tools/tests/test_base.py +282 -0
- hud/tools/tests/test_bash.py +158 -152
- hud/tools/tests/test_bash_extended.py +197 -0
- hud/tools/tests/test_computer.py +425 -52
- hud/tools/tests/test_computer_actions.py +34 -34
- hud/tools/tests/test_edit.py +259 -240
- hud/tools/tests/test_init.py +27 -27
- hud/tools/tests/test_playwright_tool.py +183 -183
- hud/tools/tests/test_tools.py +145 -157
- hud/tools/tests/test_utils.py +156 -156
- hud/tools/types.py +72 -0
- hud/tools/utils.py +50 -50
- hud/types.py +136 -89
- hud/utils/__init__.py +10 -16
- hud/utils/async_utils.py +65 -0
- hud/utils/design.py +168 -0
- hud/utils/mcp.py +55 -0
- hud/utils/progress.py +149 -149
- hud/utils/telemetry.py +66 -66
- hud/utils/tests/test_async_utils.py +173 -0
- hud/utils/tests/test_init.py +17 -21
- hud/utils/tests/test_progress.py +261 -225
- hud/utils/tests/test_telemetry.py +82 -37
- hud/utils/tests/test_version.py +8 -8
- hud/version.py +7 -7
- hud_python-0.4.0.dist-info/METADATA +474 -0
- hud_python-0.4.0.dist-info/RECORD +132 -0
- hud_python-0.4.0.dist-info/entry_points.txt +3 -0
- {hud_python-0.3.5.dist-info → hud_python-0.4.0.dist-info}/licenses/LICENSE +21 -21
- hud/adapters/__init__.py +0 -8
- hud/adapters/claude/__init__.py +0 -5
- hud/adapters/claude/adapter.py +0 -180
- hud/adapters/claude/tests/__init__.py +0 -1
- hud/adapters/claude/tests/test_adapter.py +0 -519
- hud/adapters/common/__init__.py +0 -6
- hud/adapters/common/adapter.py +0 -178
- hud/adapters/common/tests/test_adapter.py +0 -289
- hud/adapters/common/types.py +0 -446
- hud/adapters/operator/__init__.py +0 -5
- hud/adapters/operator/adapter.py +0 -108
- hud/adapters/operator/tests/__init__.py +0 -1
- hud/adapters/operator/tests/test_adapter.py +0 -370
- hud/agent/__init__.py +0 -19
- hud/agent/base.py +0 -126
- hud/agent/claude.py +0 -271
- hud/agent/langchain.py +0 -215
- hud/agent/misc/__init__.py +0 -3
- hud/agent/operator.py +0 -268
- hud/agent/tests/__init__.py +0 -1
- hud/agent/tests/test_base.py +0 -202
- hud/env/__init__.py +0 -11
- hud/env/client.py +0 -35
- hud/env/docker_client.py +0 -349
- hud/env/environment.py +0 -446
- hud/env/local_docker_client.py +0 -358
- hud/env/remote_client.py +0 -212
- hud/env/remote_docker_client.py +0 -292
- hud/gym.py +0 -130
- hud/job.py +0 -773
- hud/mcp/__init__.py +0 -17
- hud/mcp/base.py +0 -631
- hud/mcp/client.py +0 -312
- hud/mcp/tests/test_base.py +0 -512
- hud/mcp/tests/test_claude.py +0 -294
- hud/task.py +0 -149
- hud/taskset.py +0 -237
- hud/telemetry/_trace.py +0 -347
- hud/telemetry/context.py +0 -230
- hud/telemetry/exporter.py +0 -575
- hud/telemetry/instrumentation/__init__.py +0 -3
- hud/telemetry/instrumentation/mcp.py +0 -259
- hud/telemetry/instrumentation/registry.py +0 -59
- hud/telemetry/mcp_models.py +0 -270
- hud/telemetry/tests/__init__.py +0 -1
- hud/telemetry/tests/test_context.py +0 -210
- hud/telemetry/tests/test_trace.py +0 -312
- hud/tools/helper/README.md +0 -56
- hud/tools/helper/__init__.py +0 -9
- hud/tools/helper/mcp_server.py +0 -78
- hud/tools/helper/server_initialization.py +0 -115
- hud/tools/helper/utils.py +0 -58
- hud/trajectory.py +0 -94
- hud/utils/agent.py +0 -37
- hud/utils/common.py +0 -256
- hud/utils/config.py +0 -120
- hud/utils/deprecation.py +0 -115
- hud/utils/misc.py +0 -53
- hud/utils/tests/test_common.py +0 -277
- hud/utils/tests/test_config.py +0 -129
- hud_python-0.3.5.dist-info/METADATA +0 -284
- hud_python-0.3.5.dist-info/RECORD +0 -120
- /hud/{adapters/common → shared}/tests/__init__.py +0 -0
- {hud_python-0.3.5.dist-info → hud_python-0.4.0.dist-info}/WHEEL +0 -0
hud/clients/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# HUD MCP Client Architecture
|
|
2
|
+
|
|
3
|
+
This directory contains the MCP client implementations for HUD SDK. The architecture is designed to be flexible and extensible, allowing different client implementations while maintaining a consistent interface for agents.
|
|
4
|
+
|
|
5
|
+
## Architecture Overview
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
hud/clients/
|
|
9
|
+
├── base.py # Protocol definition and base class
|
|
10
|
+
├── mcp_use.py # MCP-use based implementation (legacy)
|
|
11
|
+
├── fastmcp.py # FastMCP based implementation (modern)
|
|
12
|
+
└── __init__.py # Exports and default client
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Protocol Definition
|
|
16
|
+
|
|
17
|
+
All clients must implement the `AgentMCPClient` protocol:
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
class AgentMCPClient(Protocol):
|
|
21
|
+
async def initialize(self) -> None:
|
|
22
|
+
"""Initialize the client - connect and fetch telemetry."""
|
|
23
|
+
|
|
24
|
+
async def list_tools(self) -> list[types.Tool]:
|
|
25
|
+
"""List all available tools."""
|
|
26
|
+
|
|
27
|
+
async def call_tool(name: str, arguments: dict | None = None) -> types.CallToolResult:
|
|
28
|
+
"""Execute a tool by name."""
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Available Implementations
|
|
32
|
+
|
|
33
|
+
### 1. MCPUseHUDClient
|
|
34
|
+
- Based on the `mcp_use` library
|
|
35
|
+
- Supports multiple concurrent server connections
|
|
36
|
+
- Battle-tested and stable
|
|
37
|
+
- Good for complex multi-server setups
|
|
38
|
+
|
|
39
|
+
### 2. FastMCPHUDClient (Default)
|
|
40
|
+
- Based on the `fastmcp` library
|
|
41
|
+
- Modern, clean API with better error handling
|
|
42
|
+
- Supports various transports (HTTP, WebSocket, stdio, in-memory)
|
|
43
|
+
- Better type safety and structured data support
|
|
44
|
+
|
|
45
|
+
## Usage Examples
|
|
46
|
+
|
|
47
|
+
### Basic Usage
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from hud.clients import MCPUseHUDClient, FastMCPHUDClient
|
|
51
|
+
|
|
52
|
+
# Configuration works for both clients
|
|
53
|
+
mcp_config = {
|
|
54
|
+
"server_name": {
|
|
55
|
+
"command": "python",
|
|
56
|
+
"args": ["server.py"]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# Option 1: MCP-use client
|
|
61
|
+
client = MCPUseHUDClient(mcp_config)
|
|
62
|
+
|
|
63
|
+
# Option 2: FastMCP client
|
|
64
|
+
client = FastMCPHUDClient(mcp_config)
|
|
65
|
+
|
|
66
|
+
# Both use the same API
|
|
67
|
+
async with client:
|
|
68
|
+
tools = await client.list_tools()
|
|
69
|
+
result = await client.call_tool("tool_name", {"arg": "value"})
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### With Agents
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from hud.agents import ClaudeAgent
|
|
76
|
+
|
|
77
|
+
# Either client works with agents
|
|
78
|
+
client = FastMCPHUDClient(mcp_config)
|
|
79
|
+
|
|
80
|
+
agent = ClaudeAgent(
|
|
81
|
+
mcp_client=client,
|
|
82
|
+
model="claude-3-7-sonnet-20250219"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Agent works identically with either client
|
|
86
|
+
result = await agent.run("Your task here")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
## Adding New Clients
|
|
91
|
+
|
|
92
|
+
To add a new client implementation:
|
|
93
|
+
|
|
94
|
+
1. Inherit from `BaseHUDClient`
|
|
95
|
+
2. Implement the required methods:
|
|
96
|
+
- `_connect()` - Establish connection
|
|
97
|
+
- `list_tools()` - List available tools
|
|
98
|
+
- `call_tool()` - Execute tools
|
|
99
|
+
- `_read_resource_internal()` - Read resources
|
|
100
|
+
|
|
101
|
+
3. The base class handles:
|
|
102
|
+
- Initialization flow
|
|
103
|
+
- Telemetry fetching
|
|
104
|
+
- Verbose logging
|
|
105
|
+
- Common HUD features
|
|
106
|
+
|
|
107
|
+
## Tool Output Validation
|
|
108
|
+
|
|
109
|
+
Both client implementations support tool output validation through the MCP protocol:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
# Enable strict validation
|
|
113
|
+
client = MCPClient(mcp_config, strict_validation=True)
|
|
114
|
+
|
|
115
|
+
# With strict validation:
|
|
116
|
+
# - Tools must return structured content if they define an output schema
|
|
117
|
+
# - Content must match the schema or an error is raised
|
|
118
|
+
# - Helps catch tool implementation issues early
|
|
119
|
+
|
|
120
|
+
# Default behavior (lenient validation):
|
|
121
|
+
client = MCPClient(mcp_config, strict_validation=False) # Default
|
|
122
|
+
|
|
123
|
+
# With lenient validation:
|
|
124
|
+
# - Schema violations are logged as warnings
|
|
125
|
+
# - Execution continues even if output doesn't match schema
|
|
126
|
+
# - Better for development and debugging
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Example with Validation
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from hud.clients import MCPClient
|
|
133
|
+
|
|
134
|
+
# Create client with strict validation
|
|
135
|
+
client = MCPClient(mcp_config, strict_validation=True)
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
# If tool has output schema but returns invalid data,
|
|
139
|
+
# this will raise a RuntimeError
|
|
140
|
+
result = await client.call_tool("some_tool", {"arg": "value"})
|
|
141
|
+
except RuntimeError as e:
|
|
142
|
+
print(f"Validation error: {e}")
|
|
143
|
+
```
|
hud/clients/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""HUD MCP client implementations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .base import AgentMCPClient, BaseHUDClient
|
|
6
|
+
from .fastmcp import FastMCPHUDClient
|
|
7
|
+
|
|
8
|
+
# Default to FastMCP for new features
|
|
9
|
+
MCPClient = FastMCPHUDClient
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"AgentMCPClient",
|
|
13
|
+
"BaseHUDClient",
|
|
14
|
+
"FastMCPHUDClient",
|
|
15
|
+
"MCPClient",
|
|
16
|
+
]
|
hud/clients/base.py
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"""Base protocol and implementation for HUD MCP clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from abc import abstractmethod
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Protocol, overload, runtime_checkable
|
|
9
|
+
|
|
10
|
+
from mcp.types import Implementation
|
|
11
|
+
|
|
12
|
+
from hud.types import MCPToolCall, MCPToolResult
|
|
13
|
+
from hud.utils.mcp import setup_hud_telemetry
|
|
14
|
+
from hud.version import __version__ as hud_version
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
import mcp.types as types
|
|
18
|
+
|
|
19
|
+
else:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@runtime_checkable
|
|
27
|
+
class AgentMCPClient(Protocol):
|
|
28
|
+
"""Minimal interface for MCP clients used by agents.
|
|
29
|
+
|
|
30
|
+
Any custom client must implement this interface.
|
|
31
|
+
|
|
32
|
+
Any custom agent can assume that this will be the interaction protocol.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
_initialized: bool
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def mcp_config(self) -> dict[str, dict[str, Any]]:
|
|
39
|
+
"""Get the MCP config."""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def is_connected(self) -> bool:
|
|
44
|
+
"""Check if client is connected and initialized."""
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
async def initialize(self, mcp_config: dict[str, dict[str, Any]] | None = None) -> None:
|
|
48
|
+
"""Initialize the client."""
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
async def list_tools(self) -> list[types.Tool]:
|
|
52
|
+
"""List all available tools."""
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
async def call_tool(self, tool_call: MCPToolCall) -> MCPToolResult:
|
|
56
|
+
"""Execute a tool by name."""
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
async def shutdown(self) -> None:
|
|
60
|
+
"""Shutdown the client."""
|
|
61
|
+
...
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class BaseHUDClient(AgentMCPClient):
|
|
65
|
+
"""Base class with common HUD functionality that adds:
|
|
66
|
+
- Connection management
|
|
67
|
+
- Tool discovery
|
|
68
|
+
- Telemetry fetching (hud environment-specific)
|
|
69
|
+
- Logging
|
|
70
|
+
- Strict tool output validation (optional)
|
|
71
|
+
- Environment analysis (optional)
|
|
72
|
+
|
|
73
|
+
Any custom client should inherit from this class, and implement:
|
|
74
|
+
- _connect: Connect to the MCP server
|
|
75
|
+
- list_tools: List all available tools
|
|
76
|
+
- list_resources: List all available resources
|
|
77
|
+
- call_tool: Execute a tool by name
|
|
78
|
+
- read_resource: Read a resource by URI
|
|
79
|
+
- _disconnect: Disconnect from the MCP server
|
|
80
|
+
- any other MCP client methods
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
client_info = Implementation(name="hud-mcp", title="hud MCP Client", version=hud_version)
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
mcp_config: dict[str, dict[str, Any]] | None = None,
|
|
88
|
+
verbose: bool = False,
|
|
89
|
+
strict_validation: bool = False,
|
|
90
|
+
auto_trace: bool = True,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""
|
|
93
|
+
Initialize base client.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
mcp_config: MCP server configuration dict
|
|
97
|
+
verbose: Enable verbose logging
|
|
98
|
+
strict_validation: Enable strict tool output validation
|
|
99
|
+
"""
|
|
100
|
+
self.verbose = verbose
|
|
101
|
+
self._mcp_config = mcp_config
|
|
102
|
+
self._strict_validation = strict_validation
|
|
103
|
+
self._auto_trace = auto_trace
|
|
104
|
+
|
|
105
|
+
self._initialized = False
|
|
106
|
+
self._telemetry_data = {} # Initialize telemetry data
|
|
107
|
+
|
|
108
|
+
if self.verbose:
|
|
109
|
+
self._setup_verbose_logging()
|
|
110
|
+
|
|
111
|
+
async def initialize(self, mcp_config: dict[str, dict[str, Any]] | None = None) -> None:
|
|
112
|
+
"""Initialize connection and fetch tools."""
|
|
113
|
+
if self._initialized:
|
|
114
|
+
logger.warning(
|
|
115
|
+
"Client already connected, if you want to reconnect or change the configuration, "
|
|
116
|
+
"call shutdown() first. This is especially important if you are using an agent."
|
|
117
|
+
)
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
self._mcp_config = mcp_config or self._mcp_config
|
|
121
|
+
if self._mcp_config is None:
|
|
122
|
+
raise ValueError(
|
|
123
|
+
"An MCP server configuration is required"
|
|
124
|
+
"Either pass it to the constructor or call initialize with a configuration"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
setup_hud_telemetry(self._mcp_config, auto_trace=self._auto_trace)
|
|
128
|
+
|
|
129
|
+
logger.debug("Initializing MCP client...")
|
|
130
|
+
|
|
131
|
+
# Subclasses implement connection
|
|
132
|
+
await self._connect(self._mcp_config)
|
|
133
|
+
|
|
134
|
+
# Common hud behavior - fetch telemetry
|
|
135
|
+
await self._fetch_telemetry()
|
|
136
|
+
|
|
137
|
+
self._initialized = True
|
|
138
|
+
logger.info("Client initialized")
|
|
139
|
+
|
|
140
|
+
async def shutdown(self) -> None:
|
|
141
|
+
"""Disconnect from the MCP server."""
|
|
142
|
+
if self._initialized:
|
|
143
|
+
await self._disconnect()
|
|
144
|
+
self._initialized = False
|
|
145
|
+
logger.info("Client disconnected")
|
|
146
|
+
else:
|
|
147
|
+
logger.warning("Client is not running, cannot disconnect")
|
|
148
|
+
|
|
149
|
+
@overload
|
|
150
|
+
async def call_tool(self, tool_call: MCPToolCall, /) -> MCPToolResult: ...
|
|
151
|
+
@overload
|
|
152
|
+
async def call_tool(
|
|
153
|
+
self,
|
|
154
|
+
*,
|
|
155
|
+
name: str,
|
|
156
|
+
arguments: dict[str, Any] | None = None,
|
|
157
|
+
) -> MCPToolResult: ...
|
|
158
|
+
|
|
159
|
+
async def call_tool(
|
|
160
|
+
self,
|
|
161
|
+
tool_call: MCPToolCall | None = None,
|
|
162
|
+
*,
|
|
163
|
+
name: str | None = None,
|
|
164
|
+
arguments: dict[str, Any] | None = None,
|
|
165
|
+
) -> MCPToolResult:
|
|
166
|
+
if tool_call is not None:
|
|
167
|
+
return await self._call_tool(tool_call)
|
|
168
|
+
elif name is not None:
|
|
169
|
+
return await self._call_tool(MCPToolCall(name=name, arguments=arguments))
|
|
170
|
+
else:
|
|
171
|
+
raise TypeError(
|
|
172
|
+
"call_tool() requires either an MCPToolCall positional arg "
|
|
173
|
+
"or keyword 'name' (and optional 'arguments')."
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
@abstractmethod
|
|
177
|
+
async def _connect(self, mcp_config: dict[str, dict[str, Any]]) -> None:
|
|
178
|
+
"""Subclasses implement their connection logic."""
|
|
179
|
+
raise NotImplementedError
|
|
180
|
+
|
|
181
|
+
@abstractmethod
|
|
182
|
+
async def list_tools(self) -> list[types.Tool]:
|
|
183
|
+
"""List all available tools."""
|
|
184
|
+
raise NotImplementedError
|
|
185
|
+
|
|
186
|
+
@abstractmethod
|
|
187
|
+
async def list_resources(self) -> list[types.Resource]:
|
|
188
|
+
"""List all available resources."""
|
|
189
|
+
raise NotImplementedError
|
|
190
|
+
|
|
191
|
+
@abstractmethod
|
|
192
|
+
async def _call_tool(self, tool_call: MCPToolCall) -> MCPToolResult:
|
|
193
|
+
"""Execute a tool by name."""
|
|
194
|
+
raise NotImplementedError
|
|
195
|
+
|
|
196
|
+
@abstractmethod
|
|
197
|
+
async def read_resource(self, uri: str) -> types.ReadResourceResult | None:
|
|
198
|
+
"""Read a resource by URI."""
|
|
199
|
+
raise NotImplementedError
|
|
200
|
+
|
|
201
|
+
@abstractmethod
|
|
202
|
+
async def _disconnect(self) -> None:
|
|
203
|
+
"""Subclasses implement their disconnection logic."""
|
|
204
|
+
raise NotImplementedError
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def is_connected(self) -> bool:
|
|
208
|
+
"""Check if client is connected and initialized."""
|
|
209
|
+
return self._initialized
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def mcp_config(self) -> dict[str, dict[str, Any]]:
|
|
213
|
+
"""Get the MCP config."""
|
|
214
|
+
if self._mcp_config is None:
|
|
215
|
+
raise ValueError("Please initialize the client with a valid MCP config")
|
|
216
|
+
return self._mcp_config
|
|
217
|
+
|
|
218
|
+
async def __aenter__(self: Any) -> Any:
|
|
219
|
+
"""Async context manager entry."""
|
|
220
|
+
await self.initialize()
|
|
221
|
+
return self
|
|
222
|
+
|
|
223
|
+
async def __aexit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None:
|
|
224
|
+
"""Async context manager exit."""
|
|
225
|
+
await self.shutdown()
|
|
226
|
+
|
|
227
|
+
def _setup_verbose_logging(self) -> None:
|
|
228
|
+
"""Configure verbose logging for debugging."""
|
|
229
|
+
logging.getLogger("mcp").setLevel(logging.DEBUG)
|
|
230
|
+
logging.getLogger("fastmcp").setLevel(logging.DEBUG)
|
|
231
|
+
|
|
232
|
+
if not any(isinstance(h, logging.StreamHandler) for h in logger.handlers):
|
|
233
|
+
handler = logging.StreamHandler()
|
|
234
|
+
handler.setFormatter(
|
|
235
|
+
logging.Formatter("[%(levelname)s] %(asctime)s - %(name)s - %(message)s")
|
|
236
|
+
)
|
|
237
|
+
logger.addHandler(handler)
|
|
238
|
+
logger.setLevel(logging.DEBUG)
|
|
239
|
+
|
|
240
|
+
async def _fetch_telemetry(self) -> None:
|
|
241
|
+
"""Common telemetry fetching for all hud clients."""
|
|
242
|
+
try:
|
|
243
|
+
# Try to read telemetry resource directly
|
|
244
|
+
result = await self.read_resource("telemetry://live")
|
|
245
|
+
if result and result.contents:
|
|
246
|
+
# Parse telemetry data
|
|
247
|
+
telemetry_data = json.loads(result.contents[0].text) # type: ignore
|
|
248
|
+
self._telemetry_data = telemetry_data
|
|
249
|
+
|
|
250
|
+
logger.info("📡 Telemetry data fetched:")
|
|
251
|
+
if "live_url" in telemetry_data:
|
|
252
|
+
logger.info(" 🖥️ Live URL: %s", telemetry_data["live_url"])
|
|
253
|
+
if "cdp_url" in telemetry_data:
|
|
254
|
+
logger.info(" 🦾 CDP URL: %s", telemetry_data["cdp_url"])
|
|
255
|
+
if "status" in telemetry_data:
|
|
256
|
+
logger.info(" 📊 Status: %s", telemetry_data["status"])
|
|
257
|
+
if "services" in telemetry_data:
|
|
258
|
+
logger.debug(" 📋 Services:")
|
|
259
|
+
for service, status in telemetry_data["services"].items():
|
|
260
|
+
status_icon = "✅" if status == "running" else "❌"
|
|
261
|
+
logger.debug(" %s %s: %s", status_icon, service, status)
|
|
262
|
+
|
|
263
|
+
if self.verbose:
|
|
264
|
+
logger.debug("Full telemetry data:\n%s", json.dumps(telemetry_data, indent=2))
|
|
265
|
+
except Exception as e:
|
|
266
|
+
# Telemetry is optional
|
|
267
|
+
if self.verbose:
|
|
268
|
+
logger.debug("No telemetry available: %s", e)
|
|
269
|
+
|
|
270
|
+
async def analyze_environment(self) -> dict[str, Any]:
|
|
271
|
+
"""Complete analysis of the MCP environment.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Dictionary containing:
|
|
275
|
+
- tools: All tools with full schemas
|
|
276
|
+
- hub_tools: Hub structures with subtools
|
|
277
|
+
- telemetry: Telemetry resources and data
|
|
278
|
+
- resources: All available resources
|
|
279
|
+
- metadata: Environment metadata
|
|
280
|
+
"""
|
|
281
|
+
if not self._initialized:
|
|
282
|
+
raise ValueError("Client must be initialized before analyzing the environment")
|
|
283
|
+
|
|
284
|
+
analysis: dict[str, Any] = {
|
|
285
|
+
"tools": [],
|
|
286
|
+
"hub_tools": {},
|
|
287
|
+
"telemetry": self._telemetry_data,
|
|
288
|
+
"resources": [],
|
|
289
|
+
"metadata": {
|
|
290
|
+
"servers": list(self._mcp_config.keys()), # type: ignore
|
|
291
|
+
"initialized": self._initialized,
|
|
292
|
+
},
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
# Get all tools with schemas
|
|
296
|
+
tools = await self.list_tools()
|
|
297
|
+
for tool in tools:
|
|
298
|
+
tool_info = {
|
|
299
|
+
"name": tool.name,
|
|
300
|
+
"description": tool.description,
|
|
301
|
+
"input_schema": tool.inputSchema,
|
|
302
|
+
}
|
|
303
|
+
analysis["tools"].append(tool_info)
|
|
304
|
+
|
|
305
|
+
# Check if this is a hub tool (like setup, evaluate)
|
|
306
|
+
if (
|
|
307
|
+
tool.description
|
|
308
|
+
and "internal" in tool.description.lower()
|
|
309
|
+
and "functions" in tool.description.lower()
|
|
310
|
+
):
|
|
311
|
+
# This is likely a hub dispatcher tool
|
|
312
|
+
hub_functions = await self.get_hub_tools(tool.name)
|
|
313
|
+
if hub_functions:
|
|
314
|
+
analysis["hub_tools"][tool.name] = hub_functions
|
|
315
|
+
|
|
316
|
+
# Get all resources
|
|
317
|
+
try:
|
|
318
|
+
resources = await self.list_resources()
|
|
319
|
+
for resource in resources:
|
|
320
|
+
resource_info = {
|
|
321
|
+
"uri": str(resource.uri),
|
|
322
|
+
"name": resource.name,
|
|
323
|
+
"description": resource.description,
|
|
324
|
+
"mime_type": getattr(resource, "mimeType", None),
|
|
325
|
+
}
|
|
326
|
+
analysis["resources"].append(resource_info)
|
|
327
|
+
except Exception as e:
|
|
328
|
+
if self.verbose:
|
|
329
|
+
logger.debug("Could not list resources: %s", e)
|
|
330
|
+
|
|
331
|
+
return analysis
|
|
332
|
+
|
|
333
|
+
async def get_hub_tools(self, hub_name: str) -> list[str]:
|
|
334
|
+
"""Get all subtools for a specific hub (setup/evaluate).
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
hub_name: Name of the hub (e.g., "setup", "evaluate")
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
List of available function names for the hub
|
|
341
|
+
"""
|
|
342
|
+
try:
|
|
343
|
+
# Read the hub's functions catalogue resource
|
|
344
|
+
result = await self.read_resource(f"file:///{hub_name}/functions")
|
|
345
|
+
if result and result.contents:
|
|
346
|
+
# Parse the JSON list of function names
|
|
347
|
+
import json
|
|
348
|
+
|
|
349
|
+
functions = json.loads(result.contents[0].text) # type: ignore
|
|
350
|
+
return functions
|
|
351
|
+
except Exception as e:
|
|
352
|
+
if self.verbose:
|
|
353
|
+
logger.debug("Could not read hub functions for '%s': %s", hub_name, e)
|
|
354
|
+
return []
|