use super::memory_storage::MemoryPlanStorage;
use super::storage::{PlanStorage, TaskData, TaskStatus};
use crate::mcp::{McpToolCall, McpToolResult};
use anyhow::Result;
use serde_json::Value;
use std::sync::{Arc, Mutex};
lazy_static::lazy_static! {
static ref PLAN_STORAGE: Arc<Mutex<MemoryPlanStorage>> = Arc::new(Mutex::new(MemoryPlanStorage::new()));
}
pub async fn execute_plan(call: &McpToolCall) -> Result<McpToolResult> {
let command = match call.parameters.get("command") {
Some(Value::String(cmd)) => {
if cmd.trim().is_empty() {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
"Command parameter cannot be empty".to_string(),
));
}
cmd.clone()
}
Some(_) => {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
"Command parameter must be a string".to_string(),
));
}
None => {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
"Missing required parameter 'command'".to_string(),
));
}
};
match command.as_str() {
"start" => handle_start_command(call).await,
"step" => handle_step_command(call).await,
"next" => handle_next_command(call).await,
"list" => handle_list_command(call).await,
"done" => handle_done_command(call).await,
"reset" => handle_reset_command(call).await,
_ => Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
format!(
"Unknown command '{command}'. Available commands: start, step, next, list, done, reset"
),
)),
}
}
async fn handle_start_command(call: &McpToolCall) -> Result<McpToolResult> {
let title = match call.parameters.get("title") {
Some(Value::String(t)) => {
if t.trim().is_empty() {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
"Title parameter cannot be empty".to_string(),
));
}
t.clone()
}
Some(_) => {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
"Title parameter must be a string".to_string(),
));
}
None => {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
"Missing required parameter 'title'".to_string(),
));
}
};
let tasks = match call.parameters.get("tasks") {
Some(Value::Array(task_array)) => {
let mut tasks = Vec::new();
for (i, task_value) in task_array.iter().enumerate() {
match task_value {
Value::Object(task_obj) => {
let title = match task_obj.get("title") {
Some(Value::String(t)) => {
if t.trim().is_empty() {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
format!("Task {} title cannot be empty", i + 1),
));
}
t.clone()
}
Some(_) => {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
format!("Task {} title must be a string", i + 1),
));
}
None => {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
format!("Task {} missing required 'title' field", i + 1),
));
}
};
let description = match task_obj.get("description") {
Some(Value::String(d)) => {
if d.trim().is_empty() {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
format!("Task {} description cannot be empty", i + 1),
));
}
d.clone()
}
Some(_) => {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
format!("Task {} description must be a string", i + 1),
));
}
None => {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
format!("Task {} missing required 'description' field", i + 1),
));
}
};
tasks.push(TaskData::new(title, description));
}
_ => {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
format!("Task {} must be an object with 'title' and 'description' fields. Simple strings are no longer supported - use detailed task objects for better context recovery.", i + 1),
));
}
}
}
if tasks.is_empty() {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
"Tasks array cannot be empty".to_string(),
));
}
tasks
}
Some(_) => {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
"Tasks parameter must be an array of detailed task objects with 'title' and 'description' fields".to_string(),
));
}
None => {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
"Missing required parameter 'tasks'".to_string(),
));
}
};
let mut storage = PLAN_STORAGE.lock().unwrap();
if storage.has_active_plan().unwrap_or(false) {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
"Active plan already exists. Use 'done' to complete current plan, 'reset' to clear it, or 'list' to view current progress before starting a new plan.".to_string(),
));
}
if let Err(e) = storage.create_plan(title.clone(), tasks.clone()) {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
format!("Failed to create plan: {e}"),
));
}
let mut response = format!("PLAN CREATED: {title}\n\nTASKS:\n");
for (i, task) in tasks.iter().enumerate() {
response.push_str(&format!("{}. {}\n", i + 1, task.title));
response.push_str(&format!(" π {}\n", task.description));
}
response.push_str(&format!(
"\nCURRENT: Task 1/{} - {}",
tasks.len(),
tasks[0].title
));
Ok(McpToolResult::success(
call.tool_name.clone(),
call.tool_id.clone(),
response,
))
}
async fn handle_step_command(call: &McpToolCall) -> Result<McpToolResult> {
let storage = PLAN_STORAGE.lock().unwrap();
if !storage.has_active_plan().unwrap_or(false) {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
"No active plan. Use 'start' command to create a plan first.".to_string(),
));
}
match call.parameters.get("content") {
Some(Value::String(content)) => {
if content.trim().is_empty() {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
"Content parameter cannot be empty".to_string(),
));
}
drop(storage);
let mut storage = PLAN_STORAGE.lock().unwrap();
if let Err(e) = storage.add_step_details(content.clone()) {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
format!("Failed to add step details: {e}"),
));
}
let (current, total, task_title, _task_description) = storage
.get_current_task_info()
.unwrap_or((0, 0, "Unknown".to_string(), "No description".to_string()));
Ok(McpToolResult::success(
call.tool_name.clone(),
call.tool_id.clone(),
format!("Step details added to Task {current}/{total} - {task_title}"),
))
}
Some(_) => Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
"Content parameter must be a string".to_string(),
)),
None => {
let details = storage
.get_current_step_details()
.unwrap_or_else(|_| "No details recorded yet".to_string());
let (current, total, task_title, _task_description) = storage
.get_current_task_info()
.unwrap_or((0, 0, "Unknown".to_string(), "No description".to_string()));
let response = if details.is_empty() {
format!(
"CURRENT TASK ({current}/{total}): {task_title}\n\nNo details recorded yet."
)
} else {
format!("CURRENT TASK ({current}/{total}): {task_title}\n\nDETAILS:\n{details}")
};
Ok(McpToolResult::success(
call.tool_name.clone(),
call.tool_id.clone(),
response,
))
}
}
}
async fn handle_next_command(call: &McpToolCall) -> Result<McpToolResult> {
let content = match call.parameters.get("content") {
Some(Value::String(c)) => {
if c.trim().is_empty() {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
"Content parameter cannot be empty".to_string(),
));
}
c.clone()
}
Some(_) => {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
"Content parameter must be a string".to_string(),
));
}
None => {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
"Missing required parameter 'content'".to_string(),
));
}
};
let mut storage = PLAN_STORAGE.lock().unwrap();
if !storage.has_active_plan().unwrap_or(false) {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
"No active plan. Use 'start' command to create a plan first.".to_string(),
));
}
if let Err(e) = storage.complete_current_task(content.clone()) {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
format!("Failed to complete task: {e}"),
));
}
let has_more = storage.has_more_tasks().unwrap_or(false);
let plan_title = storage
.get_plan_title()
.unwrap_or_else(|_| "Unknown Plan".to_string());
let response = if has_more {
let (current, total, task_title, _task_description) = storage
.get_current_task_info()
.unwrap_or((0, 0, "Unknown".to_string(), "No description".to_string()));
format!("Task completed: {content}\n\nNEXT TASK ({current}/{total}): {task_title}")
} else {
format!("Final task completed: {content}\n\nAll tasks in plan '{plan_title}' are now complete. Use 'done' command to finalize.")
};
Ok(McpToolResult::success(
call.tool_name.clone(),
call.tool_id.clone(),
response,
))
}
async fn handle_list_command(call: &McpToolCall) -> Result<McpToolResult> {
let storage = PLAN_STORAGE.lock().unwrap();
if !storage.has_active_plan().unwrap_or(false) {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
"No active plan. Use 'start' command to create a plan first.".to_string(),
));
}
let plan_title = storage
.get_plan_title()
.unwrap_or_else(|_| "Unknown Plan".to_string());
let task_list = storage.get_task_list().unwrap_or_else(|_| Vec::new());
let (current, total, current_task_title, current_task_description) = storage
.get_current_task_info()
.unwrap_or((0, 0, "Unknown".to_string(), "No description".to_string()));
let mut response = format!("PLAN: {plan_title}\n\nTASKS:\n");
for (i, (task_title, task_description, status)) in task_list.iter().enumerate() {
let task_num = i + 1;
let status_icon = match status {
TaskStatus::Completed => "β
",
TaskStatus::InProgress => {
if task_num == current {
"π"
} else {
"β³"
}
}
};
let status_text = if task_num == current {
" (IN PROGRESS)"
} else {
"" };
response.push_str(&format!(
"{status_icon} {task_num}. {task_title}{status_text}\n"
));
let description_lines: Vec<&str> = task_description.lines().collect();
for line in description_lines {
response.push_str(&format!(" π {}\n", line));
}
response.push('\n'); }
if current <= total {
response.push_str(&format!(
"CURRENT: Task {current}/{total} - {current_task_title}\nπ {current_task_description}"
));
}
Ok(McpToolResult::success(
call.tool_name.clone(),
call.tool_id.clone(),
response,
))
}
async fn handle_done_command(call: &McpToolCall) -> Result<McpToolResult> {
let content = match call.parameters.get("content") {
Some(Value::String(c)) => {
if c.trim().is_empty() {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
"Content parameter cannot be empty".to_string(),
));
}
c.clone()
}
Some(_) => {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
"Content parameter must be a string".to_string(),
));
}
None => {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
"Missing required parameter 'content'".to_string(),
));
}
};
let mut storage = PLAN_STORAGE.lock().unwrap();
if !storage.has_active_plan().unwrap_or(false) {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
"No active plan. Use 'start' command to create a plan first.".to_string(),
));
}
let plan_title = storage
.get_plan_title()
.unwrap_or_else(|_| "Unknown Plan".to_string());
if let Err(e) = storage.complete_plan(content.clone()) {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
format!("Failed to complete plan: {e}"),
));
}
let response = format!("PLAN COMPLETED: {plan_title}\n\nFINAL SUMMARY: {content}");
Ok(McpToolResult::success(
call.tool_name.clone(),
call.tool_id.clone(),
response,
))
}
async fn handle_reset_command(call: &McpToolCall) -> Result<McpToolResult> {
let mut storage = PLAN_STORAGE.lock().unwrap();
if let Err(e) = storage.clear_plan() {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
format!("Failed to reset plan: {e}"),
));
}
Ok(McpToolResult::success(
call.tool_name.clone(),
call.tool_id.clone(),
"Plan data cleared successfully".to_string(),
))
}
pub async fn clear_plan_data() -> Result<()> {
let mut storage = PLAN_STORAGE.lock().unwrap();
storage.clear_plan()
}
pub async fn get_current_plan_display() -> Result<String> {
let storage = PLAN_STORAGE.lock().unwrap();
if !storage.has_active_plan().unwrap_or(false) {
return Err(anyhow::anyhow!("Use plan tool only for COMPLEX, multi-step tasks that require structured breakdown. For simple tasks, just execute them directly without a plan."));
}
let plan_title = storage
.get_plan_title()
.unwrap_or_else(|_| "Unknown Plan".to_string());
let task_list = storage.get_task_list().unwrap_or_else(|_| Vec::new());
let (current, total, current_task_title, current_task_description) = storage
.get_current_task_info()
.unwrap_or((0, 0, "Unknown".to_string(), "No description".to_string()));
let mut response = format!("PLAN: {plan_title}\n\nTASKS:\n");
for (i, (task_title, task_description, status)) in task_list.iter().enumerate() {
let task_num = i + 1;
let status_icon = match status {
TaskStatus::Completed => "β
",
TaskStatus::InProgress => {
if task_num == current {
"π"
} else {
"β³"
}
}
};
let status_text = if task_num == current {
" (IN PROGRESS)"
} else {
"" };
response.push_str(&format!(
"{status_icon} {task_num}. {task_title}{status_text}\n"
));
let description_lines: Vec<&str> = task_description.lines().collect();
for line in description_lines {
response.push_str(&format!(" π {}\n", line));
}
response.push('\n'); }
if current <= total {
response.push_str(&format!(
"CURRENT: Task {current}/{total} - {current_task_title}\nπ {current_task_description}"
));
}
Ok(response)
}
pub async fn get_current_plan_json() -> Result<serde_json::Value> {
let storage = PLAN_STORAGE.lock().unwrap();
if !storage.has_active_plan().unwrap_or(false) {
return Err(anyhow::anyhow!("No active plan"));
}
let plan_title = storage
.get_plan_title()
.unwrap_or_else(|_| "Unknown Plan".to_string());
let task_list = storage.get_task_list().unwrap_or_else(|_| Vec::new());
let (current, total, current_task_title, current_task_description) = storage
.get_current_task_info()
.unwrap_or((0, 0, "Unknown".to_string(), "No description".to_string()));
Ok(serde_json::json!({
"plan_title": plan_title,
"current_task": current,
"total_tasks": total,
"current_task_title": current_task_title,
"current_task_description": current_task_description,
"tasks": task_list.iter().map(|(title, desc, status)| {
serde_json::json!({
"title": title,
"description": desc,
"status": match status {
TaskStatus::Completed => "completed",
TaskStatus::InProgress => "in_progress"
}
})
}).collect::<Vec<_>>()
}))
}