Refactor: Migrate unwrap calls to AppError in MCP handlers

This commit is contained in:
Riz Ashraf committed 2026-09-30 21:02:50 +01:00
1 parent 4e1a633dbd
commit 0e866f2465
12 files changed
+276 -326

No files matched your search

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