use crate::router::McpTool; use crate::state::MemoryState; use crate::tools::ReadFileSkeletonTool; use async_trait::async_trait; use serde_json::Value; use std::sync::Arc; use tree_sitter::{Node, Parser}; pub struct ReadFileSkeletonHandler; #[async_trait] impl McpTool for ReadFileSkeletonHandler { fn name(&self) -> &'static str { "read_file_skeleton" } fn schema(&self) -> Value { crate::mcp::tool_def::( "read_file_skeleton", "Read a source file and return only its AST structural skeleton, omitting implementation details to save tokens.", ) } async fn execute(&self, args: Value, _state: Arc) -> crate::error::Result { let tool_args: ReadFileSkeletonTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let file_path = tool_args.file_path.clone(); let result = tokio::task::spawn_blocking(move || -> crate::error::Result { let code = std::fs::read_to_string(&file_path).map_err(|e| { crate::error::AppError::Internal(format!("Failed to read file: {}", e)) })?; let mut parser = Parser::new(); let ext = std::path::Path::new(&file_path) .extension() .and_then(|s| s.to_str()) .unwrap_or(""); let language = match ext { "rs" => tree_sitter_rust::LANGUAGE, "ts" | "tsx" | "js" | "jsx" => tree_sitter_typescript::LANGUAGE_TYPESCRIPT, "py" => tree_sitter_python::LANGUAGE, "java" => tree_sitter_java::LANGUAGE, "c" | "h" => tree_sitter_c::LANGUAGE, "cpp" | "cc" | "cxx" | "hpp" | "hxx" => tree_sitter_cpp::LANGUAGE, "go" => tree_sitter_go::LANGUAGE, _ => return Ok(code), }; parser .set_language(&language.into()) .map_err(|e| e.to_string())?; let tree = parser.parse(&code, None).ok_or_else(|| { crate::error::AppError::Internal("Failed to parse code".to_string()) })?; let mut result_skeleton = String::new(); fn extract_skeleton(node: Node, code: &str, out: &mut String, depth: usize) { let kind = node.kind(); let is_structural = matches!( kind, "use_declaration" | "import_statement" | "import_from_statement" | "struct_item" | "enum_item" | "trait_item" | "impl_item" | "function_item" | "function_declaration" | "function_definition" | "method_definition" | "interface_declaration" | "type_alias_declaration" | "class_declaration" | "class_definition" ); if is_structural { let indent = " ".repeat(depth); let node_text = node.utf8_text(code.as_bytes()).unwrap_or(""); let mut signature = String::new(); for line in node_text.lines() { let trimmed = line.trim(); if trimmed.ends_with('{') || trimmed.ends_with(':') { signature.push_str(line); signature.push_str(" ... }"); break; } else { signature.push_str(line); signature.push('\n'); } } if signature.is_empty() { signature = node_text.to_string(); } out.push_str(&indent); out.push_str(signature.trim()); out.push('\n'); } else if node.is_named() { let mut cursor = node.walk(); for child in node.named_children(&mut cursor) { extract_skeleton(child, code, out, depth); } } } extract_skeleton(tree.root_node(), &code, &mut result_skeleton, 0); if result_skeleton.is_empty() { Ok(code) } else { Ok(result_skeleton) } }) .await .map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??; Ok(result) } } use crate::tools::ReplaceAstNodeTool; pub struct ReplaceAstNodeHandler; #[async_trait] impl McpTool for ReplaceAstNodeHandler { fn name(&self) -> &'static str { "replace_ast_node" } fn schema(&self) -> Value { crate::mcp::tool_def::( "replace_ast_node", "Replace a specific AST node (e.g., function, struct) entirely using tree-sitter for robust structural editing.", ) } async fn execute(&self, args: Value, _state: Arc) -> crate::error::Result { let tool_args: ReplaceAstNodeTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let file_path = tool_args.file_path.clone(); let result = tokio::task::spawn_blocking(move || -> crate::error::Result { let code = std::fs::read_to_string(&file_path).map_err(|e| { crate::error::AppError::Internal(format!("Failed to read file: {}", e)) })?; let mut parser = Parser::new(); let ext = std::path::Path::new(&file_path) .extension() .and_then(|s| s.to_str()) .unwrap_or(""); let language = match ext { "rs" => tree_sitter_rust::LANGUAGE, "ts" | "tsx" | "js" | "jsx" => tree_sitter_typescript::LANGUAGE_TYPESCRIPT, "py" => tree_sitter_python::LANGUAGE, "java" => tree_sitter_java::LANGUAGE, "c" | "h" => tree_sitter_c::LANGUAGE, "cpp" | "cc" | "cxx" | "hpp" | "hxx" => tree_sitter_cpp::LANGUAGE, "go" => tree_sitter_go::LANGUAGE, _ => { return Err(crate::error::AppError::Internal(format!( "Unsupported language for AST replacement: {}", ext ))); } }; parser .set_language(&language.into()) .map_err(|e| e.to_string())?; let tree = parser.parse(&code, None).ok_or_else(|| { crate::error::AppError::Internal("Failed to parse code".to_string()) })?; // Search for the node fn find_node<'a>( node: Node<'a>, code: &str, target_type: &str, target_name: &str, ) -> Option> { if node.kind() == target_type { // Try to find the name/identifier let mut cursor = node.walk(); for child in node.children(&mut cursor) { let kind = child.kind(); if kind == "identifier" || kind == "name" { let name = child.utf8_text(code.as_bytes()).unwrap_or(""); if name == target_name { return Some(node); } } } } let mut cursor = node.walk(); for child in node.children(&mut cursor) { if let Some(found) = find_node(child, code, target_type, target_name) { return Some(found); } } None } let target_node = find_node( tree.root_node(), &code, &tool_args.node_type, &tool_args.node_name, ); if let Some(node) = target_node { let start_byte = node.start_byte(); let end_byte = node.end_byte(); let mut new_file_content = String::new(); new_file_content.push_str(&code[..start_byte]); new_file_content.push_str(&tool_args.new_content); new_file_content.push_str(&code[end_byte..]); std::fs::write(&file_path, new_file_content).map_err(|e| e.to_string())?; Ok(format!( "Successfully replaced node {} of type {} in {}", tool_args.node_name, tool_args.node_type, file_path )) } else { Err(crate::error::AppError::Internal(format!( "Could not find node {} of type {}", tool_args.node_name, tool_args.node_type ))) } }) .await .map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??; Ok(result) } } #[cfg(test)] mod tests { use super::*; use serde_json::json; use tempfile::tempdir; #[tokio::test] async fn test_read_file_skeleton() { let dir = tempdir().unwrap(); let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap())); let file_path = dir.path().join("test_skeleton.rs"); let code = "fn my_func() {\n let x = 1;\n}\n\nstruct MyStruct {\n val: i32\n}"; std::fs::write(&file_path, code).unwrap(); let handler = ReadFileSkeletonHandler; let args = json!({ "file_path": file_path.to_str().unwrap() }); let res = handler.execute(args, state.clone()).await.unwrap(); assert!(res.contains("fn my_func()")); assert!(res.contains("struct MyStruct")); } #[tokio::test] async fn test_replace_ast_node() { let dir = tempdir().unwrap(); let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap())); let file_path = dir.path().join("test_replace.rs"); let code = "fn my_func() {\n let x = 1;\n}\n\nstruct MyStruct {\n val: i32\n}"; std::fs::write(&file_path, code).unwrap(); let handler = ReplaceAstNodeHandler; let args = json!({ "file_path": file_path.to_str().unwrap(), "node_type": "function_item", "node_name": "my_func", "new_content": "fn my_func() {\n let x = 2;\n}" }); let res = handler.execute(args, state.clone()).await.unwrap(); assert!(res.contains("Successfully replaced node")); let new_code = std::fs::read_to_string(&file_path).unwrap(); assert!(new_code.contains("let x = 2;")); assert!(!new_code.contains("let x = 1;")); } }