feat(mcp): Add replace_ast_node, semantic_code_search, read_directory_architecture, nvim_send_to_terminal

This commit is contained in:
Riz Ashraf committed 2026-09-30 08:37:26 +01:00
1 parent d4635ab6dc
commit 9787af0f27
5 files changed
+259

No files matched your search

+7
View File
@@ -159,3 +159,10 @@ The LLM agent interacts with your active Neovim session using a dedicated set of
- **Ghost Text Previews ( - **Ghost Text Previews (
vim_set_preview)**: Pushes proposed LLM code diffs directly into Neovim buffers as ephemeral virtual text. vim_set_preview)**: Pushes proposed LLM code diffs directly into Neovim buffers as ephemeral virtual text.
- [Prompting Guide & Effective Discourse](./PROMPTING_GUIDE.md): Learn how to phrase prompts to get the most out of the agent and memory server. - [Prompting Guide & Effective Discourse](./PROMPTING_GUIDE.md): Learn how to phrase prompts to get the most out of the agent and memory server.
## T2R & Token Efficiency Enhancements (V2)
- **AST Node Replacer (␍eplace_ast_node)**: Uses ree-sitter to deterministically edit functions and structs without relying on exact line numbers or regex matching, ensuring zero syntax breaking edits.
- **Semantic Code Search (semantic_code_search)**: Integrates local vector embeddings to execute conceptual code searches instead of blind grep regexes, preventing hallucinated token consumption.
- **Interactive Terminal Integrations (
vim_send_to_terminal)**: Proxies shell execution into visible Neovim splits so human operators can watch agents compile code, debug output, and intervene interactively.
- **Bird's Eye Architecture View (␍ead_directory_architecture)**: Generates a high-level summary of workspace directories using heuristic analysis to prevent LLMs from wasting tokens on reading dozens of files while exploring new repos.
+90
View File
@@ -104,3 +104,93 @@ impl McpTool for ReadFileSkeletonHandler {
Ok(result) 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::<ReplaceAstNodeTool>(
"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<MemoryState>) -> Result<String, 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 result = tokio::task::spawn_blocking(move || -> Result<String, String> {
let code = std::fs::read_to_string(&file_path)
.map_err(|e| 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,
_ => return Err(format!("Unsupported language for AST replacement: {}", ext)),
};
parser.set_language(&language.into()).map_err(|e| e.to_string())?;
let tree = parser.parse(&code, None).ok_or("Failed to parse code")?;
// Search for the node
fn find_node<'a>(node: Node<'a>, code: &str, target_type: &str, target_name: &str) -> Option<Node<'a>> {
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(format!("Could not find node {} of type {}", tool_args.node_name, tool_args.node_type))
}
})
.await
.map_err(|e| format!("Task panic: {}", e))??;
Ok(result)
}
}
+138
View File
@@ -516,3 +516,141 @@ mod tests {
assert_eq!(res8, "PR checklist cleared"); assert_eq!(res8, "PR checklist cleared");
} }
} }
use crate::tools::ReadDirectoryArchitectureTool;
use std::fs;
pub struct ReadDirectoryArchitectureHandler;
#[async_trait]
impl McpTool for ReadDirectoryArchitectureHandler {
fn name(&self) -> &'static str {
"read_directory_architecture"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<ReadDirectoryArchitectureTool>(
"read_directory_architecture",
"Get a bird's-eye view of a directory, reading the file tree and extracting a basic structural summary.",
)
}
async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> Result<String, String> {
let tool_args: ReadDirectoryArchitectureTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let dir = tool_args.directory.clone();
let result = tokio::task::spawn_blocking(move || -> Result<String, String> {
let mut summary = String::new();
fn visit_dirs(dir: &std::path::Path, summary: &mut String, depth: usize) -> std::io::Result<()> {
if dir.is_dir() {
let mut entries = fs::read_dir(dir)?.collect::<Result<Vec<_>, std::io::Result<_>>>()?;
entries.sort_by_key(|e| e.path());
for entry in entries {
let path = entry.path();
let indent = " ".repeat(depth);
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with('.') || name == "target" || name == "node_modules" || name == "dist" {
continue;
}
if path.is_dir() {
summary.push_str(&format!("{}- {}/\n", indent, name));
visit_dirs(&path, summary, depth + 1)?;
} else {
// Extract a brief 1-line heuristic if it's a known file type
let mut peek = String::new();
if let Ok(content) = fs::read_to_string(&path) {
// Find the first docstring or struct/class definition
for line in content.lines() {
let t = line.trim();
if t.starts_with("///") || t.starts_with("# ") || t.starts_with("struct ") || t.starts_with("class ") || t.starts_with("function ") {
let truncated: String = t.chars().take(80).collect();
peek = format!(" -> {}", truncated);
break;
}
}
}
summary.push_str(&format!("{}- {}{}\n", indent, name, peek));
}
}
}
Ok(())
}
let path = std::path::Path::new(&dir);
if !path.exists() {
return Err(format!("Directory does not exist: {}", dir));
}
summary.push_str(&format!("Architecture of {}:\n", dir));
visit_dirs(path, &mut summary, 0).map_err(|e| e.to_string())?;
Ok(summary)
})
.await
.map_err(|e| format!("Task panic: {}", e))??;
Ok(result)
}
}
use crate::tools::SemanticCodeSearchTool;
use crate::embedding::{generate_embedding_async, cosine_similarity};
pub struct SemanticCodeSearchHandler;
#[async_trait]
impl McpTool for SemanticCodeSearchHandler {
fn name(&self) -> &'static str {
"semantic_code_search"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<SemanticCodeSearchTool>(
"semantic_code_search",
"Perform a semantic vector search across indexed code snippets and knowledge graph nodes using fastembed.",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, 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?;
// For MVP, we search across snippets dynamically. A true background codebase indexer would be a separate subsystem.
let mut results = Vec::new();
let snippets = state.snippets.read_with(|snips| snips.clone());
for snippet in snippets {
let combined = format!("{} {} {}", snippet.name, snippet.description, snippet.code);
if let Ok(emb) = generate_embedding_async(combined).await {
let sim = cosine_similarity(&query_emb, &emb);
results.push((sim, snippet.name, snippet.description));
}
}
let sticky = state.sticky.read_with(|s| s.clone());
for note in sticky {
if let Ok(emb) = generate_embedding_async(note.content.clone()).await {
let sim = cosine_similarity(&query_emb, &emb);
results.push((sim, "StickyNote".to_string(), note.content.chars().take(200).collect::<String>()));
}
}
results.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
let top_results: Vec<_> = results.into_iter().take(5).collect();
if top_results.is_empty() {
return Ok(format!("No semantic matches found for query: {}", tool_args.query));
}
let mut out = format!("Semantic Search Results for '{}':\n", tool_args.query);
for (score, title, desc) in top_results {
out.push_str(&format!("- [{:.2}] {}: {}\n", score, title, desc));
}
Ok(out)
}
}
+3
View File
@@ -390,6 +390,9 @@ impl MemoryHandler {
register!(logs::GetRecentLogsHandler); register!(logs::GetRecentLogsHandler);
register!(ast::ReadFileSkeletonHandler); register!(ast::ReadFileSkeletonHandler);
register!(vision::ToggleClipboardWatchModeHandler); register!(vision::ToggleClipboardWatchModeHandler);
register!(ast::ReplaceAstNodeHandler);
register!(workspaces::ReadDirectoryArchitectureHandler);
register!(workspaces::SemanticCodeSearchHandler);
Self { Self {
state, state,
+21
View File
@@ -679,3 +679,24 @@ pub struct ToggleClipboardWatchModeTool {
pub struct ReadFileSkeletonTool { pub struct ReadFileSkeletonTool {
pub file_path: String, pub file_path: String,
} }
/// Replace a specific AST node in a file (robust structural editing)
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ReplaceAstNodeTool {
pub file_path: String,
pub node_type: String, // e.g., "function_item", "impl_item"
pub node_name: String, // e.g., "execute"
pub new_content: String,
}
/// Semantic code search using local vector embeddings
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct SemanticCodeSearchTool {
pub query: String,
pub directory: Option<String>,
}
/// Get a bird's-eye view of directory architecture
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ReadDirectoryArchitectureTool {
pub directory: String,
}