onetool-mcp 1.0.0b1__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.
Files changed (132) hide show
  1. bench/__init__.py +5 -0
  2. bench/cli.py +69 -0
  3. bench/harness/__init__.py +66 -0
  4. bench/harness/client.py +692 -0
  5. bench/harness/config.py +397 -0
  6. bench/harness/csv_writer.py +109 -0
  7. bench/harness/evaluate.py +512 -0
  8. bench/harness/metrics.py +283 -0
  9. bench/harness/runner.py +899 -0
  10. bench/py.typed +0 -0
  11. bench/reporter.py +629 -0
  12. bench/run.py +487 -0
  13. bench/secrets.py +101 -0
  14. bench/utils.py +16 -0
  15. onetool/__init__.py +4 -0
  16. onetool/cli.py +391 -0
  17. onetool/py.typed +0 -0
  18. onetool_mcp-1.0.0b1.dist-info/METADATA +163 -0
  19. onetool_mcp-1.0.0b1.dist-info/RECORD +132 -0
  20. onetool_mcp-1.0.0b1.dist-info/WHEEL +4 -0
  21. onetool_mcp-1.0.0b1.dist-info/entry_points.txt +3 -0
  22. onetool_mcp-1.0.0b1.dist-info/licenses/LICENSE.txt +687 -0
  23. onetool_mcp-1.0.0b1.dist-info/licenses/NOTICE.txt +64 -0
  24. ot/__init__.py +37 -0
  25. ot/__main__.py +6 -0
  26. ot/_cli.py +107 -0
  27. ot/_tui.py +53 -0
  28. ot/config/__init__.py +46 -0
  29. ot/config/defaults/bench.yaml +4 -0
  30. ot/config/defaults/diagram-templates/api-flow.mmd +33 -0
  31. ot/config/defaults/diagram-templates/c4-context.puml +30 -0
  32. ot/config/defaults/diagram-templates/class-diagram.mmd +87 -0
  33. ot/config/defaults/diagram-templates/feature-mindmap.mmd +70 -0
  34. ot/config/defaults/diagram-templates/microservices.d2 +81 -0
  35. ot/config/defaults/diagram-templates/project-gantt.mmd +37 -0
  36. ot/config/defaults/diagram-templates/state-machine.mmd +42 -0
  37. ot/config/defaults/onetool.yaml +25 -0
  38. ot/config/defaults/prompts.yaml +97 -0
  39. ot/config/defaults/servers.yaml +7 -0
  40. ot/config/defaults/snippets.yaml +4 -0
  41. ot/config/defaults/tool_templates/__init__.py +7 -0
  42. ot/config/defaults/tool_templates/extension.py +52 -0
  43. ot/config/defaults/tool_templates/isolated.py +61 -0
  44. ot/config/dynamic.py +121 -0
  45. ot/config/global_templates/__init__.py +2 -0
  46. ot/config/global_templates/bench-secrets-template.yaml +6 -0
  47. ot/config/global_templates/bench.yaml +9 -0
  48. ot/config/global_templates/onetool.yaml +27 -0
  49. ot/config/global_templates/secrets-template.yaml +44 -0
  50. ot/config/global_templates/servers.yaml +18 -0
  51. ot/config/global_templates/snippets.yaml +235 -0
  52. ot/config/loader.py +1087 -0
  53. ot/config/mcp.py +145 -0
  54. ot/config/secrets.py +190 -0
  55. ot/config/tool_config.py +125 -0
  56. ot/decorators.py +116 -0
  57. ot/executor/__init__.py +35 -0
  58. ot/executor/base.py +16 -0
  59. ot/executor/fence_processor.py +83 -0
  60. ot/executor/linter.py +142 -0
  61. ot/executor/pack_proxy.py +260 -0
  62. ot/executor/param_resolver.py +140 -0
  63. ot/executor/pep723.py +288 -0
  64. ot/executor/result_store.py +369 -0
  65. ot/executor/runner.py +496 -0
  66. ot/executor/simple.py +163 -0
  67. ot/executor/tool_loader.py +396 -0
  68. ot/executor/validator.py +398 -0
  69. ot/executor/worker_pool.py +388 -0
  70. ot/executor/worker_proxy.py +189 -0
  71. ot/http_client.py +145 -0
  72. ot/logging/__init__.py +37 -0
  73. ot/logging/config.py +315 -0
  74. ot/logging/entry.py +213 -0
  75. ot/logging/format.py +188 -0
  76. ot/logging/span.py +349 -0
  77. ot/meta.py +1555 -0
  78. ot/paths.py +453 -0
  79. ot/prompts.py +218 -0
  80. ot/proxy/__init__.py +21 -0
  81. ot/proxy/manager.py +396 -0
  82. ot/py.typed +0 -0
  83. ot/registry/__init__.py +189 -0
  84. ot/registry/models.py +57 -0
  85. ot/registry/parser.py +269 -0
  86. ot/registry/registry.py +413 -0
  87. ot/server.py +315 -0
  88. ot/shortcuts/__init__.py +15 -0
  89. ot/shortcuts/aliases.py +87 -0
  90. ot/shortcuts/snippets.py +258 -0
  91. ot/stats/__init__.py +35 -0
  92. ot/stats/html.py +250 -0
  93. ot/stats/jsonl_writer.py +283 -0
  94. ot/stats/reader.py +354 -0
  95. ot/stats/timing.py +57 -0
  96. ot/support.py +63 -0
  97. ot/tools.py +114 -0
  98. ot/utils/__init__.py +81 -0
  99. ot/utils/batch.py +161 -0
  100. ot/utils/cache.py +120 -0
  101. ot/utils/deps.py +403 -0
  102. ot/utils/exceptions.py +23 -0
  103. ot/utils/factory.py +179 -0
  104. ot/utils/format.py +65 -0
  105. ot/utils/http.py +202 -0
  106. ot/utils/platform.py +45 -0
  107. ot/utils/sanitize.py +130 -0
  108. ot/utils/truncate.py +69 -0
  109. ot_tools/__init__.py +4 -0
  110. ot_tools/_convert/__init__.py +12 -0
  111. ot_tools/_convert/excel.py +279 -0
  112. ot_tools/_convert/pdf.py +254 -0
  113. ot_tools/_convert/powerpoint.py +268 -0
  114. ot_tools/_convert/utils.py +358 -0
  115. ot_tools/_convert/word.py +283 -0
  116. ot_tools/brave_search.py +604 -0
  117. ot_tools/code_search.py +736 -0
  118. ot_tools/context7.py +495 -0
  119. ot_tools/convert.py +614 -0
  120. ot_tools/db.py +415 -0
  121. ot_tools/diagram.py +1604 -0
  122. ot_tools/diagram.yaml +167 -0
  123. ot_tools/excel.py +1372 -0
  124. ot_tools/file.py +1348 -0
  125. ot_tools/firecrawl.py +732 -0
  126. ot_tools/grounding_search.py +646 -0
  127. ot_tools/package.py +604 -0
  128. ot_tools/py.typed +0 -0
  129. ot_tools/ripgrep.py +544 -0
  130. ot_tools/scaffold.py +471 -0
  131. ot_tools/transform.py +213 -0
  132. ot_tools/web_fetch.py +384 -0
ot/proxy/manager.py ADDED
@@ -0,0 +1,396 @@
1
+ """ProxyManager for connecting to external MCP servers using FastMCP Client.
2
+
3
+ Manages connections to external MCP servers and routes tool calls
4
+ through OneTool's single `run` tool interface.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import contextlib
11
+ import os
12
+ from dataclasses import dataclass
13
+ from typing import Any
14
+
15
+ from fastmcp import Client
16
+ from fastmcp.client.transports import StdioTransport
17
+ from loguru import logger
18
+ from mcp import types
19
+
20
+ from ot.config.mcp import McpServerConfig, expand_secrets, expand_subprocess_env
21
+ from ot.logging import LogSpan
22
+
23
+
24
+ @dataclass
25
+ class ProxyToolInfo:
26
+ """Information about a proxied tool."""
27
+
28
+ server: str
29
+ name: str
30
+ description: str
31
+ input_schema: dict[str, Any]
32
+
33
+
34
+ class ProxyManager:
35
+ """Manages connections to external MCP servers using FastMCP Client.
36
+
37
+ Connects to configured MCP servers at startup and provides
38
+ a unified interface for calling their tools.
39
+ """
40
+
41
+ def __init__(self) -> None:
42
+ """Initialize the proxy manager."""
43
+ self._clients: dict[str, Client] = {} # type: ignore[type-arg]
44
+ self._tools_by_server: dict[str, list[types.Tool]] = {}
45
+ self._initialized = False
46
+ self._loop: asyncio.AbstractEventLoop | None = None
47
+
48
+ @property
49
+ def servers(self) -> list[str]:
50
+ """List of connected server names."""
51
+ return list(self._clients.keys())
52
+
53
+ @property
54
+ def tool_count(self) -> int:
55
+ """Total number of proxied tools across all servers."""
56
+ return sum(len(tools) for tools in self._tools_by_server.values())
57
+
58
+ def get_connection(self, server: str) -> Client | None: # type: ignore[type-arg]
59
+ """Get a client by server name."""
60
+ return self._clients.get(server)
61
+
62
+ def list_tools(self, server: str | None = None) -> list[ProxyToolInfo]:
63
+ """List available tools from proxied servers.
64
+
65
+ Args:
66
+ server: Optional server name to filter by. If None, returns all tools.
67
+
68
+ Returns:
69
+ List of ProxyToolInfo for available tools.
70
+ """
71
+ tools: list[ProxyToolInfo] = []
72
+
73
+ if server:
74
+ if server in self._tools_by_server:
75
+ for tool in self._tools_by_server[server]:
76
+ tools.append(
77
+ ProxyToolInfo(
78
+ server=server,
79
+ name=tool.name,
80
+ description=tool.description or "",
81
+ input_schema=tool.inputSchema,
82
+ )
83
+ )
84
+ else:
85
+ for srv_name, srv_tools in self._tools_by_server.items():
86
+ for tool in srv_tools:
87
+ tools.append(
88
+ ProxyToolInfo(
89
+ server=srv_name,
90
+ name=tool.name,
91
+ description=tool.description or "",
92
+ input_schema=tool.inputSchema,
93
+ )
94
+ )
95
+
96
+ return tools
97
+
98
+ async def call_tool(
99
+ self,
100
+ server: str,
101
+ tool: str,
102
+ arguments: dict[str, Any] | None = None,
103
+ timeout: float = 30.0,
104
+ ) -> str:
105
+ """Call a tool on a proxied MCP server.
106
+
107
+ Args:
108
+ server: Name of the server to call.
109
+ tool: Name of the tool to call.
110
+ arguments: Arguments to pass to the tool.
111
+ timeout: Timeout for the call in seconds.
112
+
113
+ Returns:
114
+ Text result from the tool.
115
+
116
+ Raises:
117
+ ValueError: If server is not connected.
118
+ RuntimeError: If the tool returns an error.
119
+ TimeoutError: If the call times out.
120
+ """
121
+ client = self._clients.get(server)
122
+ if not client:
123
+ available = ", ".join(self._clients.keys()) or "none"
124
+ raise ValueError(f"Server '{server}' not connected. Available: {available}")
125
+
126
+ arguments = arguments or {}
127
+
128
+ with LogSpan(span="proxy.tool.call", server=server, tool=tool) as span:
129
+ try:
130
+ result = await asyncio.wait_for(
131
+ client.call_tool(tool, arguments),
132
+ timeout=timeout,
133
+ )
134
+ except TimeoutError:
135
+ logger.error(
136
+ f"Proxy tool timeout | server={server} | tool={tool} | timeout={timeout}s"
137
+ )
138
+ raise TimeoutError(
139
+ f"Tool {server}.{tool} timed out after {timeout}s"
140
+ ) from None
141
+
142
+ # Extract text from result
143
+ text_parts: list[str] = []
144
+ for content in result.content:
145
+ if isinstance(content, types.TextContent):
146
+ text_parts.append(content.text)
147
+ elif hasattr(content, "data"):
148
+ text_parts.append(f"[Binary content: {type(content).__name__}]")
149
+
150
+ result_text = (
151
+ "\n".join(text_parts) if text_parts else "Tool returned empty response."
152
+ )
153
+ span.add("resultLength", len(result_text))
154
+ return result_text
155
+
156
+ def call_tool_sync(
157
+ self,
158
+ server: str,
159
+ tool: str,
160
+ arguments: dict[str, Any] | None = None,
161
+ timeout: float = 30.0,
162
+ ) -> str:
163
+ """Synchronously call a tool on a proxied MCP server.
164
+
165
+ This is a blocking wrapper around the async call_tool method,
166
+ suitable for use from sync code (like executed Python code).
167
+
168
+ Args:
169
+ server: Name of the server to call.
170
+ tool: Name of the tool to call.
171
+ arguments: Arguments to pass to the tool.
172
+ timeout: Timeout for the call in seconds.
173
+
174
+ Returns:
175
+ Text result from the tool.
176
+ """
177
+ if self._loop is None:
178
+ raise RuntimeError(
179
+ "Proxy manager not initialized - no event loop available"
180
+ )
181
+
182
+ future = asyncio.run_coroutine_threadsafe(
183
+ self.call_tool(server, tool, arguments, timeout),
184
+ self._loop,
185
+ )
186
+ return future.result(timeout=timeout + 5)
187
+
188
+ async def connect(self, configs: dict[str, McpServerConfig]) -> None:
189
+ """Connect to all enabled MCP servers.
190
+
191
+ Args:
192
+ configs: Dictionary of server name -> configuration.
193
+ """
194
+ if self._initialized:
195
+ return
196
+
197
+ self._loop = asyncio.get_running_loop()
198
+
199
+ enabled_configs = {name: cfg for name, cfg in configs.items() if cfg.enabled}
200
+
201
+ if not enabled_configs:
202
+ logger.debug("No MCP servers configured for proxying")
203
+ self._initialized = True
204
+ return
205
+
206
+ with LogSpan(span="proxy.init", serverCount=len(enabled_configs)) as span:
207
+ connected = 0
208
+ failed = 0
209
+
210
+ for name, config in enabled_configs.items():
211
+ try:
212
+ await self._connect_server(name, config)
213
+ connected += 1
214
+ except Exception as e:
215
+ failed += 1
216
+ logger.warning(f"Failed to connect to MCP server '{name}': {e}")
217
+
218
+ span.add("connected", connected)
219
+ span.add("failed", failed)
220
+ span.add("toolCount", self.tool_count)
221
+
222
+ self._initialized = True
223
+
224
+ async def _connect_server(self, name: str, config: McpServerConfig) -> None:
225
+ """Connect to a single MCP server using FastMCP Client."""
226
+ with LogSpan(span="proxy.connect", server=name, type=config.type) as span:
227
+ client = self._create_client(name, config)
228
+
229
+ # Enter the client context manager for persistent connection
230
+ await client.__aenter__() # type: ignore[no-untyped-call]
231
+
232
+ try:
233
+ # List tools to verify connection and cache tool info
234
+ tools = await client.list_tools()
235
+
236
+ self._clients[name] = client
237
+ self._tools_by_server[name] = tools
238
+
239
+ span.add("toolCount", len(tools))
240
+ logger.info(
241
+ f"Connected to {config.type} MCP server '{name}' with {len(tools)} tools"
242
+ )
243
+
244
+ except Exception:
245
+ # Clean up on failure
246
+ await client.__aexit__(None, None, None) # type: ignore[no-untyped-call]
247
+ raise
248
+
249
+ def _create_client(self, name: str, config: McpServerConfig) -> Client: # type: ignore[type-arg]
250
+ """Create a FastMCP Client for the given configuration."""
251
+ if config.type == "http":
252
+ return self._create_http_client(name, config)
253
+ elif config.type == "stdio":
254
+ return self._create_stdio_client(name, config)
255
+ else:
256
+ raise ValueError(f"Unknown server type: {config.type}")
257
+
258
+ def _create_http_client(self, name: str, config: McpServerConfig) -> Client: # type: ignore[type-arg]
259
+ """Create an HTTP/SSE client."""
260
+ if not config.url:
261
+ raise RuntimeError(f"Server {name}: HTTP server requires url")
262
+
263
+ # Auto-upgrade http:// to https://
264
+ url = config.url
265
+ if url.startswith("http://"):
266
+ url = "https://" + url[7:]
267
+ logger.debug(f"Upgraded {name} URL to HTTPS: {url}")
268
+
269
+ # Expand secrets in headers
270
+ headers = {}
271
+ for key, value in config.headers.items():
272
+ if "${" in value:
273
+ headers[key] = expand_secrets(value)
274
+ else:
275
+ headers[key] = value
276
+
277
+ return Client(url, headers=headers, timeout=float(config.timeout))
278
+
279
+ def _create_stdio_client(self, name: str, config: McpServerConfig) -> Client: # type: ignore[type-arg]
280
+ """Create a stdio client."""
281
+ if not config.command:
282
+ raise RuntimeError(f"Server {name}: stdio server requires command")
283
+
284
+ # Build environment: PATH only + explicit config.env
285
+ env = {"PATH": os.environ.get("PATH", "")}
286
+ for key, value in config.env.items():
287
+ env[key] = expand_subprocess_env(value)
288
+
289
+ transport = StdioTransport(
290
+ command=config.command,
291
+ args=config.args,
292
+ env=env,
293
+ )
294
+
295
+ return Client(transport, timeout=float(config.timeout))
296
+
297
+ async def shutdown(self) -> None:
298
+ """Disconnect from all MCP servers."""
299
+ if not self._clients:
300
+ return
301
+
302
+ with LogSpan(span="proxy.shutdown", serverCount=len(self._clients)):
303
+ for name, client in list(self._clients.items()):
304
+ try:
305
+ await client.__aexit__(None, None, None) # type: ignore[no-untyped-call]
306
+ logger.debug(f"Disconnected from MCP server '{name}'")
307
+ except (Exception, asyncio.CancelledError) as e:
308
+ logger.debug(f"Error disconnecting from '{name}': {e}")
309
+
310
+ self._clients.clear()
311
+ self._tools_by_server.clear()
312
+ self._initialized = False
313
+
314
+ async def reconnect(self, configs: dict[str, McpServerConfig]) -> None:
315
+ """Reconnect to all MCP servers.
316
+
317
+ Shuts down existing connections and reconnects with fresh config.
318
+
319
+ Args:
320
+ configs: Dictionary of server name -> configuration.
321
+ """
322
+ await self.shutdown()
323
+ await self.connect(configs)
324
+
325
+ def reconnect_sync(self, configs: dict[str, McpServerConfig]) -> None:
326
+ """Synchronously reconnect to all MCP servers.
327
+
328
+ Blocking wrapper for reconnect, suitable for calling from sync code.
329
+
330
+ Args:
331
+ configs: Dictionary of server name -> configuration.
332
+ """
333
+ loop = self._loop
334
+
335
+ # Try to get running loop if we don't have one stored
336
+ if loop is None:
337
+ with contextlib.suppress(RuntimeError):
338
+ loop = asyncio.get_running_loop()
339
+
340
+ if loop is None:
341
+ # No event loop available - just reset state, connect will happen on next use
342
+ self._clients.clear()
343
+ self._tools_by_server.clear()
344
+ self._initialized = False
345
+ return
346
+
347
+ future = asyncio.run_coroutine_threadsafe(
348
+ self.reconnect(configs),
349
+ loop,
350
+ )
351
+ try:
352
+ future.result(timeout=60)
353
+ except Exception as e:
354
+ logger.warning(f"Error during proxy reconnect: {e}")
355
+
356
+
357
+ # Global proxy manager instance
358
+ _proxy_manager: ProxyManager | None = None
359
+
360
+
361
+ def get_proxy_manager() -> ProxyManager:
362
+ """Get or create the global proxy manager instance.
363
+
364
+ Returns:
365
+ ProxyManager instance.
366
+ """
367
+ global _proxy_manager
368
+ if _proxy_manager is None:
369
+ _proxy_manager = ProxyManager()
370
+ return _proxy_manager
371
+
372
+
373
+ def reset_proxy_manager() -> None:
374
+ """Reset the global proxy manager (for testing)."""
375
+ global _proxy_manager
376
+ _proxy_manager = None
377
+
378
+
379
+ def reconnect_proxy_manager() -> None:
380
+ """Reconnect the global proxy manager with fresh config.
381
+
382
+ Loads server configs from the current configuration and reconnects
383
+ all MCP proxy servers. Call this after modifying server config.
384
+ """
385
+ from ot.config.loader import get_config
386
+
387
+ proxy = get_proxy_manager()
388
+ cfg = get_config()
389
+
390
+ if cfg.servers:
391
+ proxy.reconnect_sync(cfg.servers)
392
+ else:
393
+ # No servers configured - just reset state
394
+ proxy._clients.clear()
395
+ proxy._tools_by_server.clear()
396
+ proxy._initialized = False
ot/py.typed ADDED
File without changes
@@ -0,0 +1,189 @@
1
+ """Tool registry package with auto-discovery for user-defined Python tools.
2
+
3
+ The registry scans the `src/ot_tools/` directory, extracts function signatures and
4
+ docstrings using AST parsing, and provides formatted context for LLM code generation.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import inspect
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from docstring_parser import parse as parse_docstring
13
+
14
+ from .models import ArgInfo, ToolInfo
15
+ from .registry import ToolRegistry
16
+
17
+ if TYPE_CHECKING:
18
+ from collections.abc import Callable
19
+ from pathlib import Path
20
+
21
+ __all__ = [
22
+ "ArgInfo",
23
+ "ToolInfo",
24
+ "ToolRegistry",
25
+ "describe_tool",
26
+ "get_registry",
27
+ "list_tools",
28
+ ]
29
+
30
+ # Global registry instance
31
+ _registry: ToolRegistry | None = None
32
+
33
+
34
+ def _build_tool_info_from_callable(
35
+ name: str,
36
+ func: Callable[..., Any],
37
+ pack: str | None = None,
38
+ ) -> ToolInfo:
39
+ """Build ToolInfo from a callable using inspect.
40
+
41
+ Args:
42
+ name: Full tool name (e.g., "ot.tools").
43
+ func: The function object.
44
+ pack: Pack name if applicable.
45
+
46
+ Returns:
47
+ ToolInfo with extracted signature and docstring info.
48
+ """
49
+ # Get signature
50
+ try:
51
+ sig = inspect.signature(func)
52
+ signature = f"{name}{sig}"
53
+ except (ValueError, TypeError):
54
+ signature = f"{name}(...)"
55
+
56
+ # Parse docstring
57
+ doc = func.__doc__ or ""
58
+ parsed = parse_docstring(doc)
59
+
60
+ # Build args list
61
+ args: list[ArgInfo] = []
62
+ for param_name, param in sig.parameters.items():
63
+ if param_name in ("self", "cls"):
64
+ continue
65
+
66
+ # Get type annotation
67
+ if param.annotation != inspect.Parameter.empty:
68
+ param_type = (
69
+ param.annotation.__name__
70
+ if hasattr(param.annotation, "__name__")
71
+ else str(param.annotation)
72
+ )
73
+ else:
74
+ param_type = "Any"
75
+
76
+ # Get default value
77
+ default = None
78
+ if param.default != inspect.Parameter.empty:
79
+ default = repr(param.default)
80
+
81
+ # Get description from parsed docstring
82
+ description = ""
83
+ for doc_param in parsed.params:
84
+ if doc_param.arg_name == param_name:
85
+ description = doc_param.description or ""
86
+ break
87
+
88
+ args.append(
89
+ ArgInfo(
90
+ name=param_name,
91
+ type=param_type,
92
+ default=default,
93
+ description=description,
94
+ )
95
+ )
96
+
97
+ # Get return description
98
+ returns = (parsed.returns.description or "") if parsed.returns else ""
99
+
100
+ return ToolInfo(
101
+ name=name,
102
+ pack=pack,
103
+ module=func.__module__,
104
+ signature=signature,
105
+ description=parsed.short_description or "",
106
+ args=args,
107
+ returns=returns,
108
+ )
109
+
110
+
111
+ def _register_ot_pack(registry: ToolRegistry) -> None:
112
+ """Register the ot pack tools in the registry.
113
+
114
+ The ot pack provides introspection functions that need parameter
115
+ shorthand support like other tools.
116
+ """
117
+ from ot.meta import PACK_NAME, get_ot_pack_functions
118
+
119
+ ot_functions = get_ot_pack_functions()
120
+
121
+ for func_name, func in ot_functions.items():
122
+ full_name = f"{PACK_NAME}.{func_name}"
123
+ tool_info = _build_tool_info_from_callable(full_name, func, pack=PACK_NAME)
124
+ registry.register_tool(tool_info)
125
+
126
+
127
+ def get_registry(tools_path: Path | None = None, rescan: bool = False) -> ToolRegistry:
128
+ """Get or create the global tool registry.
129
+
130
+ Uses config's tools_dir glob patterns if available, otherwise falls back
131
+ to the provided tools_path or default 'src/ot_tools/' directory.
132
+
133
+ Args:
134
+ tools_path: Path to tools directory (fallback if no config).
135
+ rescan: If True, rescan even if registry exists.
136
+
137
+ Returns:
138
+ ToolRegistry instance with discovered tools.
139
+ """
140
+ from ot.config.loader import get_config
141
+
142
+ global _registry
143
+
144
+ if _registry is None:
145
+ _registry = ToolRegistry(tools_path)
146
+ # Use config's tool files if available
147
+ config = get_config()
148
+ tool_files = config.get_tool_files()
149
+ if tool_files:
150
+ _registry.scan_files(tool_files)
151
+ else:
152
+ _registry.scan_directory()
153
+ # Register ot pack tools for param shorthand support
154
+ _register_ot_pack(_registry)
155
+ elif rescan:
156
+ # Rescan using config's tool files
157
+ config = get_config()
158
+ tool_files = config.get_tool_files()
159
+ if tool_files:
160
+ _registry.scan_files(tool_files)
161
+ else:
162
+ _registry.scan_directory()
163
+ # Re-register ot pack tools after rescan
164
+ _register_ot_pack(_registry)
165
+
166
+ return _registry
167
+
168
+
169
+ def list_tools() -> str:
170
+ """List all registered tools.
171
+
172
+ Returns:
173
+ Summary of all registered tools.
174
+ """
175
+ registry = get_registry(rescan=True)
176
+ return registry.format_summary()
177
+
178
+
179
+ def describe_tool(name: str) -> str:
180
+ """Describe a specific tool.
181
+
182
+ Args:
183
+ name: Tool function name.
184
+
185
+ Returns:
186
+ Detailed tool description.
187
+ """
188
+ registry = get_registry()
189
+ return registry.describe_tool(name)
ot/registry/models.py ADDED
@@ -0,0 +1,57 @@
1
+ """Registry models - Pydantic models for tool and argument information."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class ArgInfo(BaseModel):
9
+ """Information about a function argument."""
10
+
11
+ name: str = Field(description="Argument name")
12
+ type: str = Field(default="Any", description="Type annotation")
13
+ default: str | None = Field(default=None, description="Default value if any")
14
+ description: str = Field(
15
+ default="", description="Argument description from docstring"
16
+ )
17
+
18
+
19
+ class ToolInfo(BaseModel):
20
+ """Information about a registered tool function."""
21
+
22
+ name: str = Field(
23
+ description="Qualified tool name (e.g., 'code.search' or 'search')"
24
+ )
25
+ pack: str | None = Field(
26
+ default=None, description="Pack name if tool belongs to a pack"
27
+ )
28
+ module: str = Field(description="Module path (e.g., 'tools.gold_prices')")
29
+ signature: str = Field(description="Full function signature")
30
+ description: str = Field(
31
+ default="", description="Function description from docstring"
32
+ )
33
+ args: list[ArgInfo] = Field(default_factory=list, description="Function arguments")
34
+ returns: str = Field(
35
+ default="", description="Return type/description from docstring"
36
+ )
37
+ examples: list[str] = Field(
38
+ default_factory=list, description="Usage examples from @tool decorator"
39
+ )
40
+ tags: list[str] = Field(
41
+ default_factory=list, description="Categorization tags from @tool decorator"
42
+ )
43
+ enabled: bool = Field(default=True, description="Whether the tool is enabled")
44
+ deprecated: bool = Field(
45
+ default=False, description="Whether the tool is deprecated"
46
+ )
47
+ deprecated_message: str | None = Field(
48
+ default=None, description="Deprecation message"
49
+ )
50
+ config_schema: str | None = Field(
51
+ default=None,
52
+ description="Config class source code extracted via AST (class Config(BaseModel))",
53
+ )
54
+ requires: dict[str, list[tuple[str, ...] | dict[str, str] | str]] | None = Field(
55
+ default=None,
56
+ description="Dependencies from __ot_requires__ (cli and lib lists)",
57
+ )