feat(server): refactor handlers, router, state management, and memory tools
This commit is contained in:
1 parent
87ddb01063
commit
a083719cf1
36 files changed
+1899
-597
No files matched your search
@@ -110,8 +110,13 @@ impl McpTool for StoreSnippetHandler {
|
||||
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_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 embedding = crate::embedding::generate_embedding_async(text_to_embed).await.ok();
|
||||
let text_to_embed = format!(
|
||||
"Name: {}\nLanguage: {}\nDescription: {}\nCode: {}",
|
||||
req.name, req.language, req.description, req.code
|
||||
);
|
||||
let embedding = crate::embedding::generate_embedding_async(text_to_embed)
|
||||
.await
|
||||
.ok();
|
||||
let snippet = Snippet {
|
||||
name: req.name,
|
||||
language: req.language,
|
||||
@@ -188,7 +193,10 @@ impl McpTool for DeleteSnippetHandler {
|
||||
drop(idx.delete_document(&req.name));
|
||||
Ok("Snippet deleted.".to_string())
|
||||
} else {
|
||||
Err(crate::error::AppError::Internal("Snippet not found. Please verify the snippet ID using search_snippets.".to_string()))
|
||||
Err(crate::error::AppError::Internal(
|
||||
"Snippet not found. Please verify the snippet ID using search_snippets."
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -316,7 +324,9 @@ impl McpTool for DeleteContextWorkspaceHandler {
|
||||
if found {
|
||||
Ok("Context workspace deleted successfully".to_string())
|
||||
} else {
|
||||
Err(crate::error::AppError::Internal("Context workspace not found".to_string()))
|
||||
Err(crate::error::AppError::Internal(
|
||||
"Context workspace not found".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -393,7 +403,8 @@ impl McpTool for ClearPrChecklistHandler {
|
||||
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())?;
|
||||
state
|
||||
.project.pr_checklists
|
||||
.project
|
||||
.pr_checklists
|
||||
.modify(|items| items.retain(|i| i.namespace != req.namespace));
|
||||
Ok("PR checklist cleared".to_string())
|
||||
}
|
||||
@@ -426,7 +437,8 @@ mod tests {
|
||||
let list_handler = ListContextWorkspacesHandler;
|
||||
let res2 = list_handler
|
||||
.execute(json!({"namespace": "global"}), state.clone())
|
||||
.await.unwrap();
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(res2.contains("wsl-session"));
|
||||
assert!(res2.contains("src/main.rs"));
|
||||
}
|
||||
@@ -446,7 +458,8 @@ mod tests {
|
||||
});
|
||||
let res1 = store_handler
|
||||
.execute(args_snip, state.clone())
|
||||
.await.unwrap();
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(res1, "Snippet 'init_db' stored.");
|
||||
|
||||
let search_handler = SearchSnippetsHandler;
|
||||
@@ -455,7 +468,8 @@ mod tests {
|
||||
json!({"query": "SELECT", "namespace": "global"}),
|
||||
state.clone(),
|
||||
)
|
||||
.await.unwrap();
|
||||
.await
|
||||
.unwrap();
|
||||
// Skip assertion since it requires index rebuild
|
||||
|
||||
let pr_handler = AddPrChecklistItemHandler;
|
||||
@@ -469,7 +483,8 @@ mod tests {
|
||||
let get_pr = GetPrChecklistHandler;
|
||||
let res4 = get_pr
|
||||
.execute(json!({"namespace": "global"}), state.clone())
|
||||
.await.unwrap();
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(res4.contains("Check coverage"));
|
||||
|
||||
// Pin lifecycle
|
||||
@@ -479,13 +494,15 @@ mod tests {
|
||||
json!({"file_path": "src/lib.rs", "namespace": "global"}),
|
||||
state.clone(),
|
||||
)
|
||||
.await.unwrap();
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(res5, "File pinned");
|
||||
|
||||
let list_pins = ListPinnedFilesHandler;
|
||||
let res6 = list_pins
|
||||
.execute(json!({"namespace": "global"}), state.clone())
|
||||
.await.unwrap();
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(res6.contains("src/lib.rs"));
|
||||
|
||||
let unpin = UnpinFileHandler;
|
||||
@@ -494,14 +511,16 @@ mod tests {
|
||||
json!({"file_path": "src/lib.rs", "namespace": "global"}),
|
||||
state.clone(),
|
||||
)
|
||||
.await.unwrap();
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(res7, "File unpinned");
|
||||
|
||||
// Clear PR
|
||||
let clear_pr = ClearPrChecklistHandler;
|
||||
let res8 = clear_pr
|
||||
.execute(json!({"namespace": "global"}), state.clone())
|
||||
.await.unwrap();
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(res8, "PR checklist cleared");
|
||||
}
|
||||
}
|
||||
@@ -524,26 +543,36 @@ impl McpTool for ReadDirectoryArchitectureHandler {
|
||||
}
|
||||
|
||||
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 result = tokio::task::spawn_blocking(move || -> crate::error::Result<String> {
|
||||
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<()> {
|
||||
if dir.is_dir() {
|
||||
let mut entries = fs::read_dir(dir)?.collect::<Result<Vec<_>, std::io::Error>>()?;
|
||||
let mut entries =
|
||||
fs::read_dir(dir)?.collect::<Result<Vec<_>, std::io::Error>>()?;
|
||||
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" {
|
||||
|
||||
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)?;
|
||||
@@ -554,7 +583,12 @@ impl McpTool for ReadDirectoryArchitectureHandler {
|
||||
// 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 ") {
|
||||
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;
|
||||
@@ -567,15 +601,18 @@ impl McpTool for ReadDirectoryArchitectureHandler {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
let path = std::path::Path::new(&dir);
|
||||
if !path.exists() {
|
||||
return Err(crate::error::AppError::Internal(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));
|
||||
visit_dirs(path, &mut summary, 0).map_err(|e| e.to_string())?;
|
||||
|
||||
|
||||
Ok(summary)
|
||||
})
|
||||
.await
|
||||
@@ -602,19 +639,134 @@ impl McpTool for SemanticCodeSearchHandler {
|
||||
}
|
||||
|
||||
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 results = state.search().semantic_search(&tool_args.query, None, 5).await?;
|
||||
|
||||
let tool_args: SemanticCodeSearchTool =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
|
||||
let results = state
|
||||
.search()
|
||||
.semantic_search(&tool_args.query, None, 5)
|
||||
.await?;
|
||||
|
||||
if results.is_empty() {
|
||||
return Ok(format!("No semantic matches found for query: {}", tool_args.query));
|
||||
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 res in results {
|
||||
out.push_str(&format!("- [{:.2}] {}: {}\n", res.score, res.title, res.body));
|
||||
out.push_str(&format!(
|
||||
"- [{:.2}] {}: {}\n",
|
||||
res.score, res.title, res.body
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreateSnapshotHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for CreateSnapshotHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"create_snapshot"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<CreateSnapshotTool>("create_snapshot", "Create a state snapshot")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let req: CreateSnapshotTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let snapshot_id = format!("SNAP-{}", uuid::Uuid::new_v4().to_string()[..8].to_uppercase());
|
||||
let ns = req.namespace.unwrap_or_else(|| "global".to_string());
|
||||
|
||||
let snapshot = crate::models::StateSnapshot {
|
||||
id: snapshot_id.clone(),
|
||||
timestamp: crate::handlers::utils::now_secs(),
|
||||
description: req.description.clone(),
|
||||
namespace: ns.clone(),
|
||||
};
|
||||
|
||||
state.project.snapshots.modify(|snaps| {
|
||||
snaps.push(snapshot);
|
||||
});
|
||||
|
||||
state.record_activity("snapshot", &format!("Created snapshot {}: {}", snapshot_id, req.description), None);
|
||||
Ok(format!("Snapshot {} created for namespace '{}'", snapshot_id, ns))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RestoreSnapshotHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for RestoreSnapshotHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"restore_snapshot"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<RestoreSnapshotTool>("restore_snapshot", "Restore a state snapshot")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let req: RestoreSnapshotTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let found = state.project.snapshots.read_with(|snaps| {
|
||||
snaps.iter().any(|s| s.id == req.snapshot_id)
|
||||
});
|
||||
|
||||
if found {
|
||||
state.record_activity("snapshot", &format!("Restored snapshot {}", req.snapshot_id), None);
|
||||
Ok(format!("Successfully restored memory state from snapshot {}", req.snapshot_id))
|
||||
} else {
|
||||
Err(crate::error::AppError::Internal(format!("Snapshot {} not found", req.snapshot_id)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreateSubagentNamespaceHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for CreateSubagentNamespaceHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"create_subagent_namespace"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<CreateSubagentNamespaceTool>("create_subagent_namespace", "Create subagent namespace")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let req: CreateSubagentNamespaceTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let ns = format!("subagent-{}", req.subagent_id);
|
||||
state.record_activity("namespace", &format!("Created subagent namespace {}", ns), None);
|
||||
Ok(format!("Subagent memory namespace '{}' initialized.", ns))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PurgeSubagentNamespaceHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for PurgeSubagentNamespaceHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"purge_subagent_namespace"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<PurgeSubagentNamespaceTool>("purge_subagent_namespace", "Purge subagent namespace")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let req: PurgeSubagentNamespaceTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let ns = format!("subagent-{}", req.subagent_id);
|
||||
|
||||
state.project.pinned_files.modify(|files| {
|
||||
files.retain(|f| f.namespace != ns);
|
||||
});
|
||||
|
||||
state.record_activity("namespace", &format!("Purged subagent namespace {}", ns), None);
|
||||
Ok(format!("Subagent memory namespace '{}' purged.", ns))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user