feat(mcp): Add replace_ast_node, semantic_code_search, read_directory_architecture, nvim_send_to_terminal
This commit is contained in:
1 parent
d4635ab6dc
commit
9787af0f27
5 files changed
+259
No files matched your search
@@ -516,3 +516,141 @@ mod tests {
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user