Skip to main content

task_graph_mcp/tools/
mod.rs

1//! MCP tool implementations.
2
3pub mod agents;
4pub mod attachments;
5pub mod claiming;
6pub mod deps;
7pub mod files;
8pub mod query;
9pub mod schema;
10pub mod search;
11pub mod skills;
12pub mod tasks;
13pub mod tracking;
14
15use crate::config::{AttachmentsConfig, AutoAdvanceConfig, DependenciesConfig, Prompts, ServerPaths, StatesConfig};
16use crate::db::Database;
17use crate::error::ToolError;
18use crate::format::{OutputFormat, ToolResult};
19use anyhow::Result;
20use rmcp::model::Tool;
21use serde_json::Value;
22use std::path::PathBuf;
23use std::sync::Arc;
24
25/// Tool handler that processes MCP tool calls.
26pub struct ToolHandler {
27    pub db: Arc<Database>,
28    pub media_dir: PathBuf,
29    pub skills_dir: PathBuf,
30    pub server_paths: Arc<ServerPaths>,
31    pub prompts: Arc<Prompts>,
32    pub states_config: Arc<StatesConfig>,
33    pub deps_config: Arc<DependenciesConfig>,
34    pub auto_advance: Arc<AutoAdvanceConfig>,
35    pub attachments_config: Arc<AttachmentsConfig>,
36    pub default_format: OutputFormat,
37}
38
39impl ToolHandler {
40    pub fn new(
41        db: Arc<Database>,
42        media_dir: PathBuf,
43        skills_dir: PathBuf,
44        server_paths: Arc<ServerPaths>,
45        prompts: Arc<Prompts>,
46        states_config: Arc<StatesConfig>,
47        deps_config: Arc<DependenciesConfig>,
48        auto_advance: Arc<AutoAdvanceConfig>,
49        attachments_config: Arc<AttachmentsConfig>,
50        default_format: OutputFormat,
51    ) -> Self {
52        Self {
53            db,
54            media_dir,
55            skills_dir,
56            server_paths,
57            prompts,
58            states_config,
59            deps_config,
60            auto_advance,
61            attachments_config,
62            default_format,
63        }
64    }
65
66    /// Get all available tools.
67    pub fn get_tools(&self) -> Vec<Tool> {
68        let mut tools = Vec::new();
69
70        // Worker tools
71        tools.extend(agents::get_tools(&self.prompts));
72
73        // Task tools (with dynamic state schema)
74        tools.extend(tasks::get_tools(&self.prompts, &self.states_config));
75
76        // Tracking tools
77        tools.extend(tracking::get_tools(&self.prompts, &self.states_config));
78
79        // Dependency tools
80        tools.extend(deps::get_tools(&self.prompts, &self.deps_config));
81
82        // Claiming tools (with dynamic state schema)
83        tools.extend(claiming::get_tools(&self.prompts, &self.states_config));
84
85        // File coordination tools
86        tools.extend(files::get_tools(&self.prompts));
87
88        // Attachment tools
89        tools.extend(attachments::get_tools(&self.prompts));
90
91        // Skill tools (no prompts needed, always available)
92        tools.extend(skills::get_tools());
93
94        // Schema introspection tools
95        tools.extend(schema::get_tools());
96
97        // Search tools
98        tools.extend(search::get_tools(&self.prompts));
99
100        // Query tools (read-only SQL)
101        tools.extend(query::get_tools());
102
103        tools
104    }
105
106    /// Call a tool by name.
107    /// Call a tool by name.
108    pub async fn call_tool(&self, name: &str, arguments: Value) -> Result<ToolResult> {
109        // Helper to wrap JSON results
110        let json = |r: Result<Value>| r.map(ToolResult::Json);
111
112        match name {
113            // Worker tools
114            "connect" => json(agents::connect(&self.db, &self.server_paths, arguments)),
115            "disconnect" => json(agents::disconnect(&self.db, &self.states_config, arguments)),
116            "list_agents" => agents::list_agents(&self.db, &self.states_config, self.default_format, arguments),
117            "cleanup_stale" => json(agents::cleanup_stale(&self.db, &self.states_config, arguments)),
118
119            // Task tools
120            "create" => json(tasks::create(&self.db, &self.states_config, arguments)),
121            "create_tree" => json(tasks::create_tree(&self.db, &self.states_config, arguments)),
122            "get" => json(tasks::get(&self.db, self.default_format, arguments)),
123            "list_tasks" => {
124                json(tasks::list_tasks(&self.db, &self.states_config, &self.deps_config, self.default_format, arguments))
125            }
126            "update" => json(tasks::update(&self.db, &self.attachments_config, &self.states_config, &self.deps_config, &self.auto_advance, arguments)),
127            "delete" => json(tasks::delete(&self.db, arguments)),
128            "scan" => json(tasks::scan(&self.db, self.default_format, arguments)),
129
130            // Tracking tools
131            "thinking" => json(tracking::thinking(&self.db, arguments)),
132            "task_history" => {
133                json(tracking::task_history(&self.db, &self.states_config, self.default_format, arguments))
134            }
135            "log_metrics" => json(tracking::log_metrics(&self.db, arguments)),
136            "get_metrics" => json(tracking::get_metrics(&self.db, arguments)),
137            "project_history" => {
138                json(tracking::project_history(&self.db, self.default_format, arguments))
139            }
140
141            // Dependency tools
142            "link" => json(deps::link(&self.db, &self.deps_config, arguments)),
143            "unlink" => json(deps::unlink(&self.db, arguments)),
144            "relink" => json(deps::relink(&self.db, &self.deps_config, arguments)),
145
146            // Claiming tools
147            "claim" => json(claiming::claim(&self.db, &self.states_config, &self.deps_config, &self.auto_advance, arguments)),
148
149            // File coordination tools
150            "mark_file" => json(files::mark_file(&self.db, arguments)),
151            "unmark_file" => json(files::unmark_file(&self.db, arguments)),
152            "list_marks" => json(files::list_marks(&self.db, self.default_format, arguments)),
153            "mark_updates" => json(files::mark_updates_async(std::sync::Arc::clone(&self.db), arguments).await),
154
155            // Attachment tools
156            "attach" => json(attachments::attach(&self.db, &self.media_dir, &self.attachments_config, arguments)),
157            "attachments" => json(attachments::attachments(&self.db, &self.media_dir, self.default_format, arguments)),
158            "detach" => json(attachments::detach(&self.db, &self.media_dir, arguments)),
159
160            // Skill tools
161            name if skills::is_skill_tool(name) => {
162                json(skills::call_tool(&self.skills_dir, name, &arguments))
163            }
164
165            // Schema introspection tools
166            "get_schema" => json(schema::get_schema(&self.db, arguments)),
167
168            // Search tools
169            "search" => json(search::search(&self.db, arguments)),
170
171            // Query tools (read-only SQL)
172            "query" => query::query(&self.db, self.default_format, arguments),
173
174            _ => Err(ToolError::unknown_tool(name).into()),
175        }
176    }
177}
178
179/// Helper to create a tool definition.
180pub fn make_tool(name: &str, description: &str, properties: Value, required: Vec<&str>) -> Tool {
181    let input_schema = rmcp::model::JsonObject::from_iter([
182        ("type".to_string(), serde_json::json!("object")),
183        ("properties".to_string(), properties),
184        (
185            "required".to_string(),
186            serde_json::json!(required),
187        ),
188    ]);
189
190    Tool::new(name.to_string(), description.to_string(), input_schema)
191}
192
193/// Helper to create a tool definition with prompt overrides.
194/// Looks up the tool description in prompts, falls back to default_description.
195pub fn make_tool_with_prompts(
196    name: &str,
197    default_description: &str,
198    properties: Value,
199    required: Vec<&str>,
200    prompts: &Prompts,
201) -> Tool {
202    let description = prompts
203        .get_tool_description(name)
204        .unwrap_or(default_description);
205    make_tool(name, description, properties, required)
206}
207
208/// Helper to get a string from arguments.
209pub fn get_string(args: &Value, key: &str) -> Option<String> {
210    args.get(key).and_then(|v| v.as_str().map(String::from))
211}
212
213/// Helper to get an i32 from arguments.
214pub fn get_i32(args: &Value, key: &str) -> Option<i32> {
215    args.get(key).and_then(|v| v.as_i64().map(|n| n as i32))
216}
217
218/// Helper to get an i64 from arguments.
219pub fn get_i64(args: &Value, key: &str) -> Option<i64> {
220    args.get(key).and_then(|v| v.as_i64())
221}
222
223/// Helper to get an f64 from arguments.
224pub fn get_f64(args: &Value, key: &str) -> Option<f64> {
225    args.get(key).and_then(|v| v.as_f64())
226}
227
228/// Helper to get a bool from arguments.
229pub fn get_bool(args: &Value, key: &str) -> Option<bool> {
230    args.get(key).and_then(|v| v.as_bool())
231}
232
233/// Helper to get a string array from arguments.
234pub fn get_string_array(args: &Value, key: &str) -> Option<Vec<String>> {
235    args.get(key).and_then(|v| {
236        v.as_array().map(|arr| {
237            arr.iter()
238                .filter_map(|v| v.as_str().map(String::from))
239                .collect()
240        })
241    })
242}
243
244/// Helper to get either a single string or array of strings from arguments.
245/// Normalizes to a Vec<String>.
246pub fn get_string_or_array(args: &Value, key: &str) -> Option<Vec<String>> {
247    args.get(key).and_then(|v| {
248        if let Some(s) = v.as_str() {
249            // Single string - wrap in vec
250            Some(vec![s.to_string()])
251        } else if let Some(arr) = v.as_array() {
252            // Array of strings
253            Some(
254                arr.iter()
255                    .filter_map(|item| item.as_str().map(String::from))
256                    .collect(),
257            )
258        } else {
259            None
260        }
261    })
262}