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.
- bench/__init__.py +5 -0
- bench/cli.py +69 -0
- bench/harness/__init__.py +66 -0
- bench/harness/client.py +692 -0
- bench/harness/config.py +397 -0
- bench/harness/csv_writer.py +109 -0
- bench/harness/evaluate.py +512 -0
- bench/harness/metrics.py +283 -0
- bench/harness/runner.py +899 -0
- bench/py.typed +0 -0
- bench/reporter.py +629 -0
- bench/run.py +487 -0
- bench/secrets.py +101 -0
- bench/utils.py +16 -0
- onetool/__init__.py +4 -0
- onetool/cli.py +391 -0
- onetool/py.typed +0 -0
- onetool_mcp-1.0.0b1.dist-info/METADATA +163 -0
- onetool_mcp-1.0.0b1.dist-info/RECORD +132 -0
- onetool_mcp-1.0.0b1.dist-info/WHEEL +4 -0
- onetool_mcp-1.0.0b1.dist-info/entry_points.txt +3 -0
- onetool_mcp-1.0.0b1.dist-info/licenses/LICENSE.txt +687 -0
- onetool_mcp-1.0.0b1.dist-info/licenses/NOTICE.txt +64 -0
- ot/__init__.py +37 -0
- ot/__main__.py +6 -0
- ot/_cli.py +107 -0
- ot/_tui.py +53 -0
- ot/config/__init__.py +46 -0
- ot/config/defaults/bench.yaml +4 -0
- ot/config/defaults/diagram-templates/api-flow.mmd +33 -0
- ot/config/defaults/diagram-templates/c4-context.puml +30 -0
- ot/config/defaults/diagram-templates/class-diagram.mmd +87 -0
- ot/config/defaults/diagram-templates/feature-mindmap.mmd +70 -0
- ot/config/defaults/diagram-templates/microservices.d2 +81 -0
- ot/config/defaults/diagram-templates/project-gantt.mmd +37 -0
- ot/config/defaults/diagram-templates/state-machine.mmd +42 -0
- ot/config/defaults/onetool.yaml +25 -0
- ot/config/defaults/prompts.yaml +97 -0
- ot/config/defaults/servers.yaml +7 -0
- ot/config/defaults/snippets.yaml +4 -0
- ot/config/defaults/tool_templates/__init__.py +7 -0
- ot/config/defaults/tool_templates/extension.py +52 -0
- ot/config/defaults/tool_templates/isolated.py +61 -0
- ot/config/dynamic.py +121 -0
- ot/config/global_templates/__init__.py +2 -0
- ot/config/global_templates/bench-secrets-template.yaml +6 -0
- ot/config/global_templates/bench.yaml +9 -0
- ot/config/global_templates/onetool.yaml +27 -0
- ot/config/global_templates/secrets-template.yaml +44 -0
- ot/config/global_templates/servers.yaml +18 -0
- ot/config/global_templates/snippets.yaml +235 -0
- ot/config/loader.py +1087 -0
- ot/config/mcp.py +145 -0
- ot/config/secrets.py +190 -0
- ot/config/tool_config.py +125 -0
- ot/decorators.py +116 -0
- ot/executor/__init__.py +35 -0
- ot/executor/base.py +16 -0
- ot/executor/fence_processor.py +83 -0
- ot/executor/linter.py +142 -0
- ot/executor/pack_proxy.py +260 -0
- ot/executor/param_resolver.py +140 -0
- ot/executor/pep723.py +288 -0
- ot/executor/result_store.py +369 -0
- ot/executor/runner.py +496 -0
- ot/executor/simple.py +163 -0
- ot/executor/tool_loader.py +396 -0
- ot/executor/validator.py +398 -0
- ot/executor/worker_pool.py +388 -0
- ot/executor/worker_proxy.py +189 -0
- ot/http_client.py +145 -0
- ot/logging/__init__.py +37 -0
- ot/logging/config.py +315 -0
- ot/logging/entry.py +213 -0
- ot/logging/format.py +188 -0
- ot/logging/span.py +349 -0
- ot/meta.py +1555 -0
- ot/paths.py +453 -0
- ot/prompts.py +218 -0
- ot/proxy/__init__.py +21 -0
- ot/proxy/manager.py +396 -0
- ot/py.typed +0 -0
- ot/registry/__init__.py +189 -0
- ot/registry/models.py +57 -0
- ot/registry/parser.py +269 -0
- ot/registry/registry.py +413 -0
- ot/server.py +315 -0
- ot/shortcuts/__init__.py +15 -0
- ot/shortcuts/aliases.py +87 -0
- ot/shortcuts/snippets.py +258 -0
- ot/stats/__init__.py +35 -0
- ot/stats/html.py +250 -0
- ot/stats/jsonl_writer.py +283 -0
- ot/stats/reader.py +354 -0
- ot/stats/timing.py +57 -0
- ot/support.py +63 -0
- ot/tools.py +114 -0
- ot/utils/__init__.py +81 -0
- ot/utils/batch.py +161 -0
- ot/utils/cache.py +120 -0
- ot/utils/deps.py +403 -0
- ot/utils/exceptions.py +23 -0
- ot/utils/factory.py +179 -0
- ot/utils/format.py +65 -0
- ot/utils/http.py +202 -0
- ot/utils/platform.py +45 -0
- ot/utils/sanitize.py +130 -0
- ot/utils/truncate.py +69 -0
- ot_tools/__init__.py +4 -0
- ot_tools/_convert/__init__.py +12 -0
- ot_tools/_convert/excel.py +279 -0
- ot_tools/_convert/pdf.py +254 -0
- ot_tools/_convert/powerpoint.py +268 -0
- ot_tools/_convert/utils.py +358 -0
- ot_tools/_convert/word.py +283 -0
- ot_tools/brave_search.py +604 -0
- ot_tools/code_search.py +736 -0
- ot_tools/context7.py +495 -0
- ot_tools/convert.py +614 -0
- ot_tools/db.py +415 -0
- ot_tools/diagram.py +1604 -0
- ot_tools/diagram.yaml +167 -0
- ot_tools/excel.py +1372 -0
- ot_tools/file.py +1348 -0
- ot_tools/firecrawl.py +732 -0
- ot_tools/grounding_search.py +646 -0
- ot_tools/package.py +604 -0
- ot_tools/py.typed +0 -0
- ot_tools/ripgrep.py +544 -0
- ot_tools/scaffold.py +471 -0
- ot_tools/transform.py +213 -0
- ot_tools/web_fetch.py +384 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Fence processing for command execution.
|
|
2
|
+
|
|
3
|
+
Handles stripping of:
|
|
4
|
+
- Execution trigger prefixes (__ot, __onetool, mcp__onetool__run)
|
|
5
|
+
- Markdown code fences (triple backticks with/without language)
|
|
6
|
+
- Inline backticks (single and double)
|
|
7
|
+
|
|
8
|
+
Used by the runner to clean commands before execution.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def strip_fences(command: str) -> tuple[str, bool]:
|
|
17
|
+
"""Strip execution prefixes, markdown code fences, and inline backticks.
|
|
18
|
+
|
|
19
|
+
Execution trigger prefixes (stripped first):
|
|
20
|
+
__ot - short name, default tool
|
|
21
|
+
__ot__run - short name, explicit tool call
|
|
22
|
+
__onetool - full name, default tool
|
|
23
|
+
__onetool__run - full name, explicit tool call
|
|
24
|
+
mcp__onetool__run - explicit MCP call
|
|
25
|
+
|
|
26
|
+
Each prefix supports three invocation styles:
|
|
27
|
+
<prefix> func(arg="value") - simple call
|
|
28
|
+
<prefix> `code` - inline backticks
|
|
29
|
+
<prefix> + code fence - multi-line code fence
|
|
30
|
+
|
|
31
|
+
Note: mcp__ot__run is NOT a valid prefix.
|
|
32
|
+
|
|
33
|
+
Markdown fences (stripped after prefix):
|
|
34
|
+
```python
|
|
35
|
+
code here
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`code here`
|
|
39
|
+
|
|
40
|
+
`` `code here` ``
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
command: Raw command string that may contain prefixes and fences
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Tuple of (stripped command, whether anything was stripped)
|
|
47
|
+
"""
|
|
48
|
+
stripped = command.strip()
|
|
49
|
+
anything_stripped = False
|
|
50
|
+
|
|
51
|
+
# Strip execution trigger prefixes:
|
|
52
|
+
# - __ot, __ot__run (short name)
|
|
53
|
+
# - __onetool, __onetool__run (full name)
|
|
54
|
+
# - mcp__onetool__run (explicit MCP call)
|
|
55
|
+
# Note: mcp__ot__run is NOT valid
|
|
56
|
+
prefix_pattern = r"^(?:mcp__onetool__run|__onetool(?:__run)?|__ot(?:__run)?)\s*"
|
|
57
|
+
match = re.match(prefix_pattern, stripped)
|
|
58
|
+
if match:
|
|
59
|
+
stripped = stripped[match.end() :].strip()
|
|
60
|
+
anything_stripped = True
|
|
61
|
+
|
|
62
|
+
# Handle triple backtick fenced blocks
|
|
63
|
+
if stripped.startswith("```"):
|
|
64
|
+
first_newline = stripped.find("\n")
|
|
65
|
+
if first_newline != -1 and stripped.endswith("```"):
|
|
66
|
+
content = stripped[first_newline + 1 : -3].strip()
|
|
67
|
+
return content, True
|
|
68
|
+
|
|
69
|
+
# Handle double backtick fenced blocks: `` `code` ``
|
|
70
|
+
if stripped.startswith("`` `") and stripped.endswith("` ``"):
|
|
71
|
+
content = stripped[4:-4].strip()
|
|
72
|
+
return content, True
|
|
73
|
+
|
|
74
|
+
if stripped.startswith("``") and stripped.endswith("``"):
|
|
75
|
+
content = stripped[2:-2].strip()
|
|
76
|
+
return content, True
|
|
77
|
+
|
|
78
|
+
# Handle inline single backticks: `code`
|
|
79
|
+
if stripped.startswith("`") and stripped.endswith("`") and stripped.count("`") == 2:
|
|
80
|
+
content = stripped[1:-1].strip()
|
|
81
|
+
return content, True
|
|
82
|
+
|
|
83
|
+
return stripped, anything_stripped
|
ot/executor/linter.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Optional Ruff linting integration for OneTool.
|
|
2
|
+
|
|
3
|
+
Provides style warnings (non-blocking) using Ruff linter if installed.
|
|
4
|
+
Falls back gracefully if Ruff is not available.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
result = lint_code(code)
|
|
8
|
+
if result.available:
|
|
9
|
+
for warning in result.warnings:
|
|
10
|
+
print(f"Style warning: {warning}")
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import contextlib
|
|
16
|
+
import subprocess
|
|
17
|
+
import tempfile
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from loguru import logger
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class LintResult:
|
|
26
|
+
"""Result of linting operation."""
|
|
27
|
+
|
|
28
|
+
available: bool = False # Whether Ruff is available
|
|
29
|
+
warnings: list[str] = field(default_factory=list)
|
|
30
|
+
error: str | None = None # Error message if linting failed
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _check_ruff_available() -> bool:
|
|
34
|
+
"""Check if Ruff is available on the system."""
|
|
35
|
+
try:
|
|
36
|
+
result = subprocess.run(
|
|
37
|
+
["ruff", "--version"],
|
|
38
|
+
capture_output=True,
|
|
39
|
+
text=True,
|
|
40
|
+
timeout=5,
|
|
41
|
+
)
|
|
42
|
+
return result.returncode == 0
|
|
43
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Cache Ruff availability check
|
|
48
|
+
_ruff_available: bool | None = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def is_ruff_available() -> bool:
|
|
52
|
+
"""Check if Ruff linter is available (cached)."""
|
|
53
|
+
global _ruff_available
|
|
54
|
+
if _ruff_available is None:
|
|
55
|
+
_ruff_available = _check_ruff_available()
|
|
56
|
+
return _ruff_available
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def lint_code(
|
|
60
|
+
code: str,
|
|
61
|
+
select: list[str] | None = None,
|
|
62
|
+
ignore: list[str] | None = None,
|
|
63
|
+
) -> LintResult:
|
|
64
|
+
"""Lint Python code using Ruff.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
code: Python code to lint
|
|
68
|
+
select: Rule codes to enable (e.g., ["E", "F", "W"])
|
|
69
|
+
ignore: Rule codes to ignore (e.g., ["E501"])
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
LintResult with warnings if Ruff is available
|
|
73
|
+
"""
|
|
74
|
+
result = LintResult()
|
|
75
|
+
|
|
76
|
+
if not is_ruff_available():
|
|
77
|
+
result.available = False
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
result.available = True
|
|
81
|
+
|
|
82
|
+
# Write code to temp file for Ruff
|
|
83
|
+
try:
|
|
84
|
+
with tempfile.NamedTemporaryFile(
|
|
85
|
+
mode="w",
|
|
86
|
+
suffix=".py",
|
|
87
|
+
delete=False,
|
|
88
|
+
) as f:
|
|
89
|
+
f.write(code)
|
|
90
|
+
temp_path = Path(f.name)
|
|
91
|
+
|
|
92
|
+
# Build Ruff command
|
|
93
|
+
cmd = ["ruff", "check", str(temp_path), "--output-format=text"]
|
|
94
|
+
|
|
95
|
+
if select:
|
|
96
|
+
cmd.extend(["--select", ",".join(select)])
|
|
97
|
+
if ignore:
|
|
98
|
+
cmd.extend(["--ignore", ",".join(ignore)])
|
|
99
|
+
|
|
100
|
+
# Run Ruff
|
|
101
|
+
proc = subprocess.run(
|
|
102
|
+
cmd,
|
|
103
|
+
capture_output=True,
|
|
104
|
+
text=True,
|
|
105
|
+
timeout=30,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Parse output - each line is a warning
|
|
109
|
+
if proc.stdout:
|
|
110
|
+
for line in proc.stdout.strip().split("\n"):
|
|
111
|
+
if line.strip():
|
|
112
|
+
# Remove temp file path from output
|
|
113
|
+
warning = line.replace(str(temp_path), "<code>")
|
|
114
|
+
result.warnings.append(warning)
|
|
115
|
+
|
|
116
|
+
except subprocess.TimeoutExpired:
|
|
117
|
+
result.error = "Ruff linting timed out"
|
|
118
|
+
logger.warning("Ruff linting timed out")
|
|
119
|
+
except OSError as e:
|
|
120
|
+
result.error = f"Failed to run Ruff: {e}"
|
|
121
|
+
logger.warning(f"Failed to run Ruff: {e}")
|
|
122
|
+
finally:
|
|
123
|
+
# Clean up temp file
|
|
124
|
+
with contextlib.suppress(NameError, OSError):
|
|
125
|
+
temp_path.unlink()
|
|
126
|
+
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def lint_code_quick(code: str) -> list[str]:
|
|
131
|
+
"""Quick lint that returns just the warnings list.
|
|
132
|
+
|
|
133
|
+
Convenience function for simple use cases.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
code: Python code to lint
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
List of warning strings (empty if Ruff unavailable)
|
|
140
|
+
"""
|
|
141
|
+
result = lint_code(code)
|
|
142
|
+
return result.warnings
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Pack proxy creation for dot notation access.
|
|
2
|
+
|
|
3
|
+
Creates proxy objects that allow:
|
|
4
|
+
- brave.web_search(query="test") - pack access to tool functions
|
|
5
|
+
- context7.resolve_library_id() - MCP proxy access
|
|
6
|
+
- proxy.list_servers() - introspection of MCP servers
|
|
7
|
+
|
|
8
|
+
Used by the runner to build the execution namespace.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections import OrderedDict
|
|
14
|
+
from functools import wraps
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
|
+
|
|
17
|
+
from ot.executor.param_resolver import (
|
|
18
|
+
get_mcp_tool_param_names,
|
|
19
|
+
get_tool_param_names,
|
|
20
|
+
resolve_kwargs,
|
|
21
|
+
)
|
|
22
|
+
from ot.stats import timed_tool_call
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from collections.abc import Callable
|
|
26
|
+
|
|
27
|
+
from ot.executor.tool_loader import LoadedTools
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _wrap_with_stats(
|
|
31
|
+
pack_name: str, func_name: str, func: Callable[..., Any]
|
|
32
|
+
) -> Callable[..., Any]:
|
|
33
|
+
"""Wrap a function to record execution-level stats, track calls, and resolve param prefixes."""
|
|
34
|
+
tool_name = f"{pack_name}.{func_name}"
|
|
35
|
+
|
|
36
|
+
@wraps(func)
|
|
37
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
38
|
+
# Resolve abbreviated parameter names (cached lookup)
|
|
39
|
+
if kwargs:
|
|
40
|
+
param_names = get_tool_param_names(tool_name)
|
|
41
|
+
if param_names:
|
|
42
|
+
kwargs = resolve_kwargs(kwargs, param_names)
|
|
43
|
+
|
|
44
|
+
with timed_tool_call(tool_name):
|
|
45
|
+
return func(*args, **kwargs)
|
|
46
|
+
|
|
47
|
+
return wrapper
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _create_pack_proxy(pack_name: str, pack_funcs: dict[str, Any]) -> Any:
|
|
51
|
+
"""Create a pack proxy instance for dot notation access.
|
|
52
|
+
|
|
53
|
+
Returns an object that allows pack.func() syntax where func is looked up
|
|
54
|
+
from pack_funcs dict. Each function call is tracked for execution-level stats.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
class PackProxy:
|
|
58
|
+
"""Proxy object that provides dot notation access to pack functions."""
|
|
59
|
+
|
|
60
|
+
def __init__(self) -> None:
|
|
61
|
+
# Cache wrapped functions to avoid recreating on each access
|
|
62
|
+
self._function_cache: dict[str, Callable[..., Any]] = {}
|
|
63
|
+
|
|
64
|
+
def __getattr__(self, name: str) -> Any:
|
|
65
|
+
if name.startswith("_"):
|
|
66
|
+
raise AttributeError(f"Cannot access private attribute '{name}'")
|
|
67
|
+
|
|
68
|
+
if name in pack_funcs:
|
|
69
|
+
# Return cached wrapper or create and cache new one
|
|
70
|
+
if name not in self._function_cache:
|
|
71
|
+
self._function_cache[name] = _wrap_with_stats(
|
|
72
|
+
pack_name, name, pack_funcs[name]
|
|
73
|
+
)
|
|
74
|
+
return self._function_cache[name]
|
|
75
|
+
|
|
76
|
+
available = ", ".join(sorted(pack_funcs.keys()))
|
|
77
|
+
raise AttributeError(
|
|
78
|
+
f"Function '{name}' not found in pack '{pack_name}'. "
|
|
79
|
+
f"Available: {available}"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return PackProxy()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _create_mcp_proxy_pack(server_name: str) -> Any:
|
|
86
|
+
"""Create a pack proxy for an MCP server.
|
|
87
|
+
|
|
88
|
+
Allows calling proxied MCP tools using dot notation:
|
|
89
|
+
- context7.resolve_library_id(library_name="next.js")
|
|
90
|
+
|
|
91
|
+
Each call is tracked for execution-level stats.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
server_name: Name of the MCP server.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Object with __getattr__ that routes to proxy manager.
|
|
98
|
+
"""
|
|
99
|
+
from ot.proxy import get_proxy_manager
|
|
100
|
+
|
|
101
|
+
class McpProxyPack:
|
|
102
|
+
"""Proxy object that routes tool calls to an MCP server."""
|
|
103
|
+
|
|
104
|
+
def __init__(self) -> None:
|
|
105
|
+
# Cache callable proxies to avoid recreating on each access
|
|
106
|
+
self._function_cache: dict[str, Callable[..., str]] = {}
|
|
107
|
+
|
|
108
|
+
def __getattr__(self, tool_name: str) -> Any:
|
|
109
|
+
if tool_name.startswith("_"):
|
|
110
|
+
raise AttributeError(f"Cannot access private attribute '{tool_name}'")
|
|
111
|
+
|
|
112
|
+
if tool_name in self._function_cache:
|
|
113
|
+
return self._function_cache[tool_name]
|
|
114
|
+
|
|
115
|
+
def call_proxy_tool(**kwargs: Any) -> str:
|
|
116
|
+
tool_full_name = f"{server_name}.{tool_name}"
|
|
117
|
+
|
|
118
|
+
# Resolve abbreviated parameter names (cached lookup)
|
|
119
|
+
if kwargs:
|
|
120
|
+
param_names = get_mcp_tool_param_names(server_name, tool_name)
|
|
121
|
+
if param_names:
|
|
122
|
+
kwargs = resolve_kwargs(kwargs, param_names)
|
|
123
|
+
|
|
124
|
+
with timed_tool_call(tool_full_name):
|
|
125
|
+
proxy = get_proxy_manager()
|
|
126
|
+
return proxy.call_tool_sync(server_name, tool_name, kwargs)
|
|
127
|
+
|
|
128
|
+
self._function_cache[tool_name] = call_proxy_tool
|
|
129
|
+
return call_proxy_tool
|
|
130
|
+
|
|
131
|
+
return McpProxyPack()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _create_proxy_introspection_pack() -> Any:
|
|
135
|
+
"""Create the 'proxy' pack for introspection.
|
|
136
|
+
|
|
137
|
+
Provides:
|
|
138
|
+
- proxy.list_servers() - List all configured MCP servers with status
|
|
139
|
+
- proxy.list_tools(server="name") - List tools available on a server
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Object with introspection methods.
|
|
143
|
+
"""
|
|
144
|
+
from ot.proxy import get_proxy_manager
|
|
145
|
+
|
|
146
|
+
class ProxyIntrospectionPack:
|
|
147
|
+
"""Provides introspection methods for proxied MCP servers."""
|
|
148
|
+
|
|
149
|
+
def list_servers(self) -> list[dict[str, Any]]:
|
|
150
|
+
"""List all configured MCP servers with connection status.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
List of dicts with server name, type, enabled, and connected status.
|
|
154
|
+
"""
|
|
155
|
+
from ot.config import get_config
|
|
156
|
+
|
|
157
|
+
config = get_config()
|
|
158
|
+
proxy = get_proxy_manager()
|
|
159
|
+
|
|
160
|
+
servers = []
|
|
161
|
+
for name, cfg in (config.servers or {}).items():
|
|
162
|
+
servers.append(
|
|
163
|
+
{
|
|
164
|
+
"name": name,
|
|
165
|
+
"type": cfg.type,
|
|
166
|
+
"enabled": cfg.enabled,
|
|
167
|
+
"connected": name in proxy.servers,
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
return servers
|
|
171
|
+
|
|
172
|
+
def list_tools(self, server: str) -> list[dict[str, str]]:
|
|
173
|
+
"""List tools available on a proxied MCP server.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
server: Name of the MCP server.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
List of dicts with tool name and description.
|
|
180
|
+
|
|
181
|
+
Raises:
|
|
182
|
+
ValueError: If server is not connected.
|
|
183
|
+
"""
|
|
184
|
+
proxy = get_proxy_manager()
|
|
185
|
+
|
|
186
|
+
if server not in proxy.servers:
|
|
187
|
+
available = ", ".join(proxy.servers) or "none"
|
|
188
|
+
raise ValueError(
|
|
189
|
+
f"Server '{server}' not connected. Available: {available}"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
tools = proxy.list_tools(server)
|
|
193
|
+
return [{"name": t.name, "description": t.description} for t in tools]
|
|
194
|
+
|
|
195
|
+
return ProxyIntrospectionPack()
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# Cache for execution namespace: key=(registry_id, frozenset of proxy servers)
|
|
199
|
+
# Uses OrderedDict for proper LRU eviction
|
|
200
|
+
_NAMESPACE_CACHE_MAXSIZE = 10
|
|
201
|
+
_namespace_cache: OrderedDict[tuple[int, frozenset[str]], dict[str, Any]] = (
|
|
202
|
+
OrderedDict()
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def build_execution_namespace(
|
|
207
|
+
registry: LoadedTools,
|
|
208
|
+
) -> dict[str, Any]:
|
|
209
|
+
"""Build execution namespace with pack proxies for dot notation access.
|
|
210
|
+
|
|
211
|
+
Results are cached based on registry identity and proxy server configuration.
|
|
212
|
+
Cache is invalidated when registry changes or proxy servers are added/removed.
|
|
213
|
+
|
|
214
|
+
Provides dot notation access to tools:
|
|
215
|
+
- brave.web_search(query="test") # pack access
|
|
216
|
+
- context7.resolve_library_id() # MCP proxy access
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
registry: LoadedTools registry with functions and packs
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Dict suitable for use as exec() globals
|
|
223
|
+
"""
|
|
224
|
+
from ot.executor.worker_proxy import WorkerPackProxy
|
|
225
|
+
from ot.proxy import get_proxy_manager
|
|
226
|
+
|
|
227
|
+
# Check cache - key is registry identity + current proxy servers
|
|
228
|
+
proxy_mgr = get_proxy_manager()
|
|
229
|
+
cache_key = (id(registry), frozenset(proxy_mgr.servers))
|
|
230
|
+
|
|
231
|
+
if cache_key in _namespace_cache:
|
|
232
|
+
# LRU: move to end on access
|
|
233
|
+
_namespace_cache.move_to_end(cache_key)
|
|
234
|
+
return _namespace_cache[cache_key]
|
|
235
|
+
|
|
236
|
+
namespace: dict[str, Any] = {}
|
|
237
|
+
|
|
238
|
+
# Add pack proxies for dot notation
|
|
239
|
+
for pack_name, pack_funcs in registry.packs.items():
|
|
240
|
+
if isinstance(pack_funcs, WorkerPackProxy):
|
|
241
|
+
# Extension tools already have a proxy - use directly
|
|
242
|
+
namespace[pack_name] = pack_funcs
|
|
243
|
+
else:
|
|
244
|
+
namespace[pack_name] = _create_pack_proxy(pack_name, pack_funcs)
|
|
245
|
+
|
|
246
|
+
# Add MCP proxy packs (only if not already defined locally)
|
|
247
|
+
for server_name in proxy_mgr.servers:
|
|
248
|
+
if server_name not in namespace:
|
|
249
|
+
namespace[server_name] = _create_mcp_proxy_pack(server_name)
|
|
250
|
+
|
|
251
|
+
# Add proxy introspection pack (always available)
|
|
252
|
+
if "proxy" not in namespace:
|
|
253
|
+
namespace["proxy"] = _create_proxy_introspection_pack()
|
|
254
|
+
|
|
255
|
+
# Cache result with LRU eviction
|
|
256
|
+
_namespace_cache[cache_key] = namespace
|
|
257
|
+
while len(_namespace_cache) > _NAMESPACE_CACHE_MAXSIZE:
|
|
258
|
+
_namespace_cache.popitem(last=False)
|
|
259
|
+
|
|
260
|
+
return namespace
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Parameter name prefix matching for tool calls.
|
|
2
|
+
|
|
3
|
+
Resolves abbreviated parameter names to full parameter names using prefix matching.
|
|
4
|
+
For example: q= -> query=, c= -> count=
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections import OrderedDict
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
from functools import lru_cache
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@lru_cache(maxsize=256)
|
|
15
|
+
def get_tool_param_names(tool_name: str) -> tuple[str, ...]:
|
|
16
|
+
"""Get parameter names for a tool from the registry (cached).
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
tool_name: Full tool name (e.g., "brave.web_search").
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Tuple of parameter names in signature order, or empty tuple if not found.
|
|
23
|
+
"""
|
|
24
|
+
from ot.registry import get_registry
|
|
25
|
+
|
|
26
|
+
registry = get_registry()
|
|
27
|
+
tool_info = registry.get_tool(tool_name)
|
|
28
|
+
if tool_info:
|
|
29
|
+
return tuple(arg.name for arg in tool_info.args)
|
|
30
|
+
return ()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Cache for MCP tool param names: (server_name, tool_name) -> param_names
|
|
34
|
+
# Uses OrderedDict for LRU eviction with bounded size
|
|
35
|
+
_MCP_PARAM_CACHE_MAXSIZE = 256
|
|
36
|
+
_mcp_param_cache: OrderedDict[tuple[str, str], tuple[str, ...]] = OrderedDict()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_mcp_tool_param_names(server_name: str, tool_name: str) -> tuple[str, ...]:
|
|
40
|
+
"""Get parameter names for an MCP tool from its input schema (cached).
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
server_name: Name of the MCP server.
|
|
44
|
+
tool_name: Name of the tool.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Tuple of parameter names, or empty tuple if not found.
|
|
48
|
+
"""
|
|
49
|
+
cache_key = (server_name, tool_name)
|
|
50
|
+
if cache_key in _mcp_param_cache:
|
|
51
|
+
_mcp_param_cache.move_to_end(cache_key)
|
|
52
|
+
return _mcp_param_cache[cache_key]
|
|
53
|
+
|
|
54
|
+
from ot.proxy import get_proxy_manager
|
|
55
|
+
|
|
56
|
+
proxy = get_proxy_manager()
|
|
57
|
+
tools = proxy.list_tools(server_name)
|
|
58
|
+
result: tuple[str, ...] = ()
|
|
59
|
+
for tool in tools:
|
|
60
|
+
if tool.name == tool_name:
|
|
61
|
+
result = tuple(get_param_names_from_schema(tool.input_schema))
|
|
62
|
+
break
|
|
63
|
+
|
|
64
|
+
_mcp_param_cache[cache_key] = result
|
|
65
|
+
while len(_mcp_param_cache) > _MCP_PARAM_CACHE_MAXSIZE:
|
|
66
|
+
_mcp_param_cache.popitem(last=False)
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def resolve_kwargs(
|
|
71
|
+
kwargs: dict[str, object], param_names: Sequence[str]
|
|
72
|
+
) -> dict[str, object]:
|
|
73
|
+
"""Resolve abbreviated parameter names to full parameter names.
|
|
74
|
+
|
|
75
|
+
Matching rules:
|
|
76
|
+
1. Exact match wins - if param name matches exactly, use it
|
|
77
|
+
2. Prefix match - find all params that start with the abbreviated name
|
|
78
|
+
3. First match wins - if multiple params match, use first in param_names order
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
kwargs: Dictionary of parameter names to values.
|
|
82
|
+
param_names: Sequence of valid parameter names in signature order.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
New dictionary with resolved parameter names.
|
|
86
|
+
|
|
87
|
+
Examples:
|
|
88
|
+
>>> resolve_kwargs({"q": "test"}, ["query", "count"])
|
|
89
|
+
{"query": "test"}
|
|
90
|
+
|
|
91
|
+
>>> resolve_kwargs({"query": "test"}, ["query", "count"])
|
|
92
|
+
{"query": "test"} # exact match
|
|
93
|
+
|
|
94
|
+
>>> resolve_kwargs({"q": "x"}, ["query_info", "query", "quality"])
|
|
95
|
+
{"query_info": "x"} # first prefix match
|
|
96
|
+
|
|
97
|
+
>>> resolve_kwargs({"xyz": "test"}, ["query"])
|
|
98
|
+
{"xyz": "test"} # no match, passthrough
|
|
99
|
+
"""
|
|
100
|
+
if not kwargs or not param_names:
|
|
101
|
+
return kwargs
|
|
102
|
+
|
|
103
|
+
param_set = set(param_names)
|
|
104
|
+
resolved: dict[str, object] = {}
|
|
105
|
+
|
|
106
|
+
for key, value in kwargs.items():
|
|
107
|
+
# Exact match - use as-is
|
|
108
|
+
if key in param_set:
|
|
109
|
+
resolved[key] = value
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
# Find prefix matches (preserve signature order)
|
|
113
|
+
matches = [p for p in param_names if p.startswith(key)]
|
|
114
|
+
|
|
115
|
+
if len(matches) == 1:
|
|
116
|
+
# Single match - use it
|
|
117
|
+
resolved[matches[0]] = value
|
|
118
|
+
elif len(matches) > 1:
|
|
119
|
+
# Multiple matches - use first in signature order
|
|
120
|
+
resolved[matches[0]] = value
|
|
121
|
+
else:
|
|
122
|
+
# No match - passthrough (let function raise its own error)
|
|
123
|
+
resolved[key] = value
|
|
124
|
+
|
|
125
|
+
return resolved
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def get_param_names_from_schema(input_schema: dict[str, object]) -> list[str]:
|
|
129
|
+
"""Extract parameter names from a JSON schema.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
input_schema: JSON schema dict with "properties" key.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
List of parameter names in schema order.
|
|
136
|
+
"""
|
|
137
|
+
properties = input_schema.get("properties", {})
|
|
138
|
+
if isinstance(properties, dict):
|
|
139
|
+
return list(properties.keys())
|
|
140
|
+
return []
|