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/prompts.py CHANGED
@@ -1,218 +1,218 @@
1
- """Prompts loader for externalized MCP server instructions.
2
-
3
- Loads prompts from prompts.yaml. File must exist and contain instructions.
4
- """
5
-
6
- from __future__ import annotations
7
-
8
- from pathlib import Path
9
- from typing import Any
10
-
11
- import yaml
12
- from loguru import logger
13
- from pydantic import BaseModel, Field
14
-
15
-
16
- class ToolPrompt(BaseModel):
17
- """Prompt configuration for a specific tool."""
18
-
19
- description: str | None = Field(
20
- default=None, description="Override tool description"
21
- )
22
- examples: list[str] = Field(default_factory=list, description="Usage examples")
23
-
24
-
25
- class PromptsConfig(BaseModel):
26
- """Configuration for MCP server prompts and tool descriptions."""
27
-
28
- instructions: str = Field(
29
- description="Main server instructions shown to the LLM",
30
- )
31
- tools: dict[str, ToolPrompt] = Field(
32
- default_factory=dict,
33
- description="Per-tool prompt overrides",
34
- )
35
- templates: dict[str, str] = Field(
36
- default_factory=dict,
37
- description="Reusable prompt templates with {variable} placeholders",
38
- )
39
- packs: dict[str, str] = Field(
40
- default_factory=dict,
41
- description="Per-pack instructions (e.g., excel, github)",
42
- )
43
-
44
-
45
- class PromptsError(Exception):
46
- """Error loading prompts configuration."""
47
-
48
-
49
- def _get_bundled_prompts_path() -> Path:
50
- """Get path to bundled default prompts.yaml."""
51
- return Path(__file__).parent / "config" / "defaults" / "prompts.yaml"
52
-
53
-
54
- def load_prompts(prompts_path: Path | str | None = None) -> PromptsConfig:
55
- """Load prompts configuration from YAML file.
56
-
57
- Args:
58
- prompts_path: Path to prompts file. Falls back to bundled default.
59
-
60
- Returns:
61
- PromptsConfig with loaded prompts.
62
-
63
- Raises:
64
- PromptsError: If file is invalid or has no instructions.
65
- """
66
- if prompts_path is not None:
67
- prompts_path = Path(prompts_path)
68
- if not prompts_path.exists():
69
- raise PromptsError(f"Prompts file not found: {prompts_path}")
70
- else:
71
- # Try config/prompts.yaml, fall back to bundled default
72
- prompts_path = Path("config/prompts.yaml")
73
- if not prompts_path.exists():
74
- prompts_path = _get_bundled_prompts_path()
75
-
76
- logger.debug(f"Loading prompts from {prompts_path}")
77
-
78
- try:
79
- with prompts_path.open() as f:
80
- raw_data = yaml.safe_load(f)
81
- except yaml.YAMLError as e:
82
- raise PromptsError(f"Invalid YAML in {prompts_path}: {e}") from e
83
- except OSError as e:
84
- raise PromptsError(f"Error reading {prompts_path}: {e}") from e
85
-
86
- if raw_data is None or not isinstance(raw_data, dict):
87
- raise PromptsError(f"Empty or invalid prompts file: {prompts_path}")
88
-
89
- # Handle nested 'prompts:' key (used in bundled default)
90
- if "prompts" in raw_data and isinstance(raw_data["prompts"], dict):
91
- raw_data = raw_data["prompts"]
92
-
93
- if "instructions" not in raw_data or not raw_data["instructions"]:
94
- raise PromptsError(f"Missing 'instructions' in {prompts_path}")
95
-
96
- try:
97
- return PromptsConfig.model_validate(raw_data)
98
- except Exception as e:
99
- raise PromptsError(f"Invalid prompts configuration: {e}") from e
100
-
101
-
102
- def render_template(
103
- config: PromptsConfig, template_name: str, **kwargs: Any
104
- ) -> str | None:
105
- """Render a prompt template with variable substitution.
106
-
107
- Args:
108
- config: PromptsConfig with templates
109
- template_name: Name of the template to render
110
- **kwargs: Variables to substitute in the template
111
-
112
- Returns:
113
- Rendered template string, or None if template not found.
114
- """
115
- template = config.templates.get(template_name)
116
- if template is None:
117
- return None
118
-
119
- try:
120
- return template.format(**kwargs)
121
- except KeyError as e:
122
- logger.warning(f"Missing template variable: {e}")
123
- return None
124
-
125
-
126
- def get_tool_description(
127
- config: PromptsConfig, tool_name: str, default: str = ""
128
- ) -> str:
129
- """Get tool description from prompts config with fallback to docstring.
130
-
131
- Args:
132
- config: PromptsConfig with tool prompts
133
- tool_name: Name of the tool
134
- default: Default description if not in config (typically from docstring)
135
-
136
- Returns:
137
- Tool description string.
138
- """
139
- tool_prompt = config.tools.get(tool_name)
140
- if tool_prompt and tool_prompt.description:
141
- return tool_prompt.description
142
- return default
143
-
144
-
145
- def get_tool_examples(config: PromptsConfig, tool_name: str) -> list[str]:
146
- """Get usage examples for a tool.
147
-
148
- Args:
149
- config: PromptsConfig with tool prompts
150
- tool_name: Name of the tool
151
-
152
- Returns:
153
- List of example strings.
154
- """
155
- tool_prompt = config.tools.get(tool_name)
156
- if tool_prompt:
157
- return tool_prompt.examples
158
- return []
159
-
160
-
161
- def get_pack_instructions(config: PromptsConfig, pack: str) -> str | None:
162
- """Get instructions for a pack from prompts config.
163
-
164
- Args:
165
- config: PromptsConfig with pack instructions
166
- pack: Name of the pack (e.g., "excel", "github")
167
-
168
- Returns:
169
- Pack instructions string, or None if not configured.
170
- """
171
- return config.packs.get(pack)
172
-
173
-
174
- # Global prompts instance
175
- _prompts: PromptsConfig | None = None
176
-
177
-
178
- def get_prompts(
179
- prompts_path: Path | str | None = None,
180
- inline_prompts: dict[str, Any] | None = None,
181
- reload: bool = False,
182
- ) -> PromptsConfig:
183
- """Get or load the global prompts configuration.
184
-
185
- Prompts are loaded with the following priority:
186
- 1. Inline prompts (if provided)
187
- 2. prompts_file (from config or explicit path)
188
-
189
- Args:
190
- prompts_path: Path to prompts file (only used on first load)
191
- inline_prompts: Inline prompts dict from config (overrides file)
192
- reload: Force reload configuration
193
-
194
- Returns:
195
- PromptsConfig instance
196
-
197
- Raises:
198
- PromptsError: If prompts cannot be loaded.
199
- """
200
- global _prompts
201
-
202
- if _prompts is None or reload:
203
- if inline_prompts is not None:
204
- # Use inline prompts from config
205
- if (
206
- "instructions" not in inline_prompts
207
- or not inline_prompts["instructions"]
208
- ):
209
- raise PromptsError("Missing 'instructions' in inline prompts")
210
- try:
211
- _prompts = PromptsConfig.model_validate(inline_prompts)
212
- logger.debug("Using inline prompts from config")
213
- except Exception as e:
214
- raise PromptsError(f"Invalid inline prompts: {e}") from e
215
- else:
216
- _prompts = load_prompts(prompts_path)
217
-
218
- return _prompts
1
+ """Prompts loader for externalized MCP server instructions.
2
+
3
+ Loads prompts from prompts.yaml. File must exist and contain instructions.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import yaml
12
+ from loguru import logger
13
+ from pydantic import BaseModel, Field
14
+
15
+
16
+ class ToolPrompt(BaseModel):
17
+ """Prompt configuration for a specific tool."""
18
+
19
+ description: str | None = Field(
20
+ default=None, description="Override tool description"
21
+ )
22
+ examples: list[str] = Field(default_factory=list, description="Usage examples")
23
+
24
+
25
+ class PromptsConfig(BaseModel):
26
+ """Configuration for MCP server prompts and tool descriptions."""
27
+
28
+ instructions: str = Field(
29
+ description="Main server instructions shown to the LLM",
30
+ )
31
+ tools: dict[str, ToolPrompt] = Field(
32
+ default_factory=dict,
33
+ description="Per-tool prompt overrides",
34
+ )
35
+ templates: dict[str, str] = Field(
36
+ default_factory=dict,
37
+ description="Reusable prompt templates with {variable} placeholders",
38
+ )
39
+ packs: dict[str, str] = Field(
40
+ default_factory=dict,
41
+ description="Per-pack instructions (e.g., excel, github)",
42
+ )
43
+
44
+
45
+ class PromptsError(Exception):
46
+ """Error loading prompts configuration."""
47
+
48
+
49
+ def _get_template_prompts_path() -> Path:
50
+ """Get path to prompts.yaml in global_templates (for development/testing)."""
51
+ return Path(__file__).parent / "config" / "global_templates" / "prompts.yaml"
52
+
53
+
54
+ def load_prompts(prompts_path: Path | str | None = None) -> PromptsConfig:
55
+ """Load prompts configuration from YAML file.
56
+
57
+ Args:
58
+ prompts_path: Path to prompts file. Falls back to global_templates for development.
59
+
60
+ Returns:
61
+ PromptsConfig with loaded prompts.
62
+
63
+ Raises:
64
+ PromptsError: If file is invalid or has no instructions.
65
+ """
66
+ if prompts_path is not None:
67
+ prompts_path = Path(prompts_path)
68
+ if not prompts_path.exists():
69
+ raise PromptsError(f"Prompts file not found: {prompts_path}")
70
+ else:
71
+ # Try config/prompts.yaml, fall back to global_templates for development
72
+ prompts_path = Path("config/prompts.yaml")
73
+ if not prompts_path.exists():
74
+ prompts_path = _get_template_prompts_path()
75
+
76
+ logger.debug(f"Loading prompts from {prompts_path}")
77
+
78
+ try:
79
+ with prompts_path.open() as f:
80
+ raw_data = yaml.safe_load(f)
81
+ except yaml.YAMLError as e:
82
+ raise PromptsError(f"Invalid YAML in {prompts_path}: {e}") from e
83
+ except OSError as e:
84
+ raise PromptsError(f"Error reading {prompts_path}: {e}") from e
85
+
86
+ if raw_data is None or not isinstance(raw_data, dict):
87
+ raise PromptsError(f"Empty or invalid prompts file: {prompts_path}")
88
+
89
+ # Handle nested 'prompts:' key (used in template files)
90
+ if "prompts" in raw_data and isinstance(raw_data["prompts"], dict):
91
+ raw_data = raw_data["prompts"]
92
+
93
+ if "instructions" not in raw_data or not raw_data["instructions"]:
94
+ raise PromptsError(f"Missing 'instructions' in {prompts_path}")
95
+
96
+ try:
97
+ return PromptsConfig.model_validate(raw_data)
98
+ except Exception as e:
99
+ raise PromptsError(f"Invalid prompts configuration: {e}") from e
100
+
101
+
102
+ def render_template(
103
+ config: PromptsConfig, template_name: str, **kwargs: Any
104
+ ) -> str | None:
105
+ """Render a prompt template with variable substitution.
106
+
107
+ Args:
108
+ config: PromptsConfig with templates
109
+ template_name: Name of the template to render
110
+ **kwargs: Variables to substitute in the template
111
+
112
+ Returns:
113
+ Rendered template string, or None if template not found.
114
+ """
115
+ template = config.templates.get(template_name)
116
+ if template is None:
117
+ return None
118
+
119
+ try:
120
+ return template.format(**kwargs)
121
+ except KeyError as e:
122
+ logger.warning(f"Missing template variable: {e}")
123
+ return None
124
+
125
+
126
+ def get_tool_description(
127
+ config: PromptsConfig, tool_name: str, default: str = ""
128
+ ) -> str:
129
+ """Get tool description from prompts config with fallback to docstring.
130
+
131
+ Args:
132
+ config: PromptsConfig with tool prompts
133
+ tool_name: Name of the tool
134
+ default: Default description if not in config (typically from docstring)
135
+
136
+ Returns:
137
+ Tool description string.
138
+ """
139
+ tool_prompt = config.tools.get(tool_name)
140
+ if tool_prompt and tool_prompt.description:
141
+ return tool_prompt.description
142
+ return default
143
+
144
+
145
+ def get_tool_examples(config: PromptsConfig, tool_name: str) -> list[str]:
146
+ """Get usage examples for a tool.
147
+
148
+ Args:
149
+ config: PromptsConfig with tool prompts
150
+ tool_name: Name of the tool
151
+
152
+ Returns:
153
+ List of example strings.
154
+ """
155
+ tool_prompt = config.tools.get(tool_name)
156
+ if tool_prompt:
157
+ return tool_prompt.examples
158
+ return []
159
+
160
+
161
+ def get_pack_instructions(config: PromptsConfig, pack: str) -> str | None:
162
+ """Get instructions for a pack from prompts config.
163
+
164
+ Args:
165
+ config: PromptsConfig with pack instructions
166
+ pack: Name of the pack (e.g., "excel", "github")
167
+
168
+ Returns:
169
+ Pack instructions string, or None if not configured.
170
+ """
171
+ return config.packs.get(pack)
172
+
173
+
174
+ # Global prompts instance
175
+ _prompts: PromptsConfig | None = None
176
+
177
+
178
+ def get_prompts(
179
+ prompts_path: Path | str | None = None,
180
+ inline_prompts: dict[str, Any] | None = None,
181
+ reload: bool = False,
182
+ ) -> PromptsConfig:
183
+ """Get or load the global prompts configuration.
184
+
185
+ Prompts are loaded with the following priority:
186
+ 1. Inline prompts (if provided)
187
+ 2. prompts_file (from config or explicit path)
188
+
189
+ Args:
190
+ prompts_path: Path to prompts file (only used on first load)
191
+ inline_prompts: Inline prompts dict from config (overrides file)
192
+ reload: Force reload configuration
193
+
194
+ Returns:
195
+ PromptsConfig instance
196
+
197
+ Raises:
198
+ PromptsError: If prompts cannot be loaded.
199
+ """
200
+ global _prompts
201
+
202
+ if _prompts is None or reload:
203
+ if inline_prompts is not None:
204
+ # Use inline prompts from config
205
+ if (
206
+ "instructions" not in inline_prompts
207
+ or not inline_prompts["instructions"]
208
+ ):
209
+ raise PromptsError("Missing 'instructions' in inline prompts")
210
+ try:
211
+ _prompts = PromptsConfig.model_validate(inline_prompts)
212
+ logger.debug("Using inline prompts from config")
213
+ except Exception as e:
214
+ raise PromptsError(f"Invalid inline prompts: {e}") from e
215
+ else:
216
+ _prompts = load_prompts(prompts_path)
217
+
218
+ return _prompts
ot/proxy/manager.py CHANGED
@@ -42,6 +42,7 @@ class ProxyManager:
42
42
  """Initialize the proxy manager."""
43
43
  self._clients: dict[str, Client] = {} # type: ignore[type-arg]
44
44
  self._tools_by_server: dict[str, list[types.Tool]] = {}
45
+ self._errors: dict[str, str] = {} # server name -> last error message
45
46
  self._initialized = False
46
47
  self._loop: asyncio.AbstractEventLoop | None = None
47
48
 
@@ -59,6 +60,10 @@ class ProxyManager:
59
60
  """Get a client by server name."""
60
61
  return self._clients.get(server)
61
62
 
63
+ def get_error(self, server: str) -> str | None:
64
+ """Get the last connection error for a server."""
65
+ return self._errors.get(server)
66
+
62
67
  def list_tools(self, server: str | None = None) -> list[ProxyToolInfo]:
63
68
  """List available tools from proxied servers.
64
69
 
@@ -211,8 +216,10 @@ class ProxyManager:
211
216
  try:
212
217
  await self._connect_server(name, config)
213
218
  connected += 1
219
+ self._errors.pop(name, None) # Clear any previous error
214
220
  except Exception as e:
215
221
  failed += 1
222
+ self._errors[name] = str(e)
216
223
  logger.warning(f"Failed to connect to MCP server '{name}': {e}")
217
224
 
218
225
  span.add("connected", connected)
@@ -309,6 +316,7 @@ class ProxyManager:
309
316
 
310
317
  self._clients.clear()
311
318
  self._tools_by_server.clear()
319
+ self._errors.clear()
312
320
  self._initialized = False
313
321
 
314
322
  async def reconnect(self, configs: dict[str, McpServerConfig]) -> None:
@@ -337,10 +345,13 @@ class ProxyManager:
337
345
  with contextlib.suppress(RuntimeError):
338
346
  loop = asyncio.get_running_loop()
339
347
 
340
- if loop is None:
341
- # No event loop available - just reset state, connect will happen on next use
348
+ # Must have a running loop to schedule the coroutine
349
+ # If loop exists but isn't running, we can't await the coroutine
350
+ if loop is None or not loop.is_running():
351
+ # No running event loop available - just reset state, connect will happen on next use
342
352
  self._clients.clear()
343
353
  self._tools_by_server.clear()
354
+ self._errors.clear()
344
355
  self._initialized = False
345
356
  return
346
357
 
@@ -393,4 +404,5 @@ def reconnect_proxy_manager() -> None:
393
404
  # No servers configured - just reset state
394
405
  proxy._clients.clear()
395
406
  proxy._tools_by_server.clear()
407
+ proxy._errors.clear()
396
408
  proxy._initialized = False