onetool-mcp 1.0.0b1__py3-none-any.whl → 1.0.0rc2__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 (81) hide show
  1. onetool/cli.py +63 -4
  2. onetool_mcp-1.0.0rc2.dist-info/METADATA +266 -0
  3. onetool_mcp-1.0.0rc2.dist-info/RECORD +129 -0
  4. {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/licenses/LICENSE.txt +1 -1
  5. {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/licenses/NOTICE.txt +54 -64
  6. ot/__main__.py +6 -6
  7. ot/config/__init__.py +48 -46
  8. ot/config/global_templates/__init__.py +2 -2
  9. ot/config/{defaults → global_templates}/diagram-templates/api-flow.mmd +33 -33
  10. ot/config/{defaults → global_templates}/diagram-templates/c4-context.puml +30 -30
  11. ot/config/{defaults → global_templates}/diagram-templates/class-diagram.mmd +87 -87
  12. ot/config/{defaults → global_templates}/diagram-templates/feature-mindmap.mmd +70 -70
  13. ot/config/{defaults → global_templates}/diagram-templates/microservices.d2 +81 -81
  14. ot/config/{defaults → global_templates}/diagram-templates/project-gantt.mmd +37 -37
  15. ot/config/{defaults → global_templates}/diagram-templates/state-machine.mmd +42 -42
  16. ot/config/global_templates/diagram.yaml +167 -0
  17. ot/config/global_templates/onetool.yaml +3 -1
  18. ot/config/{defaults → global_templates}/prompts.yaml +102 -97
  19. ot/config/global_templates/security.yaml +31 -0
  20. ot/config/global_templates/servers.yaml +93 -12
  21. ot/config/global_templates/snippets.yaml +5 -26
  22. ot/config/{defaults → global_templates}/tool_templates/__init__.py +7 -7
  23. ot/config/loader.py +221 -105
  24. ot/config/mcp.py +5 -1
  25. ot/config/secrets.py +192 -190
  26. ot/decorators.py +116 -116
  27. ot/executor/__init__.py +35 -35
  28. ot/executor/base.py +16 -16
  29. ot/executor/fence_processor.py +83 -83
  30. ot/executor/linter.py +142 -142
  31. ot/executor/pep723.py +288 -288
  32. ot/executor/runner.py +20 -6
  33. ot/executor/simple.py +163 -163
  34. ot/executor/validator.py +603 -164
  35. ot/http_client.py +145 -145
  36. ot/logging/__init__.py +37 -37
  37. ot/logging/entry.py +213 -213
  38. ot/logging/format.py +191 -188
  39. ot/logging/span.py +349 -349
  40. ot/meta.py +236 -14
  41. ot/paths.py +32 -49
  42. ot/prompts.py +218 -218
  43. ot/proxy/manager.py +14 -2
  44. ot/registry/__init__.py +189 -189
  45. ot/registry/parser.py +269 -269
  46. ot/server.py +330 -315
  47. ot/shortcuts/__init__.py +15 -15
  48. ot/shortcuts/aliases.py +87 -87
  49. ot/shortcuts/snippets.py +258 -258
  50. ot/stats/__init__.py +35 -35
  51. ot/stats/html.py +2 -2
  52. ot/stats/reader.py +354 -354
  53. ot/stats/timing.py +57 -57
  54. ot/support.py +63 -63
  55. ot/tools.py +1 -1
  56. ot/utils/batch.py +161 -161
  57. ot/utils/cache.py +120 -120
  58. ot/utils/exceptions.py +23 -23
  59. ot/utils/factory.py +178 -179
  60. ot/utils/format.py +65 -65
  61. ot/utils/http.py +202 -202
  62. ot/utils/platform.py +45 -45
  63. ot/utils/truncate.py +69 -69
  64. ot_tools/__init__.py +4 -4
  65. ot_tools/_convert/__init__.py +12 -12
  66. ot_tools/_convert/pdf.py +254 -254
  67. ot_tools/diagram.yaml +167 -167
  68. ot_tools/scaffold.py +2 -2
  69. ot_tools/transform.py +124 -19
  70. ot_tools/web_fetch.py +94 -43
  71. onetool_mcp-1.0.0b1.dist-info/METADATA +0 -163
  72. onetool_mcp-1.0.0b1.dist-info/RECORD +0 -132
  73. ot/config/defaults/bench.yaml +0 -4
  74. ot/config/defaults/onetool.yaml +0 -25
  75. ot/config/defaults/servers.yaml +0 -7
  76. ot/config/defaults/snippets.yaml +0 -4
  77. ot_tools/firecrawl.py +0 -732
  78. {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/WHEEL +0 -0
  79. {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/entry_points.txt +0 -0
  80. /ot/config/{defaults → global_templates}/tool_templates/extension.py +0 -0
  81. /ot/config/{defaults → global_templates}/tool_templates/isolated.py +0 -0
ot/server.py CHANGED
@@ -1,315 +1,330 @@
1
- """FastMCP server implementation with a single 'run' tool.
2
-
3
- The agent generates function call syntax with __ot prefix:
4
- __ot context7.search(query="next.js")
5
- __ot context7.doc(library_key="vercel/next.js", topic="routing")
6
- __ot `demo.upper(text="hello")`
7
-
8
- Or Python code blocks:
9
- __ot
10
- ```python
11
- metals = ["Gold", "Silver", "Bronze"]
12
- results = {}
13
- for metal in metals:
14
- results[metal] = brave.web_search(query=f"{metal} price", count=3)
15
- return results
16
- ```
17
-
18
- Or direct MCP calls:
19
- mcp__onetool__run(command='brave.web_search(query="test")')
20
-
21
- Supported prefixes: __ot, __ot__run, __onetool, __onetool__run, mcp__onetool__run
22
- Note: mcp__ot__run is NOT valid.
23
- """
24
-
25
- from __future__ import annotations
26
-
27
- import time
28
- from contextlib import asynccontextmanager
29
- from typing import TYPE_CHECKING, Any
30
-
31
- if TYPE_CHECKING:
32
- from collections.abc import AsyncIterator
33
-
34
- from fastmcp import Context, FastMCP
35
- from loguru import logger
36
-
37
- from ot.config.loader import get_config
38
- from ot.executor import SimpleExecutor, execute_command
39
- from ot.executor.runner import prepare_command
40
-
41
- # Import logging first to remove Loguru's default console handler
42
- from ot.logging import LogSpan, configure_logging
43
- from ot.prompts import get_prompts, get_tool_description, get_tool_examples
44
- from ot.proxy import get_proxy_manager
45
- from ot.registry import get_registry
46
- from ot.stats import (
47
- JsonlStatsWriter,
48
- get_client_name,
49
- set_stats_writer,
50
- )
51
- from ot.support import get_startup_message
52
-
53
- _config = get_config()
54
-
55
- # Initialize logging to serve.log
56
- configure_logging(log_name="serve")
57
-
58
- # Global stats writer (unified JSONL for both run and tool stats)
59
- _stats_writer: JsonlStatsWriter | None = None
60
-
61
-
62
- def _get_instructions() -> str:
63
- """Generate MCP server instructions.
64
-
65
- Note: Tool descriptions are NOT included here - they come through
66
- the MCP tool definitions which the client converts to function calling format.
67
- """
68
- # Load prompts from config (loaded via include: or inline prompts:)
69
- prompts = get_prompts(inline_prompts=_config.prompts)
70
-
71
- # Return instructions from prompts.yaml
72
- return prompts.instructions.strip()
73
-
74
-
75
- @asynccontextmanager
76
- async def _lifespan(_server: FastMCP) -> AsyncIterator[None]:
77
- """Manage server lifecycle - startup and shutdown."""
78
- global _stats_writer
79
-
80
- with LogSpan(span="mcp.server.start") as start_span:
81
- # Startup: connect to proxy MCP servers
82
- proxy = get_proxy_manager()
83
- if _config.servers:
84
- with LogSpan(span="server.startup.proxy", serverCount=len(_config.servers)):
85
- await proxy.connect(_config.servers)
86
- start_span.add("proxyCount", len(_config.servers))
87
-
88
- # Log tool count from registry
89
- registry = get_registry()
90
- start_span.add("toolCount", len(registry.tools))
91
-
92
- # Startup: initialize unified JSONL stats writer if enabled
93
- if _config.stats.enabled:
94
- stats_path = _config.get_stats_file_path()
95
- flush_interval = _config.stats.flush_interval_seconds
96
-
97
- _stats_writer = JsonlStatsWriter(
98
- path=stats_path,
99
- flush_interval=flush_interval,
100
- )
101
- await _stats_writer.start()
102
- set_stats_writer(_stats_writer)
103
-
104
- start_span.add("statsEnabled", True)
105
- start_span.add("statsPath", str(stats_path))
106
-
107
- # Log support message
108
- logger.info(get_startup_message())
109
-
110
- yield
111
-
112
- with LogSpan(span="mcp.server.stop") as stop_span:
113
- # Shutdown: stop stats writer
114
- if _stats_writer is not None:
115
- await _stats_writer.stop()
116
- set_stats_writer(None)
117
- stop_span.add("statsStopped", True)
118
-
119
- # Shutdown: disconnect from proxy MCP servers
120
- if proxy.servers:
121
- with LogSpan(span="server.shutdown.proxy", serverCount=len(proxy.servers)):
122
- await proxy.shutdown()
123
- stop_span.add("proxyCount", len(proxy.servers))
124
-
125
-
126
- mcp = FastMCP(
127
- name="ot",
128
- instructions=_get_instructions(),
129
- lifespan=_lifespan,
130
- )
131
-
132
-
133
- # =============================================================================
134
- # MCP Logging - Dynamic log level control
135
- # =============================================================================
136
-
137
-
138
- @mcp._mcp_server.set_logging_level() # type: ignore[no-untyped-call,untyped-decorator]
139
- async def handle_set_logging_level(level: str) -> None:
140
- """Handle logging/setLevel requests from MCP clients.
141
-
142
- Allows clients to dynamically change the server's log level.
143
- """
144
- # Map MCP LoggingLevel to Python logging levels
145
- level_map = {
146
- "debug": "DEBUG",
147
- "info": "INFO",
148
- "notice": "INFO", # MCP notice -> INFO
149
- "warning": "WARNING",
150
- "error": "ERROR",
151
- "critical": "CRITICAL",
152
- "alert": "CRITICAL", # MCP alert -> CRITICAL
153
- "emergency": "CRITICAL", # MCP emergency -> CRITICAL
154
- }
155
-
156
- log_level = level_map.get(str(level).lower(), "INFO")
157
- logger.info(f"Log level change requested: {level} -> {log_level}")
158
-
159
- # Reconfigure logging with new level
160
- configure_logging(log_name="serve", level=log_level)
161
- logger.info(f"Logging reconfigured at level {log_level}")
162
-
163
-
164
- # =============================================================================
165
- # MCP Resources - Tool discoverability
166
- # =============================================================================
167
-
168
-
169
- @mcp.resource("ot://tools")
170
- def list_tools_resource() -> list[dict[str, str]]:
171
- """List all available tools with signatures and descriptions."""
172
- registry = get_registry()
173
- prompts = get_prompts(inline_prompts=_config.prompts)
174
-
175
- tools_list = []
176
-
177
- # Add local tools
178
- for tool in registry.tools.values():
179
- desc = get_tool_description(prompts, tool.name, tool.description)
180
- tools_list.append(
181
- {
182
- "name": tool.name,
183
- "signature": tool.signature,
184
- "description": desc,
185
- }
186
- )
187
-
188
- # Add proxied tools
189
- proxy = get_proxy_manager()
190
- for proxy_tool in proxy.list_tools():
191
- tools_list.append(
192
- {
193
- "name": f"{proxy_tool.server}.{proxy_tool.name}",
194
- "signature": f"{proxy_tool.server}.{proxy_tool.name}(...)",
195
- "description": f"[proxy] {proxy_tool.description}",
196
- }
197
- )
198
-
199
- return tools_list
200
-
201
-
202
- @mcp.resource("ot://tool/{name}")
203
- def get_tool_resource(name: str) -> dict[str, Any]:
204
- """Get detailed information about a specific tool."""
205
- registry = get_registry()
206
- prompts = get_prompts(inline_prompts=_config.prompts)
207
-
208
- tool = registry.tools.get(name)
209
- if not tool:
210
- return {"error": f"Tool '{name}' not found"}
211
-
212
- desc = get_tool_description(prompts, tool.name, tool.description)
213
- examples = get_tool_examples(prompts, tool.name)
214
-
215
- return {
216
- "name": tool.name,
217
- "module": tool.module,
218
- "signature": tool.signature,
219
- "description": desc,
220
- "args": [
221
- {
222
- "name": arg.name,
223
- "type": arg.type,
224
- "default": arg.default,
225
- "description": arg.description,
226
- }
227
- for arg in tool.args
228
- ],
229
- "returns": tool.returns,
230
- "examples": examples or tool.examples,
231
- "tags": tool.tags,
232
- "enabled": tool.enabled,
233
- "deprecated": tool.deprecated,
234
- "deprecated_message": tool.deprecated_message,
235
- }
236
-
237
-
238
- # Global executor instance
239
- _executor: SimpleExecutor | None = None
240
-
241
-
242
- def _get_executor() -> SimpleExecutor:
243
- """Get or create the executor."""
244
- global _executor
245
-
246
- if _executor is None:
247
- _executor = SimpleExecutor()
248
-
249
- return _executor
250
-
251
-
252
- def _get_run_description() -> str:
253
- """Get run tool description from prompts config.
254
-
255
- Raises:
256
- ValueError: If run tool description not found in prompts.yaml
257
- """
258
- prompts = get_prompts(inline_prompts=_config.prompts)
259
- desc = get_tool_description(prompts, "run", "")
260
- if not desc:
261
- raise ValueError("Missing 'run' tool description in prompts.yaml")
262
- return desc
263
-
264
-
265
- @mcp.tool(
266
- description=_get_run_description(),
267
- annotations={
268
- "title": "Execute OneTool Command",
269
- "readOnlyHint": False,
270
- "destructiveHint": False,
271
- "idempotentHint": False,
272
- "openWorldHint": True,
273
- },
274
- )
275
- async def run(command: str, ctx: Context) -> str: # noqa: ARG001
276
- # Get registry (cached, no rescan per request) and executor
277
- registry = get_registry()
278
- executor = _get_executor()
279
-
280
- # Record start time for stats
281
- start_time = time.monotonic()
282
-
283
- # Step 1: Prepare and validate command
284
- prepared = prepare_command(command)
285
-
286
- if prepared.error:
287
- return f"Error: {prepared.error}"
288
-
289
- # Step 2: Execute through unified runner (skip validation since already done)
290
- result = await execute_command(
291
- command,
292
- registry,
293
- executor,
294
- prepared_code=prepared.code,
295
- skip_validation=True,
296
- )
297
-
298
- # Record run-level stats if enabled
299
- if _stats_writer is not None:
300
- duration_ms = int((time.monotonic() - start_time) * 1000)
301
- _stats_writer.record_run(
302
- client=get_client_name(),
303
- chars_in=len(command),
304
- chars_out=len(result.result),
305
- duration_ms=duration_ms,
306
- success=result.success,
307
- error_type=result.error_type,
308
- )
309
-
310
- return result.result
311
-
312
-
313
- def main() -> None:
314
- """Run the MCP server over stdio transport."""
315
- mcp.run(show_banner=False)
1
+ """FastMCP server implementation with a single 'run' tool.
2
+
3
+ The agent generates function call syntax with __ot prefix:
4
+ __ot context7.search(query="next.js")
5
+ __ot context7.doc(library_key="vercel/next.js", topic="routing")
6
+ __ot `demo.upper(text="hello")`
7
+
8
+ Or Python code blocks:
9
+ __ot
10
+ ```python
11
+ metals = ["Gold", "Silver", "Bronze"]
12
+ results = {}
13
+ for metal in metals:
14
+ results[metal] = brave.web_search(query=f"{metal} price", count=3)
15
+ return results
16
+ ```
17
+
18
+ Or direct MCP calls:
19
+ mcp__onetool__run(command='brave.web_search(query="test")')
20
+
21
+ Supported prefixes: __ot, __ot__run, __onetool, __onetool__run, mcp__onetool__run
22
+ Note: mcp__ot__run is NOT valid.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import time
28
+ from contextlib import asynccontextmanager
29
+ from typing import TYPE_CHECKING, Any
30
+
31
+ if TYPE_CHECKING:
32
+ from collections.abc import AsyncIterator
33
+
34
+ from fastmcp import Context, FastMCP
35
+ from loguru import logger
36
+
37
+ from ot.config.loader import get_config
38
+ from ot.executor import SimpleExecutor, execute_command
39
+ from ot.executor.runner import prepare_command
40
+
41
+ # Import logging first to remove Loguru's default console handler
42
+ from ot.logging import LogSpan, configure_logging
43
+ from ot.prompts import get_prompts, get_tool_description, get_tool_examples
44
+ from ot.proxy import get_proxy_manager
45
+ from ot.registry import get_registry
46
+ from ot.stats import (
47
+ JsonlStatsWriter,
48
+ get_client_name,
49
+ set_stats_writer,
50
+ )
51
+ from ot.support import get_startup_message
52
+
53
+ _config = get_config()
54
+
55
+ # Initialize logging to serve.log
56
+ configure_logging(log_name="serve")
57
+
58
+ # Global stats writer (unified JSONL for both run and tool stats)
59
+ _stats_writer: JsonlStatsWriter | None = None
60
+
61
+
62
+ def _get_instructions() -> str:
63
+ """Generate MCP server instructions.
64
+
65
+ Note: Tool descriptions are NOT included here - they come through
66
+ the MCP tool definitions which the client converts to function calling format.
67
+ """
68
+ # Load prompts from config (loaded via include: or inline prompts:)
69
+ prompts = get_prompts(inline_prompts=_config.prompts)
70
+
71
+ parts = [prompts.instructions.strip()]
72
+
73
+ # Append server-specific instructions from enabled servers
74
+ if _config.servers:
75
+ server_instructions = []
76
+ for name, cfg in _config.servers.items():
77
+ if cfg.enabled and cfg.instructions:
78
+ server_instructions.append(f"## {name}\n{cfg.instructions.strip()}")
79
+
80
+ if server_instructions:
81
+ parts.append("\n# MCP Server Instructions\n")
82
+ parts.append(
83
+ "The following MCP servers have provided instructions for how to use their tools and resources:\n"
84
+ )
85
+ parts.append("\n\n".join(server_instructions))
86
+
87
+ return "\n".join(parts)
88
+
89
+
90
+ @asynccontextmanager
91
+ async def _lifespan(_server: FastMCP) -> AsyncIterator[None]:
92
+ """Manage server lifecycle - startup and shutdown."""
93
+ global _stats_writer
94
+
95
+ with LogSpan(span="mcp.server.start") as start_span:
96
+ # Startup: connect to proxy MCP servers
97
+ proxy = get_proxy_manager()
98
+ if _config.servers:
99
+ with LogSpan(span="server.startup.proxy", serverCount=len(_config.servers)):
100
+ await proxy.connect(_config.servers)
101
+ start_span.add("proxyCount", len(_config.servers))
102
+
103
+ # Log tool count from registry
104
+ registry = get_registry()
105
+ start_span.add("toolCount", len(registry.tools))
106
+
107
+ # Startup: initialize unified JSONL stats writer if enabled
108
+ if _config.stats.enabled:
109
+ stats_path = _config.get_stats_file_path()
110
+ flush_interval = _config.stats.flush_interval_seconds
111
+
112
+ _stats_writer = JsonlStatsWriter(
113
+ path=stats_path,
114
+ flush_interval=flush_interval,
115
+ )
116
+ await _stats_writer.start()
117
+ set_stats_writer(_stats_writer)
118
+
119
+ start_span.add("statsEnabled", True)
120
+ start_span.add("statsPath", str(stats_path))
121
+
122
+ # Log support message
123
+ logger.info(get_startup_message())
124
+
125
+ yield
126
+
127
+ with LogSpan(span="mcp.server.stop") as stop_span:
128
+ # Shutdown: stop stats writer
129
+ if _stats_writer is not None:
130
+ await _stats_writer.stop()
131
+ set_stats_writer(None)
132
+ stop_span.add("statsStopped", True)
133
+
134
+ # Shutdown: disconnect from proxy MCP servers
135
+ if proxy.servers:
136
+ with LogSpan(span="server.shutdown.proxy", serverCount=len(proxy.servers)):
137
+ await proxy.shutdown()
138
+ stop_span.add("proxyCount", len(proxy.servers))
139
+
140
+
141
+ mcp = FastMCP(
142
+ name="ot",
143
+ instructions=_get_instructions(),
144
+ lifespan=_lifespan,
145
+ )
146
+
147
+
148
+ # =============================================================================
149
+ # MCP Logging - Dynamic log level control
150
+ # =============================================================================
151
+
152
+
153
+ @mcp._mcp_server.set_logging_level() # type: ignore[no-untyped-call,untyped-decorator]
154
+ async def handle_set_logging_level(level: str) -> None:
155
+ """Handle logging/setLevel requests from MCP clients.
156
+
157
+ Allows clients to dynamically change the server's log level.
158
+ """
159
+ # Map MCP LoggingLevel to Python logging levels
160
+ level_map = {
161
+ "debug": "DEBUG",
162
+ "info": "INFO",
163
+ "notice": "INFO", # MCP notice -> INFO
164
+ "warning": "WARNING",
165
+ "error": "ERROR",
166
+ "critical": "CRITICAL",
167
+ "alert": "CRITICAL", # MCP alert -> CRITICAL
168
+ "emergency": "CRITICAL", # MCP emergency -> CRITICAL
169
+ }
170
+
171
+ log_level = level_map.get(str(level).lower(), "INFO")
172
+ logger.info(f"Log level change requested: {level} -> {log_level}")
173
+
174
+ # Reconfigure logging with new level
175
+ configure_logging(log_name="serve", level=log_level)
176
+ logger.info(f"Logging reconfigured at level {log_level}")
177
+
178
+
179
+ # =============================================================================
180
+ # MCP Resources - Tool discoverability
181
+ # =============================================================================
182
+
183
+
184
+ @mcp.resource("ot://tools")
185
+ def list_tools_resource() -> list[dict[str, str]]:
186
+ """List all available tools with signatures and descriptions."""
187
+ registry = get_registry()
188
+ prompts = get_prompts(inline_prompts=_config.prompts)
189
+
190
+ tools_list = []
191
+
192
+ # Add local tools
193
+ for tool in registry.tools.values():
194
+ desc = get_tool_description(prompts, tool.name, tool.description)
195
+ tools_list.append(
196
+ {
197
+ "name": tool.name,
198
+ "signature": tool.signature,
199
+ "description": desc,
200
+ }
201
+ )
202
+
203
+ # Add proxied tools
204
+ proxy = get_proxy_manager()
205
+ for proxy_tool in proxy.list_tools():
206
+ tools_list.append(
207
+ {
208
+ "name": f"{proxy_tool.server}.{proxy_tool.name}",
209
+ "signature": f"{proxy_tool.server}.{proxy_tool.name}(...)",
210
+ "description": f"[proxy] {proxy_tool.description}",
211
+ }
212
+ )
213
+
214
+ return tools_list
215
+
216
+
217
+ @mcp.resource("ot://tool/{name}")
218
+ def get_tool_resource(name: str) -> dict[str, Any]:
219
+ """Get detailed information about a specific tool."""
220
+ registry = get_registry()
221
+ prompts = get_prompts(inline_prompts=_config.prompts)
222
+
223
+ tool = registry.tools.get(name)
224
+ if not tool:
225
+ return {"error": f"Tool '{name}' not found"}
226
+
227
+ desc = get_tool_description(prompts, tool.name, tool.description)
228
+ examples = get_tool_examples(prompts, tool.name)
229
+
230
+ return {
231
+ "name": tool.name,
232
+ "module": tool.module,
233
+ "signature": tool.signature,
234
+ "description": desc,
235
+ "args": [
236
+ {
237
+ "name": arg.name,
238
+ "type": arg.type,
239
+ "default": arg.default,
240
+ "description": arg.description,
241
+ }
242
+ for arg in tool.args
243
+ ],
244
+ "returns": tool.returns,
245
+ "examples": examples or tool.examples,
246
+ "tags": tool.tags,
247
+ "enabled": tool.enabled,
248
+ "deprecated": tool.deprecated,
249
+ "deprecated_message": tool.deprecated_message,
250
+ }
251
+
252
+
253
+ # Global executor instance
254
+ _executor: SimpleExecutor | None = None
255
+
256
+
257
+ def _get_executor() -> SimpleExecutor:
258
+ """Get or create the executor."""
259
+ global _executor
260
+
261
+ if _executor is None:
262
+ _executor = SimpleExecutor()
263
+
264
+ return _executor
265
+
266
+
267
+ def _get_run_description() -> str:
268
+ """Get run tool description from prompts config.
269
+
270
+ Raises:
271
+ ValueError: If run tool description not found in prompts.yaml
272
+ """
273
+ prompts = get_prompts(inline_prompts=_config.prompts)
274
+ desc = get_tool_description(prompts, "run", "")
275
+ if not desc:
276
+ raise ValueError("Missing 'run' tool description in prompts.yaml")
277
+ return desc
278
+
279
+
280
+ @mcp.tool(
281
+ description=_get_run_description(),
282
+ annotations={
283
+ "title": "Execute OneTool Command",
284
+ "readOnlyHint": False,
285
+ "destructiveHint": False,
286
+ "idempotentHint": False,
287
+ "openWorldHint": True,
288
+ },
289
+ )
290
+ async def run(command: str, ctx: Context) -> str: # noqa: ARG001
291
+ # Get registry (cached, no rescan per request) and executor
292
+ registry = get_registry()
293
+ executor = _get_executor()
294
+
295
+ # Record start time for stats
296
+ start_time = time.monotonic()
297
+
298
+ # Step 1: Prepare and validate command
299
+ prepared = prepare_command(command)
300
+
301
+ if prepared.error:
302
+ return f"Error: {prepared.error}"
303
+
304
+ # Step 2: Execute through unified runner (skip validation since already done)
305
+ result = await execute_command(
306
+ command,
307
+ registry,
308
+ executor,
309
+ prepared_code=prepared.code,
310
+ skip_validation=True,
311
+ )
312
+
313
+ # Record run-level stats if enabled
314
+ if _stats_writer is not None:
315
+ duration_ms = int((time.monotonic() - start_time) * 1000)
316
+ _stats_writer.record_run(
317
+ client=get_client_name(),
318
+ chars_in=len(command),
319
+ chars_out=len(result.result),
320
+ duration_ms=duration_ms,
321
+ success=result.success,
322
+ error_type=result.error_type,
323
+ )
324
+
325
+ return result.result
326
+
327
+
328
+ def main() -> None:
329
+ """Run the MCP server over stdio transport."""
330
+ mcp.run(show_banner=False)