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.

Files changed (130) hide show
  1. hud/__init__.py +22 -22
  2. hud/agents/__init__.py +13 -15
  3. hud/agents/base.py +599 -599
  4. hud/agents/claude.py +373 -373
  5. hud/agents/langchain.py +261 -250
  6. hud/agents/misc/__init__.py +7 -7
  7. hud/agents/misc/response_agent.py +82 -80
  8. hud/agents/openai.py +352 -352
  9. hud/agents/openai_chat_generic.py +154 -154
  10. hud/agents/tests/__init__.py +1 -1
  11. hud/agents/tests/test_base.py +742 -742
  12. hud/agents/tests/test_claude.py +324 -324
  13. hud/agents/tests/test_client.py +363 -363
  14. hud/agents/tests/test_openai.py +237 -237
  15. hud/cli/__init__.py +617 -617
  16. hud/cli/__main__.py +8 -8
  17. hud/cli/analyze.py +371 -371
  18. hud/cli/analyze_metadata.py +230 -230
  19. hud/cli/build.py +498 -427
  20. hud/cli/clone.py +185 -185
  21. hud/cli/cursor.py +92 -92
  22. hud/cli/debug.py +392 -392
  23. hud/cli/docker_utils.py +83 -83
  24. hud/cli/init.py +280 -281
  25. hud/cli/interactive.py +353 -353
  26. hud/cli/mcp_server.py +764 -756
  27. hud/cli/pull.py +330 -336
  28. hud/cli/push.py +404 -370
  29. hud/cli/remote_runner.py +311 -311
  30. hud/cli/runner.py +160 -160
  31. hud/cli/tests/__init__.py +3 -3
  32. hud/cli/tests/test_analyze.py +284 -284
  33. hud/cli/tests/test_cli_init.py +265 -265
  34. hud/cli/tests/test_cli_main.py +27 -27
  35. hud/cli/tests/test_clone.py +142 -142
  36. hud/cli/tests/test_cursor.py +253 -253
  37. hud/cli/tests/test_debug.py +453 -453
  38. hud/cli/tests/test_mcp_server.py +139 -139
  39. hud/cli/tests/test_utils.py +388 -388
  40. hud/cli/utils.py +263 -263
  41. hud/clients/README.md +143 -143
  42. hud/clients/__init__.py +16 -16
  43. hud/clients/base.py +378 -379
  44. hud/clients/fastmcp.py +222 -222
  45. hud/clients/mcp_use.py +298 -278
  46. hud/clients/tests/__init__.py +1 -1
  47. hud/clients/tests/test_client_integration.py +111 -111
  48. hud/clients/tests/test_fastmcp.py +342 -342
  49. hud/clients/tests/test_protocol.py +188 -188
  50. hud/clients/utils/__init__.py +1 -1
  51. hud/clients/utils/retry_transport.py +160 -160
  52. hud/datasets.py +327 -322
  53. hud/misc/__init__.py +1 -1
  54. hud/misc/claude_plays_pokemon.py +292 -292
  55. hud/otel/__init__.py +35 -35
  56. hud/otel/collector.py +142 -142
  57. hud/otel/config.py +164 -164
  58. hud/otel/context.py +536 -536
  59. hud/otel/exporters.py +366 -366
  60. hud/otel/instrumentation.py +97 -97
  61. hud/otel/processors.py +118 -118
  62. hud/otel/tests/__init__.py +1 -1
  63. hud/otel/tests/test_processors.py +197 -197
  64. hud/server/__init__.py +5 -5
  65. hud/server/context.py +114 -114
  66. hud/server/helper/__init__.py +5 -5
  67. hud/server/low_level.py +132 -132
  68. hud/server/server.py +170 -166
  69. hud/server/tests/__init__.py +3 -3
  70. hud/settings.py +73 -73
  71. hud/shared/__init__.py +5 -5
  72. hud/shared/exceptions.py +180 -180
  73. hud/shared/requests.py +264 -264
  74. hud/shared/tests/test_exceptions.py +157 -157
  75. hud/shared/tests/test_requests.py +275 -275
  76. hud/telemetry/__init__.py +25 -25
  77. hud/telemetry/instrument.py +379 -379
  78. hud/telemetry/job.py +309 -309
  79. hud/telemetry/replay.py +74 -74
  80. hud/telemetry/trace.py +83 -83
  81. hud/tools/__init__.py +33 -33
  82. hud/tools/base.py +365 -365
  83. hud/tools/bash.py +161 -161
  84. hud/tools/computer/__init__.py +15 -15
  85. hud/tools/computer/anthropic.py +437 -437
  86. hud/tools/computer/hud.py +376 -376
  87. hud/tools/computer/openai.py +295 -295
  88. hud/tools/computer/settings.py +82 -82
  89. hud/tools/edit.py +314 -314
  90. hud/tools/executors/__init__.py +30 -30
  91. hud/tools/executors/base.py +539 -539
  92. hud/tools/executors/pyautogui.py +621 -621
  93. hud/tools/executors/tests/__init__.py +1 -1
  94. hud/tools/executors/tests/test_base_executor.py +338 -338
  95. hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
  96. hud/tools/executors/xdo.py +511 -511
  97. hud/tools/playwright.py +412 -412
  98. hud/tools/tests/__init__.py +3 -3
  99. hud/tools/tests/test_base.py +282 -282
  100. hud/tools/tests/test_bash.py +158 -158
  101. hud/tools/tests/test_bash_extended.py +197 -197
  102. hud/tools/tests/test_computer.py +425 -425
  103. hud/tools/tests/test_computer_actions.py +34 -34
  104. hud/tools/tests/test_edit.py +259 -259
  105. hud/tools/tests/test_init.py +27 -27
  106. hud/tools/tests/test_playwright_tool.py +183 -183
  107. hud/tools/tests/test_tools.py +145 -145
  108. hud/tools/tests/test_utils.py +156 -156
  109. hud/tools/types.py +72 -72
  110. hud/tools/utils.py +50 -50
  111. hud/types.py +136 -136
  112. hud/utils/__init__.py +10 -10
  113. hud/utils/async_utils.py +65 -65
  114. hud/utils/design.py +236 -168
  115. hud/utils/mcp.py +55 -55
  116. hud/utils/progress.py +149 -149
  117. hud/utils/telemetry.py +66 -66
  118. hud/utils/tests/test_async_utils.py +173 -173
  119. hud/utils/tests/test_init.py +17 -17
  120. hud/utils/tests/test_progress.py +261 -261
  121. hud/utils/tests/test_telemetry.py +82 -82
  122. hud/utils/tests/test_version.py +8 -8
  123. hud/version.py +7 -7
  124. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/METADATA +10 -8
  125. hud_python-0.4.3.dist-info/RECORD +131 -0
  126. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/licenses/LICENSE +21 -21
  127. hud/agents/art.py +0 -101
  128. hud_python-0.4.1.dist-info/RECORD +0 -132
  129. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/WHEEL +0 -0
  130. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/entry_points.txt +0 -0
@@ -1,3 +1,3 @@
1
- from __future__ import annotations
2
-
3
- __all__ = []
1
+ from __future__ import annotations
2
+
3
+ __all__ = []
@@ -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__