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.

Files changed (192) hide show
  1. hud/__init__.py +22 -89
  2. hud/agents/__init__.py +17 -0
  3. hud/agents/art.py +101 -0
  4. hud/agents/base.py +599 -0
  5. hud/{mcp → agents}/claude.py +373 -321
  6. hud/{mcp → agents}/langchain.py +250 -250
  7. hud/agents/misc/__init__.py +7 -0
  8. hud/{agent → agents}/misc/response_agent.py +80 -80
  9. hud/{mcp → agents}/openai.py +352 -334
  10. hud/agents/openai_chat_generic.py +154 -0
  11. hud/{mcp → agents}/tests/__init__.py +1 -1
  12. hud/agents/tests/test_base.py +742 -0
  13. hud/agents/tests/test_claude.py +324 -0
  14. hud/{mcp → agents}/tests/test_client.py +363 -324
  15. hud/{mcp → agents}/tests/test_openai.py +237 -238
  16. hud/cli/__init__.py +617 -0
  17. hud/cli/__main__.py +8 -0
  18. hud/cli/analyze.py +371 -0
  19. hud/cli/analyze_metadata.py +230 -0
  20. hud/cli/build.py +427 -0
  21. hud/cli/clone.py +185 -0
  22. hud/cli/cursor.py +92 -0
  23. hud/cli/debug.py +392 -0
  24. hud/cli/docker_utils.py +83 -0
  25. hud/cli/init.py +281 -0
  26. hud/cli/interactive.py +353 -0
  27. hud/cli/mcp_server.py +756 -0
  28. hud/cli/pull.py +336 -0
  29. hud/cli/push.py +379 -0
  30. hud/cli/remote_runner.py +311 -0
  31. hud/cli/runner.py +160 -0
  32. hud/cli/tests/__init__.py +3 -0
  33. hud/cli/tests/test_analyze.py +284 -0
  34. hud/cli/tests/test_cli_init.py +265 -0
  35. hud/cli/tests/test_cli_main.py +27 -0
  36. hud/cli/tests/test_clone.py +142 -0
  37. hud/cli/tests/test_cursor.py +253 -0
  38. hud/cli/tests/test_debug.py +453 -0
  39. hud/cli/tests/test_mcp_server.py +139 -0
  40. hud/cli/tests/test_utils.py +388 -0
  41. hud/cli/utils.py +263 -0
  42. hud/clients/README.md +143 -0
  43. hud/clients/__init__.py +16 -0
  44. hud/clients/base.py +354 -0
  45. hud/clients/fastmcp.py +202 -0
  46. hud/clients/mcp_use.py +278 -0
  47. hud/clients/tests/__init__.py +1 -0
  48. hud/clients/tests/test_client_integration.py +111 -0
  49. hud/clients/tests/test_fastmcp.py +342 -0
  50. hud/clients/tests/test_protocol.py +188 -0
  51. hud/clients/utils/__init__.py +1 -0
  52. hud/clients/utils/retry_transport.py +160 -0
  53. hud/datasets.py +322 -192
  54. hud/misc/__init__.py +1 -0
  55. hud/{agent → misc}/claude_plays_pokemon.py +292 -283
  56. hud/otel/__init__.py +35 -0
  57. hud/otel/collector.py +142 -0
  58. hud/otel/config.py +164 -0
  59. hud/otel/context.py +536 -0
  60. hud/otel/exporters.py +366 -0
  61. hud/otel/instrumentation.py +97 -0
  62. hud/otel/processors.py +118 -0
  63. hud/otel/tests/__init__.py +1 -0
  64. hud/otel/tests/test_processors.py +197 -0
  65. hud/server/__init__.py +5 -5
  66. hud/server/context.py +114 -0
  67. hud/server/helper/__init__.py +5 -0
  68. hud/server/low_level.py +132 -0
  69. hud/server/server.py +166 -0
  70. hud/server/tests/__init__.py +3 -0
  71. hud/settings.py +73 -79
  72. hud/shared/__init__.py +5 -0
  73. hud/{exceptions.py → shared/exceptions.py} +180 -180
  74. hud/{server → shared}/requests.py +264 -264
  75. hud/shared/tests/test_exceptions.py +157 -0
  76. hud/{server → shared}/tests/test_requests.py +275 -275
  77. hud/telemetry/__init__.py +25 -30
  78. hud/telemetry/instrument.py +379 -0
  79. hud/telemetry/job.py +309 -141
  80. hud/telemetry/replay.py +74 -0
  81. hud/telemetry/trace.py +83 -0
  82. hud/tools/__init__.py +33 -34
  83. hud/tools/base.py +365 -65
  84. hud/tools/bash.py +161 -137
  85. hud/tools/computer/__init__.py +15 -13
  86. hud/tools/computer/anthropic.py +437 -420
  87. hud/tools/computer/hud.py +376 -334
  88. hud/tools/computer/openai.py +295 -292
  89. hud/tools/computer/settings.py +82 -0
  90. hud/tools/edit.py +314 -290
  91. hud/tools/executors/__init__.py +30 -30
  92. hud/tools/executors/base.py +539 -532
  93. hud/tools/executors/pyautogui.py +621 -619
  94. hud/tools/executors/tests/__init__.py +1 -1
  95. hud/tools/executors/tests/test_base_executor.py +338 -338
  96. hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
  97. hud/tools/executors/xdo.py +511 -503
  98. hud/tools/{playwright_tool.py → playwright.py} +412 -379
  99. hud/tools/tests/__init__.py +3 -3
  100. hud/tools/tests/test_base.py +282 -0
  101. hud/tools/tests/test_bash.py +158 -152
  102. hud/tools/tests/test_bash_extended.py +197 -0
  103. hud/tools/tests/test_computer.py +425 -52
  104. hud/tools/tests/test_computer_actions.py +34 -34
  105. hud/tools/tests/test_edit.py +259 -240
  106. hud/tools/tests/test_init.py +27 -27
  107. hud/tools/tests/test_playwright_tool.py +183 -183
  108. hud/tools/tests/test_tools.py +145 -157
  109. hud/tools/tests/test_utils.py +156 -156
  110. hud/tools/types.py +72 -0
  111. hud/tools/utils.py +50 -50
  112. hud/types.py +136 -89
  113. hud/utils/__init__.py +10 -16
  114. hud/utils/async_utils.py +65 -0
  115. hud/utils/design.py +168 -0
  116. hud/utils/mcp.py +55 -0
  117. hud/utils/progress.py +149 -149
  118. hud/utils/telemetry.py +66 -66
  119. hud/utils/tests/test_async_utils.py +173 -0
  120. hud/utils/tests/test_init.py +17 -21
  121. hud/utils/tests/test_progress.py +261 -225
  122. hud/utils/tests/test_telemetry.py +82 -37
  123. hud/utils/tests/test_version.py +8 -8
  124. hud/version.py +7 -7
  125. hud_python-0.4.0.dist-info/METADATA +474 -0
  126. hud_python-0.4.0.dist-info/RECORD +132 -0
  127. hud_python-0.4.0.dist-info/entry_points.txt +3 -0
  128. {hud_python-0.3.5.dist-info → hud_python-0.4.0.dist-info}/licenses/LICENSE +21 -21
  129. hud/adapters/__init__.py +0 -8
  130. hud/adapters/claude/__init__.py +0 -5
  131. hud/adapters/claude/adapter.py +0 -180
  132. hud/adapters/claude/tests/__init__.py +0 -1
  133. hud/adapters/claude/tests/test_adapter.py +0 -519
  134. hud/adapters/common/__init__.py +0 -6
  135. hud/adapters/common/adapter.py +0 -178
  136. hud/adapters/common/tests/test_adapter.py +0 -289
  137. hud/adapters/common/types.py +0 -446
  138. hud/adapters/operator/__init__.py +0 -5
  139. hud/adapters/operator/adapter.py +0 -108
  140. hud/adapters/operator/tests/__init__.py +0 -1
  141. hud/adapters/operator/tests/test_adapter.py +0 -370
  142. hud/agent/__init__.py +0 -19
  143. hud/agent/base.py +0 -126
  144. hud/agent/claude.py +0 -271
  145. hud/agent/langchain.py +0 -215
  146. hud/agent/misc/__init__.py +0 -3
  147. hud/agent/operator.py +0 -268
  148. hud/agent/tests/__init__.py +0 -1
  149. hud/agent/tests/test_base.py +0 -202
  150. hud/env/__init__.py +0 -11
  151. hud/env/client.py +0 -35
  152. hud/env/docker_client.py +0 -349
  153. hud/env/environment.py +0 -446
  154. hud/env/local_docker_client.py +0 -358
  155. hud/env/remote_client.py +0 -212
  156. hud/env/remote_docker_client.py +0 -292
  157. hud/gym.py +0 -130
  158. hud/job.py +0 -773
  159. hud/mcp/__init__.py +0 -17
  160. hud/mcp/base.py +0 -631
  161. hud/mcp/client.py +0 -312
  162. hud/mcp/tests/test_base.py +0 -512
  163. hud/mcp/tests/test_claude.py +0 -294
  164. hud/task.py +0 -149
  165. hud/taskset.py +0 -237
  166. hud/telemetry/_trace.py +0 -347
  167. hud/telemetry/context.py +0 -230
  168. hud/telemetry/exporter.py +0 -575
  169. hud/telemetry/instrumentation/__init__.py +0 -3
  170. hud/telemetry/instrumentation/mcp.py +0 -259
  171. hud/telemetry/instrumentation/registry.py +0 -59
  172. hud/telemetry/mcp_models.py +0 -270
  173. hud/telemetry/tests/__init__.py +0 -1
  174. hud/telemetry/tests/test_context.py +0 -210
  175. hud/telemetry/tests/test_trace.py +0 -312
  176. hud/tools/helper/README.md +0 -56
  177. hud/tools/helper/__init__.py +0 -9
  178. hud/tools/helper/mcp_server.py +0 -78
  179. hud/tools/helper/server_initialization.py +0 -115
  180. hud/tools/helper/utils.py +0 -58
  181. hud/trajectory.py +0 -94
  182. hud/utils/agent.py +0 -37
  183. hud/utils/common.py +0 -256
  184. hud/utils/config.py +0 -120
  185. hud/utils/deprecation.py +0 -115
  186. hud/utils/misc.py +0 -53
  187. hud/utils/tests/test_common.py +0 -277
  188. hud/utils/tests/test_config.py +0 -129
  189. hud_python-0.3.5.dist-info/METADATA +0 -284
  190. hud_python-0.3.5.dist-info/RECORD +0 -120
  191. /hud/{adapters/common → shared}/tests/__init__.py +0 -0
  192. {hud_python-0.3.5.dist-info → hud_python-0.4.0.dist-info}/WHEEL +0 -0
hud/tools/__init__.py CHANGED
@@ -1,34 +1,33 @@
1
- """HUD tools for computer control, file editing, and bash commands."""
2
-
3
- from __future__ import annotations
4
-
5
- from typing import TYPE_CHECKING, Any
6
-
7
- from .base import ToolError, ToolResult, tool_result_to_content_blocks
8
- from .bash import BashTool
9
- from .edit import EditTool
10
- from .playwright_tool import PlaywrightTool
11
-
12
- if TYPE_CHECKING:
13
- from .computer import AnthropicComputerTool, HudComputerTool, OpenAIComputerTool
14
-
15
- __all__ = [
16
- "AnthropicComputerTool",
17
- "BashTool",
18
- "EditTool",
19
- "HudComputerTool",
20
- "OpenAIComputerTool",
21
- "PlaywrightTool",
22
- "ToolError",
23
- "ToolResult",
24
- "tool_result_to_content_blocks",
25
- ]
26
-
27
-
28
- def __getattr__(name: str) -> Any:
29
- """Lazy import computer tools to avoid importing pyautogui unless needed."""
30
- if name in ("AnthropicComputerTool", "HudComputerTool", "OpenAIComputerTool"):
31
- from . import computer
32
-
33
- return getattr(computer, name)
34
- raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
1
+ """HUD tools for computer control, file editing, and bash commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from .base import BaseHub, BaseTool
8
+ from .bash import BashTool
9
+ from .edit import EditTool
10
+ from .playwright import PlaywrightTool
11
+
12
+ if TYPE_CHECKING:
13
+ from .computer import AnthropicComputerTool, HudComputerTool, OpenAIComputerTool
14
+
15
+ __all__ = [
16
+ "AnthropicComputerTool",
17
+ "BaseHub",
18
+ "BaseTool",
19
+ "BashTool",
20
+ "EditTool",
21
+ "HudComputerTool",
22
+ "OpenAIComputerTool",
23
+ "PlaywrightTool",
24
+ ]
25
+
26
+
27
+ def __getattr__(name: str) -> Any:
28
+ """Lazy import computer tools to avoid importing pyautogui unless needed."""
29
+ if name in ("AnthropicComputerTool", "HudComputerTool", "OpenAIComputerTool"):
30
+ from . import computer
31
+
32
+ return getattr(computer, name)
33
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
hud/tools/base.py CHANGED
@@ -1,65 +1,365 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass, fields, replace
4
- from typing import Any
5
-
6
- from mcp.types import ImageContent, TextContent
7
-
8
-
9
- @dataclass(kw_only=True, frozen=True)
10
- class ToolResult:
11
- """Represents the result of a tool execution."""
12
-
13
- output: str | None = None
14
- error: str | None = None
15
- base64_image: str | None = None
16
- system: str | None = None
17
-
18
- def __bool__(self) -> bool:
19
- return any(getattr(self, field.name) for field in fields(self))
20
-
21
- def __add__(self, other: ToolResult) -> ToolResult:
22
- def combine_fields(
23
- field: str | None, other_field: str | None, concatenate: bool = True
24
- ) -> str | None:
25
- if field and other_field:
26
- if concatenate:
27
- return field + other_field
28
- raise ValueError("Cannot combine tool results")
29
- return field or other_field
30
-
31
- return ToolResult(
32
- output=combine_fields(self.output, other.output),
33
- error=combine_fields(self.error, other.error),
34
- base64_image=combine_fields(self.base64_image, other.base64_image, False),
35
- system=combine_fields(self.system, other.system),
36
- )
37
-
38
- def replace(self, **kwargs: Any) -> ToolResult:
39
- """Returns a new ToolResult with the given fields replaced."""
40
- return replace(self, **kwargs)
41
-
42
-
43
- # Legacy alias for backward compatibility
44
- CLIResult = ToolResult
45
-
46
-
47
- class ToolError(Exception):
48
- """An error raised by a tool."""
49
-
50
-
51
- # Legacy alias for backward compatibility
52
- CLIError = ToolError
53
-
54
-
55
- def tool_result_to_content_blocks(result: ToolResult) -> list[ImageContent | TextContent]:
56
- """Convert a ToolResult to MCP content blocks."""
57
- blocks = []
58
-
59
- if result.output:
60
- blocks.append(TextContent(text=result.output, type="text"))
61
- if result.error:
62
- blocks.append(TextContent(text=result.error, type="text"))
63
- if result.base64_image:
64
- blocks.append(ImageContent(data=result.base64_image, mimeType="image/png", type="image"))
65
- return blocks
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import TYPE_CHECKING, Any, cast
5
+
6
+ from fastmcp import FastMCP
7
+
8
+ from hud.tools.types import ContentBlock, EvaluationResult
9
+
10
+ if TYPE_CHECKING:
11
+ from collections.abc import Callable
12
+
13
+ from fastmcp.tools import FunctionTool
14
+ from fastmcp.tools.tool import Tool, ToolResult
15
+
16
+ # Basic result types for tools
17
+ BaseResult = list[ContentBlock] | EvaluationResult
18
+
19
+
20
+ class BaseTool(ABC):
21
+ """
22
+ Base helper class for all MCP tools to constrain their output.
23
+
24
+ USAGE:
25
+ All tools should inherit from this class and implement the __call__ method.
26
+ Tools are registered with FastMCP using add_tool.
27
+
28
+ FORMAT:
29
+ Tools that return messages should return a list[ContentBlock].
30
+ Tools that return miscallaneous content should return a pydantic model such as EvaluationResult.
31
+ Both of these types of tools are processed via structuredContent.
32
+ Any other type of tool will not be processed well by the client.
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ env: Any = None,
38
+ name: str | None = None,
39
+ title: str | None = None,
40
+ description: str | None = None,
41
+ ) -> None:
42
+ """Initialize the tool.
43
+
44
+ Args:
45
+ env: Optional, often stateful, context object that the tool operates on. Could be:
46
+ - A game instance (e.g., Chess Board)
47
+ - An executor (e.g., PyAutoGUIExecutor for computer control)
48
+ - A browser/page instance (e.g., Playwright Page)
49
+ - Any stateful resource the tool needs to interact with
50
+ name: Tool name for MCP registration (auto-generated from class name if not provided)
51
+ title: Human-readable display name for the tool (auto-generated from class name)
52
+ description: Tool description (auto-generated from docstring if not provided)
53
+ """
54
+ self.env = env
55
+ self.name = name or self.__class__.__name__.lower().replace("tool", "")
56
+ self.title = title or self.__class__.__name__.replace("Tool", "").replace("_", " ").title()
57
+ self.description = description or (self.__doc__.strip() if self.__doc__ else None)
58
+
59
+ # Expose attributes FastMCP expects when registering an instance directly
60
+ self.__name__ = self.name # FastMCP uses fn.__name__ if name param omitted
61
+ if self.description:
62
+ self.__doc__ = self.description
63
+
64
+ @abstractmethod
65
+ async def __call__(self, **kwargs: Any) -> ToolResult:
66
+ """Execute the tool. Often uses the context to perform an action.
67
+
68
+ Args:
69
+ **kwargs: Tool-specific arguments
70
+
71
+ Returns:
72
+ List of ContentBlock (TextContent, ImageContent, etc.) with the tool's output
73
+ """
74
+ raise NotImplementedError("Subclasses must implement __call__")
75
+
76
+ def register(self, server: FastMCP, **meta: Any) -> BaseTool:
77
+ """Register this tool on a FastMCP server and return self for chaining."""
78
+ server.add_tool(self.mcp, **meta)
79
+ return self
80
+
81
+ @property
82
+ def mcp(self) -> FunctionTool:
83
+ """Get this tool as a FastMCP FunctionTool (cached).
84
+
85
+ This allows clean registration:
86
+ server.add_tool(my_tool.mcp)
87
+ """
88
+ if not hasattr(self, "_mcp_tool"):
89
+ from fastmcp.tools import FunctionTool
90
+
91
+ self._mcp_tool = FunctionTool.from_function(
92
+ self,
93
+ name=self.name,
94
+ title=self.title,
95
+ description=self.description,
96
+ )
97
+ return self._mcp_tool
98
+
99
+
100
+ # Prefix for internal tool names
101
+ _INTERNAL_PREFIX = "int_"
102
+
103
+
104
+ class BaseHub(FastMCP):
105
+ """A composition-friendly FastMCP server that holds an internal tool dispatcher."""
106
+
107
+ env: Any
108
+
109
+ def __init__(
110
+ self,
111
+ name: str,
112
+ *,
113
+ env: Any | None = None,
114
+ title: str | None = None,
115
+ description: str | None = None,
116
+ ) -> None:
117
+ """Create a new BaseHub.
118
+
119
+ Parameters
120
+ ----------
121
+ name:
122
+ Public name. Also becomes the *dispatcher tool* name.
123
+ env:
124
+ Optional long-lived environment object. Stored on the server
125
+ instance (``layer.env``) and therefore available to every request
126
+ via ``ctx.fastmcp.env``.
127
+ """
128
+
129
+ # Naming scheme for hidden objects
130
+ self._prefix_fn: Callable[[str], str] = lambda n: f"{_INTERNAL_PREFIX}{n}"
131
+
132
+ super().__init__(name=name)
133
+
134
+ if env is not None:
135
+ self.env = env
136
+
137
+ dispatcher_title = title or f"{name.title()} Dispatcher"
138
+ dispatcher_desc = description or f"Call internal '{name}' functions"
139
+
140
+ # Register dispatcher manually with FunctionTool
141
+ async def _dispatch( # noqa: ANN202
142
+ name: str,
143
+ arguments: dict | str | None = None,
144
+ ctx=None, # noqa: ANN001
145
+ ):
146
+ """Gateway to hidden tools.
147
+
148
+ Parameters
149
+ ----------
150
+ name : str
151
+ Internal function name *without* prefix.
152
+ arguments : dict | str | None
153
+ Arguments forwarded to the internal tool. Can be dict or JSON string.
154
+ ctx : Context
155
+ Injected by FastMCP; can be the custom subclass.
156
+ """
157
+
158
+ # Handle JSON string inputs
159
+ if isinstance(arguments, str):
160
+ import json
161
+
162
+ try:
163
+ arguments = json.loads(arguments)
164
+ except json.JSONDecodeError:
165
+ # If it's not valid JSON, treat as empty dict
166
+ arguments = {}
167
+
168
+ # Use the tool manager to call internal tools
169
+ return await self._tool_manager.call_tool(self._prefix_fn(name), arguments or {}) # type: ignore
170
+
171
+ from fastmcp.tools.tool import FunctionTool
172
+
173
+ dispatcher_tool = FunctionTool.from_function(
174
+ _dispatch,
175
+ name=name,
176
+ title=dispatcher_title,
177
+ description=dispatcher_desc,
178
+ tags=set(),
179
+ )
180
+ self._tool_manager.add_tool(dispatcher_tool)
181
+
182
+ # Expose list of internal functions via read-only resource
183
+ async def _functions_catalogue() -> list[str]:
184
+ # List all internal function names without prefix
185
+ return [
186
+ key.removeprefix(_INTERNAL_PREFIX)
187
+ for key in self._tool_manager._tools
188
+ if key.startswith(_INTERNAL_PREFIX)
189
+ ]
190
+
191
+ from fastmcp.resources import Resource
192
+
193
+ catalogue_resource = Resource.from_function(
194
+ _functions_catalogue,
195
+ uri=f"file:///{name}/functions",
196
+ name=f"{name} Functions Catalogue",
197
+ description=f"List of internal functions available in {name}",
198
+ mime_type="application/json",
199
+ tags=set(),
200
+ )
201
+ self._resource_manager.add_resource(catalogue_resource)
202
+
203
+ def tool(self, name_or_fn: Any = None, /, **kwargs: Any) -> Callable[..., Any]:
204
+ """Register an *internal* tool (hidden from clients)."""
205
+ # Handle when decorator's partial calls us back with the function
206
+ if callable(name_or_fn):
207
+ # This only happens in phase 2 of decorator application
208
+ # The name was already prefixed in phase 1, just pass through
209
+ result = super().tool(name_or_fn, **kwargs)
210
+
211
+ # Update dispatcher description after registering tool
212
+ self._update_dispatcher_description()
213
+
214
+ return cast("Callable[..., Any]", result)
215
+
216
+ # Handle the name from either positional or keyword argument
217
+ if isinstance(name_or_fn, str):
218
+ # Called as @hub.tool("name")
219
+ name = name_or_fn
220
+ elif name_or_fn is None and "name" in kwargs:
221
+ # Called as @hub.tool(name="name")
222
+ name = kwargs.pop("name")
223
+ else:
224
+ # Called as @hub.tool or @hub.tool()
225
+ name = None
226
+
227
+ new_name = self._prefix_fn(name) if name is not None else None
228
+ tags = kwargs.pop("tags", None) or set()
229
+
230
+ # Pass through correctly to parent
231
+ if new_name is not None:
232
+ return super().tool(new_name, **kwargs, tags=tags)
233
+ else:
234
+ return super().tool(**kwargs, tags=tags)
235
+
236
+ def _update_dispatcher_description(self) -> None:
237
+ """Update the dispatcher tool's description and schema with available tools."""
238
+ # Get list of internal tools with their details
239
+ internal_tools = []
240
+ for key, tool in self._tool_manager._tools.items():
241
+ if key.startswith(_INTERNAL_PREFIX):
242
+ tool_name = key.removeprefix(_INTERNAL_PREFIX)
243
+ internal_tools.append((tool_name, tool))
244
+
245
+ if internal_tools:
246
+ # Update the dispatcher tool's description
247
+ dispatcher_name = self.name
248
+ if dispatcher_name in self._tool_manager._tools:
249
+ dispatcher_tool = self._tool_manager._tools[dispatcher_name]
250
+
251
+ # Build detailed description
252
+ desc_lines = [f"Call internal '{self.name}' functions. Available tools:"]
253
+ desc_lines.append("") # Empty line for readability
254
+
255
+ # Build tool schemas for oneOf
256
+ tool_schemas = []
257
+
258
+ for tool_name, tool in sorted(internal_tools):
259
+ # Add tool name and description
260
+ tool_desc = tool.description or "No description"
261
+ desc_lines.append(f"• Name: {tool_name} ({tool_desc})")
262
+
263
+ # Build schema for this specific tool call
264
+ tool_schema = {
265
+ "type": "object",
266
+ "properties": {
267
+ "name": {
268
+ "type": "string",
269
+ "const": tool_name,
270
+ "description": f"Must be '{tool_name}'",
271
+ },
272
+ "arguments": tool.parameters
273
+ if hasattr(tool, "parameters") and tool.parameters
274
+ else {"type": "object"},
275
+ },
276
+ "required": ["name", "arguments"],
277
+ "additionalProperties": False,
278
+ }
279
+ tool_schemas.append(tool_schema)
280
+
281
+ # Add parameters from the tool's parameters field (JSON schema)
282
+ if hasattr(tool, "parameters") and tool.parameters:
283
+ schema = tool.parameters
284
+ if isinstance(schema, dict) and "properties" in schema:
285
+ params = []
286
+ required = schema.get("required", [])
287
+ for prop_name, prop_info in schema["properties"].items():
288
+ prop_type = prop_info.get("type", "any")
289
+ # Check for more detailed type info
290
+ if "anyOf" in prop_info:
291
+ types = [
292
+ t.get("type", "unknown")
293
+ for t in prop_info["anyOf"]
294
+ if isinstance(t, dict)
295
+ ]
296
+ prop_type = " | ".join(types) if types else "any"
297
+
298
+ param_str = f"{prop_name} ({prop_type})"
299
+ if prop_name not in required:
300
+ param_str += " (optional)"
301
+ params.append(param_str)
302
+
303
+ if params:
304
+ desc_lines.append(f" Arguments: {', '.join(params)}")
305
+ else:
306
+ desc_lines.append(" Arguments: none")
307
+ else:
308
+ desc_lines.append(" Arguments: none")
309
+
310
+ desc_lines.append("") # Empty line between tools
311
+
312
+ dispatcher_tool.description = "\n".join(desc_lines).strip()
313
+
314
+ # Update the input schema to better document available tools
315
+ # Build examples of tool calls
316
+ examples = []
317
+ for tool_name, tool in sorted(internal_tools)[:3]: # Show first 3 as examples
318
+ if hasattr(tool, "parameters") and tool.parameters:
319
+ schema = tool.parameters
320
+ if isinstance(schema, dict) and "properties" in schema:
321
+ example_args = {}
322
+ for prop_name, prop_info in schema["properties"].items():
323
+ # Generate example value based on type
324
+ prop_type = prop_info.get("type", "any")
325
+ if prop_type == "string":
326
+ example_args[prop_name] = f"<{prop_name}>"
327
+ elif prop_type == "integer" or prop_type == "number":
328
+ example_args[prop_name] = 0
329
+ elif prop_type == "boolean":
330
+ example_args[prop_name] = True
331
+ else:
332
+ example_args[prop_name] = None
333
+ examples.append({"name": tool_name, "arguments": example_args})
334
+ else:
335
+ examples.append({"name": tool_name, "arguments": {}})
336
+
337
+ # Enhanced schema with better documentation
338
+ dispatcher_tool.parameters = {
339
+ "type": "object",
340
+ "properties": {
341
+ "name": {
342
+ "type": "string",
343
+ "description": f"Name of the internal tool to call. Must be one of: {', '.join(t[0] for t in sorted(internal_tools))}", # noqa: E501
344
+ "enum": [t[0] for t in sorted(internal_tools)],
345
+ },
346
+ "arguments": {
347
+ "type": "object",
348
+ "description": "Arguments to pass to the internal tool. See description for details on each tool's parameters.", # noqa: E501
349
+ },
350
+ },
351
+ "required": ["name", "arguments"],
352
+ "examples": examples if examples else None,
353
+ }
354
+
355
+ # Override _list_tools to hide internal tools when mounted
356
+ async def _list_tools(self) -> list[Tool]:
357
+ """Override _list_tools to hide internal tools when mounted."""
358
+ return [
359
+ tool
360
+ for key, tool in self._tool_manager._tools.items()
361
+ if not key.startswith(_INTERNAL_PREFIX)
362
+ ]
363
+
364
+ resource = FastMCP.resource
365
+ prompt = FastMCP.prompt