Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4342615649 | ||
|
|
827f0dd511 | ||
|
|
2024d9521b | ||
|
|
f56750f596 | ||
|
|
3ee95f5c39 | ||
|
|
760ebfcd40 | ||
|
|
11f375017a | ||
|
|
5583132cdc | ||
|
|
250b70d329 | ||
|
|
9546ca4c04 | ||
|
|
b5f1da3c0e | ||
|
|
de060c4db4 | ||
|
|
39528411f4 |
No files matched your search
@@ -10,3 +10,5 @@ temp.json
|
|||||||
*.txt
|
*.txt
|
||||||
summary.md
|
summary.md
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
server/mcp_store.redb
|
||||||
|
server/tantivy_index/
|
||||||
@@ -6,3 +6,7 @@ members = [
|
|||||||
"mcp-stdio"
|
"mcp-stdio"
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
[workspace.lints.rust]
|
||||||
|
unused_imports = "deny"
|
||||||
|
|
||||||
@@ -8,3 +8,6 @@ tokio = { version = "1.53.1", features = ["io-util", "macros"] }
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1.53.1", features = ["rt", "macros"] }
|
tokio = { version = "1.53.1", features = ["rt", "macros"] }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
@@ -21,3 +21,6 @@ schemars = "0.8"
|
|||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
chrono = "0.4.45"
|
chrono = "0.4.45"
|
||||||
|
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
+4
-1
@@ -14,7 +14,7 @@ clap = { version = "4.6.6", features = ["derive"] }
|
|||||||
dirs = "7.0.0"
|
dirs = "7.0.0"
|
||||||
futures-util = "0.3.34"
|
futures-util = "0.3.34"
|
||||||
redb = "4.3.0"
|
redb = "4.3.0"
|
||||||
reqwest = { version = "0.13", default-features = false, features = ["stream", "json"] }
|
reqwest = { version = "0.13", default-features = false, features = ["stream", "json", "rustls"] }
|
||||||
schemars = "1.2.2"
|
schemars = "1.2.2"
|
||||||
serde = { version = "1.0.229", features = ["derive"] }
|
serde = { version = "1.0.229", features = ["derive"] }
|
||||||
serde_json = "1.0.151"
|
serde_json = "1.0.151"
|
||||||
@@ -76,3 +76,6 @@ windows = { version = "0.62.2", features = [
|
|||||||
"Graphics_Imaging",
|
"Graphics_Imaging",
|
||||||
"Storage_Streams",
|
"Storage_Streams",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
@@ -27,6 +27,13 @@ pub fn find_projected_knowledge(
|
|||||||
file_path: &str,
|
file_path: &str,
|
||||||
) -> (Vec<ProjectedTechDebt>, Vec<ProjectedAdr>) {
|
) -> (Vec<ProjectedTechDebt>, Vec<ProjectedAdr>) {
|
||||||
let normalized_file = file_path.replace('\\', "/").to_lowercase();
|
let normalized_file = file_path.replace('\\', "/").to_lowercase();
|
||||||
|
|
||||||
|
// An empty file path matches everything due to `ends_with("")` behavior.
|
||||||
|
// Return empty results immediately for [No Name] buffers.
|
||||||
|
if normalized_file.trim().is_empty() {
|
||||||
|
return (Vec::new(), Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
let file_name = std::path::Path::new(&normalized_file)
|
let file_name = std::path::Path::new(&normalized_file)
|
||||||
.file_name()
|
.file_name()
|
||||||
.and_then(|n| n.to_str())
|
.and_then(|n| n.to_str())
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use mcp_memory_server::tools::{DecisionsTool, TechDebtTool, ManageCheckpointTool, HypothesesTool, AgentSignalsTool};
|
use mcp_memory_server::tools::AgentSignalsTool;
|
||||||
use schemars::schema_for;
|
use schemars::schema_for;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|||||||
+236
-169
@@ -41,7 +41,7 @@ impl McpTool for ReadFileSkeletonHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let tool_args: ReadFileSkeletonTool =
|
let tool_args: ReadFileSkeletonTool =
|
||||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
|
|
||||||
let file_path = tool_args.file_path.clone();
|
let file_path = tool_args.file_path.clone();
|
||||||
validate_safe_path(&file_path)?;
|
validate_safe_path(&file_path)?;
|
||||||
@@ -202,169 +202,174 @@ impl McpTool for ReplaceAstNodeHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let tool_args: ReplaceAstNodeTool =
|
let tool_args: ReplaceAstNodeTool =
|
||||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
|
|
||||||
let file_path = tool_args.file_path.clone();
|
let replacements = tool_args.replacements;
|
||||||
validate_safe_path(&file_path)?;
|
if replacements.is_empty() {
|
||||||
|
return Ok("No replacements provided.".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
let result = tokio::task::spawn_blocking(move || -> crate::error::Result<String> {
|
let result = tokio::task::spawn_blocking(move || -> crate::error::Result<String> {
|
||||||
let code = std::fs::read_to_string(&file_path).map_err(|e| {
|
let mut msgs = Vec::new();
|
||||||
crate::error::AppError::Internal(format!("Failed to read file: {}", e))
|
for rep in replacements {
|
||||||
})?;
|
let file_path = rep.file_path.clone();
|
||||||
|
validate_safe_path(&file_path)?;
|
||||||
|
|
||||||
let mut parser = Parser::new();
|
let code = std::fs::read_to_string(&file_path).map_err(|e| {
|
||||||
|
crate::error::AppError::Internal(format!("Failed to read file: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
let ext = std::path::Path::new(&file_path)
|
let mut parser = Parser::new();
|
||||||
.extension()
|
|
||||||
.and_then(|s| s.to_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
|
|
||||||
let language = match ext {
|
let ext = std::path::Path::new(&file_path)
|
||||||
"rs" => tree_sitter_rust::LANGUAGE,
|
.extension()
|
||||||
"ts" | "tsx" | "js" | "jsx" => tree_sitter_typescript::LANGUAGE_TYPESCRIPT,
|
.and_then(|s| s.to_str())
|
||||||
"py" => tree_sitter_python::LANGUAGE,
|
.unwrap_or("");
|
||||||
"java" => tree_sitter_java::LANGUAGE,
|
|
||||||
"c" | "h" => tree_sitter_c::LANGUAGE,
|
let language = match ext {
|
||||||
"cpp" | "cc" | "cxx" | "hpp" | "hxx" => tree_sitter_cpp::LANGUAGE,
|
"rs" => tree_sitter_rust::LANGUAGE,
|
||||||
"go" => tree_sitter_go::LANGUAGE,
|
"ts" | "tsx" | "js" | "jsx" => tree_sitter_typescript::LANGUAGE_TYPESCRIPT,
|
||||||
_ => {
|
"py" => tree_sitter_python::LANGUAGE,
|
||||||
return Err(crate::error::AppError::Internal(format!(
|
"java" => tree_sitter_java::LANGUAGE,
|
||||||
"Unsupported language for AST replacement: {}",
|
"c" | "h" => tree_sitter_c::LANGUAGE,
|
||||||
ext
|
"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| crate::error::AppError::Internal(e.to_string()))?;
|
||||||
|
let tree = parser.parse(&code, None).ok_or_else(|| {
|
||||||
|
crate::error::AppError::Internal("Failed to parse code".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
fn matches_node_type(actual_kind: &str, requested_type: &str) -> bool {
|
||||||
|
if actual_kind == requested_type {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
match requested_type.to_lowercase().as_str() {
|
||||||
|
"function" | "func" | "fn" | "method" | "def" => matches!(
|
||||||
|
actual_kind,
|
||||||
|
"function_item"
|
||||||
|
| "function_declaration"
|
||||||
|
| "function_definition"
|
||||||
|
| "method_definition"
|
||||||
|
| "function"
|
||||||
|
),
|
||||||
|
"struct" => matches!(
|
||||||
|
actual_kind,
|
||||||
|
"struct_item" | "struct_declaration" | "struct_specifier"
|
||||||
|
),
|
||||||
|
"class" => matches!(
|
||||||
|
actual_kind,
|
||||||
|
"class_declaration" | "class_definition" | "class_item"
|
||||||
|
),
|
||||||
|
"enum" => matches!(actual_kind, "enum_item" | "enum_declaration"),
|
||||||
|
"trait" | "interface" => matches!(
|
||||||
|
actual_kind,
|
||||||
|
"trait_item" | "interface_declaration" | "interface_item"
|
||||||
|
),
|
||||||
|
"impl" | "impl_item" => actual_kind == "impl_item",
|
||||||
|
"type" | "type_alias" => matches!(
|
||||||
|
actual_kind,
|
||||||
|
"type_alias_declaration" | "type_item" | "type_definition"
|
||||||
|
),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
parser
|
fn find_node<'a>(
|
||||||
.set_language(&language.into())
|
node: Node<'a>,
|
||||||
.map_err(|e| e.to_string())?;
|
code: &str,
|
||||||
let tree = parser.parse(&code, None).ok_or_else(|| {
|
target_type: &str,
|
||||||
crate::error::AppError::Internal("Failed to parse code".to_string())
|
target_name: &str,
|
||||||
})?;
|
) -> Option<Node<'a>> {
|
||||||
|
if matches_node_type(node.kind(), target_type) {
|
||||||
fn matches_node_type(actual_kind: &str, requested_type: &str) -> bool {
|
let mut cursor = node.walk();
|
||||||
if actual_kind == requested_type {
|
for child in node.children(&mut cursor) {
|
||||||
return true;
|
let kind = child.kind();
|
||||||
}
|
if kind == "identifier"
|
||||||
match requested_type.to_lowercase().as_str() {
|
|| kind == "name"
|
||||||
"function" | "func" | "fn" | "method" | "def" => matches!(
|
|| kind == "type_identifier"
|
||||||
actual_kind,
|
|| kind == "property_identifier"
|
||||||
"function_item"
|
|| kind == "field_identifier"
|
||||||
| "function_declaration"
|
{
|
||||||
| "function_definition"
|
let name = child.utf8_text(code.as_bytes()).unwrap_or("");
|
||||||
| "method_definition"
|
if name == target_name {
|
||||||
| "function"
|
return Some(node);
|
||||||
),
|
}
|
||||||
"struct" => matches!(
|
|
||||||
actual_kind,
|
|
||||||
"struct_item" | "struct_declaration" | "struct_specifier"
|
|
||||||
),
|
|
||||||
"class" => matches!(
|
|
||||||
actual_kind,
|
|
||||||
"class_declaration" | "class_definition" | "class_item"
|
|
||||||
),
|
|
||||||
"enum" => matches!(actual_kind, "enum_item" | "enum_declaration"),
|
|
||||||
"trait" | "interface" => matches!(
|
|
||||||
actual_kind,
|
|
||||||
"trait_item" | "interface_declaration" | "interface_item"
|
|
||||||
),
|
|
||||||
"impl" | "impl_item" => actual_kind == "impl_item",
|
|
||||||
"type" | "type_alias" => matches!(
|
|
||||||
actual_kind,
|
|
||||||
"type_alias_declaration" | "type_item" | "type_definition"
|
|
||||||
),
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search for the node
|
|
||||||
fn find_node<'a>(
|
|
||||||
node: Node<'a>,
|
|
||||||
code: &str,
|
|
||||||
target_type: &str,
|
|
||||||
target_name: &str,
|
|
||||||
) -> Option<Node<'a>> {
|
|
||||||
if matches_node_type(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"
|
|
||||||
|| kind == "type_identifier"
|
|
||||||
|| kind == "property_identifier"
|
|
||||||
|| kind == "field_identifier"
|
|
||||||
{
|
|
||||||
let name = child.utf8_text(code.as_bytes()).unwrap_or("");
|
|
||||||
if name == target_name {
|
|
||||||
return Some(node);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let mut cursor = node.walk();
|
let mut cursor = node.walk();
|
||||||
for child in node.children(&mut cursor) {
|
for child in node.children(&mut cursor) {
|
||||||
if let Some(found) = find_node(child, code, target_type, target_name) {
|
if let Some(found) = find_node(child, code, target_type, target_name) {
|
||||||
return Some(found);
|
return Some(found);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
None
|
||||||
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();
|
|
||||||
|
|
||||||
if !code.is_char_boundary(start_byte) || !code.is_char_boundary(end_byte) {
|
|
||||||
return Err(crate::error::AppError::Internal(format!(
|
|
||||||
"Byte offsets {}..{} do not fall on UTF-8 character boundaries in {}",
|
|
||||||
start_byte, end_byte, file_path
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut new_file_content =
|
let target_node = find_node(
|
||||||
String::with_capacity(code.len() + tool_args.new_content.len());
|
tree.root_node(),
|
||||||
new_file_content.push_str(&code[..start_byte]);
|
&code,
|
||||||
new_file_content.push_str(&tool_args.new_content);
|
&rep.node_type,
|
||||||
new_file_content.push_str(&code[end_byte..]);
|
&rep.node_name,
|
||||||
|
);
|
||||||
|
|
||||||
let target_path = std::path::PathBuf::from(&file_path);
|
if let Some(node) = target_node {
|
||||||
let parent_dir = target_path
|
let start_byte = node.start_byte();
|
||||||
.parent()
|
let end_byte = node.end_byte();
|
||||||
.unwrap_or_else(|| std::path::Path::new("."));
|
|
||||||
let temp_file_path =
|
if !code.is_char_boundary(start_byte) || !code.is_char_boundary(end_byte) {
|
||||||
parent_dir.join(format!(".tmp_ast_{}.tmp", uuid::Uuid::new_v4()));
|
|
||||||
std::fs::write(&temp_file_path, new_file_content).map_err(|e| e.to_string())?;
|
|
||||||
if let Err(e) = std::fs::rename(&temp_file_path, &target_path) {
|
|
||||||
// On Windows, std::fs::rename fails if the target file already exists.
|
|
||||||
// Fall back to copy-and-remove to ensure atomic-like overwrite behavior.
|
|
||||||
if let Err(copy_err) = std::fs::copy(&temp_file_path, &target_path) {
|
|
||||||
let _ = std::fs::remove_file(&temp_file_path);
|
|
||||||
return Err(crate::error::AppError::Internal(format!(
|
return Err(crate::error::AppError::Internal(format!(
|
||||||
"Failed to atomically overwrite {}: rename failed ({}), copy failed ({})",
|
"Byte offsets {}..{} do not fall on UTF-8 character boundaries in {}",
|
||||||
file_path, e, copy_err
|
start_byte, end_byte, file_path
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
let _ = std::fs::remove_file(&temp_file_path);
|
|
||||||
|
let mut new_file_content =
|
||||||
|
String::with_capacity(code.len() + rep.new_content.len());
|
||||||
|
new_file_content.push_str(&code[..start_byte]);
|
||||||
|
new_file_content.push_str(&rep.new_content);
|
||||||
|
new_file_content.push_str(&code[end_byte..]);
|
||||||
|
|
||||||
|
let target_path = std::path::PathBuf::from(&file_path);
|
||||||
|
let parent_dir = target_path
|
||||||
|
.parent()
|
||||||
|
.unwrap_or_else(|| std::path::Path::new("."));
|
||||||
|
let temp_file_path =
|
||||||
|
parent_dir.join(format!(".tmp_ast_{}.tmp", uuid::Uuid::new_v4()));
|
||||||
|
std::fs::write(&temp_file_path, new_file_content).map_err(|e| crate::error::AppError::Internal(format!("Failed to write temporary file: {}", e)))?;
|
||||||
|
if let Err(e) = std::fs::rename(&temp_file_path, &target_path) {
|
||||||
|
if let Err(copy_err) = std::fs::copy(&temp_file_path, &target_path) {
|
||||||
|
let _ = std::fs::remove_file(&temp_file_path);
|
||||||
|
return Err(crate::error::AppError::Internal(format!(
|
||||||
|
"Failed to atomically overwrite {}: rename failed ({}), copy failed ({})",
|
||||||
|
file_path, e, copy_err
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let _ = std::fs::remove_file(&temp_file_path);
|
||||||
|
}
|
||||||
|
msgs.push(format!(
|
||||||
|
"Successfully replaced node {} of type {} in {}",
|
||||||
|
rep.node_name, rep.node_type, file_path
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
return Err(crate::error::AppError::Internal(format!(
|
||||||
|
"Could not find node {} of type {} in {}",
|
||||||
|
rep.node_name, rep.node_type, file_path
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
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
|
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
|
Ok(msgs.join("\n"))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
|
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
|
||||||
@@ -440,7 +445,7 @@ fn scan_workspace_for_symbol(
|
|||||||
|
|
||||||
if is_match {
|
if is_match {
|
||||||
results.push(serde_json::json!({
|
results.push(serde_json::json!({
|
||||||
"file_path": path.to_string_lossy(),
|
"file_path": crate::handlers::utils::sanitize_path(&path.to_string_lossy()),
|
||||||
"line": line_num + 1,
|
"line": line_num + 1,
|
||||||
"content": line.trim(),
|
"content": line.trim(),
|
||||||
}));
|
}));
|
||||||
@@ -473,7 +478,7 @@ impl McpTool for FindSymbolReferencesHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: crate::tools::FindSymbolReferencesTool =
|
let req: crate::tools::FindSymbolReferencesTool =
|
||||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
let limit = req.limit.unwrap_or(10);
|
let limit = req.limit.unwrap_or(10);
|
||||||
let target_sym = req.symbol.clone();
|
let target_sym = req.symbol.clone();
|
||||||
|
|
||||||
@@ -498,15 +503,37 @@ impl McpTool for FindSymbolReferencesHandler {
|
|||||||
|
|
||||||
if matches.len() < limit {
|
if matches.len() < limit {
|
||||||
let remaining = limit - matches.len();
|
let remaining = limit - matches.len();
|
||||||
|
let target_sym_clone = target_sym.clone();
|
||||||
let disk_matches = tokio::task::spawn_blocking(move || {
|
let disk_matches = tokio::task::spawn_blocking(move || {
|
||||||
scan_workspace_for_symbol(&target_sym, remaining, false, custom_dir)
|
scan_workspace_for_symbol(&target_sym_clone, remaining, false, custom_dir)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
matches.extend(disk_matches);
|
matches.extend(disk_matches);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(serde_yaml::to_string(&matches)?)
|
let mut out = String::new();
|
||||||
|
out.push_str(&format!("## Symbol References for `{}`\n\n", target_sym));
|
||||||
|
for match_item in &matches {
|
||||||
|
if let Some(source) = match_item.get("source").and_then(|s| s.as_str()) {
|
||||||
|
if source == "snippet" {
|
||||||
|
let name = match_item.get("name").and_then(|n| n.as_str()).unwrap_or("Unknown");
|
||||||
|
let code = match_item.get("code").and_then(|c| c.as_str()).unwrap_or("");
|
||||||
|
out.push_str(&format!("### Snippet: {}\n```rust\n{}\n```\n\n", name, code));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let file = match_item.get("file_path").and_then(|f| f.as_str()).unwrap_or("Unknown");
|
||||||
|
let line = match_item.get("line").and_then(|l| l.as_u64()).unwrap_or(0);
|
||||||
|
let content = match_item.get("content").and_then(|c| c.as_str()).unwrap_or("");
|
||||||
|
out.push_str(&format!("- `{}:{}`: `{}`\n", file, line, content));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches.is_empty() {
|
||||||
|
out.push_str("No references found.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,7 +554,7 @@ impl McpTool for GetCallersHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: crate::tools::GetCallersTool =
|
let req: crate::tools::GetCallersTool =
|
||||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
let limit = req.limit.unwrap_or(10);
|
let limit = req.limit.unwrap_or(10);
|
||||||
let target_fn = req.function_name.clone();
|
let target_fn = req.function_name.clone();
|
||||||
let call_pattern = format!("{}(", target_fn);
|
let call_pattern = format!("{}(", target_fn);
|
||||||
@@ -554,15 +581,37 @@ impl McpTool for GetCallersHandler {
|
|||||||
|
|
||||||
if callers.len() < limit {
|
if callers.len() < limit {
|
||||||
let remaining = limit - callers.len();
|
let remaining = limit - callers.len();
|
||||||
|
let target_fn_clone = target_fn.clone();
|
||||||
let disk_callers = tokio::task::spawn_blocking(move || {
|
let disk_callers = tokio::task::spawn_blocking(move || {
|
||||||
scan_workspace_for_symbol(&target_fn, remaining, true, custom_dir)
|
scan_workspace_for_symbol(&target_fn_clone, remaining, true, custom_dir)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
callers.extend(disk_callers);
|
callers.extend(disk_callers);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(serde_yaml::to_string(&callers)?)
|
let mut out = String::new();
|
||||||
|
out.push_str(&format!("## Callers for `{}`\n\n", target_fn));
|
||||||
|
for caller in &callers {
|
||||||
|
if let Some(source) = caller.get("source").and_then(|s| s.as_str()) {
|
||||||
|
if source == "snippet" {
|
||||||
|
let name = caller.get("name").and_then(|n| n.as_str()).unwrap_or("Unknown");
|
||||||
|
let code = caller.get("code").and_then(|c| c.as_str()).unwrap_or("");
|
||||||
|
out.push_str(&format!("### Snippet: {}\n```rust\n{}\n```\n\n", name, code));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let file = caller.get("file_path").and_then(|f| f.as_str()).unwrap_or("Unknown");
|
||||||
|
let line = caller.get("line").and_then(|l| l.as_u64()).unwrap_or(0);
|
||||||
|
let content = caller.get("content").and_then(|c| c.as_str()).unwrap_or("");
|
||||||
|
out.push_str(&format!("- `{}:{}`: `{}`\n", file, line, content));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if callers.is_empty() {
|
||||||
|
out.push_str("No callers found.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,7 +632,7 @@ impl McpTool for AnalyzeImpactHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: crate::tools::AnalyzeImpactTool =
|
let req: crate::tools::AnalyzeImpactTool =
|
||||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
let sym = req.target_symbol.clone();
|
let sym = req.target_symbol.clone();
|
||||||
|
|
||||||
let mut callers = Vec::new();
|
let mut callers = Vec::new();
|
||||||
@@ -637,21 +686,35 @@ impl McpTool for AnalyzeImpactHandler {
|
|||||||
"LOW"
|
"LOW"
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = serde_json::json!({
|
let mut out = String::new();
|
||||||
"target_symbol": sym,
|
out.push_str(&format!("## Impact Analysis for `{}`\n\n", sym));
|
||||||
"risk_level": risk_level,
|
out.push_str(&format!("**Risk Level:** {}\n\n", risk_level));
|
||||||
"ast_callers_count": caller_count,
|
|
||||||
"ast_callers_sample": callers.into_iter().take(10).collect::<Vec<_>>(),
|
|
||||||
"graph_relations_count": graph_count,
|
|
||||||
"graph_relations": kg_connected,
|
|
||||||
"recommendation": match risk_level {
|
|
||||||
"CRITICAL" | "HIGH" => "Requires comprehensive unit test verification and backwards compatibility checks before modifying.",
|
|
||||||
"MEDIUM" => "Verify direct call sites and run affected module tests.",
|
|
||||||
_ => "Safe to modify with standard unit test verification.",
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(serde_yaml::to_string(&result)?)
|
let rec = match risk_level {
|
||||||
|
"CRITICAL" | "HIGH" => "Requires comprehensive unit test verification and backwards compatibility checks before modifying.",
|
||||||
|
"MEDIUM" => "Verify direct call sites and run affected module tests.",
|
||||||
|
_ => "Safe to modify with standard unit test verification.",
|
||||||
|
};
|
||||||
|
out.push_str(&format!("**Recommendation:** {}\n\n", rec));
|
||||||
|
|
||||||
|
out.push_str(&format!("### AST Callers ({} total, showing up to 10)\n", caller_count));
|
||||||
|
for c in callers.into_iter().take(10) {
|
||||||
|
out.push_str(&format!("- `{}`\n", c));
|
||||||
|
}
|
||||||
|
if caller_count == 0 {
|
||||||
|
out.push_str("No callers found.\n");
|
||||||
|
}
|
||||||
|
out.push_str("\n");
|
||||||
|
|
||||||
|
out.push_str(&format!("### Graph Relations ({})\n", graph_count));
|
||||||
|
for g in kg_connected {
|
||||||
|
out.push_str(&format!("- {}\n", g));
|
||||||
|
}
|
||||||
|
if graph_count == 0 {
|
||||||
|
out.push_str("No graph relations found.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -691,10 +754,14 @@ mod tests {
|
|||||||
|
|
||||||
let handler = ReplaceAstNodeHandler;
|
let handler = ReplaceAstNodeHandler;
|
||||||
let args = json!({
|
let args = json!({
|
||||||
"file_path": file_path.to_str().unwrap(),
|
"replacements": [
|
||||||
"node_type": "function_item",
|
{
|
||||||
"node_name": "my_func",
|
"file_path": file_path.to_str().unwrap(),
|
||||||
"new_content": "fn my_func() {\n let x = 2;\n}"
|
"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();
|
let res = handler.execute(args, state.clone()).await.unwrap();
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ impl McpTool for EnvironmentHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: EnvironmentTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: EnvironmentTool = serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
let ns = req
|
let ns = req
|
||||||
.namespace
|
.namespace
|
||||||
.unwrap_or_else(crate::models::default_namespace);
|
.unwrap_or_else(crate::models::default_namespace);
|
||||||
|
|||||||
+54
-18
@@ -27,8 +27,8 @@ impl McpTool for GetActiveWorktreeContextHandler {
|
|||||||
_state: Arc<MemoryState>,
|
_state: Arc<MemoryState>,
|
||||||
) -> crate::error::Result<String> {
|
) -> crate::error::Result<String> {
|
||||||
let result =
|
let result =
|
||||||
tokio::task::spawn_blocking(move || -> crate::error::Result<serde_json::Value> {
|
tokio::task::spawn_blocking(move || -> crate::error::Result<String> {
|
||||||
let cwd = env::current_dir().map_err(|e| e.to_string())?;
|
let cwd = env::current_dir().map_err(|e| crate::error::AppError::Internal(format!("Failed to get current directory: {}", e)))?;
|
||||||
let repo = git2::Repository::discover(&cwd).map_err(|e| {
|
let repo = git2::Repository::discover(&cwd).map_err(|e| {
|
||||||
crate::error::AppError::Internal(format!("Not in a git repository: {}", e))
|
crate::error::AppError::Internal(format!("Not in a git repository: {}", e))
|
||||||
})?;
|
})?;
|
||||||
@@ -66,7 +66,8 @@ impl McpTool for GetActiveWorktreeContextHandler {
|
|||||||
|
|
||||||
for delta in diff.deltas() {
|
for delta in diff.deltas() {
|
||||||
if let Some(path) = delta.new_file().path() {
|
if let Some(path) = delta.new_file().path() {
|
||||||
files.push(path.to_string_lossy().into_owned());
|
let path_str = path.to_string_lossy().into_owned();
|
||||||
|
files.push(crate::handlers::utils::sanitize_path(&path_str));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,16 +79,34 @@ impl McpTool for GetActiveWorktreeContextHandler {
|
|||||||
diff_output.push_str("\n... [Diff truncated due to size]");
|
diff_output.push_str("\n... [Diff truncated due to size]");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(json!({
|
let mut out = String::new();
|
||||||
"branch": branch_name,
|
out.push_str("## Active Worktree Context\n\n");
|
||||||
"modified_files": files,
|
out.push_str(&format!("**Branch:** `{}`\n\n", branch_name));
|
||||||
"diff": diff_output
|
|
||||||
}))
|
out.push_str("### Modified Files\n");
|
||||||
|
if files.is_empty() {
|
||||||
|
out.push_str("No modified files.\n");
|
||||||
|
} else {
|
||||||
|
for f in files {
|
||||||
|
out.push_str(&format!("- `{}`\n", f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push_str("\n### Diff\n");
|
||||||
|
if diff_output.is_empty() {
|
||||||
|
out.push_str("No changes.\n");
|
||||||
|
} else {
|
||||||
|
out.push_str("```diff\n");
|
||||||
|
out.push_str(&diff_output);
|
||||||
|
out.push_str("\n```\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
|
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
|
||||||
|
|
||||||
Ok::<String, crate::error::AppError>(serde_yaml::to_string(&result)?)
|
Ok(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +127,7 @@ impl McpTool for QueryGitDiffsHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: crate::tools::QueryGitDiffsTool =
|
let req: crate::tools::QueryGitDiffsTool =
|
||||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
let limit = req.limit.unwrap_or(5);
|
let limit = req.limit.unwrap_or(5);
|
||||||
let q = req.query.to_lowercase();
|
let q = req.query.to_lowercase();
|
||||||
|
|
||||||
@@ -121,7 +140,7 @@ impl McpTool for QueryGitDiffsHandler {
|
|||||||
|| change.git_commit.as_ref().is_some_and(|c| c.contains(&q))
|
|| change.git_commit.as_ref().is_some_and(|c| c.contains(&q))
|
||||||
{
|
{
|
||||||
matches.push(json!({
|
matches.push(json!({
|
||||||
"file_path": change.file_path,
|
"file_path": crate::handlers::utils::sanitize_path(&change.file_path),
|
||||||
"description": change.description,
|
"description": change.description,
|
||||||
"commit": change.git_commit,
|
"commit": change.git_commit,
|
||||||
"branch": change.git_branch,
|
"branch": change.git_branch,
|
||||||
@@ -169,7 +188,25 @@ impl McpTool for QueryGitDiffsHandler {
|
|||||||
matches.extend(git_matches);
|
matches.extend(git_matches);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(serde_yaml::to_string(&matches)?)
|
let mut out = String::new();
|
||||||
|
out.push_str(&format!("## Git Diffs for `{}`\n\n", req.query));
|
||||||
|
for m in &matches {
|
||||||
|
if let Some(file) = m.get("file_path").and_then(|f| f.as_str()) {
|
||||||
|
let desc = m.get("description").and_then(|d| d.as_str()).unwrap_or("");
|
||||||
|
let commit = m.get("commit").and_then(|c| c.as_str()).unwrap_or("");
|
||||||
|
out.push_str(&format!("- **File:** `{}` | **Commit:** `{}`\n - {}\n", file, commit, desc));
|
||||||
|
} else {
|
||||||
|
let commit = m.get("commit_id").and_then(|c| c.as_str()).unwrap_or("");
|
||||||
|
let author = m.get("author").and_then(|a| a.as_str()).unwrap_or("");
|
||||||
|
let msg = m.get("message").and_then(|m| m.as_str()).unwrap_or("");
|
||||||
|
out.push_str(&format!("- **Commit:** `{}` | **Author:** {}\n - {}\n", commit, author, msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if matches.is_empty() {
|
||||||
|
out.push_str("No git diffs found.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,10 +229,10 @@ mod tests {
|
|||||||
.map_err(|e| format!("Failed to get worktree context: {}", e))
|
.map_err(|e| format!("Failed to get worktree context: {}", e))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let parsed: serde_json::Value = serde_yaml::from_str(&result).unwrap();
|
assert!(result.contains("## Active Worktree Context"));
|
||||||
assert!(parsed.get("branch").is_some());
|
assert!(result.contains("**Branch:**"));
|
||||||
assert!(parsed.get("modified_files").is_some());
|
assert!(result.contains("### Modified Files"));
|
||||||
assert!(parsed.get("diff").is_some());
|
assert!(result.contains("### Diff"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -211,7 +248,6 @@ mod tests {
|
|||||||
.map_err(|e| format!("Failed to get worktree context: {}", e))
|
.map_err(|e| format!("Failed to get worktree context: {}", e))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let parsed: serde_json::Value = serde_yaml::from_str(&result).unwrap();
|
assert!(result.contains("## Active Worktree Context"));
|
||||||
assert!(parsed.get("branch").is_some() || parsed.is_object());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+110
-75
@@ -121,7 +121,7 @@ impl McpTool for QueryGraphPathHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<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| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
|
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
state.read_graph(|graph| {
|
state.read_graph(|graph| {
|
||||||
@@ -160,7 +160,7 @@ impl McpTool for CreateEntitiesHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<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| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
let mut inserted = Vec::new();
|
let mut inserted = Vec::new();
|
||||||
state.modify_graph(|g| {
|
state.modify_graph(|g| {
|
||||||
for entity_input in req.entities {
|
for entity_input in req.entities {
|
||||||
@@ -209,7 +209,7 @@ impl McpTool for UpdateEntitiesHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: UpdateEntitiesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: UpdateEntitiesTool = serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
|
|
||||||
let mut updated_entities = Vec::new();
|
let mut updated_entities = Vec::new();
|
||||||
let mut deleted_names = Vec::new();
|
let mut deleted_names = Vec::new();
|
||||||
@@ -218,19 +218,25 @@ impl McpTool for UpdateEntitiesHandler {
|
|||||||
|
|
||||||
state.modify_graph(|g| {
|
state.modify_graph(|g| {
|
||||||
for update in req.updates {
|
for update in req.updates {
|
||||||
if !g.entities.contains_key(&update.name) {
|
let mut target_name = update.name.clone();
|
||||||
not_found.push(update.name.clone());
|
if !g.entities.contains_key(&target_name) {
|
||||||
continue;
|
let lower_target = target_name.to_lowercase();
|
||||||
|
if let Some(matched_key) = g.entities.keys().find(|k| k.to_lowercase() == lower_target).cloned() {
|
||||||
|
target_name = matched_key;
|
||||||
|
} else {
|
||||||
|
not_found.push(update.name.clone());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(new_name) = &update.new_name {
|
if let Some(new_name) = &update.new_name {
|
||||||
if update.name != *new_name && g.entities.contains_key(new_name) {
|
if target_name != *new_name && g.entities.contains_key(new_name) {
|
||||||
conflict_names.push(new_name.clone());
|
conflict_names.push(new_name.clone());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(mut entity) = g.entities.remove(&update.name) {
|
if let Some(mut entity) = g.entities.remove(&target_name) {
|
||||||
let mut renamed = false;
|
let mut renamed = false;
|
||||||
let old_name = entity.name.clone();
|
let old_name = entity.name.clone();
|
||||||
|
|
||||||
@@ -269,18 +275,6 @@ impl McpTool for UpdateEntitiesHandler {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if !not_found.is_empty() {
|
|
||||||
return Err(crate::error::AppError::Internal(format!(
|
|
||||||
"Error: Entities not found: {}", not_found.join(", ")
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !conflict_names.is_empty() {
|
|
||||||
return Err(crate::error::AppError::Internal(format!(
|
|
||||||
"Error: Cannot rename to existing entity names: {}", conflict_names.join(", ")
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let idx = state.get_search_index().await;
|
let idx = state.get_search_index().await;
|
||||||
for old_name in deleted_names {
|
for old_name in deleted_names {
|
||||||
drop(idx.delete_document(&old_name));
|
drop(idx.delete_document(&old_name));
|
||||||
@@ -291,7 +285,16 @@ impl McpTool for UpdateEntitiesHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let names: Vec<String> = updated_entities.iter().map(|e| e.name.clone()).collect();
|
let names: Vec<String> = updated_entities.iter().map(|e| e.name.clone()).collect();
|
||||||
Ok(format!("Successfully updated {} entity/entities: {}", names.len(), names.join(", ")))
|
let mut msg = format!("Successfully updated {} entity/entities: {}", names.len(), names.join(", "));
|
||||||
|
|
||||||
|
if !not_found.is_empty() {
|
||||||
|
msg.push_str(&format!("\nNote: {} entities were not found and skipped: {}", not_found.len(), not_found.join(", ")));
|
||||||
|
}
|
||||||
|
if !conflict_names.is_empty() {
|
||||||
|
msg.push_str(&format!("\nNote: {} entity renames were skipped due to name conflicts: {}", conflict_names.len(), conflict_names.join(", ")));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,7 +417,7 @@ impl McpTool for AddObservationsHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<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| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
let mut missing_entities = Vec::new();
|
let mut missing_entities = Vec::new();
|
||||||
state.read_graph(|g| {
|
state.read_graph(|g| {
|
||||||
for o in &req.observations {
|
for o in &req.observations {
|
||||||
@@ -458,38 +461,44 @@ impl McpTool for DeleteEntitiesHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<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| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
let to_delete: std::collections::HashSet<_> = req.entity_names.into_iter().collect();
|
|
||||||
let mut missing = Vec::new();
|
|
||||||
state.read_graph(|g| {
|
|
||||||
for name in &to_delete {
|
|
||||||
if !g.entities.contains_key(name) {
|
|
||||||
missing.push(name.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if !missing.is_empty() {
|
let mut actual_deletes = Vec::new();
|
||||||
return Err(crate::error::AppError::Internal(format!(
|
let mut not_found = Vec::new();
|
||||||
"Error: Entities not found: {}. Please use the search_nodes or read_graph tools to verify the exact entity names.",
|
|
||||||
missing.join(", ")
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
state.modify_graph(|master| {
|
state.modify_graph(|master| {
|
||||||
for name in &to_delete {
|
for target in req.entity_names {
|
||||||
|
if master.entities.contains_key(&target) {
|
||||||
|
actual_deletes.push(target);
|
||||||
|
} else {
|
||||||
|
let lower_target = target.to_lowercase();
|
||||||
|
if let Some(matched_key) = master.entities.keys().find(|k| k.to_lowercase() == lower_target).cloned() {
|
||||||
|
actual_deletes.push(matched_key);
|
||||||
|
} else {
|
||||||
|
not_found.push(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for name in &actual_deletes {
|
||||||
master.entities.remove(name);
|
master.entities.remove(name);
|
||||||
}
|
}
|
||||||
|
let delete_set: std::collections::HashSet<_> = actual_deletes.iter().cloned().collect();
|
||||||
master
|
master
|
||||||
.relations
|
.relations
|
||||||
.retain(|r| !to_delete.contains(&r.from) && !to_delete.contains(&r.to));
|
.retain(|r| !delete_set.contains(&r.from) && !delete_set.contains(&r.to));
|
||||||
});
|
});
|
||||||
|
|
||||||
let idx = state.get_search_index().await;
|
let idx = state.get_search_index().await;
|
||||||
for name in to_delete {
|
for name in &actual_deletes {
|
||||||
drop(idx.delete_document(&name));
|
drop(idx.delete_document(name));
|
||||||
}
|
}
|
||||||
Ok("Entities deleted".to_string())
|
|
||||||
|
let mut msg = format!("Successfully deleted {} entities.", actual_deletes.len());
|
||||||
|
if !not_found.is_empty() {
|
||||||
|
msg.push_str(&format!(" Note: {} entities were not found and skipped: {}", not_found.len(), not_found.join(", ")));
|
||||||
|
}
|
||||||
|
Ok(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,7 +519,7 @@ impl McpTool for DeleteObservationsHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<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| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
|
|
||||||
let mut missing_entities = Vec::new();
|
let mut missing_entities = Vec::new();
|
||||||
let mut missing_observations = Vec::new();
|
let mut missing_observations = Vec::new();
|
||||||
@@ -573,7 +582,7 @@ impl McpTool for DeleteRelationsHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<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| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
let mut deleted_count = 0;
|
let mut deleted_count = 0;
|
||||||
let requested_count = req.relations.len();
|
let requested_count = req.relations.len();
|
||||||
|
|
||||||
@@ -581,7 +590,12 @@ impl McpTool for DeleteRelationsHandler {
|
|||||||
let initial_len = master.relations.len();
|
let initial_len = master.relations.len();
|
||||||
master.relations.retain(|r| {
|
master.relations.retain(|r| {
|
||||||
let should_delete = req.relations.iter().any(|target| {
|
let should_delete = req.relations.iter().any(|target| {
|
||||||
target.from == r.from && target.to == r.to && target.relation_type == r.relation_type
|
if target.from == r.from && target.to == r.to && target.relation_type == r.relation_type {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
target.from.eq_ignore_ascii_case(&r.from)
|
||||||
|
&& target.to.eq_ignore_ascii_case(&r.to)
|
||||||
|
&& target.relation_type.eq_ignore_ascii_case(&r.relation_type)
|
||||||
});
|
});
|
||||||
!should_delete
|
!should_delete
|
||||||
});
|
});
|
||||||
@@ -589,13 +603,11 @@ impl McpTool for DeleteRelationsHandler {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let missing_count = requested_count.saturating_sub(deleted_count);
|
let missing_count = requested_count.saturating_sub(deleted_count);
|
||||||
|
let mut msg = format!("Successfully deleted {} relations.", deleted_count);
|
||||||
if missing_count > 0 {
|
if missing_count > 0 {
|
||||||
return Err(crate::error::AppError::Internal(format!(
|
msg.push_str(&format!(" Note: {} relations were not found and skipped.", missing_count));
|
||||||
"Error: {} relation(s) not found in graph. Please verify exact relation properties (from, to, relation_type) using read_graph or get_subgraph.",
|
|
||||||
missing_count
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
Ok("Relations deleted".to_string())
|
Ok(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -615,7 +627,7 @@ impl McpTool for ReadGraphHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<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| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
let max_tokens = req.max_tokens;
|
let max_tokens = req.max_tokens;
|
||||||
let limit = req.limit;
|
let limit = req.limit;
|
||||||
let offset = req.offset.unwrap_or(0);
|
let offset = req.offset.unwrap_or(0);
|
||||||
@@ -625,6 +637,7 @@ impl McpTool for ReadGraphHandler {
|
|||||||
.entities
|
.entities
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, v)| req.namespace.as_ref().is_none_or(|ns| &v.namespace == ns))
|
.filter(|(_, v)| req.namespace.as_ref().is_none_or(|ns| &v.namespace == ns))
|
||||||
|
.filter(|(_, v)| req.entity_type.as_ref().is_none_or(|et| &v.entity_type == et))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let total_count = filtered_entities.len();
|
let total_count = filtered_entities.len();
|
||||||
@@ -750,7 +763,7 @@ impl McpTool for SearchNodesHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<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| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
let limit = req.limit.unwrap_or(10);
|
let limit = req.limit.unwrap_or(10);
|
||||||
let include_body = req.include_body.unwrap_or(false);
|
let include_body = req.include_body.unwrap_or(false);
|
||||||
|
|
||||||
@@ -778,7 +791,7 @@ impl McpTool for SearchNodesHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok::<String, crate::error::AppError>(serde_yaml::to_string(&matched_entities)?)
|
if matched_entities.is_empty() { Ok::<String, crate::error::AppError>(format!("No entities found matching query. Try using a different keyword or relaxing the search query.")) } else { Ok::<String, crate::error::AppError>(serde_yaml::to_string(&matched_entities)?) }
|
||||||
})?;
|
})?;
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
@@ -800,7 +813,7 @@ impl McpTool for OpenNodesHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<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| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
let data = state.read_graph(|full| -> crate::error::Result<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();
|
||||||
@@ -819,7 +832,7 @@ impl McpTool for OpenNodesHandler {
|
|||||||
result.entities.insert(name, e);
|
result.entities.insert(name, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok::<String, crate::error::AppError>(serde_yaml::to_string(&result)?)
|
if result.entities.is_empty() { Ok::<String, crate::error::AppError>(format!("No matching nodes found.")) } else { Ok::<String, crate::error::AppError>(serde_yaml::to_string(&result)?) }
|
||||||
})?;
|
})?;
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
@@ -841,7 +854,7 @@ impl McpTool for VisualizeGraphHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<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| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
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| {
|
||||||
let mut included: HashSet<&str> = HashSet::new();
|
let mut included: HashSet<&str> = HashSet::new();
|
||||||
@@ -933,7 +946,7 @@ impl McpTool for CondenseEntityHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<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| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
let mut missing = false;
|
let mut missing = false;
|
||||||
|
|
||||||
if req.summarized_observations.is_empty() {
|
if req.summarized_observations.is_empty() {
|
||||||
@@ -1019,7 +1032,7 @@ impl McpTool for MergeEntitiesHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<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| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
let mut missing = false;
|
let mut missing = false;
|
||||||
state.modify_graph(|master| {
|
state.modify_graph(|master| {
|
||||||
if let Some(src) = master.entities.remove(&req.source_entity) {
|
if let Some(src) = master.entities.remove(&req.source_entity) {
|
||||||
@@ -1115,12 +1128,12 @@ impl McpTool for GetSubgraphHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: GetSubgraphTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: GetSubgraphTool = serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
let root = req.root_entity.ok_or_else(|| {
|
let root = req.root_entity.ok_or_else(|| {
|
||||||
crate::error::AppError::Internal("root_entity is required".to_string())
|
crate::error::AppError::Internal("root_entity is required".to_string())
|
||||||
})?;
|
})?;
|
||||||
let depth = req.depth.unwrap_or(2);
|
let depth = req.depth.unwrap_or(2);
|
||||||
let format = req.format.unwrap_or(SubgraphFormat::Json);
|
let format = req.format.unwrap_or(SubgraphFormat::MarkdownTree);
|
||||||
|
|
||||||
let (matched_entities, matched_relations) = state.read_graph(|full| {
|
let (matched_entities, matched_relations) = state.read_graph(|full| {
|
||||||
let mut adj: std::collections::HashMap<&str, Vec<&str>> =
|
let mut adj: std::collections::HashMap<&str, Vec<&str>> =
|
||||||
@@ -1210,7 +1223,7 @@ impl McpTool for SweepGraphHealthHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: SweepGraphHealthTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: SweepGraphHealthTool = serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
let auto_prune = req.auto_prune_orphans.unwrap_or(false);
|
let auto_prune = req.auto_prune_orphans.unwrap_or(false);
|
||||||
let auto_prune_stale = req.auto_prune_stale_files.unwrap_or(false);
|
let auto_prune_stale = req.auto_prune_stale_files.unwrap_or(false);
|
||||||
|
|
||||||
@@ -1278,16 +1291,38 @@ impl McpTool for SweepGraphHealthHandler {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let report = serde_json::json!({
|
let mut out = String::new();
|
||||||
"orphaned_entities": orphans,
|
out.push_str("## Graph Health Report\n\n");
|
||||||
"orphans_pruned": auto_prune,
|
let score = if orphans.is_empty() && duplicates.is_empty() && stale_entities.is_empty() { "100% (Healthy)" } else { "Needs Maintenance" };
|
||||||
"stale_entities": stale_entities,
|
out.push_str(&format!("**Health Status:** {}\n\n", score));
|
||||||
"stale_pruned": auto_prune_stale,
|
|
||||||
"potential_duplicates": duplicates,
|
|
||||||
"health_score": if orphans.is_empty() && duplicates.is_empty() && stale_entities.is_empty() { "100%" } else { "Needs Maintenance" }
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(serde_yaml::to_string(&report)?)
|
out.push_str(&format!("### Orphaned Entities ({})\n", orphans.len()));
|
||||||
|
if auto_prune {
|
||||||
|
out.push_str("*These orphans were automatically pruned.*\n");
|
||||||
|
}
|
||||||
|
for o in orphans {
|
||||||
|
out.push_str(&format!("- `{}`\n", o));
|
||||||
|
}
|
||||||
|
out.push_str("\n");
|
||||||
|
|
||||||
|
out.push_str(&format!("### Stale Entities ({})\n", stale_entities.len()));
|
||||||
|
if auto_prune_stale {
|
||||||
|
out.push_str("*These stale entities were automatically pruned.*\n");
|
||||||
|
}
|
||||||
|
for s in stale_entities {
|
||||||
|
out.push_str(&format!("- `{}`\n", s));
|
||||||
|
}
|
||||||
|
out.push_str("\n");
|
||||||
|
|
||||||
|
out.push_str(&format!("### Potential Duplicates ({})\n", duplicates.len()));
|
||||||
|
for d in duplicates {
|
||||||
|
let a = d.get("entity_a").and_then(|x| x.as_str()).unwrap_or("?");
|
||||||
|
let b = d.get("entity_b").and_then(|x| x.as_str()).unwrap_or("?");
|
||||||
|
let act = d.get("suggested_action").and_then(|x| x.as_str()).unwrap_or("");
|
||||||
|
out.push_str(&format!("- `{}` and `{}`\n - Suggested: `{}`\n", a, b, act));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1308,7 +1343,7 @@ impl McpTool for ResolveStaleSymbolsHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: ResolveStaleSymbolsTool =
|
let req: ResolveStaleSymbolsTool =
|
||||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
let auto_heal = req.auto_heal.unwrap_or(true);
|
let auto_heal = req.auto_heal.unwrap_or(true);
|
||||||
|
|
||||||
let mut checked = 0;
|
let mut checked = 0;
|
||||||
@@ -1366,7 +1401,7 @@ impl McpTool for SummarizeSubgraphHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: SummarizeSubgraphTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: SummarizeSubgraphTool = serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
let depth = req.depth.unwrap_or(2);
|
let depth = req.depth.unwrap_or(2);
|
||||||
|
|
||||||
let (sub_entities, sub_relations) = state.read_graph(|g| {
|
let (sub_entities, sub_relations) = state.read_graph(|g| {
|
||||||
@@ -1578,7 +1613,7 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(res4, "Entities deleted");
|
assert_eq!(res4, "Successfully deleted 1 entities.");
|
||||||
|
|
||||||
let res5 = read_graph
|
let res5 = read_graph
|
||||||
.execute(json!({"namespace": "global"}), state.clone())
|
.execute(json!({"namespace": "global"}), state.clone())
|
||||||
@@ -1751,7 +1786,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(del_rel_res, "Relations deleted");
|
assert_eq!(del_rel_res, "Successfully deleted 1 relations.");
|
||||||
|
|
||||||
let bcast_handler = AgentSignalsHandler;
|
let bcast_handler = AgentSignalsHandler;
|
||||||
let bcast_res = bcast_handler
|
let bcast_res = bcast_handler
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
use crate::router::McpTool;
|
||||||
|
use crate::state::MemoryState;
|
||||||
|
use crate::tools::FetchUrlTool;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use reqwest::Client;
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub struct FetchUrlHandler;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl McpTool for FetchUrlHandler {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"fetch_url"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schema(&self) -> Value {
|
||||||
|
crate::mcp::tool_def::<FetchUrlTool>(
|
||||||
|
"fetch_url",
|
||||||
|
"Fetch content from a URL via an HTTP GET request natively (supports proxy config via environment variables: HTTP_PROXY, HTTPS_PROXY, NO_PROXY)",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
|
let req: FetchUrlTool = serde_json::from_value(args).map_err(|e| {
|
||||||
|
crate::error::AppError::BadRequest(format!(
|
||||||
|
"Schema validation failed. Your JSON arguments do not match the expected tool schema: {}",
|
||||||
|
e
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut client_builder = Client::builder();
|
||||||
|
|
||||||
|
// Add optional user agent
|
||||||
|
if let Some(ua) = req.user_agent {
|
||||||
|
client_builder = client_builder.user_agent(ua);
|
||||||
|
} else {
|
||||||
|
client_builder = client_builder.user_agent("mcp-memory-server/1.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow bypassing SSL validation for self-signed certificates
|
||||||
|
if req.ignore_ssl_errors.unwrap_or(false) {
|
||||||
|
client_builder = client_builder.danger_accept_invalid_certs(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reqwest automatically uses HTTP_PROXY, HTTPS_PROXY, NO_PROXY
|
||||||
|
// environment variables by default, so we don't need to manually
|
||||||
|
// extract and apply them, the builder handles it.
|
||||||
|
|
||||||
|
let client = client_builder.build().map_err(|e| {
|
||||||
|
crate::error::AppError::Internal(format!("Failed to build HTTP client: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let res = client.get(&req.url).send().await.map_err(|e| {
|
||||||
|
crate::error::AppError::Internal(format!("HTTP request to {} failed: {}", req.url, e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let status = res.status();
|
||||||
|
let content = res.text().await.map_err(|e| {
|
||||||
|
crate::error::AppError::Internal(format!("Failed to read response body: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !status.is_success() {
|
||||||
|
return Ok(format!(
|
||||||
|
"HTTP Error: {} {}\n\nResponse Body:\n{}",
|
||||||
|
status.as_u16(),
|
||||||
|
status.canonical_reason().unwrap_or("Unknown"),
|
||||||
|
content
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use axum::{routing::get, Router};
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
|
async fn spawn_test_server() -> String {
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/test", get(|| async { "Hello, World!" }))
|
||||||
|
.route(
|
||||||
|
"/not_found",
|
||||||
|
get(|| async {
|
||||||
|
(
|
||||||
|
axum::http::StatusCode::NOT_FOUND,
|
||||||
|
"Not Found Error",
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let port = listener.local_addr().unwrap().port();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
axum::serve(listener, app).await.unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
format!("http://127.0.0.1:{}", port)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_fetch_url_success() {
|
||||||
|
let base_url = spawn_test_server().await;
|
||||||
|
let handler = FetchUrlHandler;
|
||||||
|
let url = format!("{}/test", base_url);
|
||||||
|
|
||||||
|
let res = handler
|
||||||
|
.execute(
|
||||||
|
serde_json::json!({
|
||||||
|
"url": url
|
||||||
|
}),
|
||||||
|
Arc::new(MemoryState::new("")),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(res, "Hello, World!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_fetch_url_not_found() {
|
||||||
|
let base_url = spawn_test_server().await;
|
||||||
|
let handler = FetchUrlHandler;
|
||||||
|
let url = format!("{}/not_found", base_url);
|
||||||
|
|
||||||
|
let res = handler
|
||||||
|
.execute(
|
||||||
|
serde_json::json!({
|
||||||
|
"url": url
|
||||||
|
}),
|
||||||
|
Arc::new(MemoryState::new("")),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(res.contains("HTTP Error: 404 Not Found"));
|
||||||
|
assert!(res.contains("Not Found Error"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ impl McpTool for ProcessLogsHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let tool_args: ProcessLogsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let tool_args: ProcessLogsTool = serde_json::from_value(args).map_err(|e| crate::error::AppError::Internal(format!("Failed to seek file: {}", e)))?;
|
||||||
let safe_path = crate::handlers::utils::validate_safe_path(&tool_args.file_path)?;
|
let safe_path = crate::handlers::utils::validate_safe_path(&tool_args.file_path)?;
|
||||||
|
|
||||||
match tool_args.action {
|
match tool_args.action {
|
||||||
@@ -43,14 +43,14 @@ impl McpTool for ProcessLogsHandler {
|
|||||||
let mut file = File::open(&safe_path).map_err(|e| {
|
let mut file = File::open(&safe_path).map_err(|e| {
|
||||||
crate::error::AppError::Internal(format!("Failed to open file: {}", 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| crate::error::AppError::Internal(format!("Failed to read file metadata: {}", e)))?.len();
|
||||||
|
|
||||||
let read_size = std::cmp::min(16 * 1024, len);
|
let read_size = std::cmp::min(16 * 1024, len);
|
||||||
file.seek(SeekFrom::End(-(read_size as i64)))
|
file.seek(SeekFrom::End(-(read_size as i64)))
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| crate::error::AppError::Internal(format!("Failed to seek file: {}", e)))?;
|
||||||
|
|
||||||
let mut vec_buf = Vec::new();
|
let mut vec_buf = Vec::new();
|
||||||
file.read_to_end(&mut vec_buf).map_err(|e| e.to_string())?;
|
file.read_to_end(&mut vec_buf).map_err(|e| crate::error::AppError::Internal(format!("Failed to seek file: {}", e)))?;
|
||||||
|
|
||||||
let buffer = String::from_utf8_lossy(&vec_buf).to_string();
|
let buffer = String::from_utf8_lossy(&vec_buf).to_string();
|
||||||
let lines: Vec<&str> = buffer.lines().collect();
|
let lines: Vec<&str> = buffer.lines().collect();
|
||||||
|
|||||||
+158
-67
@@ -22,7 +22,7 @@ impl McpTool for LogErrorFixHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<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| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
let text_to_embed = format!("Signature: {}\nSolution: {}", req.signature, req.solution);
|
let text_to_embed = format!("Signature: {}\nSolution: {}", req.signature, req.solution);
|
||||||
let solution = req.solution;
|
let solution = req.solution;
|
||||||
|
|
||||||
@@ -34,8 +34,8 @@ impl McpTool for LogErrorFixHandler {
|
|||||||
signature: req.signature.clone(),
|
signature: req.signature.clone(),
|
||||||
solution: solution.clone(),
|
solution: solution.clone(),
|
||||||
timestamp: crate::handlers::utils::now_secs(),
|
timestamp: crate::handlers::utils::now_secs(),
|
||||||
git_commit: req.git_commit,
|
git_commit: req.git_commit.or_else(|| crate::handlers::utils::get_git_context().1),
|
||||||
git_branch: req.git_branch,
|
git_branch: req.git_branch.or_else(|| crate::handlers::utils::get_git_context().0),
|
||||||
embedding,
|
embedding,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
@@ -71,7 +71,7 @@ impl McpTool for SearchErrorFixesHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<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| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
let limit = req.limit.unwrap_or(5);
|
let limit = req.limit.unwrap_or(5);
|
||||||
let include_body = req.include_body.unwrap_or(true);
|
let include_body = req.include_body.unwrap_or(true);
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ impl McpTool for SearchErrorFixesHandler {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok::<String, crate::error::AppError>(serde_yaml::to_string(&suggestions)?)
|
if suggestions.is_empty() { Ok::<String, crate::error::AppError>("No matching error fixes found. Try relaxing the search query.".to_string()) } else { Ok::<String, crate::error::AppError>(serde_yaml::to_string(&suggestions)?) }
|
||||||
})?;
|
})?;
|
||||||
return Ok(data);
|
return Ok(data);
|
||||||
}
|
}
|
||||||
@@ -153,7 +153,7 @@ impl McpTool for SearchErrorFixesHandler {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
Ok::<String, crate::error::AppError>(serde_yaml::to_string(&filtered)?)
|
if filtered.is_empty() { Ok::<String, crate::error::AppError>(format!("No matching results found.")) } else { Ok::<String, crate::error::AppError>(serde_yaml::to_string(&filtered)?) }
|
||||||
})?;
|
})?;
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
@@ -175,7 +175,7 @@ impl McpTool for LogCodeChangeHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<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| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
let mut description = req.description;
|
let mut description = req.description;
|
||||||
|
|
||||||
if let Some(range) = &req.line_range {
|
if let Some(range) = &req.line_range {
|
||||||
@@ -203,17 +203,20 @@ impl McpTool for LogCodeChangeHandler {
|
|||||||
_ => crate::models::ChangeKind::Modified,
|
_ => crate::models::ChangeKind::Modified,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let (auto_branch, auto_commit, auto_repo) = crate::handlers::utils::get_git_context();
|
||||||
|
let effective_repo = req.repo_name.clone().filter(|rn| !rn.trim().is_empty()).or(auto_repo.clone());
|
||||||
|
|
||||||
let namespace = req
|
let namespace = req
|
||||||
.namespace
|
.namespace
|
||||||
.filter(|ns| !ns.trim().is_empty())
|
.filter(|ns| !ns.trim().is_empty())
|
||||||
.or_else(|| req.repo_name.clone().filter(|rn| !rn.trim().is_empty()))
|
.or_else(|| effective_repo.clone())
|
||||||
.unwrap_or_else(crate::models::default_namespace);
|
.unwrap_or_else(crate::models::default_namespace);
|
||||||
|
|
||||||
let symbols = req.symbol_references.clone().unwrap_or_default();
|
let symbols = req.symbol_references.clone().unwrap_or_default();
|
||||||
let line_range = req.line_range.clone();
|
let line_range = req.line_range.clone();
|
||||||
|
|
||||||
let effective_rev = req.revision.clone().or_else(|| req.git_commit.clone());
|
let effective_rev = req.revision.clone().or_else(|| req.git_commit.clone()).or(auto_commit);
|
||||||
let effective_branch = req.branch.clone().or_else(|| req.git_branch.clone());
|
let effective_branch = req.branch.clone().or_else(|| req.git_branch.clone()).or(auto_branch);
|
||||||
let detected_vcs = if let Some(vcs) = req.vcs_type.clone() {
|
let detected_vcs = if let Some(vcs) = req.vcs_type.clone() {
|
||||||
Some(vcs)
|
Some(vcs)
|
||||||
} else if let Some(ref rev) = effective_rev {
|
} else if let Some(ref rev) = effective_rev {
|
||||||
@@ -248,7 +251,7 @@ impl McpTool for LogCodeChangeHandler {
|
|||||||
description: description.clone(),
|
description: description.clone(),
|
||||||
git_commit: effective_rev.clone(),
|
git_commit: effective_rev.clone(),
|
||||||
git_branch: effective_branch.clone(),
|
git_branch: effective_branch.clone(),
|
||||||
repo_name: req.repo_name,
|
repo_name: effective_repo,
|
||||||
repo_url: req.repo_url,
|
repo_url: req.repo_url,
|
||||||
namespace: namespace.clone(),
|
namespace: namespace.clone(),
|
||||||
change_kind,
|
change_kind,
|
||||||
@@ -331,8 +334,12 @@ impl McpTool for QueryRecentChangesHandler {
|
|||||||
.skip(offset)
|
.skip(offset)
|
||||||
.take(limit)
|
.take(limit)
|
||||||
.cloned()
|
.cloned()
|
||||||
|
.map(|mut c| {
|
||||||
|
c.file_path = crate::handlers::utils::sanitize_path(&c.file_path);
|
||||||
|
c
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
Ok::<String, crate::error::AppError>(serde_yaml::to_string(&filtered)?)
|
if filtered.is_empty() { Ok::<String, crate::error::AppError>(format!("No matching results found.")) } else { Ok::<String, crate::error::AppError>(serde_yaml::to_string(&filtered)?) }
|
||||||
})?;
|
})?;
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
@@ -353,7 +360,7 @@ impl McpTool for DecisionsHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: DecisionsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: DecisionsTool = serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
match req {
|
match req {
|
||||||
DecisionsTool::Log { title, status, context, decision, consequences, namespace, repo_name, author, affected_components, alternatives_considered, supersedes, git_commit, git_branch, task_id } => {
|
DecisionsTool::Log { title, status, context, decision, consequences, namespace, repo_name, author, affected_components, alternatives_considered, supersedes, git_commit, git_branch, task_id } => {
|
||||||
let ns = namespace.clone().unwrap_or_else(crate::models::default_namespace);
|
let ns = namespace.clone().unwrap_or_else(crate::models::default_namespace);
|
||||||
@@ -545,7 +552,7 @@ impl McpTool for DecisionsHandler {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if include_body {
|
if include_body {
|
||||||
Ok::<String, crate::error::AppError>(serde_yaml::to_string(&filtered)?)
|
if filtered.is_empty() { Ok::<String, crate::error::AppError>(format!("No matching results found.")) } else { Ok::<String, crate::error::AppError>(serde_yaml::to_string(&filtered)?) }
|
||||||
} else {
|
} else {
|
||||||
let compact: Vec<_> = filtered
|
let compact: Vec<_> = filtered
|
||||||
.iter()
|
.iter()
|
||||||
@@ -607,7 +614,7 @@ impl McpTool for TechDebtHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: TechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: TechDebtTool = serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
match req {
|
match req {
|
||||||
TechDebtTool::Log { description, title: _, ideal_solution, git_commit, git_branch, symbol_references: _, line_range, namespace, severity, repo_name, file_path, workaround, effort_estimate } => {
|
TechDebtTool::Log { description, title: _, ideal_solution, git_commit, git_branch, symbol_references: _, line_range, namespace, severity, repo_name, file_path, workaround, effort_estimate } => {
|
||||||
let ns = namespace.clone().unwrap_or_else(crate::models::default_namespace);
|
let ns = namespace.clone().unwrap_or_else(crate::models::default_namespace);
|
||||||
@@ -625,10 +632,10 @@ impl McpTool for TechDebtHandler {
|
|||||||
ideal_solution: ideal,
|
ideal_solution: ideal,
|
||||||
is_resolved: false,
|
is_resolved: false,
|
||||||
created_at: crate::handlers::utils::now_secs(),
|
created_at: crate::handlers::utils::now_secs(),
|
||||||
git_commit: git_commit,
|
git_commit: git_commit.or_else(|| crate::handlers::utils::get_git_context().1),
|
||||||
git_branch: git_branch,
|
git_branch: git_branch.or_else(|| crate::handlers::utils::get_git_context().0),
|
||||||
embedding,
|
embedding,
|
||||||
repo_name: repo_name,
|
repo_name: repo_name.or_else(|| crate::handlers::utils::get_git_context().2),
|
||||||
severity: severity,
|
severity: severity,
|
||||||
file_path: file_path,
|
file_path: file_path,
|
||||||
line_range: line_range,
|
line_range: line_range,
|
||||||
@@ -733,7 +740,7 @@ impl McpTool for OmniSearchHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<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| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
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);
|
||||||
|
|
||||||
@@ -1086,7 +1093,50 @@ impl McpTool for OmniSearchHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(report.to_string())
|
let mut out = String::new();
|
||||||
|
out.push_str(&format!("## OmniSearch Results for '{}'\n\n", req.query));
|
||||||
|
if report.get("_truncated_to_max_tokens").is_some() {
|
||||||
|
out.push_str("> [!WARNING] Results truncated to fit token limits.\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
let sections = [
|
||||||
|
("Knowledge Graph", "knowledge_graph"),
|
||||||
|
("Tasks", "tasks"),
|
||||||
|
("Snippets", "snippets"),
|
||||||
|
("ADRs", "adrs"),
|
||||||
|
("Tech Debt", "tech_debts"),
|
||||||
|
("Handoff Memos", "handoff_memos"),
|
||||||
|
("Error Fixes", "error_fixes"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (title, key) in sections {
|
||||||
|
if let Some(arr) = report.get(key).and_then(|v| v.as_array()) {
|
||||||
|
if arr.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.push_str(&format!("### {}\n", title));
|
||||||
|
for item in arr {
|
||||||
|
// Try to extract a title or ID
|
||||||
|
let id = item.get("id").or_else(|| item.get("name")).and_then(|v| v.as_str()).unwrap_or("Unknown");
|
||||||
|
let text = item.get("title").or_else(|| item.get("description")).or_else(|| item.get("content")).and_then(|v| v.as_str()).unwrap_or("");
|
||||||
|
let text_trunc: String = text.chars().take(200).collect();
|
||||||
|
out.push_str(&format!("- **{}**: {}\n", id, text_trunc.replace('\n', " ")));
|
||||||
|
}
|
||||||
|
out.push_str("\n");
|
||||||
|
} else if let Some(obj) = report.get(key).and_then(|v| v.as_object()) {
|
||||||
|
if obj.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.push_str(&format!("### {}\n", title));
|
||||||
|
for (id, val) in obj {
|
||||||
|
let text = val.get("entity_type").and_then(|v| v.as_str()).unwrap_or("Entity");
|
||||||
|
out.push_str(&format!("- **{}** ({})\n", id, text));
|
||||||
|
}
|
||||||
|
out.push_str("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1106,7 +1156,7 @@ impl McpTool for GetProjectHealthHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<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| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
let active_tasks = state.project.tasks.read_with(|tasks| {
|
let active_tasks = state.project.tasks.read_with(|tasks| {
|
||||||
tasks
|
tasks
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1131,13 +1181,15 @@ impl McpTool for GetProjectHealthHandler {
|
|||||||
.filter(|m| m.namespace == req.namespace && m.status != "done")
|
.filter(|m| m.namespace == req.namespace && m.status != "done")
|
||||||
.count()
|
.count()
|
||||||
});
|
});
|
||||||
let report = serde_json::json!({
|
let out = format!(
|
||||||
"active_tasks": active_tasks,
|
"## Project Health (Namespace: {})\n\n\
|
||||||
"unresolved_tech_debt": unresolved_debt,
|
- **Active Tasks:** {}\n\
|
||||||
"unread_handoff_memos": unread_memos,
|
- **Unresolved Tech Debt:** {}\n\
|
||||||
"active_milestones": active_milestones
|
- **Unread Handoff Memos:** {}\n\
|
||||||
});
|
- **Active Milestones:** {}\n",
|
||||||
Ok(report.to_string())
|
req.namespace, active_tasks, unresolved_debt, unread_memos, active_milestones
|
||||||
|
);
|
||||||
|
Ok(out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1157,7 +1209,7 @@ impl McpTool for ManageCheckpointHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: ManageCheckpointTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: ManageCheckpointTool = serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
match req {
|
match req {
|
||||||
ManageCheckpointTool::Create { name_or_id, description, namespace } => {
|
ManageCheckpointTool::Create { name_or_id, description, namespace } => {
|
||||||
let name = name_or_id.ok_or_else(|| crate::error::AppError::Internal("name_or_id is required".to_string()))?;
|
let name = name_or_id.ok_or_else(|| crate::error::AppError::Internal("name_or_id is required".to_string()))?;
|
||||||
@@ -1297,7 +1349,7 @@ impl McpTool for QueryLineageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: QueryLineageTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: QueryLineageTool = serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
let q = req.query.to_lowercase();
|
let q = req.query.to_lowercase();
|
||||||
|
|
||||||
let mut timeline = Vec::new();
|
let mut timeline = Vec::new();
|
||||||
@@ -1373,13 +1425,15 @@ impl McpTool for QueryLineageHandler {
|
|||||||
|
|
||||||
timeline.sort_by_key(|item| item["timestamp"].as_u64().unwrap_or(0));
|
timeline.sort_by_key(|item| item["timestamp"].as_u64().unwrap_or(0));
|
||||||
|
|
||||||
let res = serde_json::json!({
|
let mut out = format!("## Lineage Query Results for '{}'\n\nFound {} items:\n\n", req.query, timeline.len());
|
||||||
"query": req.query,
|
for item in timeline {
|
||||||
"lineage_count": timeline.len(),
|
out.push_str(&format!("- **{}** (Type: `{}`)\n", item["id"].as_str().unwrap_or(""), item["type"].as_str().unwrap_or("")));
|
||||||
"timeline": timeline
|
if let Some(msg) = item["message"].as_str() {
|
||||||
});
|
out.push_str(&format!(" {}\n", msg.replace('\n', " ")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(serde_yaml::to_string(&res)?)
|
Ok(out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1400,7 +1454,7 @@ impl McpTool for GetNextActionableTasksHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: GetNextActionableTasksTool =
|
let req: GetNextActionableTasksTool =
|
||||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
let limit = req.limit.unwrap_or(5);
|
let limit = req.limit.unwrap_or(5);
|
||||||
|
|
||||||
let tasks = state.project.tasks.read_with(|t| t.clone());
|
let tasks = state.project.tasks.read_with(|t| t.clone());
|
||||||
@@ -1434,12 +1488,21 @@ impl McpTool for GetNextActionableTasksHandler {
|
|||||||
|
|
||||||
actionable.truncate(limit);
|
actionable.truncate(limit);
|
||||||
|
|
||||||
let res = serde_json::json!({
|
let mut out = format!("Found {} actionable tasks:\n\n", actionable.len());
|
||||||
"actionable_count": actionable.len(),
|
for t in actionable {
|
||||||
"tasks": actionable
|
out.push_str(&format!("- [{}] **{}** (ID: `{}`)\n", t.status, t.title, t.id));
|
||||||
});
|
if !t.description.is_empty() {
|
||||||
|
let desc: String = t.description.chars().take(150).collect();
|
||||||
Ok(serde_yaml::to_string(&res)?)
|
out.push_str(&format!(" {}\n", desc.replace('\n', " ")));
|
||||||
|
}
|
||||||
|
if !t.dependencies.is_empty() {
|
||||||
|
out.push_str(&format!(" Dependencies: `{}`\n", t.dependencies.join("`, `")));
|
||||||
|
}
|
||||||
|
if let Some(b) = &t.git_branch {
|
||||||
|
out.push_str(&format!(" Branch: `{}`\n", b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1459,7 +1522,7 @@ impl McpTool for HypothesesHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: HypothesesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: HypothesesTool = serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
match req {
|
match req {
|
||||||
HypothesesTool::Log { hypothesis, task_id, status, evidence } => {
|
HypothesesTool::Log { hypothesis, task_id, status, evidence } => {
|
||||||
let hyp_text = hypothesis;
|
let hyp_text = hypothesis;
|
||||||
@@ -1527,7 +1590,7 @@ impl McpTool for GetPreflightContextHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: GetPreflightContextTool =
|
let req: GetPreflightContextTool =
|
||||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
|
|
||||||
let tasks = state.project.tasks.read_with(|t| t.clone());
|
let tasks = state.project.tasks.read_with(|t| t.clone());
|
||||||
let tech_debts = state.code.tech_debts.read_with(|d| d.clone());
|
let tech_debts = state.code.tech_debts.read_with(|d| d.clone());
|
||||||
@@ -1573,17 +1636,40 @@ impl McpTool for GetPreflightContextHandler {
|
|||||||
.take(5)
|
.take(5)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let preflight = serde_json::json!({
|
let mut out = format!("## Preflight Context (Namespace: {}, Branch: {})\n\n", req.namespace, req.git_branch.unwrap_or_default());
|
||||||
"namespace": req.namespace,
|
|
||||||
"git_branch": req.git_branch,
|
|
||||||
"active_tasks": active_tasks,
|
|
||||||
"top_open_tech_debts": open_tech_debts,
|
|
||||||
"active_hypotheses": active_hypotheses,
|
|
||||||
"recent_terminal_commands": recent_commands,
|
|
||||||
"recent_activities": recent_activities
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(serde_yaml::to_string(&preflight)?)
|
out.push_str(&format!("### Active Tasks ({})\n", active_tasks.len()));
|
||||||
|
for t in active_tasks {
|
||||||
|
out.push_str(&format!("- [{}] **{}** (ID: `{}`)\n", t["status"].as_str().unwrap_or(""), t["title"].as_str().unwrap_or(""), t["id"].as_str().unwrap_or("")));
|
||||||
|
}
|
||||||
|
out.push_str("\n");
|
||||||
|
|
||||||
|
out.push_str(&format!("### Top Open Tech Debt ({})\n", open_tech_debts.len()));
|
||||||
|
for d in open_tech_debts {
|
||||||
|
out.push_str(&format!("- **ID: {}**\n Description: {}\n", d["id"].as_str().unwrap_or(""), d["description"].as_str().unwrap_or("")));
|
||||||
|
}
|
||||||
|
out.push_str("\n");
|
||||||
|
|
||||||
|
out.push_str(&format!("### Active Hypotheses ({})\n", active_hypotheses.len()));
|
||||||
|
for h in active_hypotheses {
|
||||||
|
out.push_str(&format!("- [{}] {}\n", h.status, h.hypothesis));
|
||||||
|
}
|
||||||
|
out.push_str("\n");
|
||||||
|
|
||||||
|
out.push_str("### Recent Terminal Commands\n");
|
||||||
|
for c in recent_commands {
|
||||||
|
out.push_str(&format!("- `{}` (Exit Code: {})\n", c.command, c.exit_code));
|
||||||
|
}
|
||||||
|
out.push_str("\n");
|
||||||
|
|
||||||
|
out.push_str("### Recent Activities\n");
|
||||||
|
for a in recent_activities {
|
||||||
|
let cat = a.get("category").and_then(|v| v.as_str()).unwrap_or("Unknown");
|
||||||
|
let sum = a.get("summary").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
|
out.push_str(&format!("- {} ({})\n", cat, sum));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1603,7 +1689,7 @@ impl McpTool for AgentSignalsHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: AgentSignalsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: AgentSignalsTool = serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
match req {
|
match req {
|
||||||
AgentSignalsTool::Broadcast { sender, signal_type, payload, ttl_seconds } => {
|
AgentSignalsTool::Broadcast { sender, signal_type, payload, ttl_seconds } => {
|
||||||
let sender = sender;
|
let sender = sender;
|
||||||
@@ -1705,7 +1791,7 @@ impl McpTool for AutoSessionCheckpointHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: AutoSessionCheckpointTool =
|
let req: AutoSessionCheckpointTool =
|
||||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
|
|
||||||
let timestamp = std::time::SystemTime::now()
|
let timestamp = std::time::SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
@@ -1740,6 +1826,8 @@ impl McpTool for AutoSessionCheckpointHandler {
|
|||||||
timestamp, active_tasks, unverified_hyp, recent_changes
|
timestamp, active_tasks, unverified_hyp, recent_changes
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let (auto_branch, auto_commit, auto_repo) = crate::handlers::utils::get_git_context();
|
||||||
|
|
||||||
let memo = crate::models::HandoffMemo {
|
let memo = crate::models::HandoffMemo {
|
||||||
id: memo_id.clone(),
|
id: memo_id.clone(),
|
||||||
author,
|
author,
|
||||||
@@ -1747,6 +1835,9 @@ impl McpTool for AutoSessionCheckpointHandler {
|
|||||||
expires_at: None,
|
expires_at: None,
|
||||||
namespace: req.namespace,
|
namespace: req.namespace,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
git_branch: auto_branch,
|
||||||
|
vcs_revision: auto_commit,
|
||||||
|
repo_name: auto_repo,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1812,7 +1903,7 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(res.contains("unresolved_tech_debt"));
|
assert!(res.contains("Unresolved Tech Debt:"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -2238,14 +2329,14 @@ mod tests {
|
|||||||
.execute(serde_json::json!({}), state.clone())
|
.execute(serde_json::json!({}), state.clone())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(proj_h_res.contains("active_tasks"));
|
assert!(proj_h_res.contains("Active Tasks:"));
|
||||||
|
|
||||||
let sweep_h = SweepGraphHealthHandler;
|
let sweep_h = SweepGraphHealthHandler;
|
||||||
let sweep_h_res = sweep_h
|
let sweep_h_res = sweep_h
|
||||||
.execute(serde_json::json!({}), state.clone())
|
.execute(serde_json::json!({}), state.clone())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(sweep_h_res.contains("orphaned_entities"));
|
assert!(sweep_h_res.contains("Orphaned Entities"));
|
||||||
|
|
||||||
// AutoSessionCheckpoint
|
// AutoSessionCheckpoint
|
||||||
let chk = AutoSessionCheckpointHandler;
|
let chk = AutoSessionCheckpointHandler;
|
||||||
@@ -2268,7 +2359,7 @@ mod tests {
|
|||||||
.execute(serde_json::json!({"query": "test_sym"}), state.clone())
|
.execute(serde_json::json!({"query": "test_sym"}), state.clone())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(q_lin_res.contains("timeline"));
|
assert!(q_lin_res.contains("Lineage Query Results for"));
|
||||||
|
|
||||||
// Agent Signals
|
// Agent Signals
|
||||||
let sig_handler = AgentSignalsHandler;
|
let sig_handler = AgentSignalsHandler;
|
||||||
@@ -2326,7 +2417,7 @@ mod tests {
|
|||||||
.execute(serde_json::json!({}), state.clone())
|
.execute(serde_json::json!({}), state.clone())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(get_next_res.contains("actionable_count"));
|
assert!(get_next_res.contains("actionable tasks"));
|
||||||
|
|
||||||
// GetPreflightContext
|
// GetPreflightContext
|
||||||
let preflight = GetPreflightContextHandler;
|
let preflight = GetPreflightContextHandler;
|
||||||
@@ -2334,7 +2425,7 @@ mod tests {
|
|||||||
.execute(serde_json::json!({}), state.clone())
|
.execute(serde_json::json!({}), state.clone())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(preflight_res.contains("active_tasks"));
|
assert!(preflight_res.contains("Active Tasks"));
|
||||||
|
|
||||||
// ManageCheckpoint
|
// ManageCheckpoint
|
||||||
let mg_chk = ManageCheckpointHandler;
|
let mg_chk = ManageCheckpointHandler;
|
||||||
@@ -2551,7 +2642,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(preflight_res2.contains("active_tasks"));
|
assert!(preflight_res2.contains("Active Tasks"));
|
||||||
|
|
||||||
// Hypotheses query with task_id and query
|
// Hypotheses query with task_id and query
|
||||||
let hyp_handler = HypothesesHandler;
|
let hyp_handler = HypothesesHandler;
|
||||||
@@ -2653,7 +2744,7 @@ mod tests {
|
|||||||
.execute(serde_json::json!({"query": "E0425"}), state.clone())
|
.execute(serde_json::json!({"query": "E0425"}), state.clone())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(q_lin_res.contains("lineage_count"));
|
assert!(q_lin_res.contains("Lineage Query Results for"));
|
||||||
|
|
||||||
// Agent signals filtering
|
// Agent signals filtering
|
||||||
let sig_handler = AgentSignalsHandler;
|
let sig_handler = AgentSignalsHandler;
|
||||||
@@ -2779,7 +2870,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(next_act_res.contains("actionable_count"));
|
assert!(next_act_res.contains("actionable tasks"));
|
||||||
|
|
||||||
// GetPreflightContext with branch
|
// GetPreflightContext with branch
|
||||||
let preflight = GetPreflightContextHandler;
|
let preflight = GetPreflightContextHandler;
|
||||||
@@ -2790,7 +2881,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(pre_res.contains("active_tasks"));
|
assert!(pre_res.contains("Active Tasks"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod ast;
|
pub mod ast;
|
||||||
|
pub mod http;
|
||||||
pub mod env;
|
pub mod env;
|
||||||
pub mod git;
|
pub mod git;
|
||||||
pub mod graph;
|
pub mod graph;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ impl McpTool for HandoffMemosHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: HandoffMemosTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: HandoffMemosTool = serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
match req.action {
|
match req.action {
|
||||||
HandoffMemoAction::Leave => {
|
HandoffMemoAction::Leave => {
|
||||||
let content = req.content.ok_or_else(|| {
|
let content = req.content.ok_or_else(|| {
|
||||||
@@ -32,6 +32,7 @@ impl McpTool for HandoffMemosHandler {
|
|||||||
let ns = req
|
let ns = req
|
||||||
.namespace
|
.namespace
|
||||||
.unwrap_or_else(crate::models::default_namespace);
|
.unwrap_or_else(crate::models::default_namespace);
|
||||||
|
let (auto_branch, auto_commit, auto_repo) = crate::handlers::utils::get_git_context();
|
||||||
state.telemetry.handoff_memos.modify(|memos| {
|
state.telemetry.handoff_memos.modify(|memos| {
|
||||||
memos.push(crate::models::HandoffMemo {
|
memos.push(crate::models::HandoffMemo {
|
||||||
id: uuid::Uuid::new_v4().to_string(),
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
@@ -40,6 +41,9 @@ impl McpTool for HandoffMemosHandler {
|
|||||||
namespace: ns,
|
namespace: ns,
|
||||||
timestamp: crate::handlers::utils::now_secs(),
|
timestamp: crate::handlers::utils::now_secs(),
|
||||||
expires_at: None,
|
expires_at: None,
|
||||||
|
git_branch: auto_branch,
|
||||||
|
vcs_revision: auto_commit,
|
||||||
|
repo_name: auto_repo,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
if memos.len() > 200 {
|
if memos.len() > 200 {
|
||||||
@@ -95,7 +99,7 @@ impl McpTool for AddSessionSummaryHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<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| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
state.telemetry.session_summaries.modify(|summaries| {
|
state.telemetry.session_summaries.modify(|summaries| {
|
||||||
summaries.push(crate::models::SessionSummary {
|
summaries.push(crate::models::SessionSummary {
|
||||||
summary: req.summary,
|
summary: req.summary,
|
||||||
@@ -103,9 +107,9 @@ impl McpTool for AddSessionSummaryHandler {
|
|||||||
timestamp: crate::handlers::utils::now_secs(),
|
timestamp: crate::handlers::utils::now_secs(),
|
||||||
expires_at: None,
|
expires_at: None,
|
||||||
session_id: req.session_id,
|
session_id: req.session_id,
|
||||||
repo_name: req.repo_name,
|
repo_name: req.repo_name.or_else(|| crate::handlers::utils::get_git_context().2),
|
||||||
git_branch: req.git_branch,
|
git_branch: req.git_branch.or_else(|| crate::handlers::utils::get_git_context().0),
|
||||||
git_commit: req.git_commit,
|
git_commit: req.git_commit.or_else(|| crate::handlers::utils::get_git_context().1),
|
||||||
tasks_completed: req.tasks_completed.unwrap_or_default(),
|
tasks_completed: req.tasks_completed.unwrap_or_default(),
|
||||||
next_steps: req.next_steps.unwrap_or_default(),
|
next_steps: req.next_steps.unwrap_or_default(),
|
||||||
});
|
});
|
||||||
@@ -143,7 +147,7 @@ impl McpTool for GenerateStandupReportHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<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| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
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);
|
||||||
|
|
||||||
let filtered_tasks = state.project.tasks.read_with(|items| {
|
let filtered_tasks = state.project.tasks.read_with(|items| {
|
||||||
@@ -168,13 +172,29 @@ impl McpTool for GenerateStandupReportHandler {
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
});
|
});
|
||||||
|
|
||||||
let report = serde_json::json!({
|
let mut out = String::new();
|
||||||
"tasks_updated": filtered_tasks,
|
out.push_str(&format!("## Standup Report (Past {} hours)\n\n", req.hours_lookback));
|
||||||
"code_changes": filtered_changes,
|
|
||||||
"session_summaries": filtered_summaries
|
out.push_str(&format!("### Tasks Updated ({})\n", filtered_tasks.len()));
|
||||||
});
|
for t in filtered_tasks {
|
||||||
let report_str = report.to_string();
|
out.push_str(&format!("- [{}] **{}** (ID: `{}`)\n", t.status, t.title, t.id));
|
||||||
Ok(report_str)
|
}
|
||||||
|
out.push_str("\n");
|
||||||
|
|
||||||
|
out.push_str(&format!("### Code Changes ({})\n", filtered_changes.len()));
|
||||||
|
for c in filtered_changes {
|
||||||
|
let branch = c.git_branch.as_deref().unwrap_or("unknown");
|
||||||
|
out.push_str(&format!("- `{}` (Branch: `{}`): {}\n", c.file_path, branch, c.description));
|
||||||
|
}
|
||||||
|
out.push_str("\n");
|
||||||
|
|
||||||
|
out.push_str(&format!("### Session Summaries ({})\n", filtered_summaries.len()));
|
||||||
|
for s in filtered_summaries {
|
||||||
|
out.push_str(&format!("- {}\n", s.summary));
|
||||||
|
}
|
||||||
|
out.push_str("\n");
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,7 +214,7 @@ impl McpTool for PromoteToEntityHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: PromoteToEntityTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: PromoteToEntityTool = serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
|
|
||||||
let entity_name = req.entity_name.clone();
|
let entity_name = req.entity_name.clone();
|
||||||
let obs = req.content.clone();
|
let obs = req.content.clone();
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ impl McpTool for TasksHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: TasksTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: TasksTool = serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
match req {
|
match req {
|
||||||
TasksTool::Add { title, description, parent_id, dependencies, git_branch, namespace, repo_name, priority, assigned_agent, verification_command } => {
|
TasksTool::Add { title, description, parent_id, dependencies, git_branch, namespace, repo_name, priority, assigned_agent, verification_command } => {
|
||||||
let description = description.unwrap_or_default();
|
let description = description.unwrap_or_default();
|
||||||
@@ -37,13 +37,13 @@ impl McpTool for TasksHandler {
|
|||||||
description,
|
description,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
git_branch,
|
git_branch: git_branch.or_else(|| crate::handlers::utils::get_git_context().0),
|
||||||
parent_id,
|
parent_id,
|
||||||
dependencies: deps,
|
dependencies: deps,
|
||||||
acceptance_criteria: vec![],
|
acceptance_criteria: vec![],
|
||||||
expires_at: None,
|
expires_at: None,
|
||||||
namespace: namespace.unwrap_or_else(|| "default".to_string()),
|
namespace: namespace.unwrap_or_else(|| "default".to_string()),
|
||||||
repo_name,
|
repo_name: repo_name.or_else(|| crate::handlers::utils::get_git_context().2),
|
||||||
priority,
|
priority,
|
||||||
assigned_agent,
|
assigned_agent,
|
||||||
verification_command,
|
verification_command,
|
||||||
@@ -414,7 +414,7 @@ impl McpTool for MilestonesHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: MilestonesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: MilestonesTool = serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
match req {
|
match req {
|
||||||
MilestonesTool::Add { title, namespace, target_date, description, deliverables, task_ids, repo_name } => {
|
MilestonesTool::Add { title, namespace, target_date, description, deliverables, task_ids, repo_name } => {
|
||||||
let ns = namespace.unwrap_or_else(crate::models::default_namespace);
|
let ns = namespace.unwrap_or_else(crate::models::default_namespace);
|
||||||
@@ -430,7 +430,7 @@ impl McpTool for MilestonesHandler {
|
|||||||
description: description,
|
description: description,
|
||||||
task_ids: task_ids.unwrap_or_default(),
|
task_ids: task_ids.unwrap_or_default(),
|
||||||
deliverables: deliverables.unwrap_or_default(),
|
deliverables: deliverables.unwrap_or_default(),
|
||||||
repo_name,
|
repo_name: repo_name.or_else(|| crate::handlers::utils::get_git_context().2),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ pub fn now_secs() -> u64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_safe_path(path_str: &str) -> crate::error::Result<std::path::PathBuf> {
|
pub fn validate_safe_path(path_str: &str) -> crate::error::Result<std::path::PathBuf> {
|
||||||
let path = std::path::Path::new(path_str);
|
let sanitized_str = sanitize_path(path_str);
|
||||||
|
let path = std::path::Path::new(&sanitized_str);
|
||||||
for component in path.components() {
|
for component in path.components() {
|
||||||
if component == std::path::Component::ParentDir {
|
if component == std::path::Component::ParentDir {
|
||||||
return Err(crate::error::AppError::BadRequest(format!(
|
return Err(crate::error::AppError::BadRequest(format!(
|
||||||
@@ -27,6 +28,56 @@ pub fn validate_safe_path(path_str: &str) -> crate::error::Result<std::path::Pat
|
|||||||
Ok(path.to_path_buf())
|
Ok(path.to_path_buf())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sanitizes a given path based on the operating environment of the server.
|
||||||
|
/// This translates Windows paths to WSL paths if the server runs in Linux,
|
||||||
|
/// and translates WSL paths to Windows paths if the server runs in Windows.
|
||||||
|
pub fn sanitize_path(raw_path: &str) -> String {
|
||||||
|
if cfg!(unix) {
|
||||||
|
if raw_path.contains(":\\") {
|
||||||
|
if let Ok(output) = std::process::Command::new("wslpath").arg("-u").arg(raw_path).output() {
|
||||||
|
if output.status.success() {
|
||||||
|
return String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if raw_path.len() >= 3 && &raw_path[1..3] == ":\\" {
|
||||||
|
let drive_letter = &raw_path[0..1].to_lowercase();
|
||||||
|
let remainder = raw_path[3..].replace("\\", "/");
|
||||||
|
return format!("/mnt/{}/{}", drive_letter, remainder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if cfg!(windows) {
|
||||||
|
if raw_path.starts_with("/mnt/") && raw_path.len() > 6 {
|
||||||
|
let drive_letter = &raw_path[5..6].to_uppercase();
|
||||||
|
let remainder = raw_path[7..].replace("/", "\\");
|
||||||
|
return format!("{}:\\{}", drive_letter, remainder);
|
||||||
|
} else if raw_path.starts_with("/") {
|
||||||
|
return format!("\\\\wsl$\\Ubuntu{}", raw_path.replace("/", "\\"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
raw_path.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_git_context() -> (Option<String>, Option<String>, Option<String>) {
|
||||||
|
let mut branch = None;
|
||||||
|
let mut commit = None;
|
||||||
|
let mut repo_name = None;
|
||||||
|
|
||||||
|
if let Ok(cwd) = std::env::current_dir() {
|
||||||
|
if let Ok(repo) = git2::Repository::discover(&cwd) {
|
||||||
|
if let Ok(head) = repo.head() {
|
||||||
|
branch = head.shorthand().map(|s| s.to_string());
|
||||||
|
commit = head.target().map(|oid| oid.to_string());
|
||||||
|
}
|
||||||
|
if let Some(path) = repo.workdir() {
|
||||||
|
if let Some(name) = path.file_name() {
|
||||||
|
repo_name = name.to_str().map(|s| s.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(branch, commit, repo_name)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -700,7 +700,7 @@ impl McpTool for ClipboardHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: ClipboardTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: ClipboardTool = serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
match req.action {
|
match req.action {
|
||||||
ClipboardAction::Image => {
|
ClipboardAction::Image => {
|
||||||
let cached_opt = {
|
let cached_opt = {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ impl McpTool for SnippetsHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: SnippetsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: SnippetsTool = serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
match req.action {
|
match req.action {
|
||||||
SnippetAction::Store => {
|
SnippetAction::Store => {
|
||||||
let name = req.name.or(req.query).or(req.id).ok_or_else(|| {
|
let name = req.name.or(req.query).or(req.id).ok_or_else(|| {
|
||||||
@@ -231,7 +231,7 @@ impl McpTool for ReadDirectoryArchitectureHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let tool_args: ReadDirectoryArchitectureTool =
|
let tool_args: ReadDirectoryArchitectureTool =
|
||||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
|
|
||||||
let dir = tool_args.directory.clone();
|
let dir = tool_args.directory.clone();
|
||||||
let result = tokio::task::spawn_blocking(move || -> crate::error::Result<String> {
|
let result = tokio::task::spawn_blocking(move || -> crate::error::Result<String> {
|
||||||
@@ -301,7 +301,7 @@ impl McpTool for ReadDirectoryArchitectureHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
summary.push_str(&format!("Architecture of {}:\n", dir));
|
summary.push_str(&format!("Architecture of {}:\n", dir));
|
||||||
visit_dirs(path, &mut summary, 0).map_err(|e| e.to_string())?;
|
visit_dirs(path, &mut summary, 0).map_err(|e| crate::error::AppError::Internal(format!("Failed to read directory: {}", e)))?;
|
||||||
|
|
||||||
Ok(summary)
|
Ok(summary)
|
||||||
})
|
})
|
||||||
@@ -330,7 +330,7 @@ impl McpTool for SemanticCodeSearchHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let tool_args: SemanticCodeSearchTool =
|
let tool_args: SemanticCodeSearchTool =
|
||||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
|
|
||||||
let results = state
|
let results = state
|
||||||
.search()
|
.search()
|
||||||
@@ -388,7 +388,7 @@ impl McpTool for ManageSubagentNamespaceHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: ManageSubagentNamespaceTool =
|
let req: ManageSubagentNamespaceTool =
|
||||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
serde_json::from_value(args).map_err(|e| crate::error::AppError::BadRequest(format!("Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e)))?;
|
||||||
let subagent_ns = format!("subagent-{}", req.subagent_id);
|
let subagent_ns = format!("subagent-{}", req.subagent_id);
|
||||||
|
|
||||||
match req.action {
|
match req.action {
|
||||||
|
|||||||
+13
-12
@@ -65,9 +65,9 @@ impl McpResource for GraphEntitiesResource {
|
|||||||
"_meta": format!("Showing 100 of {} total entities. Use omni_search or get_subgraph for targeted entity discovery.", data.len()),
|
"_meta": format!("Showing 100 of {} total entities. Use omni_search or get_subgraph for targeted entity discovery.", data.len()),
|
||||||
"entities": truncated
|
"entities": truncated
|
||||||
});
|
});
|
||||||
Ok(serde_json::to_string_pretty(&res)?)
|
Ok(serde_yaml::to_string(&res)?)
|
||||||
} else {
|
} else {
|
||||||
Ok(serde_json::to_string_pretty(&data)?)
|
Ok(serde_yaml::to_string(&data)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,9 +93,9 @@ impl McpResource for GraphRelationsResource {
|
|||||||
"_meta": format!("Showing 200 of {} total relations. Use query_graph_path or get_subgraph for targeted relation discovery.", data.len()),
|
"_meta": format!("Showing 200 of {} total relations. Use query_graph_path or get_subgraph for targeted relation discovery.", data.len()),
|
||||||
"relations": truncated
|
"relations": truncated
|
||||||
});
|
});
|
||||||
Ok(serde_json::to_string_pretty(&res)?)
|
Ok(serde_yaml::to_string(&res)?)
|
||||||
} else {
|
} else {
|
||||||
Ok(serde_json::to_string_pretty(&data)?)
|
Ok(serde_yaml::to_string(&data)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,7 +120,7 @@ impl McpResource for TasksActiveResource {
|
|||||||
.read()
|
.read()
|
||||||
.unwrap_or_else(|e| e.into_inner());
|
.unwrap_or_else(|e| e.into_inner());
|
||||||
let data: Vec<_> = tasks.iter().filter(|t| t.is_active()).collect();
|
let data: Vec<_> = tasks.iter().filter(|t| t.is_active()).collect();
|
||||||
Ok(serde_json::to_string_pretty(&data)?)
|
Ok(serde_yaml::to_string(&data)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@ impl McpResource for DecisionsActiveResource {
|
|||||||
.read()
|
.read()
|
||||||
.unwrap_or_else(|e| e.into_inner());
|
.unwrap_or_else(|e| e.into_inner());
|
||||||
let data: Vec<_> = adrs.iter().filter(|a| a.status == "accepted").collect();
|
let data: Vec<_> = adrs.iter().filter(|a| a.status == "accepted").collect();
|
||||||
Ok(serde_json::to_string_pretty(&data)?)
|
Ok(serde_yaml::to_string(&data)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +168,7 @@ impl McpResource for TechDebtActiveResource {
|
|||||||
.read()
|
.read()
|
||||||
.unwrap_or_else(|e| e.into_inner());
|
.unwrap_or_else(|e| e.into_inner());
|
||||||
let data: Vec<_> = debts.iter().filter(|d| !d.is_resolved).collect();
|
let data: Vec<_> = debts.iter().filter(|d| !d.is_resolved).collect();
|
||||||
Ok(serde_json::to_string_pretty(&data)?)
|
Ok(serde_yaml::to_string(&data)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +267,7 @@ impl McpResource for SessionDeltaResource {
|
|||||||
"timestamp": now,
|
"timestamp": now,
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(serde_json::to_string_pretty(&delta)?)
|
Ok(serde_yaml::to_string(&delta)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,7 +360,7 @@ impl MemoryHandler {
|
|||||||
.cache
|
.cache
|
||||||
.read()
|
.read()
|
||||||
.unwrap_or_else(|e| e.into_inner());
|
.unwrap_or_else(|e| e.into_inner());
|
||||||
Ok(serde_json::to_string_pretty(&*items)?)
|
Ok(serde_yaml::to_string(&*items)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
struct RecentActivitiesResource;
|
struct RecentActivitiesResource;
|
||||||
@@ -382,7 +382,7 @@ impl MemoryHandler {
|
|||||||
.cache
|
.cache
|
||||||
.read()
|
.read()
|
||||||
.unwrap_or_else(|e| e.into_inner());
|
.unwrap_or_else(|e| e.into_inner());
|
||||||
Ok(serde_json::to_string_pretty(&*items)?)
|
Ok(serde_yaml::to_string(&*items)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,7 +405,7 @@ impl MemoryHandler {
|
|||||||
.cache
|
.cache
|
||||||
.read()
|
.read()
|
||||||
.unwrap_or_else(|e| e.into_inner());
|
.unwrap_or_else(|e| e.into_inner());
|
||||||
Ok(serde_json::to_string_pretty(&*items)?)
|
Ok(serde_yaml::to_string(&*items)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -536,6 +536,7 @@ impl MemoryHandler {
|
|||||||
register!(graph::SummarizeSubgraphHandler);
|
register!(graph::SummarizeSubgraphHandler);
|
||||||
register!(meta::AgentSignalsHandler);
|
register!(meta::AgentSignalsHandler);
|
||||||
register!(meta::AutoSessionCheckpointHandler);
|
register!(meta::AutoSessionCheckpointHandler);
|
||||||
|
register!(http::FetchUrlHandler);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
state,
|
state,
|
||||||
@@ -659,7 +660,7 @@ impl MemoryHandler {
|
|||||||
Err(e) => Some(crate::mcp::error(id, -32603, &e.to_string())),
|
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. To see available resources, call the list_resources tool or query the 'resources/list' endpoint." ))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+22
-3
@@ -100,6 +100,8 @@ pub struct DeleteRelationsTool {
|
|||||||
pub struct ReadGraphTool {
|
pub struct ReadGraphTool {
|
||||||
/// Optional namespace to restrict the read to.
|
/// Optional namespace to restrict the read to.
|
||||||
pub namespace: Option<String>,
|
pub namespace: Option<String>,
|
||||||
|
/// Optional entity_type to filter entities by (e.g. 'DatabaseTable').
|
||||||
|
pub entity_type: Option<String>,
|
||||||
/// Optional token budget cap. Truncates graph outputs to fit within the specified token budget.
|
/// Optional token budget cap. Truncates graph outputs to fit within the specified token budget.
|
||||||
pub max_tokens: Option<usize>,
|
pub max_tokens: Option<usize>,
|
||||||
/// Optional maximum number of entities to return (pagination).
|
/// Optional maximum number of entities to return (pagination).
|
||||||
@@ -213,7 +215,7 @@ pub struct GetSubgraphTool {
|
|||||||
pub root_entity: Option<String>,
|
pub root_entity: Option<String>,
|
||||||
/// Maximum search depth (hops). Defaults to 2.
|
/// Maximum search depth (hops). Defaults to 2.
|
||||||
pub depth: Option<u32>,
|
pub depth: Option<u32>,
|
||||||
/// Output format: 'json' (raw entities and relations) or 'markdown_tree' (compact topology tree). Defaults to 'json'.
|
/// Output format: 'json' (raw entities and relations) or 'markdown_tree' (compact topology tree). Defaults to 'markdown_tree'.
|
||||||
pub format: Option<SubgraphFormat>,
|
pub format: Option<SubgraphFormat>,
|
||||||
/// Optional namespace filter.
|
/// Optional namespace filter.
|
||||||
pub namespace: Option<String>,
|
pub namespace: Option<String>,
|
||||||
@@ -496,8 +498,8 @@ pub struct ReadFileSkeletonTool {
|
|||||||
pub file_path: String,
|
pub file_path: String,
|
||||||
}
|
}
|
||||||
/// Replace a specific AST node in a file (robust structural editing). Use this instead of regex or line-based string replacement to prevent indentation bugs and matching failures.
|
/// Replace a specific AST node in a file (robust structural editing). Use this instead of regex or line-based string replacement to prevent indentation bugs and matching failures.
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)]
|
||||||
pub struct ReplaceAstNodeTool {
|
pub struct AstNodeReplacement {
|
||||||
/// The path of the file to modify.
|
/// The path of the file to modify.
|
||||||
pub file_path: String,
|
pub file_path: String,
|
||||||
/// The AST node type to replace (e.g., 'function_item', 'impl_item').
|
/// The AST node type to replace (e.g., 'function_item', 'impl_item').
|
||||||
@@ -508,6 +510,12 @@ pub struct ReplaceAstNodeTool {
|
|||||||
pub new_content: String,
|
pub new_content: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
|
pub struct ReplaceAstNodeTool {
|
||||||
|
/// Array of AST node replacements to execute in bulk.
|
||||||
|
pub replacements: Vec<AstNodeReplacement>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Semantic code search using local vector embeddings. Use this conceptual search instead of raw regex (grep) when trying to locate abstract logic or exploring new patterns.
|
/// Semantic code search using local vector embeddings. Use this conceptual search instead of raw regex (grep) when trying to locate abstract logic or exploring new patterns.
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct SemanticCodeSearchTool {
|
pub struct SemanticCodeSearchTool {
|
||||||
@@ -1031,3 +1039,14 @@ pub struct ClipboardTool {
|
|||||||
/// Image path (for 'write').
|
/// Image path (for 'write').
|
||||||
pub image_path: Option<String>,
|
pub image_path: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetch content from a URL via an HTTP GET request natively (supports proxy config via environment variables: HTTP_PROXY, HTTPS_PROXY, NO_PROXY).
|
||||||
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
|
pub struct FetchUrlTool {
|
||||||
|
/// URL to fetch content from.
|
||||||
|
pub url: String,
|
||||||
|
/// Optional custom user-agent string.
|
||||||
|
pub user_agent: Option<String>,
|
||||||
|
/// Bypass SSL certificate verification for self-signed or invalid certs. Use with caution.
|
||||||
|
pub ignore_ssl_errors: Option<bool>,
|
||||||
|
}
|
||||||
@@ -237,7 +237,10 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_spawn_watcher_invalid_path() {
|
async fn test_spawn_watcher_invalid_path() {
|
||||||
let state = Arc::new(MemoryState::new("/nonexistent/path"));
|
let result = std::panic::catch_unwind(|| {
|
||||||
spawn_watcher(state);
|
let state = Arc::new(MemoryState::new("/nonexistent/path"));
|
||||||
|
spawn_watcher(state);
|
||||||
|
});
|
||||||
|
assert!(result.is_err(), "Expected MemoryState::new to panic due to persistence failure on invalid path");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,3 +20,6 @@ async-channel = "2.5.0"
|
|||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
chrono = "0.4.45"
|
chrono = "0.4.45"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
Reference in new issue
Block a user