1pub mod advisories;
4pub mod agents;
5pub mod attachments;
6pub mod claiming;
7pub mod context;
8pub mod deps;
9pub mod feedback;
10pub mod files;
11pub mod gates;
12pub mod prompts_tool;
13pub mod query;
14pub mod schema;
15pub mod search;
16pub mod skills;
17pub mod tasks;
18pub mod tracking;
19pub mod workflows;
20
21pub use context::ToolContext;
22
23use crate::config::{AppConfig, Prompts, ServerPaths, workflows::WorkflowsConfig};
24use crate::db::Database;
25use crate::error::ToolError;
26use crate::format::{OutputFormat, ToolResult};
27use anyhow::Result;
28use rmcp::model::Tool;
29use serde_json::Value;
30use std::path::PathBuf;
31use std::sync::Arc;
32
33pub struct ToolHandler {
35 pub db: Arc<Database>,
36 pub media_dir: PathBuf,
37 pub skills_dir: PathBuf,
38 pub server_paths: Arc<ServerPaths>,
39 pub prompts: Arc<Prompts>,
40 pub config: AppConfig,
42 pub default_format: OutputFormat,
43 pub default_page_size: i32,
44 pub path_mapper: Arc<crate::paths::PathMapper>,
45}
46
47impl ToolHandler {
48 #[allow(clippy::too_many_arguments)]
49 pub fn new(
50 db: Arc<Database>,
51 media_dir: PathBuf,
52 skills_dir: PathBuf,
53 server_paths: Arc<ServerPaths>,
54 prompts: Arc<Prompts>,
55 config: AppConfig,
56 default_format: OutputFormat,
57 default_page_size: i32,
58 path_mapper: Arc<crate::paths::PathMapper>,
59 ) -> Self {
60 Self {
61 db,
62 media_dir,
63 skills_dir,
64 server_paths,
65 prompts,
66 config,
67 default_format,
68 default_page_size,
69 path_mapper,
70 }
71 }
72
73 pub fn get_workflow_for_worker(&self, worker_id: &str) -> Arc<WorkflowsConfig> {
79 if let Ok(Some(worker)) = self.db.get_worker(worker_id) {
80 let base = if let Some(ref workflow_name) = worker.workflow {
82 self.config
83 .workflows
84 .get_named_workflow(workflow_name)
85 .map(Arc::clone)
86 } else {
87 None
88 }
89 .or_else(|| self.config.workflows.get_default_workflow().map(Arc::clone))
90 .unwrap_or_else(|| Arc::clone(&self.config.workflows));
91
92 if !worker.overlays.is_empty() {
94 let cache_key = format!(
96 "{}+{}",
97 worker.workflow.as_deref().unwrap_or("default"),
98 worker.overlays.join("+")
99 );
100 if let Some(cached) = self.config.workflows.get_named_workflow(&cache_key) {
101 return Arc::clone(cached);
102 }
103
104 let mut merged = (*base).clone();
106 for name in &worker.overlays {
107 if let Some(overlay) = self.config.workflows.named_overlays.get(name) {
108 merged.apply_overlay(overlay);
109 }
110 }
111 merged.active_overlays = worker.overlays.clone();
112 return Arc::new(merged);
113 }
114
115 return base;
116 }
117 if let Some(default_workflow) = self.config.workflows.get_default_workflow() {
119 Arc::clone(default_workflow)
120 } else {
121 Arc::clone(&self.config.workflows)
122 }
123 }
124
125 pub fn get_tools(&self) -> Vec<Tool> {
127 let mut tools = Vec::new();
128
129 tools.extend(agents::get_tools(&self.prompts));
131
132 tools.extend(tasks::get_tools(&self.prompts, &self.config.states));
134
135 tools.extend(tracking::get_tools(&self.prompts, &self.config.states));
137
138 tools.extend(deps::get_tools(&self.prompts, &self.config.deps));
140
141 tools.extend(claiming::get_tools(&self.prompts, &self.config.states));
143
144 tools.extend(files::get_tools(&self.prompts));
146
147 tools.extend(attachments::get_tools(&self.prompts));
149
150 tools.extend(skills::get_tools());
152
153 tools.extend(schema::get_tools());
155
156 tools.extend(search::get_tools(&self.prompts));
158
159 tools.extend(query::get_tools());
161
162 tools.extend(gates::get_tools(&self.prompts));
164
165 tools.extend(workflows::get_tools());
167
168 tools.extend(advisories::get_tools());
170
171 tools.extend(prompts_tool::get_tools());
173
174 if self.config.feedback.enabled {
176 tools.extend(feedback::get_tools());
177 }
178
179 tools
180 }
181
182 #[allow(unused_variables)]
184 pub async fn call_tool(
185 &self,
186 name: &str,
187 arguments: Value,
188 ctx: &ToolContext,
189 ) -> Result<ToolResult> {
190 let json = |r: Result<Value>| r.map(ToolResult::Json);
192
193 match name {
194 "connect" => {
196 let base_workflow = arguments
198 .get("workflow")
199 .and_then(|v| v.as_str())
200 .and_then(|name| self.config.workflows.get_named_workflow(name))
201 .map(Arc::clone)
202 .or_else(|| self.config.workflows.get_default_workflow().map(Arc::clone))
203 .unwrap_or_else(|| Arc::clone(&self.config.workflows));
204
205 let overlay_names: Vec<String> = arguments
207 .get("overlays")
208 .and_then(|v| v.as_array())
209 .map(|arr| {
210 arr.iter()
211 .filter_map(|v| v.as_str().map(String::from))
212 .collect()
213 })
214 .unwrap_or_default();
215
216 let workflow = if overlay_names.is_empty() {
217 base_workflow
218 } else {
219 let mut merged = (*base_workflow).clone();
220 for name in &overlay_names {
221 if let Some(overlay) = self.config.workflows.named_overlays.get(name) {
222 merged.apply_overlay(overlay);
223 }
224 }
225 merged.active_overlays = overlay_names;
226 Arc::new(merged)
227 };
228
229 json(agents::connect(
230 agents::ConnectOptions {
231 db: &self.db,
232 server_paths: &self.server_paths,
233 config: &self.config,
234 workflows: &workflow,
235 },
236 arguments,
237 ))
238 }
239 "disconnect" => json(agents::disconnect(&self.db, &self.config.states, arguments)),
240 "list_agents" => agents::list_agents(
241 &self.db,
242 &self.config.states,
243 self.default_format,
244 arguments,
245 ),
246 "cleanup_stale" => json(agents::cleanup_stale(
247 &self.db,
248 &self.config.states,
249 arguments,
250 )),
251 "add_overlay" => json(agents::add_overlay(&self.db, &self.config, arguments)),
252 "remove_overlay" => json(agents::remove_overlay(&self.db, &self.config, arguments)),
253
254 "create" => json(tasks::create(&self.db, &self.config, arguments)),
256 "create_tree" => json(tasks::create_tree(&self.db, &self.config, arguments)),
257 "get" => tasks::get(&self.db, self.default_format, arguments),
258 "list_tasks" => tasks::list_tasks(
259 &self.db,
260 &self.config.states,
261 &self.config.deps,
262 self.default_format,
263 arguments,
264 ),
265 "update" => {
266 let worker_id = arguments
268 .get("worker_id")
269 .and_then(|v| v.as_str())
270 .unwrap_or("");
271 let workflow = self.get_workflow_for_worker(worker_id);
272 json(tasks::update(
273 tasks::UpdateOptions {
274 db: &self.db,
275 config: &self.config,
276 workflows: &workflow,
277 },
278 arguments,
279 ))
280 }
281 "bulk_update" => {
282 let worker_id = arguments
284 .get("worker_id")
285 .and_then(|v| v.as_str())
286 .unwrap_or("");
287 let workflow = self.get_workflow_for_worker(worker_id);
288 json(tasks::bulk_update(
289 tasks::UpdateOptions {
290 db: &self.db,
291 config: &self.config,
292 workflows: &workflow,
293 },
294 arguments,
295 ))
296 }
297 "delete" => json(tasks::delete(&self.db, arguments)),
298 "rename" => json(tasks::rename(&self.db, arguments)),
299 "scan" => tasks::scan(&self.db, self.default_format, arguments),
300 "status_summary" => json(tasks::status_summary(
301 &self.db,
302 &self.config.states,
303 arguments,
304 )),
305
306 "thinking" => json(tracking::thinking(&self.db, &self.config.states, arguments)),
308 "task_history" => tracking::task_history(
309 &self.db,
310 &self.config.states,
311 self.default_format,
312 arguments,
313 ),
314 "log_metrics" => json(tracking::log_metrics(&self.db, arguments)),
315 "get_metrics" => json(tracking::get_metrics(&self.db, arguments)),
316 "project_history" => {
317 tracking::project_history(&self.db, self.default_format, arguments)
318 }
319
320 "link" => json(deps::link(&self.db, &self.config.deps, arguments)),
322 "unlink" => json(deps::unlink(&self.db, arguments)),
323 "relink" => json(deps::relink(&self.db, &self.config.deps, arguments)),
324
325 "claim" => {
327 let worker_id = arguments
329 .get("worker_id")
330 .and_then(|v| v.as_str())
331 .unwrap_or("");
332 let workflow = self.get_workflow_for_worker(worker_id);
333 json(claiming::claim(
334 &self.db,
335 &self.config,
336 &workflow,
337 arguments,
338 ))
339 }
340
341 "mark_file" => json(files::mark_file(&self.db, arguments)),
343 "unmark_file" => json(files::unmark_file(&self.db, arguments)),
344 "list_marks" => files::list_marks(&self.db, self.default_format, arguments),
345 "mark_updates" => {
346 json(files::mark_updates_async(std::sync::Arc::clone(&self.db), arguments).await)
347 }
348
349 "attach" => json(attachments::attach(
351 &self.db,
352 &self.media_dir,
353 &self.config.attachments,
354 arguments,
355 )),
356 "attachments" => {
357 attachments::attachments(&self.db, &self.media_dir, self.default_format, arguments)
358 }
359 "detach" => json(attachments::detach(&self.db, &self.media_dir, arguments)),
360
361 name if skills::is_skill_tool(name) => {
363 json(skills::call_tool(&self.skills_dir, name, &arguments))
364 }
365
366 "get_schema" => json(schema::get_schema(&self.db, arguments)),
368
369 "search" => json(search::search(&self.db, self.default_page_size, arguments)),
371
372 "query" => query::query(&self.db, self.default_format, arguments),
374
375 "check_gates" => {
377 json(gates::check_gates(
380 &self.db,
381 &self.config.workflows,
382 arguments,
383 ))
384 }
385
386 "get_advisory" => {
388 let worker_id = arguments
390 .get("worker_id")
391 .and_then(|v| v.as_str())
392 .unwrap_or("");
393 let workflow = self.get_workflow_for_worker(worker_id);
394 json(advisories::get_advisory(&self.db, &workflow, arguments))
395 }
396
397 "get_prompts" => {
399 let worker_id = arguments
400 .get("worker_id")
401 .and_then(|v| v.as_str())
402 .unwrap_or("");
403 let workflow = self.get_workflow_for_worker(worker_id);
404 json(prompts_tool::get_prompts(&self.db, &workflow, arguments))
405 }
406
407 "list_workflows" => json(workflows::list_workflows(&self.config.workflows)),
409
410 "give_feedback" | "list_feedback" if !self.config.feedback.enabled => {
412 Err(ToolError::unknown_tool(name).into())
413 }
414 "give_feedback" => {
415 let db_dir = self
416 .server_paths
417 .db_path
418 .parent()
419 .unwrap_or(std::path::Path::new("."));
420 json(feedback::give_feedback(
421 db_dir,
422 &self.config.feedback,
423 Some(&self.db),
424 arguments,
425 ))
426 }
427 "list_feedback" => {
428 let db_dir = self
429 .server_paths
430 .db_path
431 .parent()
432 .unwrap_or(std::path::Path::new("."));
433 json(feedback::list_feedback(db_dir))
434 }
435
436 _ => Err(ToolError::unknown_tool(name).into()),
437 }
438 }
439}
440
441pub fn make_tool(name: &str, description: &str, properties: Value, required: Vec<&str>) -> Tool {
443 let input_schema = rmcp::model::JsonObject::from_iter([
444 ("type".to_string(), serde_json::json!("object")),
445 ("properties".to_string(), properties),
446 ("required".to_string(), serde_json::json!(required)),
447 ]);
448
449 Tool::new(name.to_string(), description.to_string(), input_schema)
450}
451
452pub fn make_tool_with_prompts(
455 name: &str,
456 default_description: &str,
457 properties: Value,
458 required: Vec<&str>,
459 prompts: &Prompts,
460) -> Tool {
461 let description = prompts
462 .get_tool_description(name)
463 .unwrap_or(default_description);
464 make_tool(name, description, properties, required)
465}
466
467pub fn get_string(args: &Value, key: &str) -> Option<String> {
469 args.get(key).and_then(|v| v.as_str().map(String::from))
470}
471
472pub fn get_i32(args: &Value, key: &str) -> Option<i32> {
474 args.get(key).and_then(|v| v.as_i64().map(|n| n as i32))
475}
476
477pub fn get_i64(args: &Value, key: &str) -> Option<i64> {
479 args.get(key).and_then(|v| v.as_i64())
480}
481
482pub fn get_f64(args: &Value, key: &str) -> Option<f64> {
484 args.get(key).and_then(|v| v.as_f64())
485}
486
487pub fn get_bool(args: &Value, key: &str) -> Option<bool> {
489 args.get(key).and_then(|v| v.as_bool())
490}
491
492pub fn get_string_array(args: &Value, key: &str) -> Option<Vec<String>> {
494 args.get(key).and_then(|v| {
495 v.as_array().map(|arr| {
496 arr.iter()
497 .filter_map(|v| v.as_str().map(String::from))
498 .collect()
499 })
500 })
501}
502
503pub fn get_string_or_array(args: &Value, key: &str) -> Option<Vec<String>> {
506 args.get(key).and_then(|v| {
507 if let Some(s) = v.as_str() {
508 Some(vec![s.to_string()])
510 } else {
511 v.as_array().map(|arr| {
512 arr.iter()
513 .filter_map(|item| item.as_str().map(String::from))
514 .collect()
515 })
516 }
517 })
518}
519
520pub enum IdList {
522 Ids(Vec<String>),
523 Wildcard,
524}
525
526pub fn get_string_or_array_or_wildcard(args: &Value, key: &str) -> Option<IdList> {
528 let vals = get_string_or_array(args, key)?;
529 if vals.len() == 1 && vals[0] == "*" {
530 Some(IdList::Wildcard)
531 } else {
532 Some(IdList::Ids(vals))
533 }
534}