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_tools/db.py ADDED
@@ -0,0 +1,415 @@
1
+ # This Source Code Form is subject to the terms of the Mozilla Public
2
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
+ """Database introspection and query execution tool.
5
+
6
+ Provides SQL database access via SQLAlchemy. Supports any SQLAlchemy-compatible
7
+ database (PostgreSQL, MySQL, SQLite, Oracle, MS SQL Server, etc.).
8
+
9
+ Based on mcp-alchemy by Rui Machado (MPL 2.0).
10
+ https://github.com/runekaagaard/mcp-alchemy
11
+
12
+ Requires explicit db_url parameter for all operations.
13
+
14
+ Examples:
15
+ # Get db_url from project config
16
+ db.tables(db_url=proj.attr("myproject", "db_url"))
17
+
18
+ # Or use literal URL
19
+ db.tables(db_url="sqlite:///path/to/database.db")
20
+ db.query("SELECT 1", db_url="postgresql://user:pass@localhost/dbname")
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ # Pack for dot notation: db.tables(), db.schema(), db.query()
26
+ pack = "db"
27
+
28
+ __all__ = ["query", "schema", "tables"]
29
+
30
+ import contextlib
31
+ import threading
32
+ from collections import OrderedDict
33
+ from datetime import date, datetime
34
+ from typing import Any
35
+
36
+ from pydantic import BaseModel, Field
37
+ from sqlalchemy import Engine, create_engine, inspect, text
38
+
39
+ from ot.config import get_tool_config
40
+ from ot.logging import LogSpan
41
+ from ot.paths import resolve_cwd_path
42
+
43
+
44
+ class Config(BaseModel):
45
+ """Pack configuration - discovered by registry."""
46
+
47
+ max_chars: int = Field(
48
+ default=4000,
49
+ ge=100,
50
+ le=100000,
51
+ description="Maximum characters in query result output",
52
+ )
53
+
54
+
55
+ def _get_config() -> Config:
56
+ """Get db pack configuration."""
57
+ return get_tool_config("db", Config)
58
+
59
+
60
+ # Connection pool keyed by URL - persists across calls in process
61
+ # Uses OrderedDict for LRU eviction with bounded size
62
+ _ENGINES_MAXSIZE = 8
63
+ _engines_lock = threading.Lock()
64
+ _engines: OrderedDict[str, Engine] = OrderedDict()
65
+
66
+
67
+ def _resolve_sqlite_url(db_url: str) -> str:
68
+ """Resolve relative paths in SQLite URLs.
69
+
70
+ SQLite URLs use the format sqlite:///path/to/db.
71
+ If the path is relative, resolve it against the project working directory.
72
+
73
+ Args:
74
+ db_url: Database URL string
75
+
76
+ Returns:
77
+ URL with resolved path if SQLite with relative path, otherwise unchanged
78
+ """
79
+ if not db_url.startswith("sqlite:///"):
80
+ return db_url
81
+
82
+ # Extract path from sqlite:///path
83
+ path = db_url[10:] # len("sqlite:///") == 10
84
+
85
+ # Skip in-memory databases and absolute paths
86
+ if not path or path == ":memory:" or path.startswith("/"):
87
+ return db_url
88
+
89
+ # Resolve relative path against project directory
90
+ resolved = resolve_cwd_path(path)
91
+ return f"sqlite:///{resolved}"
92
+
93
+
94
+ def _create_engine(db_url: str) -> Engine:
95
+ """Create SQLAlchemy engine with MCP-optimized settings."""
96
+ # MCP-optimized defaults
97
+ options: dict[str, Any] = {
98
+ "isolation_level": "AUTOCOMMIT",
99
+ "pool_pre_ping": True, # Test connections before use
100
+ "pool_size": 1, # Single connection for MCP patterns
101
+ "max_overflow": 2, # Allow temporary burst capacity
102
+ "pool_recycle": 3600, # Refresh connections older than 1hr
103
+ }
104
+
105
+ return create_engine(db_url, **options)
106
+
107
+
108
+ def _get_engine(db_url: str) -> Engine:
109
+ """Get or create engine for given URL with retry logic."""
110
+ # Resolve relative paths in SQLite URLs
111
+ resolved_url = _resolve_sqlite_url(db_url)
112
+
113
+ # Fast path: check cache with lock
114
+ with _engines_lock:
115
+ if resolved_url in _engines:
116
+ # LRU: move to end on access
117
+ _engines.move_to_end(resolved_url)
118
+ return _engines[resolved_url]
119
+
120
+ # Create engine outside lock (slow operation)
121
+ with LogSpan(span="db.connect", db_url=resolved_url) as span:
122
+ try:
123
+ engine = _create_engine(resolved_url)
124
+ except Exception:
125
+ span.add(retry=True)
126
+ # One retry with fresh engine
127
+ engine = _create_engine(resolved_url)
128
+
129
+ # Double-check after acquiring lock
130
+ with _engines_lock:
131
+ if resolved_url in _engines:
132
+ # Another thread created it while we were waiting
133
+ engine.dispose()
134
+ _engines.move_to_end(resolved_url)
135
+ return _engines[resolved_url]
136
+
137
+ _engines[resolved_url] = engine
138
+
139
+ # LRU eviction: dispose oldest engine when over maxsize
140
+ while len(_engines) > _ENGINES_MAXSIZE:
141
+ _, oldest_engine = _engines.popitem(last=False)
142
+ with contextlib.suppress(Exception):
143
+ oldest_engine.dispose()
144
+
145
+ span.add(cached=False)
146
+ return engine
147
+
148
+
149
+ def _format_value(val: Any) -> str:
150
+ """Format a value for display."""
151
+ if val is None:
152
+ return "NULL"
153
+ if isinstance(val, (datetime, date)):
154
+ return val.isoformat()
155
+ return str(val)
156
+
157
+
158
+ def tables(
159
+ *, db_url: str, filter: str | None = None, ignore_case: bool = False
160
+ ) -> str:
161
+ """List table names in the database.
162
+
163
+ Args:
164
+ db_url: Database URL (required)
165
+ filter: Optional substring to filter table names
166
+ ignore_case: If True, filter matching is case-insensitive
167
+
168
+ Returns:
169
+ Comma-separated list of table names
170
+
171
+ Example:
172
+ # List all tables
173
+ db.tables(db_url=proj.attr("myproject", "db_url"))
174
+
175
+ # Filter tables containing "user"
176
+ db.tables(db_url="sqlite:///data.db", filter="user")
177
+
178
+ # Case-insensitive filter
179
+ db.tables(db_url="sqlite:///data.db", filter="USER", ignore_case=True)
180
+ """
181
+ with LogSpan(span="db.tables", db_url=db_url, filter=filter) as s:
182
+ if not db_url or not db_url.strip():
183
+ s.add(error="empty_db_url")
184
+ return "Error: db_url parameter is required"
185
+
186
+ try:
187
+ engine = _get_engine(db_url)
188
+ with engine.connect() as conn:
189
+ inspector = inspect(conn)
190
+ all_tables = inspector.get_table_names()
191
+
192
+ if filter:
193
+ if ignore_case:
194
+ filter_lower = filter.lower()
195
+ all_tables = [t for t in all_tables if filter_lower in t.lower()]
196
+ else:
197
+ all_tables = [t for t in all_tables if filter in t]
198
+
199
+ s.add(resultCount=len(all_tables))
200
+ return ", ".join(all_tables) if all_tables else "No tables found"
201
+
202
+ except Exception as e:
203
+ s.add(error=str(e))
204
+ return f"Error: {e}"
205
+
206
+
207
+ def schema(*, table_names: list[str], db_url: str) -> str:
208
+ """Get schema definitions for specified tables.
209
+
210
+ Returns column names, types, primary keys, and foreign key relationships.
211
+
212
+ Args:
213
+ table_names: List of table names to inspect
214
+ db_url: Database URL (required)
215
+
216
+ Returns:
217
+ Formatted schema definitions
218
+
219
+ Example:
220
+ # Single table
221
+ db.schema(table_names=["users"], db_url=ot.project("myproject", attr="db_url"))
222
+
223
+ # Multiple tables
224
+ db.schema(table_names=["users", "orders"], db_url="sqlite:///data.db")
225
+ """
226
+ with LogSpan(span="db.schema", tables=table_names, db_url=db_url) as s:
227
+ if not db_url or not db_url.strip():
228
+ s.add(error="empty_db_url")
229
+ return "Error: db_url parameter is required"
230
+
231
+ if not table_names:
232
+ s.add(error="no_tables")
233
+ return "Error: table_names parameter is required"
234
+
235
+ try:
236
+ engine = _get_engine(db_url)
237
+ with engine.connect() as conn:
238
+ inspector = inspect(conn)
239
+ results: list[str] = []
240
+
241
+ for table_name in table_names:
242
+ results.append(_format_table_schema(inspector, table_name))
243
+
244
+ s.add(resultCount=len(table_names))
245
+ return "\n".join(results)
246
+
247
+ except Exception as e:
248
+ s.add(error=str(e))
249
+ return f"Error: {e}"
250
+
251
+
252
+ def _format_table_schema(inspector: Any, table_name: str) -> str:
253
+ """Format schema for a single table."""
254
+ try:
255
+ columns = inspector.get_columns(table_name)
256
+ except Exception:
257
+ return f"{table_name}: [table not found]"
258
+
259
+ try:
260
+ foreign_keys = inspector.get_foreign_keys(table_name)
261
+ pk_constraint = inspector.get_pk_constraint(table_name)
262
+ except Exception:
263
+ foreign_keys = []
264
+ pk_constraint = {}
265
+
266
+ primary_keys = set(pk_constraint.get("constrained_columns", []))
267
+
268
+ result = [f"{table_name}:"]
269
+
270
+ # Process columns - use explicit key access to avoid mutating the dict
271
+ show_key_only = {"nullable", "autoincrement"}
272
+ skip_keys = {"name", "type", "comment"}
273
+ for column in columns:
274
+ name = column["name"]
275
+ col_type = str(column["type"])
276
+
277
+ parts = []
278
+ if name in primary_keys:
279
+ parts.append("primary key")
280
+ parts.append(col_type)
281
+
282
+ for k, v in column.items():
283
+ if k in skip_keys:
284
+ continue
285
+ if v:
286
+ if k in show_key_only:
287
+ parts.append(k)
288
+ else:
289
+ parts.append(f"{k}={v}")
290
+
291
+ result.append(f" {name}: " + ", ".join(parts))
292
+
293
+ # Process relationships
294
+ if foreign_keys:
295
+ result.extend(["", " Relationships:"])
296
+ for fk in foreign_keys:
297
+ constrained = ", ".join(fk["constrained_columns"])
298
+ referred_table = fk["referred_table"]
299
+ referred_cols = ", ".join(fk["referred_columns"])
300
+ result.append(f" {constrained} -> {referred_table}.{referred_cols}")
301
+
302
+ return "\n".join(result)
303
+
304
+
305
+ def query(*, sql: str, db_url: str, params: dict[str, Any] | None = None) -> str:
306
+ """Execute a SQL query and return formatted results.
307
+
308
+ IMPORTANT: Always use the params parameter for variable substitution
309
+ (e.g., 'WHERE id = :id' with params={'id': 123}) to prevent SQL injection.
310
+
311
+ Args:
312
+ sql: SQL query to execute
313
+ db_url: Database URL (required)
314
+ params: Query parameters for safe substitution
315
+
316
+ Returns:
317
+ Formatted query results or error message
318
+
319
+ Example:
320
+ # Basic query
321
+ db.query(sql="SELECT * FROM users LIMIT 5", db_url=ot.project("myproject", attr="db_url"))
322
+
323
+ # Parameterized query (safe from SQL injection)
324
+ db.query(
325
+ sql="SELECT * FROM users WHERE status = :status",
326
+ db_url="sqlite:///data.db",
327
+ params={"status": "active"}
328
+ )
329
+
330
+ # INSERT/UPDATE/DELETE
331
+ db.query(
332
+ sql="UPDATE users SET status = :status WHERE id = :id",
333
+ db_url="postgresql://user:pass@localhost/db",
334
+ params={"status": "inactive", "id": 123}
335
+ )
336
+ """
337
+ with LogSpan(span="db.query", sql=sql, db_url=db_url) as s:
338
+ if not db_url or not db_url.strip():
339
+ s.add(error="empty_db_url")
340
+ return "Error: db_url parameter is required"
341
+
342
+ if not sql or not sql.strip():
343
+ s.add(error="empty_query")
344
+ return "Error: sql parameter is required"
345
+
346
+ try:
347
+ engine = _get_engine(db_url)
348
+ with engine.connect() as conn:
349
+ cursor_result = conn.execute(text(sql), params or {})
350
+
351
+ if not cursor_result.returns_rows:
352
+ affected = cursor_result.rowcount
353
+ s.add(rowsAffected=affected)
354
+ return f"Success: {affected} rows affected"
355
+
356
+ max_chars = _get_config().max_chars
357
+ output, row_count, truncated = _format_query_results(
358
+ cursor_result, max_chars
359
+ )
360
+ s.add(rows=row_count, truncated=truncated)
361
+ return output
362
+
363
+ except Exception as e:
364
+ s.add(error=str(e))
365
+ return f"Error: {e}"
366
+
367
+
368
+ def _format_query_results(cursor_result: Any, max_chars: int) -> tuple[str, int, bool]:
369
+ """Format query results in vertical format.
370
+
371
+ Args:
372
+ cursor_result: SQLAlchemy cursor result
373
+ max_chars: Maximum characters for output
374
+
375
+ Returns:
376
+ Tuple of (formatted_output, row_count, was_truncated)
377
+ """
378
+ result: list[str] = []
379
+ size = 0
380
+ row_count = 0
381
+ displayed_count = 0
382
+ truncated = False
383
+ keys = list(cursor_result.keys())
384
+
385
+ while row := cursor_result.fetchone():
386
+ row_count += 1
387
+
388
+ if truncated:
389
+ continue
390
+
391
+ sub_result = [f"{row_count}. row"]
392
+ for col, val in zip(keys, row, strict=True):
393
+ sub_result.append(f"{col}: {_format_value(val)}")
394
+ sub_result.append("")
395
+
396
+ row_size = sum(len(x) + 1 for x in sub_result)
397
+ size += row_size
398
+
399
+ if size > max_chars:
400
+ truncated = True
401
+ else:
402
+ displayed_count += 1
403
+ result.extend(sub_result)
404
+
405
+ if row_count == 0:
406
+ return "No rows returned", 0, False
407
+
408
+ if truncated:
409
+ result.append(
410
+ f"Result: showing first {displayed_count} of {row_count} rows (output truncated)"
411
+ )
412
+ else:
413
+ result.append(f"Result: {row_count} rows")
414
+
415
+ return "\n".join(result), row_count, truncated