use crate::models::*; use crate::router::McpTool; use crate::state::MemoryState; use crate::tools::*; use async_trait::async_trait; use serde_json::Value; use std::sync::Arc; pub struct AddTaskHandler; #[async_trait] impl McpTool for AddTaskHandler { fn name(&self) -> &'static str { "add_task" } fn schema(&self) -> Value { crate::mcp::tool_def::("add_task", "Execute add_task") } async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: AddTaskTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let now = crate::handlers::utils::now_secs(); let task_id = uuid::Uuid::new_v4().to_string(); let deps = req.dependencies.unwrap_or_default(); let task = Task { id: task_id.clone(), title: req.title, status: "pending".to_string(), description: req.description, created_at: now, updated_at: now, git_branch: req.git_branch, parent_id: req.parent_id, dependencies: deps, acceptance_criteria: vec![], expires_at: None, }; let idx = state.get_search_index(); drop(idx.index_task(&task)); state.project.tasks.modify(|tasks| { tasks.push(task.clone()); }); state.record_activity("task_create", &format!("Created task: {}", task.title), Some(&task.description)); Ok(format!("Task added with ID: {}", task_id).to_string()) } } pub struct DeleteTaskHandler; #[async_trait] impl McpTool for DeleteTaskHandler { fn name(&self) -> &'static str { "delete_task" } fn schema(&self) -> Value { crate::mcp::tool_def::("delete_task", "Execute delete_task") } async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: DeleteTaskTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let mut deleted_count = 0; let mut actually_deleted = Vec::new(); state.project.tasks.modify(|tasks| { let initial_len = tasks.len(); // Build index-based children map let mut children_map: std::collections::HashMap> = std::collections::HashMap::new(); let mut id_to_index = std::collections::HashMap::new(); for (idx, t) in tasks.iter().enumerate() { id_to_index.insert(t.id.as_str(), idx); } for (idx, t) in tasks.iter().enumerate() { if let Some(pid) = &t.parent_id && let Some(&parent_idx) = id_to_index.get(pid.as_str()) { children_map.entry(parent_idx).or_default().push(idx); } } let mut to_delete_idx = std::collections::HashSet::new(); if let Some(&start_idx) = id_to_index.get(req.id.as_str()) { let mut queue = std::collections::VecDeque::new(); queue.push_back(start_idx); while let Some(curr) = queue.pop_front() { if to_delete_idx.insert(curr) && let Some(children) = children_map.get(&curr) { queue.extend(children.iter().copied()); } } } for &idx in &to_delete_idx { actually_deleted.push(tasks[idx].id.clone()); } tasks.retain(|t| !actually_deleted.contains(&t.id)); deleted_count = initial_len - tasks.len(); }); if deleted_count > 0 { let idx = state.get_search_index(); for id in actually_deleted { drop(idx.delete_document(&id)); } Ok(vec![ format!("Deleted task and its children ({} total).", deleted_count).to_string(), ][0] .clone()) } else { Err(crate::error::AppError::Internal("Task not found. Please use the list_active_tasks tool to verify the correct task ID.".to_string())) } } } pub struct UpdateTaskStatusHandler; #[async_trait] impl McpTool for UpdateTaskStatusHandler { fn name(&self) -> &'static str { "update_task_status" } fn schema(&self) -> Value { crate::mcp::tool_def::( "update_task_status", "Execute update_task_status", ) } async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: UpdateTaskStatusTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let mut found = false; let mut blocked = false; let mut blocker_details = String::new(); let target_status = req.status.to_lowercase(); state.project.tasks.modify(|tasks| { // Find target task let target_idx = tasks .iter() .position(|t| t.id == req.id || t.title == req.id); let target_idx = match target_idx { Some(idx) => idx, None => return, }; found = true; if target_status == "done" || target_status == "completed" { // 1. Check Acceptance Criteria if tasks[target_idx] .acceptance_criteria .iter() .any(|c| !c.is_met) { blocked = true; blocker_details = "Unmet acceptance criteria exist.".to_string(); } // 2. Check dependencies if !blocked { let mut uncompleted_deps = Vec::new(); for dep_id in &tasks[target_idx].dependencies { if let Some(dep_task) = tasks.iter().find(|dt| dt.id == *dep_id) && dep_task.status != "completed" && dep_task.status != "done" { uncompleted_deps.push(dep_task.title.as_str()); } } if !uncompleted_deps.is_empty() { blocked = true; blocker_details = format!("Blocked by dependencies: {}", uncompleted_deps.join(", ")); } } // 3. Check child tasks if !blocked { let target_id_ref = tasks[target_idx].id.as_str(); let mut uncompleted_children = Vec::new(); for child in tasks .iter() .filter(|t| t.parent_id.as_deref() == Some(target_id_ref)) { if child.status != "completed" && child.status != "done" { uncompleted_children.push(child.title.as_str()); } } if !uncompleted_children.is_empty() { blocked = true; blocker_details = format!( "Blocked by child tasks: {}", uncompleted_children.join(", ") ); } } } if !blocked { // Apply update tasks[target_idx].status = target_status.clone(); tasks[target_idx].updated_at = crate::handlers::utils::now_secs(); // Cascade cancellation to children if target_status == "cancelled" || target_status == "abandoned" { let mut children_map: std::collections::HashMap> = std::collections::HashMap::new(); // First pass: map string ID to index to build the adjacency list by index let mut id_to_idx = std::collections::HashMap::new(); for (idx, t) in tasks.iter().enumerate() { id_to_idx.insert(t.id.as_str(), idx); } for (idx, t) in tasks.iter().enumerate() { if let Some(pid) = &t.parent_id && let Some(&p_idx) = id_to_idx.get(pid.as_str()) { children_map.entry(p_idx).or_default().push(idx); } } if let Some(&start_idx) = id_to_idx.get(tasks[target_idx].id.as_str()) { let mut queue = std::collections::VecDeque::new(); queue.push_back(start_idx); while let Some(curr) = queue.pop_front() { if let Some(child_indices) = children_map.get(&curr) { for &idx in child_indices { if tasks[idx].status != "completed" && tasks[idx].status != target_status { tasks[idx].status = target_status.clone(); queue.push_back(idx); } } } } } } } }); if blocked { Err(crate::error::AppError::Internal(format!( "Error: Cannot transition task. {}", blocker_details ))) } else if found { state.record_activity("task_update", &format!("Task {} status -> {}", req.id, req.status), None); Ok("Task status updated.".to_string()) } else { Err(crate::error::AppError::Internal("Task not found. Please use the list_active_tasks tool to verify the correct task ID.".to_string())) } } } pub struct ListActiveTasksHandler; #[async_trait] impl McpTool for ListActiveTasksHandler { fn name(&self) -> &'static str { "list_active_tasks" } fn schema(&self) -> Value { crate::mcp::tool_def::( "list_active_tasks", "Execute list_active_tasks", ) } async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: ListActiveTasksTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let level = req.summary_level.as_deref().unwrap_or("detailed"); let data = state.project.tasks.read_with(|tasks| { let filtered: Vec<_> = tasks .iter() .filter(|t| { let status_match = t.status != "done" && t.status != "completed"; let branch_match = match &req.git_branch { Some(branch) => { t.git_branch.is_none() || t.git_branch.as_deref() == Some(branch.as_str()) } None => true, }; status_match && branch_match }) .map(|t| match level { "compact" => serde_json::json!({ "id": t.id, "title": t.title, "status": t.status, }), "full" => serde_json::to_value(t).unwrap_or_default(), _ => serde_json::json!({ "id": t.id, "title": t.title, "status": t.status, "description": t.description, "git_branch": t.git_branch, }), }) .collect(); let mut json_str = serde_json::to_string(&filtered)?; if let Some(max_t) = req.max_tokens { let char_limit = max_t * 4; if json_str.len() > char_limit { json_str.truncate(char_limit); json_str.push_str(" ...[truncated due to max_tokens]"); } } Ok::(json_str) })?; Ok(data) } } pub struct SetAcceptanceCriteriaHandler; #[async_trait] impl McpTool for SetAcceptanceCriteriaHandler { fn name(&self) -> &'static str { "set_acceptance_criteria" } fn schema(&self) -> Value { crate::mcp::tool_def::( "set_acceptance_criteria", "Execute set_acceptance_criteria", ) } async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: SetAcceptanceCriteriaTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let mut success = false; state.project.tasks.modify(|tasks| { if let Some(task) = tasks.iter_mut().rev().find(|t| t.title == req.task_title) { task.acceptance_criteria = req .criteria .into_iter() .map(|desc| crate::models::AcceptanceCriteria { id: uuid::Uuid::new_v4().to_string(), description: desc, is_met: false, }) .collect(); task.updated_at = crate::handlers::utils::now_secs(); success = true; } }); if success { Ok("Acceptance criteria set successfully.".to_string()) } else { Err(crate::error::AppError::Internal("Task not found. Please use the list_active_tasks tool to verify the correct task ID.".to_string())) } } } pub struct VerifyAcceptanceCriteriaHandler; #[async_trait] impl McpTool for VerifyAcceptanceCriteriaHandler { fn name(&self) -> &'static str { "verify_acceptance_criteria" } fn schema(&self) -> Value { crate::mcp::tool_def::( "verify_acceptance_criteria", "Execute verify_acceptance_criteria", ) } async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: VerifyAcceptanceCriteriaTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let mut success = false; let mut already_met = false; state.project.tasks.modify(|tasks| { if let Some(task) = tasks.iter_mut().find(|t| t.id == req.task_id) && let Some(ac) = task .acceptance_criteria .iter_mut() .find(|c| c.id == req.criteria || c.description == req.criteria) { if ac.is_met { already_met = true; } else { ac.is_met = true; success = true; task.updated_at = crate::handlers::utils::now_secs(); } } }); if success { Ok(format!( "Acceptance criteria verified with proof: {}", req.proof )) } else if already_met { Ok("Acceptance criteria was already met.".to_string()) } else { Err(crate::error::AppError::Internal( "Acceptance criteria or task not found.".to_string(), )) } } } pub struct AddMilestoneHandler; #[async_trait] impl McpTool for AddMilestoneHandler { fn name(&self) -> &'static str { "add_milestone" } fn schema(&self) -> Value { crate::mcp::tool_def::("add_milestone", "Execute add_milestone") } async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: AddMilestoneTool = serde_json::from_value(args).map_err(|e| e.to_string())?; state.project.milestones.modify(|ms| { ms.push(crate::models::Milestone { id: uuid::Uuid::new_v4().to_string(), title: req.title, status: "pending".to_string(), namespace: req.namespace, target_date: None, }) }); Ok("Milestone added".to_string()) } } pub struct UpdateMilestoneHandler; #[async_trait] impl McpTool for UpdateMilestoneHandler { fn name(&self) -> &'static str { "update_milestone" } fn schema(&self) -> Value { crate::mcp::tool_def::("update_milestone", "Execute update_milestone") } async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: UpdateMilestoneTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let mut found = false; state.project.milestones.modify(|ms| { for m in ms.iter_mut() { if m.id == req.id { m.status = req.status.clone(); found = true; break; } } }); if found { Ok("Milestone updated".to_string()) } else { Err(crate::error::AppError::Internal( "Milestone not found. Please verify the milestone ID using list_milestones." .to_string(), )) } } } pub struct ListMilestonesHandler; #[async_trait] impl McpTool for ListMilestonesHandler { fn name(&self) -> &'static str { "list_milestones" } fn schema(&self) -> Value { crate::mcp::tool_def::("list_milestones", "Execute list_milestones") } async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: ListMilestonesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let data = state.project.milestones.read_with(|items| { let filtered: Vec<_> = items .iter() .filter(|i| { if let Some(ns) = &req.namespace { &i.namespace == ns } else { true } }) .collect(); Ok::(serde_json::to_string(&filtered)?) })?; Ok(data) } } pub struct TasksHandler; #[async_trait] impl McpTool for TasksHandler { fn name(&self) -> &'static str { "tasks" } fn schema(&self) -> Value { crate::mcp::tool_def::( "tasks", "Consolidated task management (add, update, delete, list, set_criteria, verify)", ) } async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: TasksTool = serde_json::from_value(args).map_err(|e| e.to_string())?; match req.action { TaskAction::Add => { let title = req.title.ok_or_else(|| { crate::error::AppError::Internal("Missing required parameter 'title' for action 'add'. Next step: Provide non-empty 'title' string in request and retry.".to_string()) })?; let description = req.description.unwrap_or_default(); let add_args = serde_json::json!({ "title": title, "description": description, "git_branch": req.git_branch, "parent_id": req.parent_id, "dependencies": req.dependencies, }); AddTaskHandler.execute(add_args, state).await } TaskAction::Update => { let id = req.id.ok_or_else(|| { crate::error::AppError::Internal("Missing required parameter 'id' for action 'update'. Next step: Provide task 'id' string in request and retry.".to_string()) })?; let status = req.status.ok_or_else(|| { crate::error::AppError::Internal("Missing required parameter 'status' for action 'update'. Next step: Provide valid 'status' ('pending', 'completed', or 'cancelled') in request and retry.".to_string()) })?; let update_args = serde_json::json!({ "id": id, "status": status, }); UpdateTaskStatusHandler.execute(update_args, state).await } TaskAction::Delete => { let id = req.id.ok_or_else(|| { crate::error::AppError::Internal("Missing required parameter 'id' for action 'delete'. Next step: Provide task 'id' string in request and retry.".to_string()) })?; let del_args = serde_json::json!({ "id": id, }); DeleteTaskHandler.execute(del_args, state).await } TaskAction::List => { let list_args = serde_json::json!({ "git_branch": req.git_branch, "summary_level": req.summary_level, "max_tokens": req.max_tokens, }); ListActiveTasksHandler.execute(list_args, state).await } TaskAction::SetCriteria => { let id = req.id.ok_or_else(|| { crate::error::AppError::Internal("Missing required parameter 'id' for action 'set_criteria'. Next step: Provide task 'id' string in request and retry.".to_string()) })?; let criteria = req.criteria.ok_or_else(|| { crate::error::AppError::Internal("Missing required parameter 'criteria' for action 'set_criteria'. Next step: Provide array of acceptance criteria descriptions in request and retry.".to_string()) })?; let set_args = serde_json::json!({ "id": id, "acceptance_criteria": criteria, }); SetAcceptanceCriteriaHandler.execute(set_args, state).await } TaskAction::Verify => { let id = req.id.ok_or_else(|| { crate::error::AppError::Internal("Missing required parameter 'id' for action 'verify'. Next step: Provide task 'id' string in request and retry.".to_string()) })?; let verify_args = serde_json::json!({ "id": id, }); VerifyAcceptanceCriteriaHandler.execute(verify_args, state).await } } } } pub struct MilestonesHandler; #[async_trait] impl McpTool for MilestonesHandler { fn name(&self) -> &'static str { "milestones" } fn schema(&self) -> Value { crate::mcp::tool_def::( "milestones", "Consolidated milestone management (add, update, list)", ) } async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: MilestonesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; match req.action { MilestoneAction::Add => { let title = req.title.ok_or_else(|| { crate::error::AppError::Internal("Missing required parameter 'title' for action 'add'. Next step: Provide non-empty 'title' string in request and retry.".to_string()) })?; let add_args = serde_json::json!({ "title": title, "namespace": req.namespace.unwrap_or_else(|| crate::models::default_namespace()), }); AddMilestoneHandler.execute(add_args, state).await } MilestoneAction::Update => { let id = req.id.ok_or_else(|| { crate::error::AppError::Internal("Missing required parameter 'id' for action 'update'. Next step: Provide milestone 'id' string in request and retry.".to_string()) })?; let status = req.status.ok_or_else(|| { crate::error::AppError::Internal("Missing required parameter 'status' for action 'update'. Next step: Provide milestone 'status' in request and retry.".to_string()) })?; let update_args = serde_json::json!({ "id": id, "status": status, }); UpdateMilestoneHandler.execute(update_args, state).await } MilestoneAction::List => { let list_args = serde_json::json!({ "namespace": req.namespace, }); ListMilestonesHandler.execute(list_args, state).await } } } } #[cfg(test)] mod tests { use super::*; use serde_json::json; use tempfile::tempdir; #[tokio::test] async fn test_add_task_and_list() { let dir = tempdir().unwrap(); let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap())); let add_handler = AddTaskHandler; let args = json!({ "title": "Fix the hyperdrive", "description": "It's making a strange noise", "acceptance_criteria": ["Stop the noise", "Reach lightspeed"], }); let res = add_handler .execute(args, state.clone()) .await .map_err(|e| crate::error::AppError::Internal(e.to_string())) .unwrap(); assert!(res.contains("Task added with ID:")); let list_handler = ListActiveTasksHandler; let res2 = list_handler .execute(json!({}), state.clone()) .await .map_err(|e| crate::error::AppError::Internal(e.to_string())) .unwrap(); assert!(res2.contains("Fix the hyperdrive")); } #[tokio::test] async fn test_update_task_status() { let dir = tempdir().unwrap(); let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap())); let add_handler = AddTaskHandler; let res = add_handler .execute( json!({"title": "Test", "description": "test"}), state.clone(), ) .await .map_err(|e| crate::error::AppError::Internal(e.to_string())) .unwrap(); let id_start = res.find("ID: ").unwrap() + 4; let task_id = res[id_start..].trim(); let update_handler = UpdateTaskStatusHandler; let args = json!({ "id": task_id, "status": "done" }); let res3 = update_handler .execute(args, state.clone()) .await .map_err(|e| crate::error::AppError::Internal(e.to_string())) .unwrap(); assert_eq!(res3, "Task status updated."); let list_handler = ListActiveTasksHandler; let res4 = list_handler .execute(json!({}), state.clone()) .await .map_err(|e| crate::error::AppError::Internal(e.to_string())) .unwrap(); assert!(!res4.contains(task_id)); } #[tokio::test] async fn test_milestones_and_criteria() { let dir = tempdir().unwrap(); let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap())); // Add Milestone let add_milestone = AddMilestoneHandler; let args_ms = json!({ "name": "v1.0", "title": "Release 1.0", "description": "First release", "target_date": 1700000000, "end_date": 1700000000, "namespace": "global" }); let res1 = add_milestone .execute(args_ms, state.clone()) .await .map_err(|e| crate::error::AppError::Internal(e.to_string())) .unwrap(); assert!(res1.contains("Milestone added")); // Fetch milestone ID from state directly to update let ms_id = state.project.milestones.read_with(|ms| ms[0].id.clone()); // Update Milestone let update_ms = UpdateMilestoneHandler; let args_ums = json!({ "id": ms_id, "status": "completed" }); let res2 = update_ms .execute(args_ums, state.clone()) .await .map_err(|e| crate::error::AppError::Internal(e.to_string())) .unwrap(); assert_eq!(res2, "Milestone updated"); // List Milestones let list_ms = ListMilestonesHandler; let res3 = list_ms .execute(json!({"namespace": "global"}), state.clone()) .await .map_err(|e| crate::error::AppError::Internal(e.to_string())) .unwrap(); assert!(res3.contains("completed")); assert!(res3.contains("Release 1.0")); // Task Acceptance Criteria let add_task = AddTaskHandler; let res_task = add_task .execute( json!({"title": "Test", "description": "desc"}), state.clone(), ) .await .map_err(|e| crate::error::AppError::Internal(e.to_string())) .unwrap(); let task_id = res_task[res_task.find("ID: ").unwrap() + 4..].trim(); let set_ac = SetAcceptanceCriteriaHandler; let args_ac = json!({ "task_id": task_id, "task_title": "Test", "criteria": ["Do X", "Do Y"] }); let res4 = set_ac .execute(args_ac, state.clone()) .await .map_err(|e| crate::error::AppError::Internal(e.to_string())) .unwrap(); assert_eq!(res4, "Acceptance criteria set successfully."); let verify_ac = VerifyAcceptanceCriteriaHandler; let args_vac = json!({ "task_id": task_id, "criteria": "Do X", "proof": "I did X" }); let res5 = verify_ac .execute(args_vac, state.clone()) .await .map_err(|e| crate::error::AppError::Internal(e.to_string())) .unwrap(); assert!(res5.contains("Acceptance criteria verified")); } #[tokio::test] async fn test_delete_task_with_children() { let dir = tempfile::tempdir().unwrap(); let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap())); let add_task = AddTaskHandler; let parent = add_task .execute( json!({"title": "Parent", "description": "p"}), state.clone(), ) .await .map_err(|e| crate::error::AppError::Internal(e.to_string())) .unwrap(); let parent_id = parent[parent.find("ID: ").unwrap() + 4..] .trim() .to_string(); let child = add_task .execute( json!({"title": "Child", "description": "c", "parent_id": parent_id}), state.clone(), ) .await .map_err(|e| crate::error::AppError::Internal(e.to_string())) .unwrap(); let _child_id = child[child.find("ID: ").unwrap() + 4..].trim().to_string(); let del_task = DeleteTaskHandler; let res_del = del_task .execute(json!({"id": parent_id}), state.clone()) .await .map_err(|e| crate::error::AppError::Internal(e.to_string())) .unwrap(); assert!(res_del.contains("Deleted task and its children (2 total).")); } #[tokio::test] async fn test_list_milestones_with_namespace() { let dir = tempfile::tempdir().unwrap(); let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap())); let add_milestone = AddMilestoneHandler; let args_ms = serde_json::json!({ "name": "v1.0", "title": "Release 1.0", "description": "First release", "target_date": 1700000000, "end_date": 1700000000, "namespace": "global" }); let res1 = add_milestone .execute(args_ms, state.clone()) .await .map_err(|e| crate::error::AppError::Internal(e.to_string())) .unwrap(); assert!(res1.contains("Milestone added")); let list_ms = ListMilestonesHandler; let res2 = list_ms .execute(serde_json::json!({"namespace": "global"}), state.clone()) .await .map_err(|e| crate::error::AppError::Internal(e.to_string())) .unwrap(); assert!(res2.contains("Release 1.0")); } }