codemaster-cli 2.2.0__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.
- codemaster_cli-2.2.0.dist-info/METADATA +645 -0
- codemaster_cli-2.2.0.dist-info/RECORD +170 -0
- codemaster_cli-2.2.0.dist-info/WHEEL +4 -0
- codemaster_cli-2.2.0.dist-info/entry_points.txt +3 -0
- vibe/__init__.py +6 -0
- vibe/acp/__init__.py +0 -0
- vibe/acp/acp_agent_loop.py +746 -0
- vibe/acp/entrypoint.py +81 -0
- vibe/acp/tools/__init__.py +0 -0
- vibe/acp/tools/base.py +100 -0
- vibe/acp/tools/builtins/bash.py +134 -0
- vibe/acp/tools/builtins/read_file.py +54 -0
- vibe/acp/tools/builtins/search_replace.py +129 -0
- vibe/acp/tools/builtins/todo.py +65 -0
- vibe/acp/tools/builtins/write_file.py +98 -0
- vibe/acp/tools/session_update.py +118 -0
- vibe/acp/utils.py +213 -0
- vibe/cli/__init__.py +0 -0
- vibe/cli/autocompletion/__init__.py +0 -0
- vibe/cli/autocompletion/base.py +22 -0
- vibe/cli/autocompletion/path_completion.py +177 -0
- vibe/cli/autocompletion/slash_command.py +99 -0
- vibe/cli/cli.py +188 -0
- vibe/cli/clipboard.py +69 -0
- vibe/cli/commands.py +116 -0
- vibe/cli/entrypoint.py +163 -0
- vibe/cli/history_manager.py +91 -0
- vibe/cli/plan_offer/adapters/http_whoami_gateway.py +67 -0
- vibe/cli/plan_offer/decide_plan_offer.py +87 -0
- vibe/cli/plan_offer/ports/whoami_gateway.py +23 -0
- vibe/cli/terminal_setup.py +323 -0
- vibe/cli/textual_ui/__init__.py +0 -0
- vibe/cli/textual_ui/ansi_markdown.py +58 -0
- vibe/cli/textual_ui/app.py +1546 -0
- vibe/cli/textual_ui/app.tcss +1020 -0
- vibe/cli/textual_ui/external_editor.py +32 -0
- vibe/cli/textual_ui/handlers/__init__.py +5 -0
- vibe/cli/textual_ui/handlers/event_handler.py +147 -0
- vibe/cli/textual_ui/widgets/__init__.py +0 -0
- vibe/cli/textual_ui/widgets/approval_app.py +192 -0
- vibe/cli/textual_ui/widgets/banner/banner.py +85 -0
- vibe/cli/textual_ui/widgets/banner/petit_chat.py +195 -0
- vibe/cli/textual_ui/widgets/braille_renderer.py +58 -0
- vibe/cli/textual_ui/widgets/chat_input/__init__.py +7 -0
- vibe/cli/textual_ui/widgets/chat_input/body.py +214 -0
- vibe/cli/textual_ui/widgets/chat_input/completion_manager.py +58 -0
- vibe/cli/textual_ui/widgets/chat_input/completion_popup.py +43 -0
- vibe/cli/textual_ui/widgets/chat_input/container.py +195 -0
- vibe/cli/textual_ui/widgets/chat_input/text_area.py +365 -0
- vibe/cli/textual_ui/widgets/compact.py +41 -0
- vibe/cli/textual_ui/widgets/config_app.py +171 -0
- vibe/cli/textual_ui/widgets/context_progress.py +30 -0
- vibe/cli/textual_ui/widgets/load_more.py +43 -0
- vibe/cli/textual_ui/widgets/loading.py +201 -0
- vibe/cli/textual_ui/widgets/messages.py +277 -0
- vibe/cli/textual_ui/widgets/no_markup_static.py +11 -0
- vibe/cli/textual_ui/widgets/path_display.py +28 -0
- vibe/cli/textual_ui/widgets/proxy_setup_app.py +127 -0
- vibe/cli/textual_ui/widgets/question_app.py +496 -0
- vibe/cli/textual_ui/widgets/spinner.py +194 -0
- vibe/cli/textual_ui/widgets/status_message.py +76 -0
- vibe/cli/textual_ui/widgets/teleport_message.py +31 -0
- vibe/cli/textual_ui/widgets/tool_widgets.py +371 -0
- vibe/cli/textual_ui/widgets/tools.py +201 -0
- vibe/cli/textual_ui/windowing/__init__.py +29 -0
- vibe/cli/textual_ui/windowing/history.py +105 -0
- vibe/cli/textual_ui/windowing/history_windowing.py +71 -0
- vibe/cli/textual_ui/windowing/state.py +105 -0
- vibe/cli/update_notifier/__init__.py +47 -0
- vibe/cli/update_notifier/adapters/filesystem_update_cache_repository.py +59 -0
- vibe/cli/update_notifier/adapters/github_update_gateway.py +101 -0
- vibe/cli/update_notifier/adapters/pypi_update_gateway.py +107 -0
- vibe/cli/update_notifier/ports/update_cache_repository.py +16 -0
- vibe/cli/update_notifier/ports/update_gateway.py +53 -0
- vibe/cli/update_notifier/update.py +139 -0
- vibe/cli/update_notifier/whats_new.py +49 -0
- vibe/core/__init__.py +5 -0
- vibe/core/agent_loop.py +1075 -0
- vibe/core/agents/__init__.py +31 -0
- vibe/core/agents/manager.py +165 -0
- vibe/core/agents/models.py +122 -0
- vibe/core/auth/__init__.py +6 -0
- vibe/core/auth/crypto.py +137 -0
- vibe/core/auth/github.py +178 -0
- vibe/core/autocompletion/__init__.py +0 -0
- vibe/core/autocompletion/completers.py +257 -0
- vibe/core/autocompletion/file_indexer/__init__.py +10 -0
- vibe/core/autocompletion/file_indexer/ignore_rules.py +156 -0
- vibe/core/autocompletion/file_indexer/indexer.py +179 -0
- vibe/core/autocompletion/file_indexer/store.py +169 -0
- vibe/core/autocompletion/file_indexer/watcher.py +71 -0
- vibe/core/autocompletion/fuzzy.py +189 -0
- vibe/core/autocompletion/path_prompt.py +108 -0
- vibe/core/autocompletion/path_prompt_adapter.py +149 -0
- vibe/core/config.py +673 -0
- vibe/core/config_PATCH_INSTRUCTIONS.md +77 -0
- vibe/core/llm/__init__.py +0 -0
- vibe/core/llm/backend/anthropic.py +630 -0
- vibe/core/llm/backend/base.py +38 -0
- vibe/core/llm/backend/factory.py +7 -0
- vibe/core/llm/backend/generic.py +425 -0
- vibe/core/llm/backend/mistral.py +381 -0
- vibe/core/llm/backend/vertex.py +115 -0
- vibe/core/llm/exceptions.py +195 -0
- vibe/core/llm/format.py +184 -0
- vibe/core/llm/message_utils.py +24 -0
- vibe/core/llm/types.py +120 -0
- vibe/core/middleware.py +209 -0
- vibe/core/output_formatters.py +85 -0
- vibe/core/paths/__init__.py +0 -0
- vibe/core/paths/config_paths.py +68 -0
- vibe/core/paths/global_paths.py +40 -0
- vibe/core/programmatic.py +56 -0
- vibe/core/prompts/__init__.py +32 -0
- vibe/core/prompts/cli.md +111 -0
- vibe/core/prompts/compact.md +48 -0
- vibe/core/prompts/dangerous_directory.md +5 -0
- vibe/core/prompts/explore.md +50 -0
- vibe/core/prompts/project_context.md +8 -0
- vibe/core/prompts/tests.md +1 -0
- vibe/core/proxy_setup.py +65 -0
- vibe/core/session/session_loader.py +222 -0
- vibe/core/session/session_logger.py +318 -0
- vibe/core/session/session_migration.py +41 -0
- vibe/core/skills/__init__.py +7 -0
- vibe/core/skills/manager.py +132 -0
- vibe/core/skills/models.py +92 -0
- vibe/core/skills/parser.py +39 -0
- vibe/core/system_prompt.py +466 -0
- vibe/core/telemetry/__init__.py +0 -0
- vibe/core/telemetry/send.py +185 -0
- vibe/core/teleport/errors.py +9 -0
- vibe/core/teleport/git.py +196 -0
- vibe/core/teleport/nuage.py +180 -0
- vibe/core/teleport/teleport.py +208 -0
- vibe/core/teleport/types.py +54 -0
- vibe/core/tools/base.py +336 -0
- vibe/core/tools/builtins/ask_user_question.py +134 -0
- vibe/core/tools/builtins/bash.py +357 -0
- vibe/core/tools/builtins/grep.py +310 -0
- vibe/core/tools/builtins/prompts/__init__.py +0 -0
- vibe/core/tools/builtins/prompts/ask_user_question.md +84 -0
- vibe/core/tools/builtins/prompts/bash.md +73 -0
- vibe/core/tools/builtins/prompts/grep.md +4 -0
- vibe/core/tools/builtins/prompts/read_file.md +13 -0
- vibe/core/tools/builtins/prompts/search_replace.md +43 -0
- vibe/core/tools/builtins/prompts/task.md +24 -0
- vibe/core/tools/builtins/prompts/todo.md +199 -0
- vibe/core/tools/builtins/prompts/write_file.md +42 -0
- vibe/core/tools/builtins/read_file.py +222 -0
- vibe/core/tools/builtins/search_replace.py +456 -0
- vibe/core/tools/builtins/task.py +154 -0
- vibe/core/tools/builtins/todo.py +134 -0
- vibe/core/tools/builtins/write_file.py +160 -0
- vibe/core/tools/manager.py +341 -0
- vibe/core/tools/mcp.py +397 -0
- vibe/core/tools/ui.py +68 -0
- vibe/core/trusted_folders.py +86 -0
- vibe/core/types.py +405 -0
- vibe/core/utils.py +396 -0
- vibe/setup/onboarding/__init__.py +39 -0
- vibe/setup/onboarding/base.py +14 -0
- vibe/setup/onboarding/onboarding.tcss +134 -0
- vibe/setup/onboarding/screens/__init__.py +5 -0
- vibe/setup/onboarding/screens/api_key.py +200 -0
- vibe/setup/onboarding/screens/provider_selection.py +87 -0
- vibe/setup/onboarding/screens/welcome.py +136 -0
- vibe/setup/trusted_folders/trust_folder_dialog.py +180 -0
- vibe/setup/trusted_folders/trust_folder_dialog.tcss +83 -0
- vibe/whats_new.md +5 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Iterator
|
|
4
|
+
import hashlib
|
|
5
|
+
import importlib.util
|
|
6
|
+
import inspect
|
|
7
|
+
from logging import getLogger
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
from vibe.core.paths.config_paths import resolve_local_tools_dir
|
|
14
|
+
from vibe.core.paths.global_paths import DEFAULT_TOOL_DIR, GLOBAL_TOOLS_DIR
|
|
15
|
+
from vibe.core.tools.base import BaseTool, BaseToolConfig
|
|
16
|
+
from vibe.core.tools.mcp import (
|
|
17
|
+
RemoteTool,
|
|
18
|
+
create_mcp_http_proxy_tool_class,
|
|
19
|
+
create_mcp_stdio_proxy_tool_class,
|
|
20
|
+
list_tools_http,
|
|
21
|
+
list_tools_stdio,
|
|
22
|
+
)
|
|
23
|
+
from vibe.core.utils import name_matches, run_sync
|
|
24
|
+
|
|
25
|
+
logger = getLogger("vibe")
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from vibe.core.config import MCPHttp, MCPStdio, MCPStreamableHttp, VibeConfig
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _try_canonical_module_name(path: Path) -> str | None:
|
|
32
|
+
"""Extract canonical module name for vibe package files.
|
|
33
|
+
|
|
34
|
+
Prevents Pydantic class identity mismatches when the same module
|
|
35
|
+
is imported via dynamic discovery and regular imports.
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
parts = path.resolve().parts
|
|
39
|
+
except (OSError, ValueError):
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
vibe_idx = parts.index("vibe")
|
|
44
|
+
except ValueError:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
if vibe_idx + 1 >= len(parts):
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
module_parts = [p.removesuffix(".py") for p in parts[vibe_idx:]]
|
|
51
|
+
return ".".join(module_parts)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _compute_module_name(path: Path) -> str:
|
|
55
|
+
"""Return canonical module name for vibe files, hash-based synthetic name otherwise."""
|
|
56
|
+
if canonical := _try_canonical_module_name(path):
|
|
57
|
+
return canonical
|
|
58
|
+
|
|
59
|
+
resolved = path.resolve()
|
|
60
|
+
path_hash = hashlib.md5(str(resolved).encode()).hexdigest()[:8]
|
|
61
|
+
stem = re.sub(r"[^0-9A-Za-z_]", "_", path.stem) or "mod"
|
|
62
|
+
return f"vibe_tools_discovered_{stem}_{path_hash}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class NoSuchToolError(Exception):
|
|
66
|
+
"""Exception raised when a tool is not found."""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ToolManager:
|
|
70
|
+
"""Manages tool discovery and instantiation for an Agent.
|
|
71
|
+
|
|
72
|
+
Discovers available tools from the provided search paths. Each Agent
|
|
73
|
+
should have its own ToolManager instance.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(self, config_getter: Callable[[], VibeConfig]) -> None:
|
|
77
|
+
self._config_getter = config_getter
|
|
78
|
+
self._instances: dict[str, BaseTool] = {}
|
|
79
|
+
self._search_paths: list[Path] = self._compute_search_paths(self._config)
|
|
80
|
+
|
|
81
|
+
self._available: dict[str, type[BaseTool]] = {
|
|
82
|
+
cls.get_name(): cls for cls in self._iter_tool_classes(self._search_paths)
|
|
83
|
+
}
|
|
84
|
+
self._integrate_mcp()
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def _config(self) -> VibeConfig:
|
|
88
|
+
return self._config_getter()
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def _compute_search_paths(config: VibeConfig) -> list[Path]:
|
|
92
|
+
paths: list[Path] = [DEFAULT_TOOL_DIR.path]
|
|
93
|
+
|
|
94
|
+
paths.extend(config.tool_paths)
|
|
95
|
+
|
|
96
|
+
if (tools_dir := resolve_local_tools_dir(Path.cwd())) is not None:
|
|
97
|
+
paths.append(tools_dir)
|
|
98
|
+
|
|
99
|
+
paths.append(GLOBAL_TOOLS_DIR.path)
|
|
100
|
+
|
|
101
|
+
unique: list[Path] = []
|
|
102
|
+
seen: set[Path] = set()
|
|
103
|
+
for p in paths:
|
|
104
|
+
rp = p.resolve()
|
|
105
|
+
if rp not in seen:
|
|
106
|
+
seen.add(rp)
|
|
107
|
+
unique.append(rp)
|
|
108
|
+
return unique
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def _iter_tool_classes(search_paths: list[Path]) -> Iterator[type[BaseTool]]:
|
|
112
|
+
"""Iterate over all search_paths to find tool classes.
|
|
113
|
+
|
|
114
|
+
Note: if a search path is not a directory, it is treated as a single tool file.
|
|
115
|
+
"""
|
|
116
|
+
for base in search_paths:
|
|
117
|
+
if not base.is_dir() and base.name.endswith(".py"):
|
|
118
|
+
if tools := ToolManager._load_tools_from_file(base):
|
|
119
|
+
for tool in tools:
|
|
120
|
+
yield tool
|
|
121
|
+
|
|
122
|
+
for path in base.rglob("*.py"):
|
|
123
|
+
if tools := ToolManager._load_tools_from_file(path):
|
|
124
|
+
for tool in tools:
|
|
125
|
+
yield tool
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def _load_tools_from_file(file_path: Path) -> list[type[BaseTool]] | None:
|
|
129
|
+
if not file_path.is_file():
|
|
130
|
+
return
|
|
131
|
+
name = file_path.name
|
|
132
|
+
if name.startswith("_"):
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
module_name = _compute_module_name(file_path)
|
|
136
|
+
|
|
137
|
+
if module_name in sys.modules:
|
|
138
|
+
module = sys.modules[module_name]
|
|
139
|
+
else:
|
|
140
|
+
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
|
141
|
+
if spec is None or spec.loader is None:
|
|
142
|
+
return
|
|
143
|
+
module = importlib.util.module_from_spec(spec)
|
|
144
|
+
sys.modules[module_name] = module
|
|
145
|
+
try:
|
|
146
|
+
spec.loader.exec_module(module)
|
|
147
|
+
except Exception:
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
tools = []
|
|
151
|
+
for tool_obj in vars(module).values():
|
|
152
|
+
if not inspect.isclass(tool_obj):
|
|
153
|
+
continue
|
|
154
|
+
if not issubclass(tool_obj, BaseTool) or tool_obj is BaseTool:
|
|
155
|
+
continue
|
|
156
|
+
if inspect.isabstract(tool_obj):
|
|
157
|
+
continue
|
|
158
|
+
tools.append(tool_obj)
|
|
159
|
+
return tools
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
def discover_tool_defaults(
|
|
163
|
+
search_paths: list[Path] | None = None,
|
|
164
|
+
) -> dict[str, dict[str, Any]]:
|
|
165
|
+
if search_paths is None:
|
|
166
|
+
search_paths = [DEFAULT_TOOL_DIR.path]
|
|
167
|
+
|
|
168
|
+
defaults: dict[str, dict[str, Any]] = {}
|
|
169
|
+
for cls in ToolManager._iter_tool_classes(search_paths):
|
|
170
|
+
try:
|
|
171
|
+
tool_name = cls.get_name()
|
|
172
|
+
config_class = cls._get_tool_config_class()
|
|
173
|
+
defaults[tool_name] = config_class().model_dump(exclude_none=True)
|
|
174
|
+
except Exception as e:
|
|
175
|
+
logger.warning(
|
|
176
|
+
"Failed to get defaults for tool %s: %s", cls.__name__, e
|
|
177
|
+
)
|
|
178
|
+
continue
|
|
179
|
+
return defaults
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def available_tools(self) -> dict[str, type[BaseTool]]:
|
|
183
|
+
if self._config.enabled_tools:
|
|
184
|
+
return {
|
|
185
|
+
name: cls
|
|
186
|
+
for name, cls in self._available.items()
|
|
187
|
+
if name_matches(name, self._config.enabled_tools)
|
|
188
|
+
}
|
|
189
|
+
if self._config.disabled_tools:
|
|
190
|
+
return {
|
|
191
|
+
name: cls
|
|
192
|
+
for name, cls in self._available.items()
|
|
193
|
+
if not name_matches(name, self._config.disabled_tools)
|
|
194
|
+
}
|
|
195
|
+
return dict(self._available)
|
|
196
|
+
|
|
197
|
+
def _integrate_mcp(self) -> None:
|
|
198
|
+
if not self._config.mcp_servers:
|
|
199
|
+
return
|
|
200
|
+
run_sync(self._integrate_mcp_async())
|
|
201
|
+
|
|
202
|
+
async def _integrate_mcp_async(self) -> None:
|
|
203
|
+
try:
|
|
204
|
+
http_count = 0
|
|
205
|
+
stdio_count = 0
|
|
206
|
+
|
|
207
|
+
for srv in self._config.mcp_servers:
|
|
208
|
+
match srv.transport:
|
|
209
|
+
case "http" | "streamable-http":
|
|
210
|
+
http_count += await self._register_http_server(srv)
|
|
211
|
+
case "stdio":
|
|
212
|
+
stdio_count += await self._register_stdio_server(srv)
|
|
213
|
+
case _:
|
|
214
|
+
logger.warning("Unsupported MCP transport: %r", srv.transport)
|
|
215
|
+
|
|
216
|
+
logger.info(
|
|
217
|
+
"MCP integration registered %d tools (http=%d, stdio=%d)",
|
|
218
|
+
http_count + stdio_count,
|
|
219
|
+
http_count,
|
|
220
|
+
stdio_count,
|
|
221
|
+
)
|
|
222
|
+
except Exception as exc:
|
|
223
|
+
logger.warning("Failed to integrate MCP tools: %s", exc)
|
|
224
|
+
|
|
225
|
+
async def _register_http_server(self, srv: MCPHttp | MCPStreamableHttp) -> int:
|
|
226
|
+
url = (srv.url or "").strip()
|
|
227
|
+
if not url:
|
|
228
|
+
logger.warning("MCP server '%s' missing url for http transport", srv.name)
|
|
229
|
+
return 0
|
|
230
|
+
|
|
231
|
+
headers = srv.http_headers()
|
|
232
|
+
try:
|
|
233
|
+
tools: list[RemoteTool] = await list_tools_http(
|
|
234
|
+
url, headers=headers, startup_timeout_sec=srv.startup_timeout_sec
|
|
235
|
+
)
|
|
236
|
+
except Exception as exc:
|
|
237
|
+
logger.warning("MCP HTTP discovery failed for %s: %s", url, exc)
|
|
238
|
+
return 0
|
|
239
|
+
|
|
240
|
+
added = 0
|
|
241
|
+
for remote in tools:
|
|
242
|
+
try:
|
|
243
|
+
proxy_cls = create_mcp_http_proxy_tool_class(
|
|
244
|
+
url=url,
|
|
245
|
+
remote=remote,
|
|
246
|
+
alias=srv.name,
|
|
247
|
+
server_hint=srv.prompt,
|
|
248
|
+
headers=headers,
|
|
249
|
+
startup_timeout_sec=srv.startup_timeout_sec,
|
|
250
|
+
tool_timeout_sec=srv.tool_timeout_sec,
|
|
251
|
+
)
|
|
252
|
+
self._available[proxy_cls.get_name()] = proxy_cls
|
|
253
|
+
added += 1
|
|
254
|
+
except Exception as exc:
|
|
255
|
+
logger.warning(
|
|
256
|
+
"Failed to register MCP HTTP tool '%s' from %s: %r",
|
|
257
|
+
getattr(remote, "name", "<unknown>"),
|
|
258
|
+
url,
|
|
259
|
+
exc,
|
|
260
|
+
)
|
|
261
|
+
return added
|
|
262
|
+
|
|
263
|
+
async def _register_stdio_server(self, srv: MCPStdio) -> int:
|
|
264
|
+
cmd = srv.argv()
|
|
265
|
+
if not cmd:
|
|
266
|
+
logger.warning("MCP stdio server '%s' has invalid/empty command", srv.name)
|
|
267
|
+
return 0
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
tools: list[RemoteTool] = await list_tools_stdio(
|
|
271
|
+
cmd, env=srv.env or None, startup_timeout_sec=srv.startup_timeout_sec
|
|
272
|
+
)
|
|
273
|
+
except Exception as exc:
|
|
274
|
+
logger.warning("MCP stdio discovery failed for %r: %s", cmd, exc)
|
|
275
|
+
return 0
|
|
276
|
+
|
|
277
|
+
added = 0
|
|
278
|
+
for remote in tools:
|
|
279
|
+
try:
|
|
280
|
+
proxy_cls = create_mcp_stdio_proxy_tool_class(
|
|
281
|
+
command=cmd,
|
|
282
|
+
remote=remote,
|
|
283
|
+
alias=srv.name,
|
|
284
|
+
server_hint=srv.prompt,
|
|
285
|
+
env=srv.env or None,
|
|
286
|
+
startup_timeout_sec=srv.startup_timeout_sec,
|
|
287
|
+
tool_timeout_sec=srv.tool_timeout_sec,
|
|
288
|
+
)
|
|
289
|
+
self._available[proxy_cls.get_name()] = proxy_cls
|
|
290
|
+
added += 1
|
|
291
|
+
except Exception as exc:
|
|
292
|
+
logger.warning(
|
|
293
|
+
"Failed to register MCP stdio tool '%s' from %r: %r",
|
|
294
|
+
getattr(remote, "name", "<unknown>"),
|
|
295
|
+
cmd,
|
|
296
|
+
exc,
|
|
297
|
+
)
|
|
298
|
+
return added
|
|
299
|
+
|
|
300
|
+
def get_tool_config(self, tool_name: str) -> BaseToolConfig:
|
|
301
|
+
tool_class = self._available.get(tool_name)
|
|
302
|
+
|
|
303
|
+
if tool_class:
|
|
304
|
+
config_class = tool_class._get_tool_config_class()
|
|
305
|
+
default_config = config_class()
|
|
306
|
+
else:
|
|
307
|
+
config_class = BaseToolConfig
|
|
308
|
+
default_config = BaseToolConfig()
|
|
309
|
+
|
|
310
|
+
user_overrides = self._config.tools.get(tool_name)
|
|
311
|
+
if user_overrides is None:
|
|
312
|
+
merged_dict = default_config.model_dump()
|
|
313
|
+
else:
|
|
314
|
+
merged_dict = {**default_config.model_dump(), **user_overrides.model_dump()}
|
|
315
|
+
|
|
316
|
+
return config_class.model_validate(merged_dict)
|
|
317
|
+
|
|
318
|
+
def get(self, tool_name: str) -> BaseTool:
|
|
319
|
+
"""Get a tool instance, creating it lazily on first call.
|
|
320
|
+
|
|
321
|
+
Raises:
|
|
322
|
+
NoSuchToolError: If the requested tool is not available.
|
|
323
|
+
"""
|
|
324
|
+
if tool_name in self._instances:
|
|
325
|
+
return self._instances[tool_name]
|
|
326
|
+
|
|
327
|
+
if tool_name not in self._available:
|
|
328
|
+
raise NoSuchToolError(
|
|
329
|
+
f"Unknown tool: {tool_name}. Available: {list(self._available.keys())}"
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
tool_class = self._available[tool_name]
|
|
333
|
+
tool_config = self.get_tool_config(tool_name)
|
|
334
|
+
self._instances[tool_name] = tool_class.from_config(tool_config)
|
|
335
|
+
return self._instances[tool_name]
|
|
336
|
+
|
|
337
|
+
def reset_all(self) -> None:
|
|
338
|
+
self._instances.clear()
|
|
339
|
+
|
|
340
|
+
def invalidate_tool(self, tool_name: str) -> None:
|
|
341
|
+
self._instances.pop(tool_name, None)
|