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.
Files changed (34) hide show
  1. onetool/cli.py +2 -0
  2. {onetool_mcp-1.0.0rc2.dist-info → onetool_mcp-1.0.0rc3.dist-info}/METADATA +26 -33
  3. {onetool_mcp-1.0.0rc2.dist-info → onetool_mcp-1.0.0rc3.dist-info}/RECORD +31 -33
  4. ot/config/__init__.py +90 -48
  5. ot/config/global_templates/__init__.py +2 -2
  6. ot/config/global_templates/diagram-templates/api-flow.mmd +33 -33
  7. ot/config/global_templates/diagram-templates/c4-context.puml +30 -30
  8. ot/config/global_templates/diagram-templates/class-diagram.mmd +87 -87
  9. ot/config/global_templates/diagram-templates/feature-mindmap.mmd +70 -70
  10. ot/config/global_templates/diagram-templates/microservices.d2 +81 -81
  11. ot/config/global_templates/diagram-templates/project-gantt.mmd +37 -37
  12. ot/config/global_templates/diagram-templates/state-machine.mmd +42 -42
  13. ot/config/global_templates/diagram.yaml +167 -167
  14. ot/config/global_templates/onetool.yaml +2 -0
  15. ot/config/global_templates/prompts.yaml +102 -102
  16. ot/config/global_templates/security.yaml +1 -4
  17. ot/config/global_templates/servers.yaml +1 -1
  18. ot/config/global_templates/tool_templates/__init__.py +7 -7
  19. ot/config/loader.py +226 -869
  20. ot/config/models.py +735 -0
  21. ot/config/secrets.py +243 -192
  22. ot/executor/tool_loader.py +10 -1
  23. ot/executor/validator.py +11 -1
  24. ot/meta.py +338 -33
  25. ot/prompts.py +228 -218
  26. ot/proxy/manager.py +168 -8
  27. ot/registry/__init__.py +199 -189
  28. ot/config/dynamic.py +0 -121
  29. ot/config/mcp.py +0 -149
  30. ot/config/tool_config.py +0 -125
  31. {onetool_mcp-1.0.0rc2.dist-info → onetool_mcp-1.0.0rc3.dist-info}/WHEEL +0 -0
  32. {onetool_mcp-1.0.0rc2.dist-info → onetool_mcp-1.0.0rc3.dist-info}/entry_points.txt +0 -0
  33. {onetool_mcp-1.0.0rc2.dist-info → onetool_mcp-1.0.0rc3.dist-info}/licenses/LICENSE.txt +0 -0
  34. {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 list_tools() -> str:
170
- """List all registered tools.
171
-
172
- Returns:
173
- Summary of all registered tools.
174
- """
175
- registry = get_registry(rescan=True)
176
- return registry.format_summary()
177
-
178
-
179
- def describe_tool(name: str) -> str:
180
- """Describe a specific tool.
181
-
182
- Args:
183
- name: Tool function name.
184
-
185
- Returns:
186
- Detailed tool description.
187
- """
188
- registry = get_registry()
189
- return registry.describe_tool(name)
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)