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; use std::time::{SystemTime, UNIX_EPOCH}; pub struct LogDecisionHandler; #[async_trait] impl McpTool for LogDecisionHandler { fn name(&self) -> &'static str { "log_decision" } fn schema(&self) -> Value { crate::mcp::tool_def::("log_decision", "Execute log_decision") } async fn execute(&self, args: Value, state: Arc) -> Result { let req: LogDecisionTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let mut adr_id = String::new(); let mut new_adr = None; state.adrs.modify(|adrs| { adr_id = format!("ADR-{:04}", adrs.len() + 1); let a = Adr { id: adr_id.clone(), title: req.title, context: req.context, decision: req.decision, consequence: req.consequence, timestamp: SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs(), }; new_adr = Some(a.clone()); adrs.push(a); }); if let Some(adr) = new_adr { let idx = state.search_index.read().unwrap_or_else(|e| e.into_inner()).clone(); let _ = idx.index_adr(&adr).await; } Ok(format!("Decision logged as {}", adr_id).to_string()) } } pub struct QueryDecisionsHandler; #[async_trait] impl McpTool for QueryDecisionsHandler { fn name(&self) -> &'static str { "query_decisions" } fn schema(&self) -> Value { crate::mcp::tool_def::("query_decisions", "Execute query_decisions") } async fn execute(&self, args: Value, state: Arc) -> Result { let req: QueryDecisionsTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let data = state.adrs.read_with(|adrs| { let filtered: Vec<_> = adrs.iter().filter(|a| { if let Some(q) = &req.query { contains_ignore_ascii_case(&a.title, q) || contains_ignore_ascii_case(&a.context, q) || contains_ignore_ascii_case(&a.decision, q) } else { true } }).collect(); serde_json::to_string(&filtered).unwrap_or_default() }); Ok(data.to_string()) } } pub struct LogErrorFixHandler; #[async_trait] impl McpTool for LogErrorFixHandler { fn name(&self) -> &'static str { "log_error_fix" } fn schema(&self) -> Value { crate::mcp::tool_def::("log_error_fix", "Execute log_error_fix") } async fn execute(&self, args: Value, state: Arc) -> Result { let req: LogErrorFixTool = serde_json::from_value(args).map_err(|e| e.to_string())?; state.error_fixes.modify(|fixes| { fixes.push(crate::models::ErrorFix { signature: req.signature, solution: req.solution, timestamp: SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs(), git_commit: req.git_commit, git_branch: req.git_branch, }) }); Ok("Error fix logged".to_string()) } } pub struct SearchErrorFixesHandler; #[async_trait] impl McpTool for SearchErrorFixesHandler { fn name(&self) -> &'static str { "search_error_fixes" } fn schema(&self) -> Value { crate::mcp::tool_def::( "search_error_fixes", "Execute search_error_fixes", ) } async fn execute(&self, args: Value, state: Arc) -> Result { let req: SearchErrorFixesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let q = req.query; let data = state.error_fixes.read_with(|fixes| { let filtered: Vec<_> = fixes.iter().filter(|f| { contains_ignore_ascii_case(&f.signature, &q) || contains_ignore_ascii_case(&f.solution, &q) }).collect(); serde_json::to_string(&filtered).unwrap_or_default() }); Ok(data.to_string()) } } pub struct LogCodeChangeHandler; #[async_trait] impl McpTool for LogCodeChangeHandler { fn name(&self) -> &'static str { "log_code_change" } fn schema(&self) -> Value { crate::mcp::tool_def::("log_code_change", "Execute log_code_change") } async fn execute(&self, args: Value, state: Arc) -> Result { let req: LogCodeChangeTool = serde_json::from_value(args).map_err(|e| e.to_string())?; state.ledger.modify(|ledger| { ledger.push(CodeChange { timestamp: SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs(), file_path: req.file_path, description: req.description, git_commit: req.git_commit, git_branch: req.git_branch, }); }); Ok("Code change logged".to_string()) } } pub struct QueryRecentChangesHandler; #[async_trait] impl McpTool for QueryRecentChangesHandler { fn name(&self) -> &'static str { "query_recent_changes" } fn schema(&self) -> Value { crate::mcp::tool_def::( "query_recent_changes", "Execute query_recent_changes", ) } async fn execute(&self, _args: Value, state: Arc) -> Result { let data = state.ledger.read_with(|l| serde_json::to_string(l).unwrap_or_else(|_| "[]".to_string())); Ok(data.to_string()) } } pub struct LearnPreferenceHandler; #[async_trait] impl McpTool for LearnPreferenceHandler { fn name(&self) -> &'static str { "learn_preference" } fn schema(&self) -> Value { crate::mcp::tool_def::("learn_preference", "Execute learn_preference") } async fn execute(&self, args: Value, state: Arc) -> Result { let req: LearnPreferenceTool = serde_json::from_value(args).map_err(|e| e.to_string())?; state.prefs.modify(|prefs| { prefs.insert( req.key.clone(), crate::models::Preference { key: req.key.clone(), value: req.value, updated_at: SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs(), }, ); }); Ok("Preference learned".to_string()) } } pub struct ReadPreferencesHandler; #[async_trait] impl McpTool for ReadPreferencesHandler { fn name(&self) -> &'static str { "read_preferences" } fn schema(&self) -> Value { crate::mcp::tool_def::("read_preferences", "Execute read_preferences") } async fn execute(&self, _args: Value, state: Arc) -> Result { let data = state.prefs.read_with(|prefs| serde_json::to_string(prefs).unwrap_or_default()); Ok(data.to_string()) } } pub struct LogTechDebtHandler; #[async_trait] impl McpTool for LogTechDebtHandler { fn name(&self) -> &'static str { "log_tech_debt" } fn schema(&self) -> Value { crate::mcp::tool_def::("log_tech_debt", "Execute log_tech_debt") } async fn execute(&self, args: Value, state: Arc) -> Result { let req: LogTechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?; state.tech_debts.modify(|debts| { debts.push(crate::models::TechDebt { id: uuid::Uuid::new_v4().to_string(), namespace: req.namespace, description: req.description, ideal_solution: req.ideal_solution, is_resolved: false, created_at: SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs(), git_commit: req.git_commit, git_branch: req.git_branch, }) }); Ok("Tech debt logged".to_string()) } } pub struct ResolveTechDebtHandler; #[async_trait] impl McpTool for ResolveTechDebtHandler { fn name(&self) -> &'static str { "resolve_tech_debt" } fn schema(&self) -> Value { crate::mcp::tool_def::( "resolve_tech_debt", "Execute resolve_tech_debt", ) } async fn execute(&self, args: Value, state: Arc) -> Result { let req: ResolveTechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let mut found = false; state.tech_debts.modify(|debts| { for d in debts.iter_mut() { if d.id == req.id { d.is_resolved = true; found = true; break; } } }); if found { Ok("Tech debt resolved".to_string()) } else { Ok("Tech debt not found".to_string()) } } } pub struct ListTechDebtHandler; #[async_trait] impl McpTool for ListTechDebtHandler { fn name(&self) -> &'static str { "list_tech_debt" } fn schema(&self) -> Value { crate::mcp::tool_def::("list_tech_debt", "Execute list_tech_debt") } async fn execute(&self, args: Value, state: Arc) -> Result { let req: ListTechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let data = state.tech_debts.read_with(|debts| { let filtered: Vec<_> = debts.iter().filter(|d| { d.namespace == req.namespace && (req.include_resolved || !d.is_resolved) }).collect(); serde_json::to_string(&filtered).unwrap_or_default() }); Ok(data.to_string()) } } pub struct OmniSearchHandler; #[async_trait] impl McpTool for OmniSearchHandler { fn name(&self) -> &'static str { "omni_search" } fn schema(&self) -> Value { crate::mcp::tool_def::("omni_search", "Execute omni_search") } async fn execute(&self, args: Value, state: Arc) -> Result { let req: OmniSearchTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let matches = { let idx = state.search_index.read().unwrap_or_else(|e| e.into_inner()); idx.search(&req.query, req.namespace.as_deref()) .unwrap_or_default() }; let kg_json = state.read_graph(|full| { let mut kg_entities = std::collections::HashMap::new(); for (id, doc_type, _, _, _) in &matches { if doc_type == "entity" && let Some(e) = full.entities.get(id) { kg_entities.insert(id, e); } } serde_json::to_value(&kg_entities).unwrap_or_default() }); let tasks_json = state.tasks.read_with(|all_tasks| { let filtered: Vec<_> = all_tasks.iter().filter(|t| { matches.iter().any(|(id, typ, _, _, _)| id == &t.id && typ == "task") }).collect(); serde_json::to_value(&filtered).unwrap_or_default() }); let snippets_json = state.snippets.read_with(|all_snippets| { let filtered: Vec<_> = all_snippets.iter().filter(|s| { matches.iter().any(|(id, typ, _, _, _)| id == &s.name && typ == "snippet") }).collect(); serde_json::to_value(&filtered).unwrap_or_default() }); let adrs_json = state.adrs.read_with(|all_adrs| { let filtered: Vec<_> = all_adrs.iter().filter(|a| { matches.iter().any(|(id, typ, _, _, _)| id == &a.id && typ == "adr") }).collect(); serde_json::to_value(&filtered).unwrap_or_default() }); let q = req.query; let tech_debts_json = state.tech_debts.read_with(|debts| { let filtered: Vec<_> = debts.iter().filter(|d| { req.namespace.as_ref().is_none_or(|ns| d.namespace == *ns) && (contains_ignore_ascii_case(&d.description, &q) || contains_ignore_ascii_case(&d.ideal_solution, &q)) }).collect(); serde_json::to_value(&filtered).unwrap_or_default() }); let memos_json = state.handoff_memos.read_with(|memos| { let filtered: Vec<_> = memos.iter().filter(|m| { req.namespace.as_ref().is_none_or(|ns| m.namespace == *ns) && contains_ignore_ascii_case(&m.content, &q) }).collect(); serde_json::to_value(&filtered).unwrap_or_default() }); let error_fixes_json = state.error_fixes.read_with(|fixes| { let filtered: Vec<_> = fixes.iter().filter(|f| { contains_ignore_ascii_case(&f.signature, &q) || contains_ignore_ascii_case(&f.solution, &q) }).collect(); serde_json::to_value(&filtered).unwrap_or_default() }); let report = serde_json::json!({ "knowledge_graph": kg_json, "tasks": tasks_json, "snippets": snippets_json, "adrs": adrs_json, "tech_debts": tech_debts_json, "handoff_memos": memos_json, "error_fixes": error_fixes_json }); Ok(report.to_string()) } } pub struct GetProjectHealthHandler; #[async_trait] impl McpTool for GetProjectHealthHandler { fn name(&self) -> &'static str { "get_project_health" } fn schema(&self) -> Value { crate::mcp::tool_def::( "get_project_health", "Execute get_project_health", ) } async fn execute(&self, args: Value, state: Arc) -> Result { let req: GetProjectHealthTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let active_tasks = state.tasks.read_with(|tasks| tasks.iter().filter(|t| t.status != "done").count()); let unresolved_debt = state.tech_debts.read_with(|debts| debts.iter().filter(|d| d.namespace == req.namespace && !d.is_resolved).count()); let unread_memos = state.handoff_memos.read_with(|memos| memos.iter().filter(|m| m.namespace == req.namespace).count()); let active_milestones = state.milestones.read_with(|milestones| milestones.iter().filter(|m| m.namespace == req.namespace && m.status != "done").count()); let remaining_checklists = state.pr_checklists.read_with(|checklists| checklists.iter().filter(|c| c.namespace == req.namespace).count()); let report = serde_json::json!({ "active_tasks": active_tasks, "unresolved_tech_debt": unresolved_debt, "unread_handoff_memos": unread_memos, "active_milestones": active_milestones, "remaining_pr_checklist_items": remaining_checklists }); Ok(report.to_string()) } } use crate::handlers_v2::utils::*;