onetool-mcp 1.0.0rc2__py3-none-any.whl → 1.0.0rc3__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.
- onetool/cli.py +2 -0
- {onetool_mcp-1.0.0rc2.dist-info → onetool_mcp-1.0.0rc3.dist-info}/METADATA +26 -33
- {onetool_mcp-1.0.0rc2.dist-info → onetool_mcp-1.0.0rc3.dist-info}/RECORD +31 -33
- ot/config/__init__.py +90 -48
- ot/config/global_templates/__init__.py +2 -2
- ot/config/global_templates/diagram-templates/api-flow.mmd +33 -33
- ot/config/global_templates/diagram-templates/c4-context.puml +30 -30
- ot/config/global_templates/diagram-templates/class-diagram.mmd +87 -87
- ot/config/global_templates/diagram-templates/feature-mindmap.mmd +70 -70
- ot/config/global_templates/diagram-templates/microservices.d2 +81 -81
- ot/config/global_templates/diagram-templates/project-gantt.mmd +37 -37
- ot/config/global_templates/diagram-templates/state-machine.mmd +42 -42
- ot/config/global_templates/diagram.yaml +167 -167
- ot/config/global_templates/onetool.yaml +2 -0
- ot/config/global_templates/prompts.yaml +102 -102
- ot/config/global_templates/security.yaml +1 -4
- ot/config/global_templates/servers.yaml +1 -1
- ot/config/global_templates/tool_templates/__init__.py +7 -7
- ot/config/loader.py +226 -869
- ot/config/models.py +735 -0
- ot/config/secrets.py +243 -192
- ot/executor/tool_loader.py +10 -1
- ot/executor/validator.py +11 -1
- ot/meta.py +338 -33
- ot/prompts.py +228 -218
- ot/proxy/manager.py +168 -8
- ot/registry/__init__.py +199 -189
- ot/config/dynamic.py +0 -121
- ot/config/mcp.py +0 -149
- ot/config/tool_config.py +0 -125
- {onetool_mcp-1.0.0rc2.dist-info → onetool_mcp-1.0.0rc3.dist-info}/WHEEL +0 -0
- {onetool_mcp-1.0.0rc2.dist-info → onetool_mcp-1.0.0rc3.dist-info}/entry_points.txt +0 -0
- {onetool_mcp-1.0.0rc2.dist-info → onetool_mcp-1.0.0rc3.dist-info}/licenses/LICENSE.txt +0 -0
- {onetool_mcp-1.0.0rc2.dist-info → onetool_mcp-1.0.0rc3.dist-info}/licenses/NOTICE.txt +0 -0
ot/registry/__init__.py
CHANGED
|
@@ -1,189 +1,199 @@
|
|
|
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
|
|
170
|
-
"""
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
"""
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def
|
|
180
|
-
"""
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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 reset() -> None:
|
|
170
|
+
"""Clear registry cache for reload.
|
|
171
|
+
|
|
172
|
+
Use this as part of the config reload flow to force registry to be
|
|
173
|
+
rescanned on next access.
|
|
174
|
+
"""
|
|
175
|
+
global _registry
|
|
176
|
+
_registry = None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def list_tools() -> str:
|
|
180
|
+
"""List all registered tools.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Summary of all registered tools.
|
|
184
|
+
"""
|
|
185
|
+
registry = get_registry(rescan=True)
|
|
186
|
+
return registry.format_summary()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def describe_tool(name: str) -> str:
|
|
190
|
+
"""Describe a specific tool.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
name: Tool function name.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Detailed tool description.
|
|
197
|
+
"""
|
|
198
|
+
registry = get_registry()
|
|
199
|
+
return registry.describe_tool(name)
|
ot/config/dynamic.py
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
"""Dynamic tool configuration building.
|
|
2
|
-
|
|
3
|
-
This module provides dynamic configuration building for tools based on
|
|
4
|
-
discovered Config classes in tool files. Instead of hardcoding tool configs
|
|
5
|
-
in loader.py, each tool declares its own Config(BaseModel) class which is
|
|
6
|
-
discovered and used for validation.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
from typing import TYPE_CHECKING, Any, cast
|
|
12
|
-
|
|
13
|
-
from loguru import logger
|
|
14
|
-
from pydantic import BaseModel, Field, create_model
|
|
15
|
-
|
|
16
|
-
if TYPE_CHECKING:
|
|
17
|
-
from ot.executor.pep723 import ToolFileInfo
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def build_tools_config_model(
|
|
21
|
-
tool_files: list[ToolFileInfo],
|
|
22
|
-
) -> type[BaseModel]:
|
|
23
|
-
"""Generate a dynamic ToolsConfig model from discovered tool schemas.
|
|
24
|
-
|
|
25
|
-
Creates a Pydantic model where each pack with a Config class gets
|
|
26
|
-
a corresponding field. Packs without Config classes are not included.
|
|
27
|
-
|
|
28
|
-
Args:
|
|
29
|
-
tool_files: List of analyzed tool files with config_class_source
|
|
30
|
-
|
|
31
|
-
Returns:
|
|
32
|
-
A dynamically generated Pydantic model class
|
|
33
|
-
|
|
34
|
-
Example:
|
|
35
|
-
If brave_search.py has:
|
|
36
|
-
class Config(BaseModel):
|
|
37
|
-
timeout: float = Field(default=60.0, ge=1.0, le=300.0)
|
|
38
|
-
|
|
39
|
-
Then build_tools_config_model returns a model with:
|
|
40
|
-
class DynamicToolsConfig(BaseModel):
|
|
41
|
-
brave: BraveConfig = Field(default_factory=BraveConfig)
|
|
42
|
-
"""
|
|
43
|
-
fields: dict[str, Any] = {}
|
|
44
|
-
config_classes: dict[str, type[BaseModel]] = {}
|
|
45
|
-
|
|
46
|
-
for tool_file in tool_files:
|
|
47
|
-
if not tool_file.pack or not tool_file.config_class_source:
|
|
48
|
-
continue
|
|
49
|
-
|
|
50
|
-
pack_name = tool_file.pack
|
|
51
|
-
|
|
52
|
-
# Skip if we already processed this pack
|
|
53
|
-
if pack_name in config_classes:
|
|
54
|
-
continue
|
|
55
|
-
|
|
56
|
-
try:
|
|
57
|
-
# Execute the config class source to get the actual class
|
|
58
|
-
config_class = _compile_config_class(
|
|
59
|
-
pack_name, tool_file.config_class_source
|
|
60
|
-
)
|
|
61
|
-
if config_class:
|
|
62
|
-
config_classes[pack_name] = config_class
|
|
63
|
-
fields[pack_name] = (
|
|
64
|
-
config_class,
|
|
65
|
-
Field(default_factory=config_class),
|
|
66
|
-
)
|
|
67
|
-
logger.debug(f"Registered config for pack '{pack_name}'")
|
|
68
|
-
except Exception as e:
|
|
69
|
-
logger.warning(f"Failed to compile config for pack '{pack_name}': {e}")
|
|
70
|
-
|
|
71
|
-
return create_model("DynamicToolsConfig", **fields)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def _compile_config_class(
|
|
75
|
-
_pack_name: str, config_source: str
|
|
76
|
-
) -> type[BaseModel] | None:
|
|
77
|
-
"""Compile a Config class source into an actual class.
|
|
78
|
-
|
|
79
|
-
Args:
|
|
80
|
-
pack_name: Pack name for context
|
|
81
|
-
config_source: Source code of the Config class
|
|
82
|
-
|
|
83
|
-
Returns:
|
|
84
|
-
The compiled Config class, or None if compilation fails
|
|
85
|
-
"""
|
|
86
|
-
# Create a namespace with required imports
|
|
87
|
-
namespace: dict[str, Any] = {
|
|
88
|
-
"BaseModel": BaseModel,
|
|
89
|
-
"Field": Field,
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
try:
|
|
93
|
-
exec(config_source, namespace)
|
|
94
|
-
config_class = namespace.get("Config")
|
|
95
|
-
if config_class and isinstance(config_class, type) and issubclass(
|
|
96
|
-
config_class, BaseModel
|
|
97
|
-
):
|
|
98
|
-
return cast("type[BaseModel]", config_class)
|
|
99
|
-
except Exception:
|
|
100
|
-
pass
|
|
101
|
-
|
|
102
|
-
return None
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def get_pack_config_raw(pack: str) -> dict[str, Any]:
|
|
106
|
-
"""Get raw config dict for a pack from loaded configuration.
|
|
107
|
-
|
|
108
|
-
Args:
|
|
109
|
-
pack: Pack name (e.g., "brave", "ground")
|
|
110
|
-
|
|
111
|
-
Returns:
|
|
112
|
-
Raw config dict for the pack, or empty dict if not configured
|
|
113
|
-
"""
|
|
114
|
-
from ot.config.loader import get_config
|
|
115
|
-
|
|
116
|
-
config = get_config()
|
|
117
|
-
|
|
118
|
-
# Try to get from tools section as raw dict
|
|
119
|
-
tools_dict = config.model_dump().get("tools", {})
|
|
120
|
-
result: dict[str, Any] = tools_dict.get(pack, {})
|
|
121
|
-
return result
|
ot/config/mcp.py
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
"""MCP server configuration for OneTool proxy.
|
|
2
|
-
|
|
3
|
-
Defines configuration for connecting to external MCP servers that are
|
|
4
|
-
proxied through OneTool's single `run` tool.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import re
|
|
10
|
-
from typing import Literal
|
|
11
|
-
|
|
12
|
-
from pydantic import BaseModel, Field, field_validator
|
|
13
|
-
|
|
14
|
-
# Use canonical early secrets loader from secrets.py (eliminates duplication)
|
|
15
|
-
from ot.config.secrets import get_early_secret
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def expand_secrets(value: str) -> str:
|
|
19
|
-
"""Expand ${VAR} patterns using secrets.yaml ONLY.
|
|
20
|
-
|
|
21
|
-
Use this for configuration values that MUST be in secrets.yaml.
|
|
22
|
-
This enforces that sensitive values are stored in the gitignored secrets file,
|
|
23
|
-
not in environment variables that might leak into logs or process lists.
|
|
24
|
-
|
|
25
|
-
Supports ${VAR_NAME} and ${VAR_NAME:-default} syntax.
|
|
26
|
-
|
|
27
|
-
When to use:
|
|
28
|
-
- Config file values (URLs, API keys, database connections)
|
|
29
|
-
- Anywhere secrets should be explicit and fail loudly if missing
|
|
30
|
-
|
|
31
|
-
When NOT to use:
|
|
32
|
-
- Subprocess environment pass-through (use expand_subprocess_env instead)
|
|
33
|
-
|
|
34
|
-
Args:
|
|
35
|
-
value: String potentially containing ${VAR} patterns.
|
|
36
|
-
|
|
37
|
-
Returns:
|
|
38
|
-
String with variables expanded from secrets.
|
|
39
|
-
|
|
40
|
-
Raises:
|
|
41
|
-
ValueError: If variable not found in secrets and no default provided.
|
|
42
|
-
"""
|
|
43
|
-
pattern = re.compile(r"\$\{([^}:]+)(?::-([^}]*))?\}")
|
|
44
|
-
missing_vars: list[str] = []
|
|
45
|
-
|
|
46
|
-
def replace(match: re.Match[str]) -> str:
|
|
47
|
-
var_name = match.group(1)
|
|
48
|
-
default_value = match.group(2)
|
|
49
|
-
# Read from secrets only - no os.environ
|
|
50
|
-
secret_value = get_early_secret(var_name)
|
|
51
|
-
if secret_value is not None:
|
|
52
|
-
return secret_value
|
|
53
|
-
if default_value is not None:
|
|
54
|
-
return default_value
|
|
55
|
-
missing_vars.append(var_name)
|
|
56
|
-
return match.group(0)
|
|
57
|
-
|
|
58
|
-
result = pattern.sub(replace, value)
|
|
59
|
-
|
|
60
|
-
if missing_vars:
|
|
61
|
-
raise ValueError(
|
|
62
|
-
f"Missing variables in secrets.yaml: {', '.join(missing_vars)}. "
|
|
63
|
-
f"Add them to .onetool/config/secrets.yaml or use ${{VAR:-default}} syntax."
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
return result
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def expand_subprocess_env(value: str) -> str:
|
|
70
|
-
"""Expand ${VAR} for subprocess environment variables.
|
|
71
|
-
|
|
72
|
-
Use this ONLY for subprocess env configuration where pass-through is needed.
|
|
73
|
-
Searches: secrets.yaml first, then os.environ. Returns empty string if not found.
|
|
74
|
-
|
|
75
|
-
This is the ONLY place where reading os.environ is allowed. This enables
|
|
76
|
-
explicit pass-through of system environment variables like ${HOME}, ${PATH},
|
|
77
|
-
or ${USER} to subprocesses without requiring them to be in secrets.yaml.
|
|
78
|
-
|
|
79
|
-
When to use:
|
|
80
|
-
- MCP server 'env' configuration (subprocess environment)
|
|
81
|
-
- Any subprocess that needs access to system env vars
|
|
82
|
-
|
|
83
|
-
When NOT to use:
|
|
84
|
-
- Config file values (use expand_secrets instead - it enforces secrets.yaml)
|
|
85
|
-
- Anything that should fail if the secret is missing
|
|
86
|
-
|
|
87
|
-
Args:
|
|
88
|
-
value: String potentially containing ${VAR} patterns.
|
|
89
|
-
|
|
90
|
-
Returns:
|
|
91
|
-
String with variables expanded. Empty string if not found (silent failure).
|
|
92
|
-
"""
|
|
93
|
-
import os
|
|
94
|
-
|
|
95
|
-
pattern = re.compile(r"\$\{([^}:]+)(?::-([^}]*))?\}")
|
|
96
|
-
|
|
97
|
-
def replace(match: re.Match[str]) -> str:
|
|
98
|
-
var_name = match.group(1)
|
|
99
|
-
default_value = match.group(2)
|
|
100
|
-
# Secrets first
|
|
101
|
-
secret_value = get_early_secret(var_name)
|
|
102
|
-
if secret_value is not None:
|
|
103
|
-
return secret_value
|
|
104
|
-
# Then os.environ (for pass-through like ${HOME})
|
|
105
|
-
env_val = os.environ.get(var_name)
|
|
106
|
-
if env_val is not None:
|
|
107
|
-
return env_val
|
|
108
|
-
# Use default if provided
|
|
109
|
-
if default_value is not None:
|
|
110
|
-
return default_value
|
|
111
|
-
# Empty string if not found
|
|
112
|
-
return ""
|
|
113
|
-
|
|
114
|
-
return pattern.sub(replace, value)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
class McpServerConfig(BaseModel):
|
|
118
|
-
"""Configuration for an MCP server connection.
|
|
119
|
-
|
|
120
|
-
Compatible with bench ServerConfig format, with additional
|
|
121
|
-
`enabled` field for toggling servers without removing config.
|
|
122
|
-
"""
|
|
123
|
-
|
|
124
|
-
type: Literal["http", "stdio"] = Field(description="Server connection type")
|
|
125
|
-
enabled: bool = Field(default=True, description="Whether this server is enabled")
|
|
126
|
-
url: str | None = Field(default=None, description="URL for HTTP servers")
|
|
127
|
-
headers: dict[str, str] = Field(
|
|
128
|
-
default_factory=dict, description="Headers for HTTP servers"
|
|
129
|
-
)
|
|
130
|
-
command: str | None = Field(default=None, description="Command for stdio servers")
|
|
131
|
-
args: list[str] = Field(
|
|
132
|
-
default_factory=list, description="Arguments for stdio command"
|
|
133
|
-
)
|
|
134
|
-
env: dict[str, str] = Field(
|
|
135
|
-
default_factory=dict, description="Environment variables for stdio servers"
|
|
136
|
-
)
|
|
137
|
-
timeout: int = Field(default=30, description="Connection timeout in seconds")
|
|
138
|
-
instructions: str | None = Field(
|
|
139
|
-
default=None,
|
|
140
|
-
description="Agent instructions for using this server's tools (surfaced in MCP instructions)",
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
@field_validator("url", "command", mode="before")
|
|
144
|
-
@classmethod
|
|
145
|
-
def expand_secrets_validator(cls, v: str | None) -> str | None:
|
|
146
|
-
"""Expand ${VAR} from secrets.yaml in URL and command."""
|
|
147
|
-
if v is None:
|
|
148
|
-
return None
|
|
149
|
-
return expand_secrets(v)
|