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.
- onetool/cli.py +63 -4
- onetool_mcp-1.0.0rc2.dist-info/METADATA +266 -0
- onetool_mcp-1.0.0rc2.dist-info/RECORD +129 -0
- {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/licenses/LICENSE.txt +1 -1
- {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/licenses/NOTICE.txt +54 -64
- ot/__main__.py +6 -6
- ot/config/__init__.py +48 -46
- ot/config/global_templates/__init__.py +2 -2
- ot/config/{defaults → global_templates}/diagram-templates/api-flow.mmd +33 -33
- ot/config/{defaults → global_templates}/diagram-templates/c4-context.puml +30 -30
- ot/config/{defaults → global_templates}/diagram-templates/class-diagram.mmd +87 -87
- ot/config/{defaults → global_templates}/diagram-templates/feature-mindmap.mmd +70 -70
- ot/config/{defaults → global_templates}/diagram-templates/microservices.d2 +81 -81
- ot/config/{defaults → global_templates}/diagram-templates/project-gantt.mmd +37 -37
- ot/config/{defaults → global_templates}/diagram-templates/state-machine.mmd +42 -42
- ot/config/global_templates/diagram.yaml +167 -0
- ot/config/global_templates/onetool.yaml +3 -1
- ot/config/{defaults → global_templates}/prompts.yaml +102 -97
- ot/config/global_templates/security.yaml +31 -0
- ot/config/global_templates/servers.yaml +93 -12
- ot/config/global_templates/snippets.yaml +5 -26
- ot/config/{defaults → global_templates}/tool_templates/__init__.py +7 -7
- ot/config/loader.py +221 -105
- ot/config/mcp.py +5 -1
- ot/config/secrets.py +192 -190
- ot/decorators.py +116 -116
- ot/executor/__init__.py +35 -35
- ot/executor/base.py +16 -16
- ot/executor/fence_processor.py +83 -83
- ot/executor/linter.py +142 -142
- ot/executor/pep723.py +288 -288
- ot/executor/runner.py +20 -6
- ot/executor/simple.py +163 -163
- ot/executor/validator.py +603 -164
- ot/http_client.py +145 -145
- ot/logging/__init__.py +37 -37
- ot/logging/entry.py +213 -213
- ot/logging/format.py +191 -188
- ot/logging/span.py +349 -349
- ot/meta.py +236 -14
- ot/paths.py +32 -49
- ot/prompts.py +218 -218
- ot/proxy/manager.py +14 -2
- ot/registry/__init__.py +189 -189
- ot/registry/parser.py +269 -269
- ot/server.py +330 -315
- ot/shortcuts/__init__.py +15 -15
- ot/shortcuts/aliases.py +87 -87
- ot/shortcuts/snippets.py +258 -258
- ot/stats/__init__.py +35 -35
- ot/stats/html.py +2 -2
- ot/stats/reader.py +354 -354
- ot/stats/timing.py +57 -57
- ot/support.py +63 -63
- ot/tools.py +1 -1
- ot/utils/batch.py +161 -161
- ot/utils/cache.py +120 -120
- ot/utils/exceptions.py +23 -23
- ot/utils/factory.py +178 -179
- ot/utils/format.py +65 -65
- ot/utils/http.py +202 -202
- ot/utils/platform.py +45 -45
- ot/utils/truncate.py +69 -69
- ot_tools/__init__.py +4 -4
- ot_tools/_convert/__init__.py +12 -12
- ot_tools/_convert/pdf.py +254 -254
- ot_tools/diagram.yaml +167 -167
- ot_tools/scaffold.py +2 -2
- ot_tools/transform.py +124 -19
- ot_tools/web_fetch.py +94 -43
- onetool_mcp-1.0.0b1.dist-info/METADATA +0 -163
- onetool_mcp-1.0.0b1.dist-info/RECORD +0 -132
- ot/config/defaults/bench.yaml +0 -4
- ot/config/defaults/onetool.yaml +0 -25
- ot/config/defaults/servers.yaml +0 -7
- ot/config/defaults/snippets.yaml +0 -4
- ot_tools/firecrawl.py +0 -732
- {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/WHEEL +0 -0
- {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/entry_points.txt +0 -0
- /ot/config/{defaults → global_templates}/tool_templates/extension.py +0 -0
- /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
|
|
50
|
-
"""Get path to
|
|
51
|
-
return Path(__file__).parent / "config" / "
|
|
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
|
|
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
|
|
72
|
-
prompts_path = Path("config/prompts.yaml")
|
|
73
|
-
if not prompts_path.exists():
|
|
74
|
-
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
|
|
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
|
-
|
|
341
|
-
|
|
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
|