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