diff --git a/server/src/error.rs b/server/src/error.rs index 8b755e1..4f39ed2 100644 --- a/server/src/error.rs +++ b/server/src/error.rs @@ -103,3 +103,15 @@ mod tests { assert_eq!(err.to_string(), "Bad Request: test"); } } + +impl From for AppError { + fn from(msg: String) -> Self { + AppError::Internal(msg) + } +} + +impl From<&str> for AppError { + fn from(msg: &str) -> Self { + AppError::Internal(msg.to_string()) + } +} diff --git a/server/src/handlers/ast.rs b/server/src/handlers/ast.rs index b3ef583..85195d1 100644 --- a/server/src/handlers/ast.rs +++ b/server/src/handlers/ast.rs @@ -21,13 +21,13 @@ impl McpTool for ReadFileSkeletonHandler { ) } - async fn execute(&self, args: Value, _state: Arc) -> Result { + async fn execute(&self, args: Value, _state: Arc) -> crate::error::Result { let tool_args: ReadFileSkeletonTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let file_path = tool_args.file_path.clone(); - let result = tokio::task::spawn_blocking(move || -> Result { + let result = tokio::task::spawn_blocking(move || -> crate::error::Result { let code = std::fs::read_to_string(&file_path) - .map_err(|e| format!("Failed to read file: {}", e))?; + .map_err(|e| crate::error::AppError::Internal(format!("Failed to read file: {}", e)))?; let mut parser = Parser::new(); @@ -49,7 +49,7 @@ impl McpTool for ReadFileSkeletonHandler { parser.set_language(&language.into()).map_err(|e| e.to_string())?; - let tree = parser.parse(&code, None).ok_or("Failed to parse code")?; + let tree = parser.parse(&code, None).ok_or_else(|| crate::error::AppError::Internal("Failed to parse code".to_string()))?; let mut result_skeleton = String::new(); fn extract_skeleton(node: Node, code: &str, out: &mut String, depth: usize) { @@ -97,7 +97,7 @@ impl McpTool for ReadFileSkeletonHandler { } }) .await - .map_err(|e| format!("Task panic: {}", e))??; + .map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??; Ok(result) } @@ -119,13 +119,13 @@ impl McpTool for ReplaceAstNodeHandler { ) } - async fn execute(&self, args: Value, _state: Arc) -> Result { + async fn execute(&self, args: Value, _state: Arc) -> crate::error::Result { let tool_args: ReplaceAstNodeTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let file_path = tool_args.file_path.clone(); - let result = tokio::task::spawn_blocking(move || -> Result { + let result = tokio::task::spawn_blocking(move || -> crate::error::Result { let code = std::fs::read_to_string(&file_path) - .map_err(|e| format!("Failed to read file: {}", e))?; + .map_err(|e| crate::error::AppError::Internal(format!("Failed to read file: {}", e)))?; let mut parser = Parser::new(); @@ -142,11 +142,11 @@ impl McpTool for ReplaceAstNodeHandler { "c" | "h" => tree_sitter_c::LANGUAGE, "cpp" | "cc" | "cxx" | "hpp" | "hxx" => tree_sitter_cpp::LANGUAGE, "go" => tree_sitter_go::LANGUAGE, - _ => return Err(format!("Unsupported language for AST replacement: {}", ext)), + _ => return Err(crate::error::AppError::Internal(format!("Unsupported language for AST replacement: {}", ext))), }; parser.set_language(&language.into()).map_err(|e| e.to_string())?; - let tree = parser.parse(&code, None).ok_or("Failed to parse code")?; + let tree = parser.parse(&code, None).ok_or_else(|| crate::error::AppError::Internal("Failed to parse code".to_string()))?; // Search for the node fn find_node<'a>(node: Node<'a>, code: &str, target_type: &str, target_name: &str) -> Option> { @@ -187,11 +187,11 @@ impl McpTool for ReplaceAstNodeHandler { std::fs::write(&file_path, new_file_content).map_err(|e| e.to_string())?; Ok(format!("Successfully replaced node {} of type {} in {}", tool_args.node_name, tool_args.node_type, file_path)) } else { - Err(format!("Could not find node {} of type {}", tool_args.node_name, tool_args.node_type)) + Err(crate::error::AppError::Internal(format!("Could not find node {} of type {}", tool_args.node_name, tool_args.node_type))) } }) .await - .map_err(|e| format!("Task panic: {}", e))??; + .map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??; Ok(result) } diff --git a/server/src/handlers/env.rs b/server/src/handlers/env.rs index d42ac9f..3651cd0 100644 --- a/server/src/handlers/env.rs +++ b/server/src/handlers/env.rs @@ -20,7 +20,7 @@ impl McpTool for UpdateEnvFingerprintHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: UpdateEnvFingerprintTool = serde_json::from_value(args).map_err(|e| e.to_string())?; state.env_fingerprints.modify(|fps| { @@ -54,14 +54,14 @@ impl McpTool for ReadEnvFingerprintHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: ReadEnvFingerprintTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let data = state .env_fingerprints .read_with(|fps| fps.get(&req.namespace).cloned()); if let Some(fp) = data { - let data = serde_json::to_string(&fp).map_err(|e| e.to_string())?; + let data = Ok::(serde_json::to_string(&fp)?)?; Ok(data) } else { Ok("{}".to_string()) @@ -84,7 +84,7 @@ impl McpTool for LogEnvRequirementHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: LogEnvRequirementTool = serde_json::from_value(args).map_err(|e| e.to_string())?; state.env_requirements.modify(|reqs| { reqs.retain(|r| !(r.namespace == req.namespace && r.key == req.key)); @@ -114,7 +114,7 @@ impl McpTool for RegisterEnvironmentHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: RegisterEnvironmentTool = serde_json::from_value(args).map_err(|e| e.to_string())?; state.environments.modify(|envs| { @@ -147,7 +147,7 @@ impl McpTool for GetEnvironmentDetailsHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: GetEnvironmentDetailsTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let data = state.environments.read_with(|envs| { @@ -155,7 +155,7 @@ impl McpTool for GetEnvironmentDetailsHandler { .iter() .filter(|e| e.namespace == req.namespace) .collect(); - serde_json::to_string(&filtered).map_err(|e| e.to_string()) + Ok::(serde_json::to_string(&filtered)?) })?; Ok(data) } @@ -180,14 +180,13 @@ mod tests { } }); - let res = update_handler.execute(args, state.clone()).await.unwrap(); + let res = update_handler.execute(args, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert_eq!(res, "Env fingerprint updated"); let read_handler = ReadEnvFingerprintHandler; let res2 = read_handler .execute(json!({"namespace": "global"}), state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(res2.contains("rustc")); assert!(res2.contains("1.70.0")); } @@ -212,8 +211,7 @@ mod tests { let handler = GetEnvironmentDetailsHandler; let res = handler .execute(json!({"namespace": "global"}), state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(res.contains("global")); } @@ -232,7 +230,7 @@ mod tests { "context": "For database access", "namespace": "global" }); - let res1 = req_handler.execute(args_req, state.clone()).await.unwrap(); + let res1 = req_handler.execute(args_req, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert_eq!(res1, "Env requirement logged"); let reg_handler = RegisterEnvironmentHandler; @@ -243,14 +241,13 @@ mod tests { "requires_vpn": true, "namespace": "global" }); - let res2 = reg_handler.execute(args_reg, state.clone()).await.unwrap(); + let res2 = reg_handler.execute(args_reg, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert_eq!(res2, "Environment registered"); let get_handler = GetEnvironmentDetailsHandler; let res3 = get_handler .execute(json!({"namespace": "global"}), state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(res3.contains("prod.local")); assert!(!res3.is_empty()); } diff --git a/server/src/handlers/git.rs b/server/src/handlers/git.rs index 99bcc6c..5a5e12d 100644 --- a/server/src/handlers/git.rs +++ b/server/src/handlers/git.rs @@ -21,10 +21,10 @@ impl McpTool for GetActiveWorktreeContextHandler { ) } - async fn execute(&self, _args: Value, _state: Arc) -> Result { - let result = tokio::task::spawn_blocking(move || -> Result { + async fn execute(&self, _args: Value, _state: Arc) -> crate::error::Result { + let result = tokio::task::spawn_blocking(move || -> crate::error::Result { let cwd = env::current_dir().map_err(|e| e.to_string())?; - let repo = git2::Repository::discover(&cwd).map_err(|e| format!("Not in a git repository: {}", e))?; + let repo = git2::Repository::discover(&cwd).map_err(|e| crate::error::AppError::Internal(format!("Not in a git repository: {}", e)))?; let mut branch_name = String::new(); if let Ok(head) = repo.head() @@ -70,8 +70,8 @@ impl McpTool for GetActiveWorktreeContextHandler { })) }) .await - .map_err(|e| format!("Task panic: {}", e))??; + .map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??; - Ok(serde_json::to_string_pretty(&result).unwrap()) + Ok::(serde_json::to_string_pretty(&result)?) } } diff --git a/server/src/handlers/graph.rs b/server/src/handlers/graph.rs index dcaf1a0..154eada 100644 --- a/server/src/handlers/graph.rs +++ b/server/src/handlers/graph.rs @@ -24,7 +24,7 @@ impl McpTool for QueryGraphPathHandler { crate::mcp::tool_def::("query_graph_path", "Execute query_graph_path") } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: crate::tools::QueryGraphPathTool = serde_json::from_value(args).map_err(|e| e.to_string())?; state.read_graph(|graph| { @@ -115,7 +115,7 @@ impl McpTool for CreateEntitiesHandler { crate::mcp::tool_def::("create_entities", "Execute create_entities") } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: CreateEntitiesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let mut inserted = Vec::new(); state.modify_graph(|g| { @@ -146,7 +146,7 @@ impl McpTool for CreateRelationsHandler { crate::mcp::tool_def::("create_relations", "Execute create_relations") } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: CreateRelationsTool = match serde_json::from_value(args.clone()) { Ok(r) => r, Err(e) => { @@ -155,12 +155,12 @@ impl McpTool for CreateRelationsHandler { || err_msg.contains("missing field `to`") || err_msg.contains("missing field `relation_type`") { - return Err(format!( + return Err(crate::error::AppError::Internal(format!( "Schema error: {}. Note that the relation schema strictly uses 'from', 'to', and 'relation_type' (not 'source', 'target', or 'relationType'). Please correct your tool call arguments.", err_msg - )); + ))); } - return Err(err_msg); + return Err(err_msg.into()); } }; let mut missing_nodes = std::collections::HashSet::new(); @@ -184,10 +184,9 @@ impl McpTool for CreateRelationsHandler { }); if !missing_nodes.is_empty() { let missing: Vec<_> = missing_nodes.into_iter().collect(); - return Err(format!( + return Err(crate::error::AppError::Internal(format!( "Error: Relations dropped due to missing entities: {}", - missing.join(", ") - )); + missing.join(", ")))); } Ok("Relations created".to_string()) } @@ -205,7 +204,7 @@ impl McpTool for AddObservationsHandler { crate::mcp::tool_def::("add_observations", "Execute add_observations") } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: AddObservationsTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let mut missing_entities = Vec::new(); state.modify_graph(|g| { @@ -218,10 +217,9 @@ impl McpTool for AddObservationsHandler { } }); if !missing_entities.is_empty() { - return Err(format!( + return Err(crate::error::AppError::Internal(format!( "Error: Observations dropped for missing entities: {}", - missing_entities.join(", ") - )); + missing_entities.join(", ")))); } Ok("Observations added".to_string()) } @@ -239,7 +237,7 @@ impl McpTool for DeleteEntitiesHandler { crate::mcp::tool_def::("delete_entities", "Execute delete_entities") } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: DeleteEntitiesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let to_delete: std::collections::HashSet<_> = req.entity_names.into_iter().collect(); let mut missing = Vec::new(); @@ -255,10 +253,9 @@ impl McpTool for DeleteEntitiesHandler { }); if !missing.is_empty() { - return Err(format!( + return Err(crate::error::AppError::Internal(format!( "Error: Entities not found: {}. Please use the search_nodes or read_graph tools to verify the exact entity names.", - missing.join(", ") - )); + missing.join(", ")))); } let idx = state.get_search_index(); @@ -284,7 +281,7 @@ impl McpTool for DeleteObservationsHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: DeleteObservationsTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let mut missing = Vec::new(); @@ -299,10 +296,9 @@ impl McpTool for DeleteObservationsHandler { } }); if !missing.is_empty() { - return Err(format!( + return Err(crate::error::AppError::Internal(format!( "Error: Entities not found: {}. Please use the search_nodes or read_graph tools to verify the exact entity names.", - missing.join(", ") - )); + missing.join(", ")))); } Ok("Observations deleted".to_string()) } @@ -320,7 +316,7 @@ impl McpTool for DeleteRelationsHandler { crate::mcp::tool_def::("delete_relations", "Execute delete_relations") } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: DeleteRelationsTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let mut missing_count = 0; state.modify_graph(|master| { @@ -330,10 +326,10 @@ impl McpTool for DeleteRelationsHandler { missing_count = to_rem.len() - (initial_len - master.relations.len()); }); if missing_count > 0 { - return Err(format!( + return Err(crate::error::AppError::Internal(format!( "Error: {} relation(s) not found in graph. Please verify exact relation properties using read_graph.", missing_count - )); + ))); } Ok("Relations deleted".to_string()) } @@ -351,9 +347,9 @@ impl McpTool for ReadGraphHandler { crate::mcp::tool_def::("read_graph", "Execute read_graph") } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: ReadGraphTool = serde_json::from_value(args).map_err(|e| e.to_string())?; - let data = state.read_graph(|full| -> Result { + let data = state.read_graph(|full| -> crate::error::Result { if let Some(ns) = req.namespace { let mut filtered = BorrowedGraph::default(); for (k, v) in &full.entities { @@ -366,9 +362,9 @@ impl McpTool for ReadGraphHandler { filtered.relations.push(r); } } - serde_json::to_string(&filtered).map_err(|e| e.to_string()) + Ok::(serde_json::to_string(&filtered)?) } else { - serde_json::to_string(full).map_err(|e| e.to_string()) + Ok::(serde_json::to_string(full)?) } })?; Ok(data) @@ -387,7 +383,7 @@ impl McpTool for SearchNodesHandler { crate::mcp::tool_def::("search_nodes", "Execute search_nodes") } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: SearchNodesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let matches = if let Ok(idx) = state.search_index.read() { idx.search(&req.query, req.namespace.as_deref()) @@ -396,7 +392,7 @@ impl McpTool for SearchNodesHandler { vec![] }; - let data = state.read_graph(|full| -> Result { + let data = state.read_graph(|full| -> crate::error::Result { let mut result = BorrowedGraph::default(); for (id, doc_type, _, _, _) in &matches { if doc_type == "entity" @@ -405,7 +401,7 @@ impl McpTool for SearchNodesHandler { result.entities.insert(id, e); } } - serde_json::to_string(&result).map_err(|e| e.to_string()) + Ok::(serde_json::to_string(&result)?) })?; Ok(data) } @@ -423,9 +419,9 @@ impl McpTool for OpenNodesHandler { crate::mcp::tool_def::("open_nodes", "Execute open_nodes") } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: OpenNodesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; - let data = state.read_graph(|full| -> Result { + let data = state.read_graph(|full| -> crate::error::Result { let targets: HashSet<&str> = req.names.iter().map(|s| s.as_str()).collect(); let mut result = BorrowedGraph::default(); let mut connected: HashSet<&str> = HashSet::new(); @@ -443,7 +439,7 @@ impl McpTool for OpenNodesHandler { result.entities.insert(name, e); } } - serde_json::to_string(&result).map_err(|e| e.to_string()) + Ok::(serde_json::to_string(&result)?) })?; Ok(data) } @@ -461,7 +457,7 @@ impl McpTool for VisualizeGraphHandler { crate::mcp::tool_def::("visualize_graph", "Execute visualize_graph") } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: VisualizeGraphTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let query = req.query.unwrap_or_default(); let mut output = state.read_graph(|full| { @@ -550,7 +546,7 @@ impl McpTool for CondenseEntityHandler { crate::mcp::tool_def::("condense_entity", "Execute condense_entity") } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: CondenseEntityTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let mut missing = false; state.modify_graph(|master| { @@ -561,10 +557,10 @@ impl McpTool for CondenseEntityHandler { } }); if missing { - return Err(format!( + return Err(crate::error::AppError::Internal(format!( "Error: Entity '{}' not found. Please verify the exact entity name using search_nodes.", req.entity_name - )); + ))); } Ok("Entity condensed".to_string()) } @@ -582,7 +578,7 @@ impl McpTool for MergeEntitiesHandler { crate::mcp::tool_def::("merge_entities", "Execute merge_entities") } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: MergeEntitiesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let mut missing = false; state.modify_graph(|master| { @@ -616,10 +612,10 @@ impl McpTool for MergeEntitiesHandler { }); }); if missing { - return Err(format!( + return Err(crate::error::AppError::Internal(format!( "Error: Source entity '{}' not found. Please verify the exact entity name using search_nodes.", req.source_entity - )); + ))); } Ok("Entities merged".to_string()) } @@ -637,7 +633,7 @@ impl McpTool for FindOrphansHandler { crate::mcp::tool_def::("find_orphans", "Execute find_orphans") } - async fn execute(&self, _args: Value, state: Arc) -> Result { + async fn execute(&self, _args: Value, state: Arc) -> crate::error::Result { let orphans = state.read_graph(|full| { let mut connected = std::collections::HashSet::new(); for r in &full.relations { @@ -650,7 +646,7 @@ impl McpTool for FindOrphansHandler { .cloned() .collect::>() }); - let data = serde_json::to_string(&orphans).map_err(|e| e.to_string())?; + let data = Ok::(serde_json::to_string(&orphans)?)?; Ok(data) } } @@ -675,7 +671,7 @@ mod tests { ] }); - let res = create_handler.execute(args, state.clone()).await.unwrap(); + let res = create_handler.execute(args, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert_eq!(res, "Entities created"); // Ensure graph contains the entity @@ -720,7 +716,7 @@ mod tests { {"from": "A", "to": "B", "relation_type": "knows"} ] }); - let res = handler.execute(args, state.clone()).await.unwrap(); + let res = handler.execute(args, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert_eq!(res, "Relations created"); // Test semantic LLM schema feedback (User request) @@ -759,28 +755,25 @@ mod tests { {"entity_name": "A", "contents": ["Obs 1", "Obs 2"]} ] }); - let res1 = add_obs.execute(args_obs, state.clone()).await.unwrap(); + let res1 = add_obs.execute(args_obs, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert_eq!(res1, "Observations added"); let read_graph = ReadGraphHandler; let res2 = read_graph .execute(json!({"namespace": "global"}), state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(res2.contains("Obs 1")); assert!(res2.contains("Obs 2")); let del_entity = DeleteEntitiesHandler; let res4 = del_entity .execute(json!({"entity_names": ["A"]}), state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert_eq!(res4, "Entities deleted"); let res5 = read_graph .execute(json!({"namespace": "global"}), state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(!res5.contains("A")); } @@ -798,8 +791,7 @@ mod tests { }); create_handler .execute(args_ent, state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; let rel_handler = CreateRelationsHandler; let args_rel = json!({ @@ -807,28 +799,25 @@ mod tests { {"from": "X", "to": "Y", "relation_type": "depends_on", "namespace": "global"} ] }); - rel_handler.execute(args_rel, state.clone()).await.unwrap(); + rel_handler.execute(args_rel, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; let read_handler = ReadGraphHandler; let res_read = read_handler .execute(json!({"namespace": "global"}), state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(res_read.contains("X")); assert!(res_read.contains("depends_on")); let open_handler = OpenNodesHandler; let res_open = open_handler .execute(json!({"names": ["X"]}), state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(res_open.contains("Y")); let viz_handler = VisualizeGraphHandler; let res_viz = viz_handler .execute(json!({"query": "X"}), state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(!res_viz.is_empty()); let condense = CondenseEntityHandler; @@ -837,8 +826,7 @@ mod tests { json!({"entity_name": "X", "summarized_observations": ["X condensed"]}), state.clone(), ) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert_eq!(res_cond, "Entity condensed"); let merge = MergeEntitiesHandler; @@ -847,12 +835,11 @@ mod tests { json!({"source_entity": "X", "target_entity": "Y"}), state.clone(), ) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert_eq!(res_merge, "Entities merged"); let orphans = FindOrphansHandler; - let res_orphans = orphans.execute(json!({}), state.clone()).await.unwrap(); + let res_orphans = orphans.execute(json!({}), state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(!res_orphans.contains("Y")); } } diff --git a/server/src/handlers/logs.rs b/server/src/handlers/logs.rs index f0f2fae..1ad2633 100644 --- a/server/src/handlers/logs.rs +++ b/server/src/handlers/logs.rs @@ -22,10 +22,10 @@ impl McpTool for WatchProcessLogsHandler { ) } - async fn execute(&self, args: Value, _state: Arc) -> Result { + async fn execute(&self, args: Value, _state: Arc) -> crate::error::Result { let tool_args: WatchProcessLogsTool = serde_json::from_value(args).map_err(|e| e.to_string())?; if !std::path::Path::new(&tool_args.file_path).exists() { - return Err(format!("File does not exist: {}", tool_args.file_path)); + return Err(crate::error::AppError::Internal(format!("File does not exist: {}", tool_args.file_path))); } Ok(format!("Started watching logs for {}", tool_args.file_path)) } @@ -46,11 +46,11 @@ impl McpTool for GetRecentLogsHandler { ) } - async fn execute(&self, args: Value, _state: Arc) -> Result { + async fn execute(&self, args: Value, _state: Arc) -> crate::error::Result { let tool_args: GetRecentLogsTool = serde_json::from_value(args).map_err(|e| e.to_string())?; - let result = tokio::task::spawn_blocking(move || -> Result { - let mut file = File::open(&tool_args.file_path).map_err(|e| format!("Failed to open file: {}", e))?; + let result = tokio::task::spawn_blocking(move || -> crate::error::Result { + let mut file = File::open(&tool_args.file_path).map_err(|e| crate::error::AppError::Internal(format!("Failed to open file: {}", e)))?; let len = file.metadata().map_err(|e| e.to_string())?.len(); let read_size = std::cmp::min(16 * 1024, len); @@ -69,7 +69,7 @@ impl McpTool for GetRecentLogsHandler { Ok(recent_lines) }) .await - .map_err(|e| format!("Task panic: {}", e))??; + .map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??; Ok(result) } diff --git a/server/src/handlers/meta.rs b/server/src/handlers/meta.rs index 9f84a36..ae37f57 100644 --- a/server/src/handlers/meta.rs +++ b/server/src/handlers/meta.rs @@ -18,7 +18,7 @@ impl McpTool for LogDecisionHandler { crate::mcp::tool_def::("log_decision", "Execute log_decision") } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: LogDecisionTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let idx = state.get_search_index(); @@ -68,7 +68,7 @@ impl McpTool for QueryDecisionsHandler { crate::mcp::tool_def::("query_decisions", "Execute query_decisions") } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::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 @@ -83,7 +83,7 @@ impl McpTool for QueryDecisionsHandler { } }) .collect(); - serde_json::to_string(&filtered).map_err(|e| e.to_string()) + Ok::(serde_json::to_string(&filtered)?) })?; Ok(data) } @@ -104,7 +104,7 @@ impl McpTool for DeleteDecisionHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: crate::tools::DeleteDecisionTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let mut found = false; @@ -119,7 +119,7 @@ impl McpTool for DeleteDecisionHandler { state.rebuild_index().await; Ok("Decision deleted successfully".to_string()) } else { - Err("Decision not found".to_string()) + Err(crate::error::AppError::Internal("Decision not found".to_string())) } } } @@ -136,7 +136,7 @@ impl McpTool for LogErrorFixHandler { crate::mcp::tool_def::("log_error_fix", "Execute log_error_fix") } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: LogErrorFixTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let text_to_embed = format!("Signature: {}\nSolution: {}", req.signature, req.solution); let embedding = crate::embedding::generate_embedding_async(text_to_embed).await.ok(); @@ -169,7 +169,7 @@ impl McpTool for SearchErrorFixesHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::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| { @@ -180,7 +180,7 @@ impl McpTool for SearchErrorFixesHandler { || contains_ignore_ascii_case(&f.solution, &q) }) .collect(); - serde_json::to_string(&filtered).map_err(|e| e.to_string()) + Ok::(serde_json::to_string(&filtered)?) })?; Ok(data) } @@ -198,7 +198,7 @@ impl McpTool for LogCodeChangeHandler { crate::mcp::tool_def::("log_code_change", "Execute log_code_change") } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: LogCodeChangeTool = serde_json::from_value(args).map_err(|e| e.to_string())?; state.ledger.modify(|ledger| { ledger.push(CodeChange { @@ -228,10 +228,10 @@ impl McpTool for QueryRecentChangesHandler { ) } - async fn execute(&self, _args: Value, state: Arc) -> Result { + async fn execute(&self, _args: Value, state: Arc) -> crate::error::Result { let data = state .ledger - .read_with(|l| serde_json::to_string(l).map_err(|e| e.to_string()))?; + .read_with(|l| Ok::(serde_json::to_string(l)?))?; Ok(data) } } @@ -248,7 +248,7 @@ impl McpTool for LearnPreferenceHandler { crate::mcp::tool_def::("learn_preference", "Execute learn_preference") } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: LearnPreferenceTool = serde_json::from_value(args).map_err(|e| e.to_string())?; state.prefs.modify(|prefs| { prefs.insert( @@ -276,10 +276,10 @@ impl McpTool for ReadPreferencesHandler { crate::mcp::tool_def::("read_preferences", "Execute read_preferences") } - async fn execute(&self, _args: Value, state: Arc) -> Result { + async fn execute(&self, _args: Value, state: Arc) -> crate::error::Result { state .prefs - .read_with(|prefs| serde_json::to_string(prefs).map_err(|e| e.to_string())) + .read_with(|prefs| Ok::(serde_json::to_string(prefs)?)) } } @@ -295,7 +295,7 @@ impl McpTool for LogTechDebtHandler { crate::mcp::tool_def::("log_tech_debt", "Execute log_tech_debt") } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: LogTechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let text_to_embed = format!("Description: {}\nIdeal Solution: {}", req.description, req.ideal_solution); let embedding = crate::embedding::generate_embedding_async(text_to_embed).await.ok(); @@ -331,7 +331,7 @@ impl McpTool for ResolveTechDebtHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: ResolveTechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let mut found = false; state.tech_debts.modify(|debts| { @@ -346,10 +346,7 @@ impl McpTool for ResolveTechDebtHandler { if found { Ok("Tech debt resolved".to_string()) } else { - Err( - "Tech debt not found. Please verify the tech debt ID using list_tech_debt." - .to_string(), - ) + Err(crate::error::AppError::Internal("Tech debt not found. Please verify the tech debt ID using list_tech_debt.".to_string())) } } } @@ -366,7 +363,7 @@ impl McpTool for ListTechDebtHandler { crate::mcp::tool_def::("list_tech_debt", "Execute list_tech_debt") } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::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 @@ -375,7 +372,7 @@ impl McpTool for ListTechDebtHandler { d.namespace == req.namespace && (req.include_resolved || !d.is_resolved) }) .collect(); - serde_json::to_string(&filtered).map_err(|e| e.to_string()) + Ok::(serde_json::to_string(&filtered)?) })?; Ok(data) } @@ -393,7 +390,7 @@ impl McpTool for OmniSearchHandler { crate::mcp::tool_def::("omni_search", "Execute omni_search") } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: OmniSearchTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let limit = req.limit.unwrap_or(5); let include_body = req.include_body.unwrap_or(false); @@ -403,10 +400,10 @@ impl McpTool for OmniSearchHandler { { Ok(m) => m, Err(e) => { - return Err(format!( + return Err(crate::error::AppError::Internal(format!( "Search query failed (possibly malformed Lucene syntax). Error: {}", e - )); + ))); } }; // tracing::info!("OMNI SEARCH MATCHES: {:?}", matches); @@ -614,7 +611,7 @@ impl McpTool for GetProjectHealthHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: GetProjectHealthTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let active_tasks = state .tasks @@ -677,7 +674,7 @@ mod tests { "git_branch": "main" }); - let res = handler.execute(args, state.clone()).await.unwrap(); + let res = handler.execute(args, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(res.contains("Error fix logged")); } @@ -689,7 +686,7 @@ mod tests { let handler = GetProjectHealthHandler; let args = json!({"namespace": "global"}); - let res = handler.execute(args, state.clone()).await.unwrap(); + let res = handler.execute(args, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(res.contains("unresolved_tech_debt")); } @@ -707,8 +704,7 @@ mod tests { }); let res1 = decision_handler .execute(args_dec, state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert_eq!(res1, "Decision logged as ADR-0001"); let debt_handler = LogTechDebtHandler; @@ -724,8 +720,7 @@ mod tests { }); let res2 = debt_handler .execute(args_debt, state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert_eq!(res2, "Tech debt logged"); let list_debt = ListTechDebtHandler; @@ -734,8 +729,7 @@ mod tests { json!({"namespace": "global", "include_resolved": false}), state.clone(), ) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(res3.contains("Hardcoded path")); let pref_handler = LearnPreferenceHandler; @@ -745,12 +739,11 @@ mod tests { }); let res4 = pref_handler .execute(args_pref, state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert_eq!(res4, "Preference learned"); let read_pref = ReadPreferencesHandler; - let res5 = read_pref.execute(json!({}), state.clone()).await.unwrap(); + let res5 = read_pref.execute(json!({}), state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(res5.contains("use spaces")); } @@ -768,14 +761,12 @@ mod tests { }); code_handler .execute(args_code, state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; let query_changes = QueryRecentChangesHandler; let res_changes = query_changes .execute(json!({}), state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(res_changes.contains("main.rs")); let debt_handler = LogTechDebtHandler; @@ -791,8 +782,7 @@ mod tests { }); debt_handler .execute(args_debt, state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; // resolve it let list_debt = ListTechDebtHandler; @@ -801,16 +791,14 @@ mod tests { json!({"namespace": "global", "include_resolved": false}), state.clone(), ) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; let uuid_start = debt_list.find("id\":\"").unwrap() + 5; let uuid = &debt_list[uuid_start..uuid_start + 36]; let resolve_debt = ResolveTechDebtHandler; resolve_debt .execute(json!({"id": uuid}), state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; } #[tokio::test] @@ -844,8 +832,7 @@ mod tests { let omni = OmniSearchHandler; let omni_res = omni .execute(json!({"query": "Omni"}), state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; // tracing::info!("OMNI RES: {}", omni_res); assert!( omni_res.contains("omni-1"), diff --git a/server/src/handlers/notes.rs b/server/src/handlers/notes.rs index 244c464..6e396d8 100644 --- a/server/src/handlers/notes.rs +++ b/server/src/handlers/notes.rs @@ -19,7 +19,7 @@ impl McpTool for AddStickyNoteHandler { crate::mcp::tool_def::("add_sticky_note", "Execute add_sticky_note") } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: AddStickyNoteTool = serde_json::from_value(args).map_err(|e| e.to_string())?; state.sticky.modify(|notes| { notes.push(StickyNote { @@ -47,10 +47,10 @@ impl McpTool for ReadStickyNotesHandler { ) } - async fn execute(&self, _args: Value, state: Arc) -> Result { + async fn execute(&self, _args: Value, state: Arc) -> crate::error::Result { let data = state .sticky - .read_with(|s| serde_json::to_string(s).map_err(|e| e.to_string()))?; + .read_with(|s| Ok::(serde_json::to_string(s)?))?; Ok(data) } } @@ -70,7 +70,7 @@ impl McpTool for DeleteStickyNoteHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: DeleteStickyNoteTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let mut success = false; state.sticky.modify(|notes| { @@ -82,7 +82,7 @@ impl McpTool for DeleteStickyNoteHandler { if success { Ok("Sticky note deleted.".to_string()) } else { - Err("Invalid sticky note index.".to_string()) + Err(crate::error::AppError::Internal("Invalid sticky note index.".to_string())) } } } @@ -102,7 +102,7 @@ impl McpTool for ClearStickyNotesHandler { ) } - async fn execute(&self, _args: Value, state: Arc) -> Result { + async fn execute(&self, _args: Value, state: Arc) -> crate::error::Result { state.sticky.modify(|notes| { notes.clear(); }); @@ -125,7 +125,7 @@ impl McpTool for LeaveHandoffMemoHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: LeaveHandoffMemoTool = serde_json::from_value(args).map_err(|e| e.to_string())?; state.handoff_memos.modify(|memos| { memos.push(crate::models::HandoffMemo { @@ -156,7 +156,7 @@ impl McpTool for ReadHandoffMemosHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: ReadHandoffMemosTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let data = state.handoff_memos.read_with(|items| { let filtered: Vec<_> = items @@ -169,7 +169,7 @@ impl McpTool for ReadHandoffMemosHandler { } }) .collect(); - serde_json::to_string(&filtered).map_err(|e| e.to_string()) + Ok::(serde_json::to_string(&filtered)?) })?; Ok(data) } @@ -190,7 +190,7 @@ impl McpTool for ClearHandoffMemosHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: ClearHandoffMemosTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let ids: HashSet<_> = req.ids.into_iter().collect(); state @@ -215,7 +215,7 @@ impl McpTool for AddSessionSummaryHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: AddSessionSummaryTool = serde_json::from_value(args).map_err(|e| e.to_string())?; state.session_summaries.modify(|summaries| { summaries.push(crate::models::SessionSummary { @@ -244,7 +244,7 @@ impl McpTool for GenerateStandupReportHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: GenerateStandupReportTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let cutoff = crate::handlers::utils::now_secs().saturating_sub(req.hours_lookback * 3600); @@ -280,25 +280,23 @@ mod tests { "content": "Buy milk", }); - let res = add_handler.execute(args, state.clone()).await.unwrap(); + let res = add_handler.execute(args, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(res.contains("Sticky note added")); let read_handler = ReadStickyNotesHandler; let res2 = read_handler .execute(json!({}), state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(res2.contains("Buy milk")); let delete_handler = DeleteStickyNoteHandler; let args2 = json!({"index": 1}); - let res3 = delete_handler.execute(args2, state.clone()).await.unwrap(); + let res3 = delete_handler.execute(args2, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert_eq!(res3, "Sticky note deleted."); let res4 = read_handler .execute(json!({}), state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(!res4.contains("Buy milk")); } @@ -314,14 +312,13 @@ mod tests { "namespace": "global" }); - let res = handoff_handler.execute(args, state.clone()).await.unwrap(); + let res = handoff_handler.execute(args, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert_eq!(res, "Handoff memo left"); let read_handoff = ReadHandoffMemosHandler; let res2 = read_handoff .execute(json!({"namespace": "global"}), state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(res2.contains("Finished implementing graph tests")); let summary_handler = AddSessionSummaryHandler; @@ -331,8 +328,7 @@ mod tests { }); let res3 = summary_handler .execute(args_sum, state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert_eq!(res3, "Session summary added"); let standup_handler = GenerateStandupReportHandler; @@ -341,8 +337,7 @@ mod tests { json!({"namespace": "global", "hours_lookback": 24}), state.clone(), ) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(!res4.is_empty()); } } diff --git a/server/src/handlers/tasks.rs b/server/src/handlers/tasks.rs index 915d7df..676aa89 100644 --- a/server/src/handlers/tasks.rs +++ b/server/src/handlers/tasks.rs @@ -18,7 +18,7 @@ impl McpTool for AddTaskHandler { crate::mcp::tool_def::("add_task", "Execute add_task") } - async fn execute(&self, args: Value, state: Arc) -> Result { + 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(); @@ -59,7 +59,7 @@ impl McpTool for DeleteTaskHandler { crate::mcp::tool_def::("delete_task", "Execute delete_task") } - async fn execute(&self, args: Value, state: Arc) -> Result { + 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(); @@ -114,7 +114,7 @@ impl McpTool for DeleteTaskHandler { ][0] .clone()) } else { - Err("Task not found. Please use the list_active_tasks tool to verify the correct task ID.".to_string()) + Err(crate::error::AppError::Internal("Task not found. Please use the list_active_tasks tool to verify the correct task ID.".to_string())) } } } @@ -134,7 +134,7 @@ impl McpTool for UpdateTaskStatusHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + 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; @@ -250,14 +250,14 @@ impl McpTool for UpdateTaskStatusHandler { }); if blocked { - Err(format!( + Err(crate::error::AppError::Internal(format!( "Error: Cannot transition task. {}", blocker_details - )) + ))) } else if found { Ok("Task status updated.".to_string()) } else { - Err("Task not found. Please use the list_active_tasks tool to verify the correct task ID.".to_string()) + Err(crate::error::AppError::Internal("Task not found. Please use the list_active_tasks tool to verify the correct task ID.".to_string())) } } } @@ -277,7 +277,7 @@ impl McpTool for ListActiveTasksHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + 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 data = state.tasks.read_with(|tasks| { let filtered: Vec<_> = tasks @@ -294,7 +294,7 @@ impl McpTool for ListActiveTasksHandler { status_match && branch_match }) .collect(); - serde_json::to_string(&filtered).map_err(|e| e.to_string()) + Ok::(serde_json::to_string(&filtered)?) })?; Ok(data) } @@ -315,7 +315,7 @@ impl McpTool for SetAcceptanceCriteriaHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + 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; @@ -337,7 +337,7 @@ impl McpTool for SetAcceptanceCriteriaHandler { if success { Ok("Acceptance criteria set successfully.".to_string()) } else { - Err("Task not found. Please use the list_active_tasks tool to verify the correct task ID.".to_string()) + Err(crate::error::AppError::Internal("Task not found. Please use the list_active_tasks tool to verify the correct task ID.".to_string())) } } } @@ -357,7 +357,7 @@ impl McpTool for VerifyAcceptanceCriteriaHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + 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; @@ -386,7 +386,7 @@ impl McpTool for VerifyAcceptanceCriteriaHandler { } else if already_met { Ok("Acceptance criteria was already met.".to_string()) } else { - Err("Acceptance criteria or task not found.".to_string()) + Err(crate::error::AppError::Internal("Acceptance criteria or task not found.".to_string())) } } } @@ -403,7 +403,7 @@ impl McpTool for AddMilestoneHandler { crate::mcp::tool_def::("add_milestone", "Execute add_milestone") } - async fn execute(&self, args: Value, state: Arc) -> Result { + 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.milestones.modify(|ms| { ms.push(crate::models::Milestone { @@ -430,7 +430,7 @@ impl McpTool for UpdateMilestoneHandler { crate::mcp::tool_def::("update_milestone", "Execute update_milestone") } - async fn execute(&self, args: Value, state: Arc) -> Result { + 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.milestones.modify(|ms| { @@ -445,10 +445,7 @@ impl McpTool for UpdateMilestoneHandler { if found { Ok("Milestone updated".to_string()) } else { - Err( - "Milestone not found. Please verify the milestone ID using list_milestones." - .to_string(), - ) + Err(crate::error::AppError::Internal("Milestone not found. Please verify the milestone ID using list_milestones.".to_string())) } } } @@ -465,7 +462,7 @@ impl McpTool for ListMilestonesHandler { crate::mcp::tool_def::("list_milestones", "Execute list_milestones") } - async fn execute(&self, args: Value, state: Arc) -> Result { + 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.milestones.read_with(|items| { let filtered: Vec<_> = items @@ -478,7 +475,7 @@ impl McpTool for ListMilestonesHandler { } }) .collect(); - serde_json::to_string(&filtered).map_err(|e| e.to_string()) + Ok::(serde_json::to_string(&filtered)?) })?; Ok(data) } @@ -502,14 +499,13 @@ mod tests { "acceptance_criteria": ["Stop the noise", "Reach lightspeed"], }); - let res = add_handler.execute(args, state.clone()).await.unwrap(); + let res = add_handler.execute(args, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(res.contains("Task added with ID:")); let list_handler = ListActiveTasksHandler; let res2 = list_handler .execute(json!({}), state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(res2.contains("Fix the hyperdrive")); } @@ -524,8 +520,7 @@ mod tests { json!({"title": "Test", "description": "test"}), state.clone(), ) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; let id_start = res.find("ID: ").unwrap() + 4; let task_id = res[id_start..].trim(); @@ -535,14 +530,13 @@ mod tests { "id": task_id, "status": "done" }); - let res3 = update_handler.execute(args, state.clone()).await.unwrap(); + let res3 = update_handler.execute(args, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert_eq!(res3, "Task status updated."); let list_handler = ListActiveTasksHandler; let res4 = list_handler .execute(json!({}), state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(!res4.contains(task_id)); } @@ -561,7 +555,7 @@ mod tests { "end_date": 1700000000, "namespace": "global" }); - let res1 = add_milestone.execute(args_ms, state.clone()).await.unwrap(); + let res1 = add_milestone.execute(args_ms, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(res1.contains("Milestone added")); // Fetch milestone ID from state directly to update @@ -573,15 +567,14 @@ mod tests { "id": ms_id, "status": "completed" }); - let res2 = update_ms.execute(args_ums, state.clone()).await.unwrap(); + let res2 = update_ms.execute(args_ums, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert_eq!(res2, "Milestone updated"); // List Milestones let list_ms = ListMilestonesHandler; let res3 = list_ms .execute(json!({"namespace": "global"}), state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(res3.contains("completed")); assert!(res3.contains("Release 1.0")); @@ -592,8 +585,7 @@ mod tests { json!({"title": "Test", "description": "desc"}), state.clone(), ) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; let task_id = res_task[res_task.find("ID: ").unwrap() + 4..].trim(); let set_ac = SetAcceptanceCriteriaHandler; @@ -602,7 +594,7 @@ mod tests { "task_title": "Test", "criteria": ["Do X", "Do Y"] }); - let res4 = set_ac.execute(args_ac, state.clone()).await.unwrap(); + let res4 = set_ac.execute(args_ac, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert_eq!(res4, "Acceptance criteria set successfully."); let verify_ac = VerifyAcceptanceCriteriaHandler; @@ -611,7 +603,7 @@ mod tests { "criteria": "Do X", "proof": "I did X" }); - let res5 = verify_ac.execute(args_vac, state.clone()).await.unwrap(); + let res5 = verify_ac.execute(args_vac, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(res5.contains("Acceptance criteria verified")); } @@ -626,8 +618,7 @@ mod tests { json!({"title": "Parent", "description": "p"}), state.clone(), ) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; let parent_id = parent[parent.find("ID: ").unwrap() + 4..] .trim() .to_string(); @@ -637,15 +628,13 @@ mod tests { json!({"title": "Child", "description": "c", "parent_id": parent_id}), state.clone(), ) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; 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 - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(res_del.contains("Deleted task and its children (2 total).")); } } diff --git a/server/src/handlers/vision.rs b/server/src/handlers/vision.rs index 322c83f..91141b8 100644 --- a/server/src/handlers/vision.rs +++ b/server/src/handlers/vision.rs @@ -24,9 +24,9 @@ impl McpTool for WriteClipboardHandler { ) } - async fn execute(&self, args: Value, _state: Arc) -> Result { + async fn execute(&self, args: Value, _state: Arc) -> crate::error::Result { let tool_args: WriteClipboardTool = - serde_json::from_value(args).map_err(|e| format!("Invalid args: {}", e))?; + serde_json::from_value(args).map_err(|e| crate::error::AppError::Internal(format!("Invalid args: {}", e)))?; tokio::task::spawn_blocking(move || { let mut msgs = Vec::new(); @@ -63,11 +63,11 @@ impl McpTool for WriteClipboardHandler { if clipboard.set_image(img_data).is_ok() { msgs.push("Wrote image"); } else { - return Err("Failed to write image to clipboard".to_string()); + return Err(crate::error::AppError::Internal("Failed to write image to clipboard".to_string())); } } }, - Err(e) => return Err(format!("Failed to open image {}: {}", image_path, e)), + Err(e) => return Err(crate::error::AppError::Internal(format!("Failed to open image {}: {}", image_path, e))), } } @@ -78,7 +78,7 @@ impl McpTool for WriteClipboardHandler { } }) .await - .map_err(|e| format!("Task panic: {}", e))? + .map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))? } } @@ -97,8 +97,8 @@ impl McpTool for ReadClipboardHandler { ) } - async fn execute(&self, _args: Value, _state: Arc) -> Result { - let result = tokio::task::spawn_blocking(move || -> Result { + async fn execute(&self, _args: Value, _state: Arc) -> crate::error::Result { + let result = tokio::task::spawn_blocking(move || -> crate::error::Result { let mut out = serde_json::Map::new(); if let Ok(_clip) = Clipboard::new_attempts(3) { @@ -149,9 +149,9 @@ impl McpTool for ReadClipboardHandler { Ok(Value::Object(out)) }) .await - .map_err(|e| format!("Task panic: {}", e))??; + .map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??; - Ok(serde_json::to_string_pretty(&result).unwrap()) + Ok::(serde_json::to_string_pretty(&result)?) } } use crate::tools::ToggleClipboardWatchModeTool; @@ -171,7 +171,7 @@ impl McpTool for ToggleClipboardWatchModeHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let tool_args: ToggleClipboardWatchModeTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let mut watch_mode = state.clipboard_watch_mode.write().await; diff --git a/server/src/handlers/workspaces.rs b/server/src/handlers/workspaces.rs index 0168f01..90435db 100644 --- a/server/src/handlers/workspaces.rs +++ b/server/src/handlers/workspaces.rs @@ -18,7 +18,7 @@ impl McpTool for PinFileHandler { crate::mcp::tool_def::("pin_file", "Execute pin_file") } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: PinFileTool = serde_json::from_value(args).map_err(|e| e.to_string())?; state.pinned_files.modify(|pinned| { pinned.retain(|p| p.namespace != req.namespace || p.file_path != req.file_path); @@ -45,7 +45,7 @@ impl McpTool for UnpinFileHandler { crate::mcp::tool_def::("unpin_file", "Execute unpin_file") } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: UnpinFileTool = serde_json::from_value(args).map_err(|e| e.to_string())?; state.pinned_files.modify(|pinned| { pinned.retain(|p| p.namespace != req.namespace || p.file_path != req.file_path) @@ -69,7 +69,7 @@ impl McpTool for ListPinnedFilesHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: ListPinnedFilesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let data = state.pinned_files.read_with(|pinned| { let filtered: Vec<_> = pinned @@ -89,7 +89,7 @@ impl McpTool for ListPinnedFilesHandler { ns_match && branch_match }) .collect(); - serde_json::to_string(&filtered).map_err(|e| e.to_string()) + Ok::(serde_json::to_string(&filtered)?) })?; Ok(data) } @@ -107,7 +107,7 @@ impl McpTool for StoreSnippetHandler { crate::mcp::tool_def::("store_snippet", "Execute store_snippet") } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: StoreSnippetTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req_name = req.name.clone(); // Keep for the OK message and retain closure let text_to_embed = format!("Name: {}\nLanguage: {}\nDescription: {}\nCode: {}", req.name, req.language, req.description, req.code); @@ -145,7 +145,7 @@ impl McpTool for SearchSnippetsHandler { crate::mcp::tool_def::("search_snippets", "Execute search_snippets") } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: SearchSnippetsTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let query = req.query; let data = state.snippets.read_with(|snippets| { @@ -157,7 +157,7 @@ impl McpTool for SearchSnippetsHandler { || contains_ignore_ascii_case(&s.language, &query) }) .collect(); - serde_json::to_string(&results).map_err(|e| e.to_string()) + Ok::(serde_json::to_string(&results)?) })?; Ok(data) } @@ -175,7 +175,7 @@ impl McpTool for DeleteSnippetHandler { crate::mcp::tool_def::("delete_snippet", "Execute delete_snippet") } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: DeleteSnippetTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let mut deleted = false; state.snippets.modify(|snippets| { @@ -188,10 +188,7 @@ impl McpTool for DeleteSnippetHandler { drop(idx.delete_document(&req.name)); Ok("Snippet deleted.".to_string()) } else { - Err( - "Snippet not found. Please verify the snippet ID using search_snippets." - .to_string(), - ) + Err(crate::error::AppError::Internal("Snippet not found. Please verify the snippet ID using search_snippets.".to_string())) } } } @@ -211,7 +208,7 @@ impl McpTool for SaveContextWorkspaceHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: SaveContextWorkspaceTool = serde_json::from_value(args).map_err(|e| e.to_string())?; state.context_workspaces.modify(|ws| { @@ -243,7 +240,7 @@ impl McpTool for LoadContextWorkspaceHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: LoadContextWorkspaceTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let data = state.context_workspaces.read_with(|ws| { @@ -272,7 +269,7 @@ impl McpTool for ListContextWorkspacesHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: ListContextWorkspacesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let data = state.context_workspaces.read_with(|ws| { @@ -280,7 +277,7 @@ impl McpTool for ListContextWorkspacesHandler { .iter() .filter(|w| req.namespace.as_ref().is_none_or(|ns| &w.namespace == ns)) .collect(); - serde_json::to_string(&filtered).map_err(|e| e.to_string()) + Ok::(serde_json::to_string(&filtered)?) })?; Ok(data) } @@ -301,7 +298,7 @@ impl McpTool for DeleteContextWorkspaceHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: crate::tools::DeleteContextWorkspaceTool = serde_json::from_value(args).map_err(|e| e.to_string())?; @@ -319,7 +316,7 @@ impl McpTool for DeleteContextWorkspaceHandler { if found { Ok("Context workspace deleted successfully".to_string()) } else { - Err("Context workspace not found".to_string()) + Err(crate::error::AppError::Internal("Context workspace not found".to_string())) } } } @@ -339,7 +336,7 @@ impl McpTool for AddPrChecklistItemHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: AddPrChecklistItemTool = serde_json::from_value(args).map_err(|e| e.to_string())?; state.pr_checklists.modify(|items| { @@ -365,14 +362,14 @@ impl McpTool for GetPrChecklistHandler { crate::mcp::tool_def::("get_pr_checklist", "Execute get_pr_checklist") } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: GetPrChecklistTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let data = state.pr_checklists.read_with(|items| { let filtered: Vec<_> = items .iter() .filter(|i| i.namespace == req.namespace) .collect(); - serde_json::to_string(&filtered).map_err(|e| e.to_string()) + Ok::(serde_json::to_string(&filtered)?) })?; Ok(data) } @@ -393,7 +390,7 @@ impl McpTool for ClearPrChecklistHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: ClearPrChecklistTool = serde_json::from_value(args).map_err(|e| e.to_string())?; state .pr_checklists @@ -423,14 +420,13 @@ mod tests { "active_task_ids": ["123"] }); - let res = save_handler.execute(args, state.clone()).await.unwrap(); + let res = save_handler.execute(args, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert_eq!(res, "Context workspace saved"); let list_handler = ListContextWorkspacesHandler; let res2 = list_handler .execute(json!({"namespace": "global"}), state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(res2.contains("wsl-session")); assert!(res2.contains("src/main.rs")); } @@ -450,8 +446,7 @@ mod tests { }); let res1 = store_handler .execute(args_snip, state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert_eq!(res1, "Snippet 'init_db' stored."); let search_handler = SearchSnippetsHandler; @@ -460,8 +455,7 @@ mod tests { json!({"query": "SELECT", "namespace": "global"}), state.clone(), ) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; // Skip assertion since it requires index rebuild let pr_handler = AddPrChecklistItemHandler; @@ -469,14 +463,13 @@ mod tests { "description": "Check coverage", "namespace": "global" }); - let res3 = pr_handler.execute(args_pr, state.clone()).await.unwrap(); + let res3 = pr_handler.execute(args_pr, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert_eq!(res3, "PR checklist item added"); let get_pr = GetPrChecklistHandler; let res4 = get_pr .execute(json!({"namespace": "global"}), state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(res4.contains("Check coverage")); // Pin lifecycle @@ -486,15 +479,13 @@ mod tests { json!({"file_path": "src/lib.rs", "namespace": "global"}), state.clone(), ) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert_eq!(res5, "File pinned"); let list_pins = ListPinnedFilesHandler; let res6 = list_pins .execute(json!({"namespace": "global"}), state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(res6.contains("src/lib.rs")); let unpin = UnpinFileHandler; @@ -503,16 +494,14 @@ mod tests { json!({"file_path": "src/lib.rs", "namespace": "global"}), state.clone(), ) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert_eq!(res7, "File unpinned"); // Clear PR let clear_pr = ClearPrChecklistHandler; let res8 = clear_pr .execute(json!({"namespace": "global"}), state.clone()) - .await - .unwrap(); + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert_eq!(res8, "PR checklist cleared"); } } @@ -534,11 +523,11 @@ impl McpTool for ReadDirectoryArchitectureHandler { ) } - async fn execute(&self, args: Value, _state: Arc) -> Result { + async fn execute(&self, args: Value, _state: Arc) -> crate::error::Result { let tool_args: ReadDirectoryArchitectureTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let dir = tool_args.directory.clone(); - let result = tokio::task::spawn_blocking(move || -> Result { + let result = tokio::task::spawn_blocking(move || -> crate::error::Result { let mut summary = String::new(); fn visit_dirs(dir: &std::path::Path, summary: &mut String, depth: usize) -> std::io::Result<()> { @@ -581,7 +570,7 @@ impl McpTool for ReadDirectoryArchitectureHandler { let path = std::path::Path::new(&dir); if !path.exists() { - return Err(format!("Directory does not exist: {}", dir)); + return Err(crate::error::AppError::Internal(format!("Directory does not exist: {}", dir))); } summary.push_str(&format!("Architecture of {}:\n", dir)); @@ -590,7 +579,7 @@ impl McpTool for ReadDirectoryArchitectureHandler { Ok(summary) }) .await - .map_err(|e| format!("Task panic: {}", e))??; + .map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??; Ok(result) } @@ -613,7 +602,7 @@ impl McpTool for SemanticCodeSearchHandler { ) } - async fn execute(&self, args: Value, state: Arc) -> Result { + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let tool_args: SemanticCodeSearchTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let query_emb = generate_embedding_async(tool_args.query.clone()).await?; diff --git a/server/src/router.rs b/server/src/router.rs index 7d7eaff..2e7b8c9 100644 --- a/server/src/router.rs +++ b/server/src/router.rs @@ -12,7 +12,7 @@ pub trait McpTool: Send + Sync { fn schema(&self) -> Value; /// Execute the tool with the given arguments - async fn execute(&self, args: Value, state: Arc) -> Result; + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result; } #[async_trait] @@ -25,7 +25,7 @@ pub trait McpResource: Send + Sync { fn mime_type(&self) -> Option<&'static str> { Some("application/json") } - async fn read(&self, state: Arc) -> Result; + async fn read(&self, state: Arc) -> crate::error::Result; } #[async_trait] @@ -37,7 +37,7 @@ pub trait McpPrompt: Send + Sync { fn arguments(&self) -> serde_json::Value { serde_json::json!([]) } - async fn get(&self, args: Value, state: Arc) -> Result; + async fn get(&self, args: Value, state: Arc) -> crate::error::Result; } struct GraphEntitiesResource; @@ -52,15 +52,14 @@ impl McpResource for GraphEntitiesResource { fn description(&self) -> Option<&'static str> { Some("All nodes and entities currently stored in the knowledge graph") } - async fn read(&self, state: Arc) -> Result { + async fn read(&self, state: Arc) -> crate::error::Result { let state_clone = Arc::clone(&state); - tokio::task::spawn_blocking(move || { + tokio::task::spawn_blocking(move || -> crate::error::Result { let graph = state_clone.graph.cache.read().unwrap(); let data: Vec<_> = graph.entities.values().collect(); - serde_json::to_string_pretty(&data).map_err(|e| e.to_string()) + Ok(serde_json::to_string_pretty(&data)?) }) - .await - .unwrap() + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))? } } @@ -76,15 +75,14 @@ impl McpResource for GraphRelationsResource { fn description(&self) -> Option<&'static str> { Some("All relationships between entities currently stored in the knowledge graph") } - async fn read(&self, state: Arc) -> Result { + async fn read(&self, state: Arc) -> crate::error::Result { let state_clone = Arc::clone(&state); - tokio::task::spawn_blocking(move || { + tokio::task::spawn_blocking(move || -> crate::error::Result { let graph = state_clone.graph.cache.read().unwrap(); let data = &graph.relations; - serde_json::to_string_pretty(&data).map_err(|e| e.to_string()) + Ok(serde_json::to_string_pretty(&data)?) }) - .await - .unwrap() + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))? } } @@ -100,18 +98,17 @@ impl McpResource for TasksActiveResource { fn description(&self) -> Option<&'static str> { Some("List of currently active tasks") } - async fn read(&self, state: Arc) -> Result { + async fn read(&self, state: Arc) -> crate::error::Result { let state_clone = Arc::clone(&state); - tokio::task::spawn_blocking(move || { + tokio::task::spawn_blocking(move || -> crate::error::Result { let tasks = state_clone.tasks.cache.read().unwrap(); let data: Vec<_> = tasks .iter() .filter(|t| t.status != "completed" && t.status != "done") .collect(); - serde_json::to_string_pretty(&data).map_err(|e| e.to_string()) + Ok(serde_json::to_string_pretty(&data)?) }) - .await - .unwrap() + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))? } } @@ -128,7 +125,7 @@ impl McpPrompt for AnalyzeTechDebtPrompt { &self, _args: Value, _state: Arc, - ) -> Result { + ) -> crate::error::Result { Ok(serde_json::json!({ "messages": [ { @@ -190,14 +187,13 @@ impl MemoryHandler { fn description(&self) -> Option<&'static str> { Some("Recent terminal execution history and exit codes") } - async fn read(&self, state: Arc) -> Result { + async fn read(&self, state: Arc) -> crate::error::Result { let state_clone = Arc::clone(&state); - tokio::task::spawn_blocking(move || { + tokio::task::spawn_blocking(move || -> crate::error::Result { let items = state_clone.terminal_history.cache.read().unwrap(); - serde_json::to_string_pretty(&*items).map_err(|e| e.to_string()) + Ok(serde_json::to_string_pretty(&*items)?) }) - .await - .unwrap() + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))? } } struct PinnedFilesResource; @@ -212,14 +208,13 @@ impl MemoryHandler { fn description(&self) -> Option<&'static str> { Some("Currently pinned files in the workspace") } - async fn read(&self, state: Arc) -> Result { + async fn read(&self, state: Arc) -> crate::error::Result { let state_clone = Arc::clone(&state); - tokio::task::spawn_blocking(move || { + tokio::task::spawn_blocking(move || -> crate::error::Result { let items = state_clone.pinned_files.cache.read().unwrap(); - serde_json::to_string_pretty(&*items).map_err(|e| e.to_string()) + Ok(serde_json::to_string_pretty(&*items)?) }) - .await - .unwrap() + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))? } } @@ -235,14 +230,13 @@ impl MemoryHandler { fn description(&self) -> Option<&'static str> { Some("Project milestones and their status") } - async fn read(&self, state: Arc) -> Result { + async fn read(&self, state: Arc) -> crate::error::Result { let state_clone = Arc::clone(&state); - tokio::task::spawn_blocking(move || { + tokio::task::spawn_blocking(move || -> crate::error::Result { let items = state_clone.milestones.cache.read().unwrap(); - serde_json::to_string_pretty(&*items).map_err(|e| e.to_string()) + Ok(serde_json::to_string_pretty(&*items)?) }) - .await - .unwrap() + .await.map_err(|e| crate::error::AppError::Internal(e.to_string()))? } } @@ -259,7 +253,7 @@ impl MemoryHandler { &self, _args: Value, _state: Arc, - ) -> Result { + ) -> crate::error::Result { Ok(serde_json::json!({ "messages": [ { @@ -290,7 +284,7 @@ impl MemoryHandler { &self, _args: Value, _state: Arc, - ) -> Result { + ) -> crate::error::Result { Ok(serde_json::json!({" messages": [ { @@ -506,7 +500,7 @@ impl MemoryHandler { }); Some(crate::mcp::success(id, payload)) } - Err(e) => Some(crate::mcp::error(id, -32603, &e)), + Err(e) => Some(crate::mcp::error(id, -32603, &e.to_string())), } } else { Some(crate::mcp::error(id, -32602, "Resource not found")) @@ -544,7 +538,7 @@ impl MemoryHandler { if let Some(prompt) = self.prompts.get(name) { match prompt.get(args, Arc::clone(&self.state)).await { Ok(messages) => Some(crate::mcp::success(id, messages)), - Err(e) => Some(crate::mcp::error(id, -32603, &e)), + Err(e) => Some(crate::mcp::error(id, -32603, &e.to_string())), } } else { Some(crate::mcp::error(id, -32602, "Prompt not found")) @@ -562,10 +556,10 @@ impl MemoryHandler { self.state .broadcast_activity(&format!("Agent executed tool: {}", name)); - let result: Result = if let Some(tool) = self.tools.get(name) { + let result: crate::error::Result = if let Some(tool) = self.tools.get(name) { tool.execute(args, self.state.clone()).await } else { - Err(format!("Unknown tool: {}", name)) + Err(crate::error::AppError::Internal(format!("Unknown tool: {}", name))) }; match result { @@ -579,7 +573,7 @@ impl MemoryHandler { Err(e) => { tracing::error!("Tool {} failed: {}", name, e); let payload = serde_json::json!({ - "content": [{"type": "text", "text": e}], + "content": [{"type": "text", "text": e.to_string()}], "isError": true }); Some(crate::mcp::success(id_clone, payload)) @@ -627,7 +621,7 @@ mod tests { "params": {} }); - let res_list = handler.handle_request(list_tools_req).await.unwrap(); + let res_list = handler.handle_request(list_tools_req).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert_eq!(res_list["jsonrpc"], "2.0"); assert_eq!(res_list["id"], 1); assert!(res_list["result"]["tools"].as_array().unwrap().len() > 10); @@ -646,7 +640,7 @@ mod tests { "method": "resources/list", "params": {} }); - let res_list = handler.handle_request(req_list_res).await.unwrap(); + let res_list = handler.handle_request(req_list_res).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; let resources_arr = res_list["result"]["resources"].as_array().unwrap(); assert!( resources_arr @@ -668,7 +662,7 @@ mod tests { "uri": "memory://tasks/active" } }); - let res_read = handler.handle_request(req_read_res).await.unwrap(); + let res_read = handler.handle_request(req_read_res).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert_eq!( res_read["result"]["contents"][0]["uri"], "memory://tasks/active" @@ -687,7 +681,7 @@ mod tests { "method": "prompts/list", "params": {} }); - let res_prompts = handler.handle_request(req_list_prompts).await.unwrap(); + let res_prompts = handler.handle_request(req_list_prompts).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; let prompts_arr = res_prompts["result"]["prompts"].as_array().unwrap(); assert!(prompts_arr.iter().any(|p| p["name"] == "handoff_routine")); @@ -701,7 +695,7 @@ mod tests { "arguments": {} } }); - let res_get = handler.handle_request(req_get_prompt).await.unwrap(); + let res_get = handler.handle_request(req_get_prompt).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; let messages = res_get["result"]["messages"].as_array().unwrap(); assert_eq!(messages[0]["role"], "user"); assert!( @@ -728,7 +722,7 @@ mod tests { "arguments": {} } }); - let res_success = handler.handle_request(req_success).await.unwrap(); + let res_success = handler.handle_request(req_success).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert_eq!(res_success["jsonrpc"], "2.0"); assert_eq!(res_success["id"], 2); // A successful tool call should return a result with isError: false @@ -748,7 +742,7 @@ mod tests { } } }); - let res_fail = handler.handle_request(req_fail).await.unwrap(); + let res_fail = handler.handle_request(req_fail).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert_eq!(res_fail["jsonrpc"], "2.0"); assert_eq!(res_fail["id"], 3); // Semantic failures must explicitly return isError: true inside the result to halt the LLM @@ -766,7 +760,7 @@ mod tests { "id": 4, "method": "unknown_method_xyz" }); - let res_unknown = handler.handle_request(req_unknown).await.unwrap(); + let res_unknown = handler.handle_request(req_unknown).await.map_err(|e| crate::error::AppError::Internal(e.to_string()))?; assert!(res_unknown.get("error").is_some()); assert_eq!(res_unknown["error"]["code"], -32601); }