hud-python 0.4.1__py3-none-any.whl → 0.4.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of hud-python might be problematic. Click here for more details.
- hud/__init__.py +22 -22
- hud/agents/__init__.py +13 -15
- hud/agents/base.py +599 -599
- hud/agents/claude.py +373 -373
- hud/agents/langchain.py +261 -250
- hud/agents/misc/__init__.py +7 -7
- hud/agents/misc/response_agent.py +82 -80
- hud/agents/openai.py +352 -352
- hud/agents/openai_chat_generic.py +154 -154
- hud/agents/tests/__init__.py +1 -1
- hud/agents/tests/test_base.py +742 -742
- hud/agents/tests/test_claude.py +324 -324
- hud/agents/tests/test_client.py +363 -363
- hud/agents/tests/test_openai.py +237 -237
- hud/cli/__init__.py +617 -617
- hud/cli/__main__.py +8 -8
- hud/cli/analyze.py +371 -371
- hud/cli/analyze_metadata.py +230 -230
- hud/cli/build.py +498 -427
- hud/cli/clone.py +185 -185
- hud/cli/cursor.py +92 -92
- hud/cli/debug.py +392 -392
- hud/cli/docker_utils.py +83 -83
- hud/cli/init.py +280 -281
- hud/cli/interactive.py +353 -353
- hud/cli/mcp_server.py +764 -756
- hud/cli/pull.py +330 -336
- hud/cli/push.py +404 -370
- hud/cli/remote_runner.py +311 -311
- hud/cli/runner.py +160 -160
- hud/cli/tests/__init__.py +3 -3
- hud/cli/tests/test_analyze.py +284 -284
- hud/cli/tests/test_cli_init.py +265 -265
- hud/cli/tests/test_cli_main.py +27 -27
- hud/cli/tests/test_clone.py +142 -142
- hud/cli/tests/test_cursor.py +253 -253
- hud/cli/tests/test_debug.py +453 -453
- hud/cli/tests/test_mcp_server.py +139 -139
- hud/cli/tests/test_utils.py +388 -388
- hud/cli/utils.py +263 -263
- hud/clients/README.md +143 -143
- hud/clients/__init__.py +16 -16
- hud/clients/base.py +378 -379
- hud/clients/fastmcp.py +222 -222
- hud/clients/mcp_use.py +298 -278
- hud/clients/tests/__init__.py +1 -1
- hud/clients/tests/test_client_integration.py +111 -111
- hud/clients/tests/test_fastmcp.py +342 -342
- hud/clients/tests/test_protocol.py +188 -188
- hud/clients/utils/__init__.py +1 -1
- hud/clients/utils/retry_transport.py +160 -160
- hud/datasets.py +327 -322
- hud/misc/__init__.py +1 -1
- hud/misc/claude_plays_pokemon.py +292 -292
- hud/otel/__init__.py +35 -35
- hud/otel/collector.py +142 -142
- hud/otel/config.py +164 -164
- hud/otel/context.py +536 -536
- hud/otel/exporters.py +366 -366
- hud/otel/instrumentation.py +97 -97
- hud/otel/processors.py +118 -118
- hud/otel/tests/__init__.py +1 -1
- hud/otel/tests/test_processors.py +197 -197
- hud/server/__init__.py +5 -5
- hud/server/context.py +114 -114
- hud/server/helper/__init__.py +5 -5
- hud/server/low_level.py +132 -132
- hud/server/server.py +170 -166
- hud/server/tests/__init__.py +3 -3
- hud/settings.py +73 -73
- hud/shared/__init__.py +5 -5
- hud/shared/exceptions.py +180 -180
- hud/shared/requests.py +264 -264
- hud/shared/tests/test_exceptions.py +157 -157
- hud/shared/tests/test_requests.py +275 -275
- hud/telemetry/__init__.py +25 -25
- hud/telemetry/instrument.py +379 -379
- hud/telemetry/job.py +309 -309
- hud/telemetry/replay.py +74 -74
- hud/telemetry/trace.py +83 -83
- hud/tools/__init__.py +33 -33
- hud/tools/base.py +365 -365
- hud/tools/bash.py +161 -161
- hud/tools/computer/__init__.py +15 -15
- hud/tools/computer/anthropic.py +437 -437
- hud/tools/computer/hud.py +376 -376
- hud/tools/computer/openai.py +295 -295
- hud/tools/computer/settings.py +82 -82
- hud/tools/edit.py +314 -314
- hud/tools/executors/__init__.py +30 -30
- hud/tools/executors/base.py +539 -539
- hud/tools/executors/pyautogui.py +621 -621
- hud/tools/executors/tests/__init__.py +1 -1
- hud/tools/executors/tests/test_base_executor.py +338 -338
- hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
- hud/tools/executors/xdo.py +511 -511
- hud/tools/playwright.py +412 -412
- hud/tools/tests/__init__.py +3 -3
- hud/tools/tests/test_base.py +282 -282
- hud/tools/tests/test_bash.py +158 -158
- hud/tools/tests/test_bash_extended.py +197 -197
- hud/tools/tests/test_computer.py +425 -425
- hud/tools/tests/test_computer_actions.py +34 -34
- hud/tools/tests/test_edit.py +259 -259
- hud/tools/tests/test_init.py +27 -27
- hud/tools/tests/test_playwright_tool.py +183 -183
- hud/tools/tests/test_tools.py +145 -145
- hud/tools/tests/test_utils.py +156 -156
- hud/tools/types.py +72 -72
- hud/tools/utils.py +50 -50
- hud/types.py +136 -136
- hud/utils/__init__.py +10 -10
- hud/utils/async_utils.py +65 -65
- hud/utils/design.py +236 -168
- hud/utils/mcp.py +55 -55
- hud/utils/progress.py +149 -149
- hud/utils/telemetry.py +66 -66
- hud/utils/tests/test_async_utils.py +173 -173
- hud/utils/tests/test_init.py +17 -17
- hud/utils/tests/test_progress.py +261 -261
- hud/utils/tests/test_telemetry.py +82 -82
- hud/utils/tests/test_version.py +8 -8
- hud/version.py +7 -7
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/METADATA +10 -8
- hud_python-0.4.3.dist-info/RECORD +131 -0
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/licenses/LICENSE +21 -21
- hud/agents/art.py +0 -101
- hud_python-0.4.1.dist-info/RECORD +0 -132
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/WHEEL +0 -0
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/entry_points.txt +0 -0
hud/tools/tests/__init__.py
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
__all__ = []
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
__all__ = []
|
hud/tools/tests/test_base.py
CHANGED
|
@@ -1,282 +1,282 @@
|
|
|
1
|
-
"""Tests for base tool classes."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import Any
|
|
6
|
-
from unittest.mock import MagicMock, patch
|
|
7
|
-
|
|
8
|
-
import pytest
|
|
9
|
-
from fastmcp import FastMCP
|
|
10
|
-
from mcp.types import ContentBlock, TextContent
|
|
11
|
-
|
|
12
|
-
from hud.tools.base import _INTERNAL_PREFIX, BaseHub, BaseTool
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class MockTool(BaseTool):
|
|
16
|
-
"""Mock tool for testing."""
|
|
17
|
-
|
|
18
|
-
async def __call__(self, param1: Any = None, param2: Any = None) -> list[ContentBlock]:
|
|
19
|
-
"""Execute the mock tool."""
|
|
20
|
-
kwargs = {"param1": param1, "param2": param2}
|
|
21
|
-
return [TextContent(type="text", text=f"Mock result: {kwargs}")]
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class TestBaseTool:
|
|
25
|
-
"""Test BaseTool class."""
|
|
26
|
-
|
|
27
|
-
def test_init_with_defaults(self):
|
|
28
|
-
"""Test BaseTool initialization with default values."""
|
|
29
|
-
|
|
30
|
-
class TestTool(BaseTool):
|
|
31
|
-
"""A test tool."""
|
|
32
|
-
|
|
33
|
-
async def __call__(self, **kwargs: Any) -> list[ContentBlock]:
|
|
34
|
-
return []
|
|
35
|
-
|
|
36
|
-
tool = TestTool()
|
|
37
|
-
|
|
38
|
-
# Check auto-generated values
|
|
39
|
-
assert tool.name == "test"
|
|
40
|
-
assert tool.title == "Test"
|
|
41
|
-
assert tool.description == "A test tool."
|
|
42
|
-
assert tool.env is None
|
|
43
|
-
assert tool.__name__ == "test"
|
|
44
|
-
assert tool.__doc__ == "A test tool."
|
|
45
|
-
|
|
46
|
-
def test_init_with_custom_values(self):
|
|
47
|
-
"""Test BaseTool initialization with custom values."""
|
|
48
|
-
|
|
49
|
-
env = {"key": "value"}
|
|
50
|
-
tool = MockTool(
|
|
51
|
-
env=env, name="custom_tool", title="Custom Tool", description="Custom description"
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
assert tool.env == env
|
|
55
|
-
assert tool.name == "custom_tool"
|
|
56
|
-
assert tool.title == "Custom Tool"
|
|
57
|
-
assert tool.description == "Custom description"
|
|
58
|
-
assert tool.__name__ == "custom_tool"
|
|
59
|
-
assert tool.__doc__ == "Custom description"
|
|
60
|
-
|
|
61
|
-
def test_init_no_docstring(self):
|
|
62
|
-
"""Test BaseTool with no docstring."""
|
|
63
|
-
|
|
64
|
-
class NoDocTool(BaseTool):
|
|
65
|
-
async def __call__(self, **kwargs: Any) -> list[ContentBlock]:
|
|
66
|
-
return []
|
|
67
|
-
|
|
68
|
-
tool = NoDocTool()
|
|
69
|
-
assert tool.description is None
|
|
70
|
-
assert not hasattr(tool, "__doc__") or tool.__doc__ is None
|
|
71
|
-
|
|
72
|
-
def test_register(self):
|
|
73
|
-
"""Test registering tool with FastMCP server."""
|
|
74
|
-
|
|
75
|
-
server = MagicMock(spec=FastMCP)
|
|
76
|
-
tool = MockTool(name="test_tool")
|
|
77
|
-
|
|
78
|
-
# Test register returns self for chaining
|
|
79
|
-
result = tool.register(server, tag="test")
|
|
80
|
-
|
|
81
|
-
assert result is tool
|
|
82
|
-
server.add_tool.assert_called_once()
|
|
83
|
-
|
|
84
|
-
# Check the tool passed has correct name
|
|
85
|
-
call_args = server.add_tool.call_args
|
|
86
|
-
assert call_args[1]["tag"] == "test"
|
|
87
|
-
|
|
88
|
-
def test_mcp_property_cached(self):
|
|
89
|
-
"""Test mcp property returns cached FunctionTool."""
|
|
90
|
-
|
|
91
|
-
tool = MockTool(name="cached_tool", title="Cached Tool", description="Test caching")
|
|
92
|
-
|
|
93
|
-
# First access creates the tool
|
|
94
|
-
mcp_tool1 = tool.mcp
|
|
95
|
-
assert hasattr(tool, "_mcp_tool")
|
|
96
|
-
|
|
97
|
-
# Second access returns cached
|
|
98
|
-
mcp_tool2 = tool.mcp
|
|
99
|
-
assert mcp_tool1 is mcp_tool2
|
|
100
|
-
|
|
101
|
-
def test_mcp_property_attributes(self):
|
|
102
|
-
"""Test mcp property creates FunctionTool with correct attributes."""
|
|
103
|
-
|
|
104
|
-
tool = MockTool(
|
|
105
|
-
name="mcp_test", title="MCP Test Tool", description="Testing MCP conversion"
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
with patch("fastmcp.tools.FunctionTool") as MockFunctionTool:
|
|
109
|
-
mock_ft = MagicMock()
|
|
110
|
-
MockFunctionTool.from_function.return_value = mock_ft
|
|
111
|
-
|
|
112
|
-
result = tool.mcp
|
|
113
|
-
|
|
114
|
-
# The wrapper function is passed, not the tool itself
|
|
115
|
-
MockFunctionTool.from_function.assert_called_once()
|
|
116
|
-
call_args = MockFunctionTool.from_function.call_args
|
|
117
|
-
|
|
118
|
-
# Check that the correct parameters were passed
|
|
119
|
-
assert call_args[1]["name"] == "mcp_test"
|
|
120
|
-
assert call_args[1]["title"] == "MCP Test Tool"
|
|
121
|
-
assert call_args[1]["description"] == "Testing MCP conversion"
|
|
122
|
-
assert result is mock_ft
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
class TestBaseHub:
|
|
126
|
-
"""Test BaseHub class."""
|
|
127
|
-
|
|
128
|
-
def test_init_basic(self):
|
|
129
|
-
"""Test BaseHub basic initialization."""
|
|
130
|
-
|
|
131
|
-
hub = BaseHub("test_hub")
|
|
132
|
-
|
|
133
|
-
assert hub._prefix_fn("tool") == f"{_INTERNAL_PREFIX}tool"
|
|
134
|
-
assert hasattr(hub, "_tool_manager")
|
|
135
|
-
assert hasattr(hub, "_resource_manager")
|
|
136
|
-
|
|
137
|
-
def test_init_with_env(self):
|
|
138
|
-
"""Test BaseHub initialization with environment."""
|
|
139
|
-
|
|
140
|
-
env = {"test": "env"}
|
|
141
|
-
hub = BaseHub("test_hub", env=env, title="Test Hub", description="A test hub")
|
|
142
|
-
|
|
143
|
-
assert hub.env == env
|
|
144
|
-
|
|
145
|
-
@pytest.mark.asyncio
|
|
146
|
-
async def test_dispatcher_tool_registered(self):
|
|
147
|
-
"""Test that dispatcher tool is registered on init."""
|
|
148
|
-
|
|
149
|
-
hub = BaseHub("dispatcher_test")
|
|
150
|
-
|
|
151
|
-
# Check dispatcher tool exists
|
|
152
|
-
tools = hub._tool_manager._tools
|
|
153
|
-
assert "dispatcher_test" in tools
|
|
154
|
-
|
|
155
|
-
# Test calling dispatcher with internal tool
|
|
156
|
-
@hub.tool("internal_func")
|
|
157
|
-
async def internal_func(value: int) -> Any:
|
|
158
|
-
return [TextContent(type="text", text=f"Internal: {value}")]
|
|
159
|
-
|
|
160
|
-
# Call dispatcher
|
|
161
|
-
result = await hub._tool_manager.call_tool(
|
|
162
|
-
"dispatcher_test", {"name": "internal_func", "arguments": {"value": 42}}
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
# ToolResult has content attribute
|
|
166
|
-
assert len(result.content) == 1
|
|
167
|
-
assert isinstance(result.content[0], TextContent)
|
|
168
|
-
assert result.content[0].text == "Internal: 42"
|
|
169
|
-
|
|
170
|
-
@pytest.mark.asyncio
|
|
171
|
-
async def test_functions_catalogue_resource(self):
|
|
172
|
-
"""Test functions catalogue resource lists internal tools."""
|
|
173
|
-
|
|
174
|
-
hub = BaseHub("catalogue_test")
|
|
175
|
-
|
|
176
|
-
# Add some internal tools
|
|
177
|
-
@hub.tool("func1")
|
|
178
|
-
async def func1() -> Any:
|
|
179
|
-
return []
|
|
180
|
-
|
|
181
|
-
@hub.tool("func2")
|
|
182
|
-
async def func2() -> Any:
|
|
183
|
-
return []
|
|
184
|
-
|
|
185
|
-
# Get the catalogue resource
|
|
186
|
-
resources = hub._resource_manager._resources
|
|
187
|
-
catalogue_uri = "file:///catalogue_test/functions"
|
|
188
|
-
assert catalogue_uri in resources
|
|
189
|
-
|
|
190
|
-
# Call the resource
|
|
191
|
-
resource = resources[catalogue_uri]
|
|
192
|
-
content = await resource.read()
|
|
193
|
-
# The resource returns JSON content, parse it
|
|
194
|
-
import json
|
|
195
|
-
|
|
196
|
-
funcs = json.loads(content)
|
|
197
|
-
|
|
198
|
-
assert sorted(funcs) == ["func1", "func2"]
|
|
199
|
-
|
|
200
|
-
def test_tool_decorator_with_name(self):
|
|
201
|
-
"""Test tool decorator with explicit name."""
|
|
202
|
-
|
|
203
|
-
hub = BaseHub("decorator_test")
|
|
204
|
-
|
|
205
|
-
# Test positional name
|
|
206
|
-
decorator = hub.tool("my_tool")
|
|
207
|
-
assert callable(decorator)
|
|
208
|
-
|
|
209
|
-
# Test keyword name
|
|
210
|
-
decorator2 = hub.tool(name="my_tool2", tags={"test"})
|
|
211
|
-
assert callable(decorator2)
|
|
212
|
-
|
|
213
|
-
def test_tool_decorator_without_name(self):
|
|
214
|
-
"""Test tool decorator without name."""
|
|
215
|
-
|
|
216
|
-
hub = BaseHub("decorator_test")
|
|
217
|
-
|
|
218
|
-
# Test bare decorator
|
|
219
|
-
decorator = hub.tool()
|
|
220
|
-
assert callable(decorator)
|
|
221
|
-
|
|
222
|
-
# Test decorator with only kwargs
|
|
223
|
-
decorator2 = hub.tool(tags={"test"})
|
|
224
|
-
assert callable(decorator2)
|
|
225
|
-
|
|
226
|
-
def test_tool_decorator_phase2(self):
|
|
227
|
-
"""Test tool decorator phase 2 (when function is passed)."""
|
|
228
|
-
|
|
229
|
-
hub = BaseHub("phase2_test")
|
|
230
|
-
|
|
231
|
-
async def my_func() -> Any:
|
|
232
|
-
return []
|
|
233
|
-
|
|
234
|
-
# Simulate phase 2 of decorator application
|
|
235
|
-
with patch.object(FastMCP, "tool") as mock_super_tool:
|
|
236
|
-
mock_super_tool.return_value = my_func
|
|
237
|
-
|
|
238
|
-
# Call with function directly (phase 2)
|
|
239
|
-
result = hub.tool(my_func, tags={"test"})
|
|
240
|
-
|
|
241
|
-
assert result is my_func
|
|
242
|
-
mock_super_tool.assert_called_once_with(my_func, tags={"test"})
|
|
243
|
-
|
|
244
|
-
@pytest.mark.asyncio
|
|
245
|
-
async def test_list_tools_hides_internal(self):
|
|
246
|
-
"""Test _list_tools hides internal tools."""
|
|
247
|
-
|
|
248
|
-
hub = BaseHub("list_test")
|
|
249
|
-
|
|
250
|
-
# Add public tool (use @hub.tool() without prefix for public tools in FastMCP)
|
|
251
|
-
from fastmcp.tools import Tool
|
|
252
|
-
|
|
253
|
-
async def public_tool() -> Any:
|
|
254
|
-
return []
|
|
255
|
-
|
|
256
|
-
public_tool_obj = Tool.from_function(public_tool)
|
|
257
|
-
hub.add_tool(public_tool_obj)
|
|
258
|
-
|
|
259
|
-
# Add internal tool
|
|
260
|
-
@hub.tool("internal_tool")
|
|
261
|
-
async def internal_tool() -> Any:
|
|
262
|
-
return []
|
|
263
|
-
|
|
264
|
-
# List tools should only show public
|
|
265
|
-
tools = await hub._list_tools()
|
|
266
|
-
tool_names = [t.name for t in tools]
|
|
267
|
-
|
|
268
|
-
assert "public_tool" in tool_names
|
|
269
|
-
assert "internal_tool" not in tool_names
|
|
270
|
-
assert f"{_INTERNAL_PREFIX}internal_tool" not in tool_names
|
|
271
|
-
|
|
272
|
-
def test_resource_and_prompt_passthrough(self):
|
|
273
|
-
"""Test that resource and prompt decorators pass through."""
|
|
274
|
-
|
|
275
|
-
hub = BaseHub("passthrough_test")
|
|
276
|
-
|
|
277
|
-
# These should be inherited from FastMCP
|
|
278
|
-
assert hasattr(hub, "resource")
|
|
279
|
-
assert hasattr(hub, "prompt")
|
|
280
|
-
# Check they're the same methods (by name)
|
|
281
|
-
assert hub.resource.__name__ == FastMCP.resource.__name__
|
|
282
|
-
assert hub.prompt.__name__ == FastMCP.prompt.__name__
|
|
1
|
+
"""Tests for base tool classes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
from unittest.mock import MagicMock, patch
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from fastmcp import FastMCP
|
|
10
|
+
from mcp.types import ContentBlock, TextContent
|
|
11
|
+
|
|
12
|
+
from hud.tools.base import _INTERNAL_PREFIX, BaseHub, BaseTool
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MockTool(BaseTool):
|
|
16
|
+
"""Mock tool for testing."""
|
|
17
|
+
|
|
18
|
+
async def __call__(self, param1: Any = None, param2: Any = None) -> list[ContentBlock]:
|
|
19
|
+
"""Execute the mock tool."""
|
|
20
|
+
kwargs = {"param1": param1, "param2": param2}
|
|
21
|
+
return [TextContent(type="text", text=f"Mock result: {kwargs}")]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestBaseTool:
|
|
25
|
+
"""Test BaseTool class."""
|
|
26
|
+
|
|
27
|
+
def test_init_with_defaults(self):
|
|
28
|
+
"""Test BaseTool initialization with default values."""
|
|
29
|
+
|
|
30
|
+
class TestTool(BaseTool):
|
|
31
|
+
"""A test tool."""
|
|
32
|
+
|
|
33
|
+
async def __call__(self, **kwargs: Any) -> list[ContentBlock]:
|
|
34
|
+
return []
|
|
35
|
+
|
|
36
|
+
tool = TestTool()
|
|
37
|
+
|
|
38
|
+
# Check auto-generated values
|
|
39
|
+
assert tool.name == "test"
|
|
40
|
+
assert tool.title == "Test"
|
|
41
|
+
assert tool.description == "A test tool."
|
|
42
|
+
assert tool.env is None
|
|
43
|
+
assert tool.__name__ == "test"
|
|
44
|
+
assert tool.__doc__ == "A test tool."
|
|
45
|
+
|
|
46
|
+
def test_init_with_custom_values(self):
|
|
47
|
+
"""Test BaseTool initialization with custom values."""
|
|
48
|
+
|
|
49
|
+
env = {"key": "value"}
|
|
50
|
+
tool = MockTool(
|
|
51
|
+
env=env, name="custom_tool", title="Custom Tool", description="Custom description"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
assert tool.env == env
|
|
55
|
+
assert tool.name == "custom_tool"
|
|
56
|
+
assert tool.title == "Custom Tool"
|
|
57
|
+
assert tool.description == "Custom description"
|
|
58
|
+
assert tool.__name__ == "custom_tool"
|
|
59
|
+
assert tool.__doc__ == "Custom description"
|
|
60
|
+
|
|
61
|
+
def test_init_no_docstring(self):
|
|
62
|
+
"""Test BaseTool with no docstring."""
|
|
63
|
+
|
|
64
|
+
class NoDocTool(BaseTool):
|
|
65
|
+
async def __call__(self, **kwargs: Any) -> list[ContentBlock]:
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
tool = NoDocTool()
|
|
69
|
+
assert tool.description is None
|
|
70
|
+
assert not hasattr(tool, "__doc__") or tool.__doc__ is None
|
|
71
|
+
|
|
72
|
+
def test_register(self):
|
|
73
|
+
"""Test registering tool with FastMCP server."""
|
|
74
|
+
|
|
75
|
+
server = MagicMock(spec=FastMCP)
|
|
76
|
+
tool = MockTool(name="test_tool")
|
|
77
|
+
|
|
78
|
+
# Test register returns self for chaining
|
|
79
|
+
result = tool.register(server, tag="test")
|
|
80
|
+
|
|
81
|
+
assert result is tool
|
|
82
|
+
server.add_tool.assert_called_once()
|
|
83
|
+
|
|
84
|
+
# Check the tool passed has correct name
|
|
85
|
+
call_args = server.add_tool.call_args
|
|
86
|
+
assert call_args[1]["tag"] == "test"
|
|
87
|
+
|
|
88
|
+
def test_mcp_property_cached(self):
|
|
89
|
+
"""Test mcp property returns cached FunctionTool."""
|
|
90
|
+
|
|
91
|
+
tool = MockTool(name="cached_tool", title="Cached Tool", description="Test caching")
|
|
92
|
+
|
|
93
|
+
# First access creates the tool
|
|
94
|
+
mcp_tool1 = tool.mcp
|
|
95
|
+
assert hasattr(tool, "_mcp_tool")
|
|
96
|
+
|
|
97
|
+
# Second access returns cached
|
|
98
|
+
mcp_tool2 = tool.mcp
|
|
99
|
+
assert mcp_tool1 is mcp_tool2
|
|
100
|
+
|
|
101
|
+
def test_mcp_property_attributes(self):
|
|
102
|
+
"""Test mcp property creates FunctionTool with correct attributes."""
|
|
103
|
+
|
|
104
|
+
tool = MockTool(
|
|
105
|
+
name="mcp_test", title="MCP Test Tool", description="Testing MCP conversion"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
with patch("fastmcp.tools.FunctionTool") as MockFunctionTool:
|
|
109
|
+
mock_ft = MagicMock()
|
|
110
|
+
MockFunctionTool.from_function.return_value = mock_ft
|
|
111
|
+
|
|
112
|
+
result = tool.mcp
|
|
113
|
+
|
|
114
|
+
# The wrapper function is passed, not the tool itself
|
|
115
|
+
MockFunctionTool.from_function.assert_called_once()
|
|
116
|
+
call_args = MockFunctionTool.from_function.call_args
|
|
117
|
+
|
|
118
|
+
# Check that the correct parameters were passed
|
|
119
|
+
assert call_args[1]["name"] == "mcp_test"
|
|
120
|
+
assert call_args[1]["title"] == "MCP Test Tool"
|
|
121
|
+
assert call_args[1]["description"] == "Testing MCP conversion"
|
|
122
|
+
assert result is mock_ft
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class TestBaseHub:
|
|
126
|
+
"""Test BaseHub class."""
|
|
127
|
+
|
|
128
|
+
def test_init_basic(self):
|
|
129
|
+
"""Test BaseHub basic initialization."""
|
|
130
|
+
|
|
131
|
+
hub = BaseHub("test_hub")
|
|
132
|
+
|
|
133
|
+
assert hub._prefix_fn("tool") == f"{_INTERNAL_PREFIX}tool"
|
|
134
|
+
assert hasattr(hub, "_tool_manager")
|
|
135
|
+
assert hasattr(hub, "_resource_manager")
|
|
136
|
+
|
|
137
|
+
def test_init_with_env(self):
|
|
138
|
+
"""Test BaseHub initialization with environment."""
|
|
139
|
+
|
|
140
|
+
env = {"test": "env"}
|
|
141
|
+
hub = BaseHub("test_hub", env=env, title="Test Hub", description="A test hub")
|
|
142
|
+
|
|
143
|
+
assert hub.env == env
|
|
144
|
+
|
|
145
|
+
@pytest.mark.asyncio
|
|
146
|
+
async def test_dispatcher_tool_registered(self):
|
|
147
|
+
"""Test that dispatcher tool is registered on init."""
|
|
148
|
+
|
|
149
|
+
hub = BaseHub("dispatcher_test")
|
|
150
|
+
|
|
151
|
+
# Check dispatcher tool exists
|
|
152
|
+
tools = hub._tool_manager._tools
|
|
153
|
+
assert "dispatcher_test" in tools
|
|
154
|
+
|
|
155
|
+
# Test calling dispatcher with internal tool
|
|
156
|
+
@hub.tool("internal_func")
|
|
157
|
+
async def internal_func(value: int) -> Any:
|
|
158
|
+
return [TextContent(type="text", text=f"Internal: {value}")]
|
|
159
|
+
|
|
160
|
+
# Call dispatcher
|
|
161
|
+
result = await hub._tool_manager.call_tool(
|
|
162
|
+
"dispatcher_test", {"name": "internal_func", "arguments": {"value": 42}}
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# ToolResult has content attribute
|
|
166
|
+
assert len(result.content) == 1
|
|
167
|
+
assert isinstance(result.content[0], TextContent)
|
|
168
|
+
assert result.content[0].text == "Internal: 42"
|
|
169
|
+
|
|
170
|
+
@pytest.mark.asyncio
|
|
171
|
+
async def test_functions_catalogue_resource(self):
|
|
172
|
+
"""Test functions catalogue resource lists internal tools."""
|
|
173
|
+
|
|
174
|
+
hub = BaseHub("catalogue_test")
|
|
175
|
+
|
|
176
|
+
# Add some internal tools
|
|
177
|
+
@hub.tool("func1")
|
|
178
|
+
async def func1() -> Any:
|
|
179
|
+
return []
|
|
180
|
+
|
|
181
|
+
@hub.tool("func2")
|
|
182
|
+
async def func2() -> Any:
|
|
183
|
+
return []
|
|
184
|
+
|
|
185
|
+
# Get the catalogue resource
|
|
186
|
+
resources = hub._resource_manager._resources
|
|
187
|
+
catalogue_uri = "file:///catalogue_test/functions"
|
|
188
|
+
assert catalogue_uri in resources
|
|
189
|
+
|
|
190
|
+
# Call the resource
|
|
191
|
+
resource = resources[catalogue_uri]
|
|
192
|
+
content = await resource.read()
|
|
193
|
+
# The resource returns JSON content, parse it
|
|
194
|
+
import json
|
|
195
|
+
|
|
196
|
+
funcs = json.loads(content)
|
|
197
|
+
|
|
198
|
+
assert sorted(funcs) == ["func1", "func2"]
|
|
199
|
+
|
|
200
|
+
def test_tool_decorator_with_name(self):
|
|
201
|
+
"""Test tool decorator with explicit name."""
|
|
202
|
+
|
|
203
|
+
hub = BaseHub("decorator_test")
|
|
204
|
+
|
|
205
|
+
# Test positional name
|
|
206
|
+
decorator = hub.tool("my_tool")
|
|
207
|
+
assert callable(decorator)
|
|
208
|
+
|
|
209
|
+
# Test keyword name
|
|
210
|
+
decorator2 = hub.tool(name="my_tool2", tags={"test"})
|
|
211
|
+
assert callable(decorator2)
|
|
212
|
+
|
|
213
|
+
def test_tool_decorator_without_name(self):
|
|
214
|
+
"""Test tool decorator without name."""
|
|
215
|
+
|
|
216
|
+
hub = BaseHub("decorator_test")
|
|
217
|
+
|
|
218
|
+
# Test bare decorator
|
|
219
|
+
decorator = hub.tool()
|
|
220
|
+
assert callable(decorator)
|
|
221
|
+
|
|
222
|
+
# Test decorator with only kwargs
|
|
223
|
+
decorator2 = hub.tool(tags={"test"})
|
|
224
|
+
assert callable(decorator2)
|
|
225
|
+
|
|
226
|
+
def test_tool_decorator_phase2(self):
|
|
227
|
+
"""Test tool decorator phase 2 (when function is passed)."""
|
|
228
|
+
|
|
229
|
+
hub = BaseHub("phase2_test")
|
|
230
|
+
|
|
231
|
+
async def my_func() -> Any:
|
|
232
|
+
return []
|
|
233
|
+
|
|
234
|
+
# Simulate phase 2 of decorator application
|
|
235
|
+
with patch.object(FastMCP, "tool") as mock_super_tool:
|
|
236
|
+
mock_super_tool.return_value = my_func
|
|
237
|
+
|
|
238
|
+
# Call with function directly (phase 2)
|
|
239
|
+
result = hub.tool(my_func, tags={"test"})
|
|
240
|
+
|
|
241
|
+
assert result is my_func
|
|
242
|
+
mock_super_tool.assert_called_once_with(my_func, tags={"test"})
|
|
243
|
+
|
|
244
|
+
@pytest.mark.asyncio
|
|
245
|
+
async def test_list_tools_hides_internal(self):
|
|
246
|
+
"""Test _list_tools hides internal tools."""
|
|
247
|
+
|
|
248
|
+
hub = BaseHub("list_test")
|
|
249
|
+
|
|
250
|
+
# Add public tool (use @hub.tool() without prefix for public tools in FastMCP)
|
|
251
|
+
from fastmcp.tools import Tool
|
|
252
|
+
|
|
253
|
+
async def public_tool() -> Any:
|
|
254
|
+
return []
|
|
255
|
+
|
|
256
|
+
public_tool_obj = Tool.from_function(public_tool)
|
|
257
|
+
hub.add_tool(public_tool_obj)
|
|
258
|
+
|
|
259
|
+
# Add internal tool
|
|
260
|
+
@hub.tool("internal_tool")
|
|
261
|
+
async def internal_tool() -> Any:
|
|
262
|
+
return []
|
|
263
|
+
|
|
264
|
+
# List tools should only show public
|
|
265
|
+
tools = await hub._list_tools()
|
|
266
|
+
tool_names = [t.name for t in tools]
|
|
267
|
+
|
|
268
|
+
assert "public_tool" in tool_names
|
|
269
|
+
assert "internal_tool" not in tool_names
|
|
270
|
+
assert f"{_INTERNAL_PREFIX}internal_tool" not in tool_names
|
|
271
|
+
|
|
272
|
+
def test_resource_and_prompt_passthrough(self):
|
|
273
|
+
"""Test that resource and prompt decorators pass through."""
|
|
274
|
+
|
|
275
|
+
hub = BaseHub("passthrough_test")
|
|
276
|
+
|
|
277
|
+
# These should be inherited from FastMCP
|
|
278
|
+
assert hasattr(hub, "resource")
|
|
279
|
+
assert hasattr(hub, "prompt")
|
|
280
|
+
# Check they're the same methods (by name)
|
|
281
|
+
assert hub.resource.__name__ == FastMCP.resource.__name__
|
|
282
|
+
assert hub.prompt.__name__ == FastMCP.prompt.__name__
|