onetool-mcp 1.0.0b1__py3-none-any.whl → 1.0.0rc2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. onetool/cli.py +63 -4
  2. onetool_mcp-1.0.0rc2.dist-info/METADATA +266 -0
  3. onetool_mcp-1.0.0rc2.dist-info/RECORD +129 -0
  4. {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/licenses/LICENSE.txt +1 -1
  5. {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/licenses/NOTICE.txt +54 -64
  6. ot/__main__.py +6 -6
  7. ot/config/__init__.py +48 -46
  8. ot/config/global_templates/__init__.py +2 -2
  9. ot/config/{defaults → global_templates}/diagram-templates/api-flow.mmd +33 -33
  10. ot/config/{defaults → global_templates}/diagram-templates/c4-context.puml +30 -30
  11. ot/config/{defaults → global_templates}/diagram-templates/class-diagram.mmd +87 -87
  12. ot/config/{defaults → global_templates}/diagram-templates/feature-mindmap.mmd +70 -70
  13. ot/config/{defaults → global_templates}/diagram-templates/microservices.d2 +81 -81
  14. ot/config/{defaults → global_templates}/diagram-templates/project-gantt.mmd +37 -37
  15. ot/config/{defaults → global_templates}/diagram-templates/state-machine.mmd +42 -42
  16. ot/config/global_templates/diagram.yaml +167 -0
  17. ot/config/global_templates/onetool.yaml +3 -1
  18. ot/config/{defaults → global_templates}/prompts.yaml +102 -97
  19. ot/config/global_templates/security.yaml +31 -0
  20. ot/config/global_templates/servers.yaml +93 -12
  21. ot/config/global_templates/snippets.yaml +5 -26
  22. ot/config/{defaults → global_templates}/tool_templates/__init__.py +7 -7
  23. ot/config/loader.py +221 -105
  24. ot/config/mcp.py +5 -1
  25. ot/config/secrets.py +192 -190
  26. ot/decorators.py +116 -116
  27. ot/executor/__init__.py +35 -35
  28. ot/executor/base.py +16 -16
  29. ot/executor/fence_processor.py +83 -83
  30. ot/executor/linter.py +142 -142
  31. ot/executor/pep723.py +288 -288
  32. ot/executor/runner.py +20 -6
  33. ot/executor/simple.py +163 -163
  34. ot/executor/validator.py +603 -164
  35. ot/http_client.py +145 -145
  36. ot/logging/__init__.py +37 -37
  37. ot/logging/entry.py +213 -213
  38. ot/logging/format.py +191 -188
  39. ot/logging/span.py +349 -349
  40. ot/meta.py +236 -14
  41. ot/paths.py +32 -49
  42. ot/prompts.py +218 -218
  43. ot/proxy/manager.py +14 -2
  44. ot/registry/__init__.py +189 -189
  45. ot/registry/parser.py +269 -269
  46. ot/server.py +330 -315
  47. ot/shortcuts/__init__.py +15 -15
  48. ot/shortcuts/aliases.py +87 -87
  49. ot/shortcuts/snippets.py +258 -258
  50. ot/stats/__init__.py +35 -35
  51. ot/stats/html.py +2 -2
  52. ot/stats/reader.py +354 -354
  53. ot/stats/timing.py +57 -57
  54. ot/support.py +63 -63
  55. ot/tools.py +1 -1
  56. ot/utils/batch.py +161 -161
  57. ot/utils/cache.py +120 -120
  58. ot/utils/exceptions.py +23 -23
  59. ot/utils/factory.py +178 -179
  60. ot/utils/format.py +65 -65
  61. ot/utils/http.py +202 -202
  62. ot/utils/platform.py +45 -45
  63. ot/utils/truncate.py +69 -69
  64. ot_tools/__init__.py +4 -4
  65. ot_tools/_convert/__init__.py +12 -12
  66. ot_tools/_convert/pdf.py +254 -254
  67. ot_tools/diagram.yaml +167 -167
  68. ot_tools/scaffold.py +2 -2
  69. ot_tools/transform.py +124 -19
  70. ot_tools/web_fetch.py +94 -43
  71. onetool_mcp-1.0.0b1.dist-info/METADATA +0 -163
  72. onetool_mcp-1.0.0b1.dist-info/RECORD +0 -132
  73. ot/config/defaults/bench.yaml +0 -4
  74. ot/config/defaults/onetool.yaml +0 -25
  75. ot/config/defaults/servers.yaml +0 -7
  76. ot/config/defaults/snippets.yaml +0 -4
  77. ot_tools/firecrawl.py +0 -732
  78. {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/WHEEL +0 -0
  79. {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/entry_points.txt +0 -0
  80. /ot/config/{defaults → global_templates}/tool_templates/extension.py +0 -0
  81. /ot/config/{defaults → global_templates}/tool_templates/isolated.py +0 -0
ot/config/secrets.py CHANGED
@@ -1,190 +1,192 @@
1
- """Secrets loading for OneTool.
2
-
3
- Loads secrets from secrets.yaml (gitignored) separate from committed configuration.
4
- Secrets are passed to workers via JSON-RPC, not exposed as environment variables.
5
-
6
- The secrets file path is resolved in order:
7
- 1. Explicit path passed to get_secrets()
8
- 2. Config's secrets_file setting (if config loaded and file exists)
9
- 3. Default locations: project (.onetool/config/secrets.yaml) then global (~/.onetool/config/secrets.yaml)
10
- 4. OT_SECRETS_FILE environment variable
11
-
12
- Example secrets.yaml:
13
-
14
- BRAVE_API_KEY: "your-brave-api-key"
15
- OPENAI_API_KEY: "sk-..."
16
- DATABASE_URL: "postgresql://..."
17
- """
18
-
19
- from __future__ import annotations
20
-
21
- import os
22
- from pathlib import Path
23
-
24
- import yaml
25
- from loguru import logger
26
-
27
- # Single global secrets cache
28
- _secrets: dict[str, str] | None = None
29
-
30
-
31
- def load_secrets(secrets_path: Path | str | None = None) -> dict[str, str]:
32
- """Load secrets from YAML file.
33
-
34
- Args:
35
- secrets_path: Path to secrets file. If None or doesn't exist,
36
- returns empty dict (no secrets).
37
-
38
- Returns:
39
- Dictionary of secret name -> value
40
-
41
- Raises:
42
- ValueError: If YAML is invalid
43
- """
44
- if secrets_path is None:
45
- logger.debug("No secrets path provided")
46
- return {}
47
-
48
- secrets_path = Path(secrets_path)
49
-
50
- if not secrets_path.exists():
51
- logger.debug(f"Secrets file not found: {secrets_path}")
52
- return {}
53
-
54
- logger.debug(f"Loading secrets from {secrets_path}")
55
-
56
- try:
57
- with secrets_path.open() as f:
58
- raw_data = yaml.safe_load(f)
59
- except yaml.YAMLError as e:
60
- raise ValueError(f"Invalid YAML in secrets file {secrets_path}: {e}") from e
61
- except OSError as e:
62
- raise ValueError(f"Error reading secrets file {secrets_path}: {e}") from e
63
-
64
- if raw_data is None:
65
- return {}
66
-
67
- if not isinstance(raw_data, dict):
68
- raise ValueError(
69
- f"Secrets file {secrets_path} must be a YAML mapping, not {type(raw_data).__name__}"
70
- )
71
-
72
- # Values are literal - no env var expansion
73
- secrets: dict[str, str] = {}
74
- for key, value in raw_data.items():
75
- if not isinstance(key, str):
76
- logger.warning(f"Ignoring non-string secret key: {key}")
77
- continue
78
-
79
- if value is None:
80
- continue
81
-
82
- # Store as literal string - no ${VAR} expansion
83
- secrets[key] = str(value)
84
-
85
- logger.info(f"Loaded {len(secrets)} secrets")
86
- return secrets
87
-
88
-
89
- def _load_from_default_locations() -> dict[str, str]:
90
- """Load secrets from default project and global locations.
91
-
92
- Searches in order (first found wins):
93
- 1. Project: {effective_cwd}/.onetool/config/secrets.yaml
94
- 2. Global: ~/.onetool/config/secrets.yaml
95
-
96
- Returns:
97
- Dictionary of secret name -> value (empty if no secrets found)
98
- """
99
- # Import here to avoid circular imports at module level
100
- from ot.paths import CONFIG_SUBDIR, get_effective_cwd, get_global_dir
101
-
102
- # Try project secrets first, then global
103
- paths_to_try = [
104
- get_effective_cwd() / ".onetool" / CONFIG_SUBDIR / "secrets.yaml",
105
- get_global_dir() / CONFIG_SUBDIR / "secrets.yaml",
106
- ]
107
-
108
- for secrets_path in paths_to_try:
109
- if secrets_path.exists():
110
- try:
111
- return load_secrets(secrets_path)
112
- except ValueError as e:
113
- # Silent during bootstrap - don't spam logs
114
- logger.debug(f"Error loading secrets from {secrets_path}: {e}")
115
- continue
116
-
117
- return {}
118
-
119
-
120
- def get_secrets(
121
- secrets_path: Path | str | None = None, reload: bool = False
122
- ) -> dict[str, str]:
123
- """Get or load the cached secrets.
124
-
125
- Resolution order (first match wins):
126
- 1. Explicit secrets_path argument
127
- 2. Config's secrets_file setting (if config loaded and file exists)
128
- 3. Default locations (.onetool/config/secrets.yaml)
129
- 4. OT_SECRETS_FILE environment variable
130
-
131
- Args:
132
- secrets_path: Path to secrets file (only used on first load or reload).
133
- reload: Force reload secrets from disk.
134
-
135
- Returns:
136
- Dictionary of secret name -> value
137
- """
138
- global _secrets
139
-
140
- if _secrets is None or reload:
141
- # Resolution chain: explicit > config (if loaded) > defaults > env var
142
- if secrets_path is None:
143
- # WARNING: Do NOT call get_config() here!
144
- # =========================================
145
- # This function is called during config loading via:
146
- # get_config() load_config() → expand_secrets() → get_early_secret() → get_secrets()
147
- #
148
- # If we call get_config() here, it triggers config loading again → infinite recursion.
149
- # Instead, we check _config directly - if it's None, config is still loading.
150
- try:
151
- import ot.config.loader
152
-
153
- if ot.config.loader._config is not None:
154
- config_path = ot.config.loader._config.get_secrets_file_path()
155
- if config_path.exists():
156
- secrets_path = config_path
157
- except Exception:
158
- pass # Module not loaded yet, fall through
159
-
160
- # Try default locations if still no path
161
- if secrets_path is None:
162
- loaded = _load_from_default_locations()
163
- if loaded:
164
- _secrets = loaded
165
- return _secrets
166
-
167
- # Final fallback to OT_SECRETS_FILE env var
168
- if secrets_path is None:
169
- secrets_path = os.getenv("OT_SECRETS_FILE")
170
-
171
- _secrets = load_secrets(secrets_path)
172
-
173
- return _secrets
174
-
175
-
176
- def get_secret(name: str) -> str | None:
177
- """Get a single secret value by name.
178
-
179
- Args:
180
- name: Secret name (e.g., "BRAVE_API_KEY")
181
-
182
- Returns:
183
- Secret value, or None if not found
184
- """
185
- return get_secrets().get(name)
186
-
187
-
188
- # Alias for backward compatibility and semantic clarity during config loading
189
- # Both functions now use the same unified cache
190
- get_early_secret = get_secret
1
+ """Secrets loading for OneTool.
2
+
3
+ Loads secrets from secrets.yaml (gitignored) separate from committed configuration.
4
+ Secrets are passed to workers via JSON-RPC, not exposed as environment variables.
5
+
6
+ The secrets file path is resolved in order:
7
+ 1. Explicit path passed to get_secrets()
8
+ 2. OT_SECRETS_FILE environment variable
9
+ 3. Config's secrets_file setting (if config loaded and file exists)
10
+ 4. Default locations: project (.onetool/config/secrets.yaml) then global (~/.onetool/config/secrets.yaml)
11
+
12
+ Example secrets.yaml:
13
+
14
+ BRAVE_API_KEY: "your-brave-api-key"
15
+ OPENAI_API_KEY: "sk-..."
16
+ DATABASE_URL: "postgresql://..."
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ from pathlib import Path
23
+
24
+ import yaml
25
+ from loguru import logger
26
+
27
+ # Single global secrets cache
28
+ _secrets: dict[str, str] | None = None
29
+
30
+
31
+ def load_secrets(secrets_path: Path | str | None = None) -> dict[str, str]:
32
+ """Load secrets from YAML file.
33
+
34
+ Args:
35
+ secrets_path: Path to secrets file. If None or doesn't exist,
36
+ returns empty dict (no secrets).
37
+
38
+ Returns:
39
+ Dictionary of secret name -> value
40
+
41
+ Raises:
42
+ ValueError: If YAML is invalid
43
+ """
44
+ if secrets_path is None:
45
+ logger.debug("No secrets path provided")
46
+ return {}
47
+
48
+ secrets_path = Path(secrets_path)
49
+
50
+ if not secrets_path.exists():
51
+ logger.debug(f"Secrets file not found: {secrets_path}")
52
+ return {}
53
+
54
+ logger.debug(f"Loading secrets from {secrets_path}")
55
+
56
+ try:
57
+ with secrets_path.open() as f:
58
+ raw_data = yaml.safe_load(f)
59
+ except yaml.YAMLError as e:
60
+ raise ValueError(f"Invalid YAML in secrets file {secrets_path}: {e}") from e
61
+ except OSError as e:
62
+ raise ValueError(f"Error reading secrets file {secrets_path}: {e}") from e
63
+
64
+ if raw_data is None:
65
+ return {}
66
+
67
+ if not isinstance(raw_data, dict):
68
+ raise ValueError(
69
+ f"Secrets file {secrets_path} must be a YAML mapping, not {type(raw_data).__name__}"
70
+ )
71
+
72
+ # Values are literal - no env var expansion
73
+ secrets: dict[str, str] = {}
74
+ for key, value in raw_data.items():
75
+ if not isinstance(key, str):
76
+ logger.warning(f"Ignoring non-string secret key: {key}")
77
+ continue
78
+
79
+ if value is None:
80
+ continue
81
+
82
+ # Store as literal string - no ${VAR} expansion
83
+ secrets[key] = str(value)
84
+
85
+ logger.info(f"Loaded {len(secrets)} secrets")
86
+ return secrets
87
+
88
+
89
+ def _load_from_default_locations() -> dict[str, str]:
90
+ """Load secrets from default project and global locations.
91
+
92
+ Searches in order (first found wins):
93
+ 1. Project: {effective_cwd}/.onetool/config/secrets.yaml
94
+ 2. Global: ~/.onetool/config/secrets.yaml
95
+
96
+ Returns:
97
+ Dictionary of secret name -> value (empty if no secrets found)
98
+ """
99
+ # Import here to avoid circular imports at module level
100
+ from ot.paths import CONFIG_SUBDIR, get_effective_cwd, get_global_dir
101
+
102
+ # Try project secrets first, then global
103
+ paths_to_try = [
104
+ get_effective_cwd() / ".onetool" / CONFIG_SUBDIR / "secrets.yaml",
105
+ get_global_dir() / CONFIG_SUBDIR / "secrets.yaml",
106
+ ]
107
+
108
+ for secrets_path in paths_to_try:
109
+ if secrets_path.exists():
110
+ try:
111
+ return load_secrets(secrets_path)
112
+ except ValueError as e:
113
+ # Silent during bootstrap - don't spam logs
114
+ logger.debug(f"Error loading secrets from {secrets_path}: {e}")
115
+ continue
116
+
117
+ return {}
118
+
119
+
120
+ def get_secrets(
121
+ secrets_path: Path | str | None = None, reload: bool = False
122
+ ) -> dict[str, str]:
123
+ """Get or load the cached secrets.
124
+
125
+ Resolution order (first match wins):
126
+ 1. Explicit secrets_path argument
127
+ 2. OT_SECRETS_FILE environment variable
128
+ 3. Config's secrets_file setting (if config loaded and file exists)
129
+ 4. Default locations (.onetool/config/secrets.yaml)
130
+
131
+ Args:
132
+ secrets_path: Path to secrets file (only used on first load or reload).
133
+ reload: Force reload secrets from disk.
134
+
135
+ Returns:
136
+ Dictionary of secret name -> value
137
+ """
138
+ global _secrets
139
+
140
+ if _secrets is None or reload:
141
+ # Resolution chain: explicit > env var > config (if loaded) > defaults
142
+ if secrets_path is None:
143
+ # Check OT_SECRETS_FILE env var first (highest priority after explicit path)
144
+ env_path = os.getenv("OT_SECRETS_FILE")
145
+ if env_path:
146
+ secrets_path = env_path
147
+
148
+ if secrets_path is None:
149
+ # WARNING: Do NOT call get_config() here!
150
+ # =========================================
151
+ # This function is called during config loading via:
152
+ # get_config() → load_config() → expand_secrets() → get_early_secret() → get_secrets()
153
+ #
154
+ # If we call get_config() here, it triggers config loading again → infinite recursion.
155
+ # Instead, we check _config directly - if it's None, config is still loading.
156
+ try:
157
+ import ot.config.loader
158
+
159
+ if ot.config.loader._config is not None:
160
+ config_path = ot.config.loader._config.get_secrets_file_path()
161
+ if config_path.exists():
162
+ secrets_path = config_path
163
+ except Exception:
164
+ pass # Module not loaded yet, fall through
165
+
166
+ # Try default locations if still no path
167
+ if secrets_path is None:
168
+ loaded = _load_from_default_locations()
169
+ if loaded:
170
+ _secrets = loaded
171
+ return _secrets
172
+
173
+ _secrets = load_secrets(secrets_path)
174
+
175
+ return _secrets
176
+
177
+
178
+ def get_secret(name: str) -> str | None:
179
+ """Get a single secret value by name.
180
+
181
+ Args:
182
+ name: Secret name (e.g., "BRAVE_API_KEY")
183
+
184
+ Returns:
185
+ Secret value, or None if not found
186
+ """
187
+ return get_secrets().get(name)
188
+
189
+
190
+ # Alias for backward compatibility and semantic clarity during config loading
191
+ # Both functions now use the same unified cache
192
+ get_early_secret = get_secret
ot/decorators.py CHANGED
@@ -1,116 +1,116 @@
1
- """Tool decorators for enhanced metadata.
2
-
3
- The @tool decorator attaches metadata to tool functions for better
4
- LLM comprehension. Plain functions work without decorators.
5
-
6
- Example:
7
- @tool(
8
- description="Search the web using Brave Search",
9
- examples=["search(query='Python news')"],
10
- tags=["search", "web"],
11
- )
12
- def brave_search(query: str, count: int = 10) -> str:
13
- '''Search the web.'''
14
- ...
15
- """
16
-
17
- from __future__ import annotations
18
-
19
- from collections.abc import Callable
20
- from dataclasses import dataclass, field
21
- from typing import Any, TypeVar
22
-
23
- F = TypeVar("F", bound=Callable[..., Any])
24
-
25
-
26
- @dataclass
27
- class ToolMetadata:
28
- """Metadata attached to tool functions by the @tool decorator."""
29
-
30
- description: str | None = None
31
- examples: list[str] = field(default_factory=list)
32
- tags: list[str] = field(default_factory=list)
33
- enabled: bool = True
34
- deprecated: bool = False
35
- deprecated_message: str | None = None
36
-
37
-
38
- # Attribute name used to store metadata on decorated functions
39
- TOOL_METADATA_ATTR = "_tool_metadata"
40
-
41
-
42
- def tool(
43
- description: str | None = None,
44
- examples: list[str] | None = None,
45
- tags: list[str] | None = None,
46
- enabled: bool = True,
47
- deprecated: bool = False,
48
- deprecated_message: str | None = None,
49
- ) -> Callable[[F], F]:
50
- """Decorator to add metadata to a tool function.
51
-
52
- Attaches a ToolMetadata instance to the function for the registry
53
- to extract and use for enhanced descriptions.
54
-
55
- Args:
56
- description: Override the docstring description
57
- examples: List of usage examples
58
- tags: Categorization tags
59
- enabled: Whether the tool is enabled (default True)
60
- deprecated: Mark as deprecated
61
- deprecated_message: Message shown when deprecated
62
-
63
- Returns:
64
- Decorator function
65
-
66
- Example:
67
- @tool(
68
- description="Search the web using Brave Search API",
69
- examples=[
70
- 'brave_search(query="Python news", count=5)',
71
- 'brave_search(query="weather today")',
72
- ],
73
- tags=["search", "web"],
74
- )
75
- def brave_search(query: str, count: int = 10) -> str:
76
- '''Search the web.'''
77
- ...
78
- """
79
-
80
- def decorator(func: F) -> F:
81
- metadata = ToolMetadata(
82
- description=description,
83
- examples=examples or [],
84
- tags=tags or [],
85
- enabled=enabled,
86
- deprecated=deprecated,
87
- deprecated_message=deprecated_message,
88
- )
89
- setattr(func, TOOL_METADATA_ATTR, metadata)
90
- return func
91
-
92
- return decorator
93
-
94
-
95
- def get_tool_metadata(func: Callable[..., Any]) -> ToolMetadata | None:
96
- """Extract ToolMetadata from a function if present.
97
-
98
- Args:
99
- func: Function to check for metadata
100
-
101
- Returns:
102
- ToolMetadata if decorated with @tool, None otherwise
103
- """
104
- return getattr(func, TOOL_METADATA_ATTR, None)
105
-
106
-
107
- def has_tool_metadata(func: Callable[..., Any]) -> bool:
108
- """Check if a function has @tool decorator metadata.
109
-
110
- Args:
111
- func: Function to check
112
-
113
- Returns:
114
- True if function has ToolMetadata
115
- """
116
- return hasattr(func, TOOL_METADATA_ATTR)
1
+ """Tool decorators for enhanced metadata.
2
+
3
+ The @tool decorator attaches metadata to tool functions for better
4
+ LLM comprehension. Plain functions work without decorators.
5
+
6
+ Example:
7
+ @tool(
8
+ description="Search the web using Brave Search",
9
+ examples=["search(query='Python news')"],
10
+ tags=["search", "web"],
11
+ )
12
+ def brave_search(query: str, count: int = 10) -> str:
13
+ '''Search the web.'''
14
+ ...
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from collections.abc import Callable
20
+ from dataclasses import dataclass, field
21
+ from typing import Any, TypeVar
22
+
23
+ F = TypeVar("F", bound=Callable[..., Any])
24
+
25
+
26
+ @dataclass
27
+ class ToolMetadata:
28
+ """Metadata attached to tool functions by the @tool decorator."""
29
+
30
+ description: str | None = None
31
+ examples: list[str] = field(default_factory=list)
32
+ tags: list[str] = field(default_factory=list)
33
+ enabled: bool = True
34
+ deprecated: bool = False
35
+ deprecated_message: str | None = None
36
+
37
+
38
+ # Attribute name used to store metadata on decorated functions
39
+ TOOL_METADATA_ATTR = "_tool_metadata"
40
+
41
+
42
+ def tool(
43
+ description: str | None = None,
44
+ examples: list[str] | None = None,
45
+ tags: list[str] | None = None,
46
+ enabled: bool = True,
47
+ deprecated: bool = False,
48
+ deprecated_message: str | None = None,
49
+ ) -> Callable[[F], F]:
50
+ """Decorator to add metadata to a tool function.
51
+
52
+ Attaches a ToolMetadata instance to the function for the registry
53
+ to extract and use for enhanced descriptions.
54
+
55
+ Args:
56
+ description: Override the docstring description
57
+ examples: List of usage examples
58
+ tags: Categorization tags
59
+ enabled: Whether the tool is enabled (default True)
60
+ deprecated: Mark as deprecated
61
+ deprecated_message: Message shown when deprecated
62
+
63
+ Returns:
64
+ Decorator function
65
+
66
+ Example:
67
+ @tool(
68
+ description="Search the web using Brave Search API",
69
+ examples=[
70
+ 'brave_search(query="Python news", count=5)',
71
+ 'brave_search(query="weather today")',
72
+ ],
73
+ tags=["search", "web"],
74
+ )
75
+ def brave_search(query: str, count: int = 10) -> str:
76
+ '''Search the web.'''
77
+ ...
78
+ """
79
+
80
+ def decorator(func: F) -> F:
81
+ metadata = ToolMetadata(
82
+ description=description,
83
+ examples=examples or [],
84
+ tags=tags or [],
85
+ enabled=enabled,
86
+ deprecated=deprecated,
87
+ deprecated_message=deprecated_message,
88
+ )
89
+ setattr(func, TOOL_METADATA_ATTR, metadata)
90
+ return func
91
+
92
+ return decorator
93
+
94
+
95
+ def get_tool_metadata(func: Callable[..., Any]) -> ToolMetadata | None:
96
+ """Extract ToolMetadata from a function if present.
97
+
98
+ Args:
99
+ func: Function to check for metadata
100
+
101
+ Returns:
102
+ ToolMetadata if decorated with @tool, None otherwise
103
+ """
104
+ return getattr(func, TOOL_METADATA_ATTR, None)
105
+
106
+
107
+ def has_tool_metadata(func: Callable[..., Any]) -> bool:
108
+ """Check if a function has @tool decorator metadata.
109
+
110
+ Args:
111
+ func: Function to check
112
+
113
+ Returns:
114
+ True if function has ToolMetadata
115
+ """
116
+ return hasattr(func, TOOL_METADATA_ATTR)