mermaid_cli/providers/tool/
mod.rs1pub mod computer_use;
19pub mod exec;
20pub mod filesystem;
21pub mod mcp;
22pub mod policy_gate;
23pub mod subagent;
24pub mod web;
25pub mod web_client;
26
27use async_trait::async_trait;
28use std::collections::HashMap;
29use std::sync::Arc;
30
31use crate::domain::{ToolDefinition, ToolOutcome};
32
33use super::ctx::ExecContext;
34
35#[async_trait]
39pub trait ToolExecutor: Send + Sync {
40 fn name(&self) -> &'static str;
43
44 fn schema(&self) -> ToolDefinition;
50
51 fn is_internal(&self) -> bool {
57 false
58 }
59
60 async fn execute(&self, args: serde_json::Value, ctx: ExecContext) -> ToolOutcome;
64}
65
66pub struct ToolRegistry {
70 entries: HashMap<&'static str, Arc<dyn ToolExecutor>>,
71}
72
73impl ToolRegistry {
74 pub fn new() -> Self {
75 Self {
76 entries: HashMap::new(),
77 }
78 }
79
80 pub fn register(&mut self, tool: Arc<dyn ToolExecutor>) {
81 self.entries.insert(tool.name(), tool);
82 }
83
84 pub fn get(&self, name: &str) -> Option<Arc<dyn ToolExecutor>> {
85 self.entries.get(name).cloned()
86 }
87
88 pub fn len(&self) -> usize {
89 self.entries.len()
90 }
91
92 pub fn is_empty(&self) -> bool {
93 self.entries.is_empty()
94 }
95
96 pub fn names(&self) -> impl Iterator<Item = &'static str> + '_ {
97 self.entries.keys().copied()
98 }
99
100 pub fn describe_all(&self) -> Vec<ToolDefinition> {
106 self.entries
107 .values()
108 .filter(|t| !t.is_internal())
109 .map(|t| t.schema())
110 .collect()
111 }
112}
113
114impl Default for ToolRegistry {
115 fn default() -> Self {
116 let mut r = Self::new();
117 r.register(Arc::new(filesystem::ReadFileTool));
118 r.register(Arc::new(filesystem::WriteFileTool));
119 r.register(Arc::new(filesystem::EditFileTool));
120 r.register(Arc::new(filesystem::DeleteFileTool));
121 r.register(Arc::new(filesystem::CreateDirectoryTool));
122 r.register(Arc::new(exec::ExecuteCommandTool));
123 r.register(Arc::new(mcp::McpToolProxy));
127 r
128 }
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub enum TuiMode {
138 Interactive,
139 Headless,
140}
141
142impl ToolRegistry {
143 pub fn build(
160 _config: &crate::app::Config,
161 mode: TuiMode,
162 providers: Arc<crate::providers::ProviderFactory>,
163 ) -> Arc<Self> {
164 let mut r = Self::new();
165 r.register(Arc::new(filesystem::ReadFileTool));
166 r.register(Arc::new(filesystem::WriteFileTool));
167 r.register(Arc::new(filesystem::EditFileTool));
168 r.register(Arc::new(filesystem::DeleteFileTool));
169 r.register(Arc::new(filesystem::CreateDirectoryTool));
170 r.register(Arc::new(exec::ExecuteCommandTool));
171 r.register(Arc::new(mcp::McpToolProxy));
172
173 if let Some(key) = crate::utils::resolve_api_key("OLLAMA_API_KEY", None) {
174 r.register(Arc::new(web::WebSearchTool::new(key.clone())));
175 r.register(Arc::new(web::WebFetchTool::new(key)));
176 }
177
178 if mode == TuiMode::Interactive {
183 let backend = computer_use::probe();
184 if backend.is_usable() {
185 let driver = Arc::new(computer_use::ComputerUseDriver::new(backend));
186 r.register(Arc::new(computer_use::ScreenshotTool::new(driver.clone())));
187 r.register(Arc::new(computer_use::ClickTool::new(driver.clone())));
188 r.register(Arc::new(computer_use::TypeTextTool::new(driver.clone())));
189 r.register(Arc::new(computer_use::PressKeyTool::new(driver.clone())));
190 r.register(Arc::new(computer_use::ScrollTool::new(driver.clone())));
191 r.register(Arc::new(computer_use::MouseMoveTool::new(driver.clone())));
192 r.register(Arc::new(computer_use::ListWindowsTool::new(driver)));
193 }
194 }
195
196 let spawner = Arc::new(subagent::SubagentSpawner::new(providers));
201 r.register(Arc::new(subagent::SubagentTool::new(spawner)));
202
203 Arc::new(r)
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn default_registry_has_builtin_tools() {
213 let r = ToolRegistry::default();
214 for name in &[
215 "read_file",
216 "write_file",
217 "edit_file",
218 "delete_file",
219 "create_directory",
220 "execute_command",
221 ] {
222 assert!(r.get(name).is_some(), "missing: {}", name);
223 }
224 assert!(r.get("not_a_tool").is_none());
225 assert!(r.len() >= 6);
226 }
227
228 #[test]
229 fn describe_all_returns_one_per_user_facing_tool() {
230 let r = ToolRegistry::default();
231 let schemas = r.describe_all();
232 let visible = r
235 .names()
236 .filter(|n| r.get(n).map(|t| !t.is_internal()).unwrap_or(false))
237 .count();
238 assert_eq!(schemas.len(), visible);
239 for schema in &schemas {
240 assert!(
241 r.get(&schema.name).is_some(),
242 "schema for unknown tool: {}",
243 schema.name
244 );
245 }
246 }
247
248 #[test]
249 fn mcp_proxy_is_registered_but_internal() {
250 let r = ToolRegistry::default();
251 let proxy = r.get("mcp_proxy").expect("mcp_proxy registered");
252 assert!(proxy.is_internal());
253 assert!(!r.describe_all().iter().any(|s| s.name == "mcp_proxy"));
254 }
255
256 #[test]
257 fn schema_name_matches_executor_name() {
258 let r = ToolRegistry::default();
259 for name in r.names() {
260 let tool = r.get(name).unwrap();
261 assert_eq!(tool.name(), tool.schema().name.as_str());
262 }
263 }
264
265 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
270
271 #[test]
272 fn build_registers_web_tools_when_key_present() {
273 let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
274 let prior = std::env::var("OLLAMA_API_KEY").ok();
275 unsafe {
276 std::env::set_var("OLLAMA_API_KEY", "test-key-build");
277 }
278 let cfg = crate::app::Config::default();
279 let providers = Arc::new(crate::providers::ProviderFactory::new(cfg.clone()));
280 let r = ToolRegistry::build(&cfg, TuiMode::Interactive, providers);
281 assert!(r.get("web_search").is_some(), "web_search registered");
282 assert!(r.get("web_fetch").is_some(), "web_fetch registered");
283 unsafe {
284 match prior {
285 Some(v) => std::env::set_var("OLLAMA_API_KEY", v),
286 None => std::env::remove_var("OLLAMA_API_KEY"),
287 }
288 }
289 }
290
291 #[test]
292 fn build_skips_web_tools_without_key() {
293 let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
294 let prior = std::env::var("OLLAMA_API_KEY").ok();
295 unsafe {
296 std::env::remove_var("OLLAMA_API_KEY");
297 }
298 let cfg = crate::app::Config::default();
299 let providers = Arc::new(crate::providers::ProviderFactory::new(cfg.clone()));
300 let r = ToolRegistry::build(&cfg, TuiMode::Headless, providers);
301 assert!(r.get("web_search").is_none(), "web_search skipped");
302 assert!(r.get("web_fetch").is_none(), "web_fetch skipped");
303 assert!(r.get("read_file").is_some());
304 assert!(r.get("execute_command").is_some());
305 unsafe {
306 if let Some(v) = prior {
307 std::env::set_var("OLLAMA_API_KEY", v);
308 }
309 }
310 }
311}