perf: fix memory leaks, optimize tracing logs, fix networking eof fragmentation bug in nvim-core and stub, and deduplicate json-rpc handlers
This commit is contained in:
1 parent
ce88bef45e
commit
f76d8ccce8
23 files changed
+1373
-803
No files matched your search
@@ -0,0 +1,162 @@
|
||||
use crate::router::McpTool;
|
||||
use crate::state::MemoryState;
|
||||
use crate::tools::*;
|
||||
use async_trait::async_trait;
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct UpdateEnvFingerprintHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for UpdateEnvFingerprintHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"update_env_fingerprint"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<UpdateEnvFingerprintTool>(
|
||||
"update_env_fingerprint",
|
||||
"Execute update_env_fingerprint",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: UpdateEnvFingerprintTool =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
state.env_fingerprints.modify(|fps| {
|
||||
fps.insert(
|
||||
req.namespace.clone(),
|
||||
crate::models::EnvFingerprint {
|
||||
namespace: req.namespace.clone(),
|
||||
os: std::env::consts::OS.to_string(),
|
||||
shell: std::env::var("SHELL").unwrap_or_else(|_| "unknown".to_string()),
|
||||
tool_versions: req.tool_versions,
|
||||
updated_at: crate::handlers::utils::now_secs(),
|
||||
},
|
||||
);
|
||||
});
|
||||
Ok("Env fingerprint updated".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ReadEnvFingerprintHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for ReadEnvFingerprintHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"read_env_fingerprint"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<ReadEnvFingerprintTool>(
|
||||
"read_env_fingerprint",
|
||||
"Execute read_env_fingerprint",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: ReadEnvFingerprintTool =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let data = state
|
||||
.env_fingerprints
|
||||
.read_with(|fps| fps.get(&req.namespace).cloned());
|
||||
if let Some(fp) = data {
|
||||
let data = serde_json::to_string(&fp).unwrap_or_default();
|
||||
Ok(data.to_string())
|
||||
} else {
|
||||
Ok("{}".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LogEnvRequirementHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for LogEnvRequirementHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"log_env_requirement"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<LogEnvRequirementTool>(
|
||||
"log_env_requirement",
|
||||
"Execute log_env_requirement",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: LogEnvRequirementTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
state.env_requirements.modify(|reqs| {
|
||||
reqs.retain(|r| !(r.namespace == req.namespace && r.key == req.key));
|
||||
reqs.push(crate::models::EnvRequirement {
|
||||
namespace: req.namespace,
|
||||
key: req.key,
|
||||
description: req.description,
|
||||
is_secret: req.is_secret,
|
||||
});
|
||||
});
|
||||
Ok("Env requirement logged".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RegisterEnvironmentHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for RegisterEnvironmentHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"register_environment"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<RegisterEnvironmentTool>(
|
||||
"register_environment",
|
||||
"Execute register_environment",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: RegisterEnvironmentTool =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
state.environments.modify(|envs| {
|
||||
envs.retain(|e| !(e.namespace == req.namespace && e.name == req.name));
|
||||
envs.push(crate::models::EnvironmentDetail {
|
||||
namespace: req.namespace,
|
||||
name: req.name,
|
||||
url: req.url,
|
||||
description: req.description,
|
||||
requires_vpn: req.requires_vpn,
|
||||
updated_at: crate::handlers::utils::now_secs(),
|
||||
});
|
||||
});
|
||||
Ok("Environment registered".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GetEnvironmentDetailsHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for GetEnvironmentDetailsHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"get_environment_details"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<GetEnvironmentDetailsTool>(
|
||||
"get_environment_details",
|
||||
"Execute get_environment_details",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: GetEnvironmentDetailsTool =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let data = state.environments.read_with(|envs| {
|
||||
let filtered: Vec<_> = envs
|
||||
.iter()
|
||||
.filter(|e| e.namespace == req.namespace)
|
||||
.collect();
|
||||
serde_json::to_string(&filtered).unwrap_or_default()
|
||||
});
|
||||
Ok(data.to_string())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,579 @@
|
||||
use crate::router::McpTool;
|
||||
use crate::state::MemoryState;
|
||||
use crate::tools::*;
|
||||
use async_trait::async_trait;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(serde::Serialize, Default)]
|
||||
struct BorrowedGraph<'a> {
|
||||
entities: std::collections::HashMap<&'a String, &'a crate::models::Entity>,
|
||||
relations: Vec<&'a crate::models::Relation>,
|
||||
}
|
||||
|
||||
pub struct QueryGraphPathHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for QueryGraphPathHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"query_graph_path"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<QueryGraphPathTool>("query_graph_path", "Execute query_graph_path")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: crate::tools::QueryGraphPathTool =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
state.read_graph(|graph| {
|
||||
let max_depth = req.max_depth.unwrap_or(5);
|
||||
let mut queue: std::collections::VecDeque<&str> = std::collections::VecDeque::new();
|
||||
let mut visited: std::collections::HashSet<&str> = std::collections::HashSet::new();
|
||||
let mut parents: std::collections::HashMap<&str, (&str, std::borrow::Cow<'_, str>)> =
|
||||
std::collections::HashMap::new();
|
||||
|
||||
queue.push_back(req.start_node.as_str());
|
||||
visited.insert(req.start_node.as_str());
|
||||
|
||||
let mut found = false;
|
||||
let mut current_depth = 0;
|
||||
let mut nodes_at_current_depth = 1;
|
||||
let mut nodes_at_next_depth = 0;
|
||||
|
||||
while let Some(current) = queue.pop_front() {
|
||||
if current == req.end_node {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
nodes_at_current_depth -= 1;
|
||||
if current_depth < max_depth {
|
||||
for rel in &graph.relations {
|
||||
if rel.from == current && !visited.contains(rel.to.as_str()) {
|
||||
visited.insert(rel.to.as_str());
|
||||
parents.insert(
|
||||
rel.to.as_str(),
|
||||
(
|
||||
current,
|
||||
std::borrow::Cow::Borrowed(rel.relation_type.as_str()),
|
||||
),
|
||||
);
|
||||
queue.push_back(rel.to.as_str());
|
||||
nodes_at_next_depth += 1;
|
||||
} else if rel.to == current && !visited.contains(rel.from.as_str()) {
|
||||
visited.insert(rel.from.as_str());
|
||||
parents.insert(
|
||||
rel.from.as_str(),
|
||||
(
|
||||
current,
|
||||
std::borrow::Cow::Owned(format!(
|
||||
"inverse({})",
|
||||
rel.relation_type
|
||||
)),
|
||||
),
|
||||
);
|
||||
queue.push_back(rel.from.as_str());
|
||||
nodes_at_next_depth += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if nodes_at_current_depth == 0 {
|
||||
current_depth += 1;
|
||||
nodes_at_current_depth = nodes_at_next_depth;
|
||||
nodes_at_next_depth = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
let mut path = Vec::new();
|
||||
let mut curr = req.end_node.as_str();
|
||||
while curr != req.start_node {
|
||||
if let Some((parent, rel_type)) = parents.get(&curr) {
|
||||
path.push(format!("{} -[{}]-> {}", parent, rel_type, curr));
|
||||
curr = parent;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
path.reverse();
|
||||
Ok(format!("Path found:\n{}", path.join("\n")))
|
||||
} else {
|
||||
Ok(format!(
|
||||
"No path found between {} and {} within depth {}",
|
||||
req.start_node, req.end_node, max_depth
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreateEntitiesHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for CreateEntitiesHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"create_entities"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Execute create_entities")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: CreateEntitiesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let mut inserted = Vec::new();
|
||||
state.modify_graph(|g| {
|
||||
for entity in req.entities {
|
||||
if !entity.name.is_empty() {
|
||||
inserted.push(entity.clone());
|
||||
g.entities.insert(entity.name.clone(), entity);
|
||||
}
|
||||
}
|
||||
});
|
||||
let idx = state
|
||||
.search_index
|
||||
.read()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clone();
|
||||
for entity in inserted {
|
||||
drop(idx.index_entity(&entity));
|
||||
}
|
||||
Ok("Entities created".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreateRelationsHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for CreateRelationsHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"create_relations"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<CreateRelationsTool>("create_relations", "Execute create_relations")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: CreateRelationsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
state.modify_graph(|g| {
|
||||
for relation in req.relations {
|
||||
if !relation.from.is_empty() && !relation.to.is_empty() {
|
||||
g.relations.push(relation);
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok("Relations created".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AddObservationsHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for AddObservationsHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"add_observations"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<AddObservationsTool>("add_observations", "Execute add_observations")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: AddObservationsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
state.modify_graph(|g| {
|
||||
for o in req.observations {
|
||||
if let Some(e) = g.entities.get_mut(&o.entity_name) {
|
||||
e.observations.extend(o.contents);
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok("Observations added".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DeleteEntitiesHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for DeleteEntitiesHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"delete_entities"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<DeleteEntitiesTool>("delete_entities", "Execute delete_entities")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: DeleteEntitiesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let to_delete: std::collections::HashSet<_> = req.entity_names.into_iter().collect();
|
||||
state.modify_graph(|master| {
|
||||
for name in &to_delete {
|
||||
master.entities.remove(name);
|
||||
}
|
||||
master
|
||||
.relations
|
||||
.retain(|r| !to_delete.contains(&r.from) && !to_delete.contains(&r.to));
|
||||
});
|
||||
|
||||
let idx = state
|
||||
.search_index
|
||||
.read()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clone();
|
||||
for name in to_delete {
|
||||
drop(idx.delete_document(&name));
|
||||
}
|
||||
Ok("Entities deleted".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DeleteObservationsHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for DeleteObservationsHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"delete_observations"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<DeleteObservationsTool>(
|
||||
"delete_observations",
|
||||
"Execute delete_observations",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: DeleteObservationsTool =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
state.modify_graph(|master| {
|
||||
for d in req.deletions {
|
||||
if let Some(e) = master.entities.get_mut(&d.entity_name) {
|
||||
let to_rem: HashSet<_> = d.observations.into_iter().collect();
|
||||
e.observations.retain(|o| !to_rem.contains(o));
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok("Observations deleted".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DeleteRelationsHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for DeleteRelationsHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"delete_relations"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<DeleteRelationsTool>("delete_relations", "Execute delete_relations")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: DeleteRelationsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
state.modify_graph(|master| {
|
||||
let to_rem: HashSet<_> = req.relations.into_iter().collect();
|
||||
master.relations.retain(|r| !to_rem.contains(r));
|
||||
});
|
||||
Ok("Relations deleted".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ReadGraphHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for ReadGraphHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"read_graph"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<ReadGraphTool>("read_graph", "Execute read_graph")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: ReadGraphTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let data = state.read_graph(|full| {
|
||||
if let Some(ns) = req.namespace {
|
||||
let mut filtered = BorrowedGraph::default();
|
||||
for (k, v) in &full.entities {
|
||||
if v.namespace == ns {
|
||||
filtered.entities.insert(k, v);
|
||||
}
|
||||
}
|
||||
for r in &full.relations {
|
||||
if r.namespace == ns {
|
||||
filtered.relations.push(r);
|
||||
}
|
||||
}
|
||||
serde_json::to_string(&filtered).unwrap_or_default()
|
||||
} else {
|
||||
serde_json::to_string(full).unwrap_or_default()
|
||||
}
|
||||
});
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SearchNodesHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for SearchNodesHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"search_nodes"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<SearchNodesTool>("search_nodes", "Execute search_nodes")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: SearchNodesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let matches = if let Ok(idx) = state.search_index.read() {
|
||||
idx.search(&req.query, req.namespace.as_deref())
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
let data = state.read_graph(|full| {
|
||||
let mut result = BorrowedGraph::default();
|
||||
for (id, doc_type, _, _, _) in &matches {
|
||||
if doc_type == "entity"
|
||||
&& let Some(e) = full.entities.get(id)
|
||||
{
|
||||
result.entities.insert(id, e);
|
||||
}
|
||||
}
|
||||
serde_json::to_string(&result).unwrap_or_default()
|
||||
});
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OpenNodesHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for OpenNodesHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"open_nodes"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<OpenNodesTool>("open_nodes", "Execute open_nodes")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: OpenNodesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let data = state.read_graph(|full| {
|
||||
let targets: HashSet<&str> = req.names.iter().map(|s| s.as_str()).collect();
|
||||
let mut result = BorrowedGraph::default();
|
||||
let mut connected: HashSet<&str> = HashSet::new();
|
||||
for r in &full.relations {
|
||||
if targets.contains(r.from.as_str()) {
|
||||
connected.insert(r.to.as_str());
|
||||
result.relations.push(r);
|
||||
} else if targets.contains(r.to.as_str()) {
|
||||
connected.insert(r.from.as_str());
|
||||
result.relations.push(r);
|
||||
}
|
||||
}
|
||||
for (name, e) in &full.entities {
|
||||
if targets.contains(name.as_str()) || connected.contains(name.as_str()) {
|
||||
result.entities.insert(name, e);
|
||||
}
|
||||
}
|
||||
serde_json::to_string(&result).unwrap_or_default()
|
||||
});
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VisualizeGraphHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for VisualizeGraphHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"visualize_graph"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<VisualizeGraphTool>("visualize_graph", "Execute visualize_graph")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: VisualizeGraphTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let query = req.query.unwrap_or_default();
|
||||
use std::fmt::Write;
|
||||
let mut output = state.read_graph(|full| {
|
||||
let mut included: HashSet<&str> = HashSet::new();
|
||||
let mut to_draw = Vec::new();
|
||||
|
||||
for (name, e) in &full.entities {
|
||||
if let Some(ns) = &req.namespace
|
||||
&& e.namespace != *ns
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if query.is_empty()
|
||||
|| contains_ignore_ascii_case(name, &query)
|
||||
|| contains_ignore_ascii_case(&e.entity_type, &query)
|
||||
{
|
||||
included.insert(name.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
for r in &full.relations {
|
||||
if let Some(ns) = &req.namespace
|
||||
&& r.namespace != *ns
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if query.is_empty()
|
||||
|| included.contains(r.from.as_str())
|
||||
|| included.contains(r.to.as_str())
|
||||
{
|
||||
included.insert(r.from.as_str());
|
||||
included.insert(r.to.as_str());
|
||||
to_draw.push(r);
|
||||
}
|
||||
}
|
||||
|
||||
let mut out = String::with_capacity(included.len() * 40 + to_draw.len() * 60);
|
||||
out.push_str("graph TD;\n");
|
||||
|
||||
let sanitize = |s: &str, id_mode: bool| -> String {
|
||||
let mut o = String::with_capacity(s.len());
|
||||
for c in s.chars() {
|
||||
if c != '"' && c != '(' && c != ')' {
|
||||
if id_mode && (c == ' ' || c == '-' || c == '.') {
|
||||
o.push('_');
|
||||
} else {
|
||||
o.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
o
|
||||
};
|
||||
|
||||
for name in &included {
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" id_{}[\"{}\"];",
|
||||
sanitize(name, true),
|
||||
sanitize(name, false)
|
||||
);
|
||||
}
|
||||
for r in to_draw {
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" id_{}-->|\"{}\"|id_{};",
|
||||
sanitize(&r.from, true),
|
||||
r.relation_type.replace("\"", ""),
|
||||
sanitize(&r.to, true)
|
||||
);
|
||||
}
|
||||
out
|
||||
});
|
||||
if output == "graph TD;\n" {
|
||||
output = "No nodes found to visualize.".to_string();
|
||||
}
|
||||
Ok(output.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CondenseEntityHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for CondenseEntityHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"condense_entity"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<CondenseEntityTool>("condense_entity", "Execute condense_entity")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: CondenseEntityTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
state.modify_graph(|master| {
|
||||
if let Some(e) = master.entities.get_mut(&req.entity_name) {
|
||||
e.observations = req.summarized_observations;
|
||||
}
|
||||
});
|
||||
Ok("Entity condensed".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MergeEntitiesHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for MergeEntitiesHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"merge_entities"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<MergeEntitiesTool>("merge_entities", "Execute merge_entities")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: MergeEntitiesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
state.modify_graph(|master| {
|
||||
if let Some(src) = master.entities.remove(&req.source_entity) {
|
||||
if let Some(tgt) = master.entities.get_mut(&req.target_entity) {
|
||||
tgt.observations.extend(src.observations);
|
||||
MemoryState::deduplicate(&mut tgt.observations);
|
||||
} else {
|
||||
let mut new_tgt = src.clone();
|
||||
new_tgt.name = req.target_entity.clone();
|
||||
master.entities.insert(req.target_entity.clone(), new_tgt);
|
||||
}
|
||||
}
|
||||
for r in &mut master.relations {
|
||||
if r.from == req.source_entity {
|
||||
r.from = req.target_entity.clone();
|
||||
}
|
||||
if r.to == req.source_entity {
|
||||
r.to = req.target_entity.clone();
|
||||
}
|
||||
}
|
||||
MemoryState::deduplicate(&mut master.relations);
|
||||
});
|
||||
Ok("Entities merged".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FindOrphansHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for FindOrphansHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"find_orphans"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<FindOrphansTool>("find_orphans", "Execute find_orphans")
|
||||
}
|
||||
|
||||
async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let orphans = state.read_graph(|full| {
|
||||
let mut connected = std::collections::HashSet::new();
|
||||
for r in &full.relations {
|
||||
connected.insert(r.from.as_str());
|
||||
connected.insert(r.to.as_str());
|
||||
}
|
||||
full.entities
|
||||
.keys()
|
||||
.filter(|k| !connected.contains(k.as_str()))
|
||||
.cloned()
|
||||
.collect::<Vec<String>>()
|
||||
});
|
||||
let data = serde_json::to_string(&orphans).unwrap_or_default();
|
||||
Ok(data.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
use crate::handlers::utils::*;
|
||||
@@ -0,0 +1,504 @@
|
||||
use crate::models::*;
|
||||
use crate::router::McpTool;
|
||||
use crate::state::MemoryState;
|
||||
use crate::tools::*;
|
||||
use async_trait::async_trait;
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct LogDecisionHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for LogDecisionHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"log_decision"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<LogDecisionTool>("log_decision", "Execute log_decision")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: LogDecisionTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
|
||||
let idx = state
|
||||
.search_index
|
||||
.read()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clone();
|
||||
let mut final_id = String::new();
|
||||
|
||||
state.adrs.modify(|adrs| {
|
||||
final_id = format!("ADR-{:04}", adrs.len() + 1);
|
||||
let a = Adr {
|
||||
id: final_id.clone(),
|
||||
title: req.title,
|
||||
context: req.context,
|
||||
decision: req.decision,
|
||||
consequence: req.consequence,
|
||||
timestamp: crate::handlers::utils::now_secs(),
|
||||
};
|
||||
|
||||
drop(idx.index_adr(&a));
|
||||
adrs.push(a);
|
||||
});
|
||||
|
||||
Ok(format!("Decision logged as {}", final_id))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct QueryDecisionsHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for QueryDecisionsHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"query_decisions"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<QueryDecisionsTool>("query_decisions", "Execute query_decisions")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: QueryDecisionsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let data = state.adrs.read_with(|adrs| {
|
||||
let filtered: Vec<_> = adrs
|
||||
.iter()
|
||||
.filter(|a| {
|
||||
if let Some(q) = &req.query {
|
||||
contains_ignore_ascii_case(&a.title, q)
|
||||
|| contains_ignore_ascii_case(&a.context, q)
|
||||
|| contains_ignore_ascii_case(&a.decision, q)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
serde_json::to_string(&filtered).unwrap_or_default()
|
||||
});
|
||||
Ok(data.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LogErrorFixHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for LogErrorFixHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"log_error_fix"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<LogErrorFixTool>("log_error_fix", "Execute log_error_fix")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: LogErrorFixTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
state.error_fixes.modify(|fixes| {
|
||||
fixes.push(crate::models::ErrorFix {
|
||||
signature: req.signature,
|
||||
solution: req.solution,
|
||||
timestamp: crate::handlers::utils::now_secs(),
|
||||
git_commit: req.git_commit,
|
||||
git_branch: req.git_branch,
|
||||
})
|
||||
});
|
||||
Ok("Error fix logged".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SearchErrorFixesHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for SearchErrorFixesHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"search_error_fixes"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<SearchErrorFixesTool>(
|
||||
"search_error_fixes",
|
||||
"Execute search_error_fixes",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: SearchErrorFixesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let q = req.query;
|
||||
let data = state.error_fixes.read_with(|fixes| {
|
||||
let filtered: Vec<_> = fixes
|
||||
.iter()
|
||||
.filter(|f| {
|
||||
contains_ignore_ascii_case(&f.signature, &q)
|
||||
|| contains_ignore_ascii_case(&f.solution, &q)
|
||||
})
|
||||
.collect();
|
||||
serde_json::to_string(&filtered).unwrap_or_default()
|
||||
});
|
||||
Ok(data.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LogCodeChangeHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for LogCodeChangeHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"log_code_change"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<LogCodeChangeTool>("log_code_change", "Execute log_code_change")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: LogCodeChangeTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
state.ledger.modify(|ledger| {
|
||||
ledger.push(CodeChange {
|
||||
timestamp: crate::handlers::utils::now_secs(),
|
||||
file_path: req.file_path,
|
||||
description: req.description,
|
||||
git_commit: req.git_commit,
|
||||
git_branch: req.git_branch,
|
||||
});
|
||||
});
|
||||
Ok("Code change logged".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct QueryRecentChangesHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for QueryRecentChangesHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"query_recent_changes"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<QueryRecentChangesTool>(
|
||||
"query_recent_changes",
|
||||
"Execute query_recent_changes",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let data = state
|
||||
.ledger
|
||||
.read_with(|l| serde_json::to_string(l).unwrap_or_else(|_| "[]".to_string()));
|
||||
Ok(data.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LearnPreferenceHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for LearnPreferenceHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"learn_preference"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<LearnPreferenceTool>("learn_preference", "Execute learn_preference")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: LearnPreferenceTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
state.prefs.modify(|prefs| {
|
||||
prefs.insert(
|
||||
req.key.clone(),
|
||||
crate::models::Preference {
|
||||
key: req.key.clone(),
|
||||
value: req.value,
|
||||
updated_at: crate::handlers::utils::now_secs(),
|
||||
},
|
||||
);
|
||||
});
|
||||
Ok("Preference learned".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ReadPreferencesHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for ReadPreferencesHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"read_preferences"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<ReadPreferencesTool>("read_preferences", "Execute read_preferences")
|
||||
}
|
||||
|
||||
async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let data = state
|
||||
.prefs
|
||||
.read_with(|prefs| serde_json::to_string(prefs).unwrap_or_default());
|
||||
Ok(data.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LogTechDebtHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for LogTechDebtHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"log_tech_debt"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<LogTechDebtTool>("log_tech_debt", "Execute log_tech_debt")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: LogTechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
state.tech_debts.modify(|debts| {
|
||||
debts.push(crate::models::TechDebt {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
namespace: req.namespace,
|
||||
description: req.description,
|
||||
ideal_solution: req.ideal_solution,
|
||||
is_resolved: false,
|
||||
created_at: crate::handlers::utils::now_secs(),
|
||||
git_commit: req.git_commit,
|
||||
git_branch: req.git_branch,
|
||||
})
|
||||
});
|
||||
Ok("Tech debt logged".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ResolveTechDebtHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for ResolveTechDebtHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"resolve_tech_debt"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<ResolveTechDebtTool>(
|
||||
"resolve_tech_debt",
|
||||
"Execute resolve_tech_debt",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: ResolveTechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let mut found = false;
|
||||
state.tech_debts.modify(|debts| {
|
||||
for d in debts.iter_mut() {
|
||||
if d.id == req.id {
|
||||
d.is_resolved = true;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
if found {
|
||||
Ok("Tech debt resolved".to_string())
|
||||
} else {
|
||||
Ok("Tech debt not found".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ListTechDebtHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for ListTechDebtHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"list_tech_debt"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<ListTechDebtTool>("list_tech_debt", "Execute list_tech_debt")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: ListTechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let data = state.tech_debts.read_with(|debts| {
|
||||
let filtered: Vec<_> = debts
|
||||
.iter()
|
||||
.filter(|d| {
|
||||
d.namespace == req.namespace && (req.include_resolved || !d.is_resolved)
|
||||
})
|
||||
.collect();
|
||||
serde_json::to_string(&filtered).unwrap_or_default()
|
||||
});
|
||||
Ok(data.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OmniSearchHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for OmniSearchHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"omni_search"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<OmniSearchTool>("omni_search", "Execute omni_search")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: OmniSearchTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let matches = {
|
||||
let idx = state.search_index.read().unwrap_or_else(|e| e.into_inner());
|
||||
idx.search(&req.query, req.namespace.as_deref())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
let kg_json = state.read_graph(|full| {
|
||||
let mut kg_entities = std::collections::HashMap::new();
|
||||
for (id, doc_type, _, _, _) in &matches {
|
||||
if doc_type == "entity"
|
||||
&& let Some(e) = full.entities.get(id)
|
||||
{
|
||||
kg_entities.insert(id, e);
|
||||
}
|
||||
}
|
||||
serde_json::to_value(&kg_entities).unwrap_or_default()
|
||||
});
|
||||
|
||||
let tasks_json = state.tasks.read_with(|all_tasks| {
|
||||
let filtered: Vec<_> = all_tasks
|
||||
.iter()
|
||||
.filter(|t| {
|
||||
matches
|
||||
.iter()
|
||||
.any(|(id, typ, _, _, _)| id == &t.id && typ == "task")
|
||||
})
|
||||
.collect();
|
||||
serde_json::to_value(&filtered).unwrap_or_default()
|
||||
});
|
||||
|
||||
let snippets_json = state.snippets.read_with(|all_snippets| {
|
||||
let filtered: Vec<_> = all_snippets
|
||||
.iter()
|
||||
.filter(|s| {
|
||||
matches
|
||||
.iter()
|
||||
.any(|(id, typ, _, _, _)| id == &s.name && typ == "snippet")
|
||||
})
|
||||
.collect();
|
||||
serde_json::to_value(&filtered).unwrap_or_default()
|
||||
});
|
||||
|
||||
let adrs_json = state.adrs.read_with(|all_adrs| {
|
||||
let filtered: Vec<_> = all_adrs
|
||||
.iter()
|
||||
.filter(|a| {
|
||||
matches
|
||||
.iter()
|
||||
.any(|(id, typ, _, _, _)| id == &a.id && typ == "adr")
|
||||
})
|
||||
.collect();
|
||||
serde_json::to_value(&filtered).unwrap_or_default()
|
||||
});
|
||||
|
||||
let q = req.query;
|
||||
let tech_debts_json = state.tech_debts.read_with(|debts| {
|
||||
let filtered: Vec<_> = debts
|
||||
.iter()
|
||||
.filter(|d| {
|
||||
req.namespace.as_ref().is_none_or(|ns| d.namespace == *ns)
|
||||
&& (contains_ignore_ascii_case(&d.description, &q)
|
||||
|| contains_ignore_ascii_case(&d.ideal_solution, &q))
|
||||
})
|
||||
.collect();
|
||||
serde_json::to_value(&filtered).unwrap_or_default()
|
||||
});
|
||||
|
||||
let memos_json = state.handoff_memos.read_with(|memos| {
|
||||
let filtered: Vec<_> = memos
|
||||
.iter()
|
||||
.filter(|m| {
|
||||
req.namespace.as_ref().is_none_or(|ns| m.namespace == *ns)
|
||||
&& contains_ignore_ascii_case(&m.content, &q)
|
||||
})
|
||||
.collect();
|
||||
serde_json::to_value(&filtered).unwrap_or_default()
|
||||
});
|
||||
|
||||
let error_fixes_json = state.error_fixes.read_with(|fixes| {
|
||||
let filtered: Vec<_> = fixes
|
||||
.iter()
|
||||
.filter(|f| {
|
||||
contains_ignore_ascii_case(&f.signature, &q)
|
||||
|| contains_ignore_ascii_case(&f.solution, &q)
|
||||
})
|
||||
.collect();
|
||||
serde_json::to_value(&filtered).unwrap_or_default()
|
||||
});
|
||||
|
||||
let report = serde_json::json!({
|
||||
"knowledge_graph": kg_json,
|
||||
"tasks": tasks_json,
|
||||
"snippets": snippets_json,
|
||||
"adrs": adrs_json,
|
||||
"tech_debts": tech_debts_json,
|
||||
"handoff_memos": memos_json,
|
||||
"error_fixes": error_fixes_json
|
||||
});
|
||||
Ok(report.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GetProjectHealthHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for GetProjectHealthHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"get_project_health"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<GetProjectHealthTool>(
|
||||
"get_project_health",
|
||||
"Execute get_project_health",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: GetProjectHealthTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let active_tasks = state
|
||||
.tasks
|
||||
.read_with(|tasks| tasks.iter().filter(|t| t.status != "done").count());
|
||||
let unresolved_debt = state.tech_debts.read_with(|debts| {
|
||||
debts
|
||||
.iter()
|
||||
.filter(|d| d.namespace == req.namespace && !d.is_resolved)
|
||||
.count()
|
||||
});
|
||||
let unread_memos = state.handoff_memos.read_with(|memos| {
|
||||
memos
|
||||
.iter()
|
||||
.filter(|m| m.namespace == req.namespace)
|
||||
.count()
|
||||
});
|
||||
let active_milestones = state.milestones.read_with(|milestones| {
|
||||
milestones
|
||||
.iter()
|
||||
.filter(|m| m.namespace == req.namespace && m.status != "done")
|
||||
.count()
|
||||
});
|
||||
let remaining_checklists = state.pr_checklists.read_with(|checklists| {
|
||||
checklists
|
||||
.iter()
|
||||
.filter(|c| c.namespace == req.namespace)
|
||||
.count()
|
||||
});
|
||||
|
||||
let report = serde_json::json!({
|
||||
"active_tasks": active_tasks,
|
||||
"unresolved_tech_debt": unresolved_debt,
|
||||
"unread_handoff_memos": unread_memos,
|
||||
"active_milestones": active_milestones,
|
||||
"remaining_pr_checklist_items": remaining_checklists
|
||||
});
|
||||
Ok(report.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
use crate::handlers::utils::*;
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod env;
|
||||
pub mod graph;
|
||||
pub mod meta;
|
||||
pub mod notes;
|
||||
pub mod tasks;
|
||||
pub mod utils;
|
||||
pub mod workspaces;
|
||||
@@ -0,0 +1,263 @@
|
||||
use crate::models::*;
|
||||
use crate::router::McpTool;
|
||||
use crate::state::MemoryState;
|
||||
use crate::tools::*;
|
||||
use async_trait::async_trait;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct AddStickyNoteHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for AddStickyNoteHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"add_sticky_note"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<AddStickyNoteTool>("add_sticky_note", "Execute add_sticky_note")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: AddStickyNoteTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
state.sticky.modify(|notes| {
|
||||
notes.push(StickyNote {
|
||||
timestamp: crate::handlers::utils::now_secs(),
|
||||
content: req.content,
|
||||
});
|
||||
});
|
||||
Ok("Sticky note added.".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ReadStickyNotesHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for ReadStickyNotesHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"read_sticky_notes"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<ReadStickyNotesTool>(
|
||||
"read_sticky_notes",
|
||||
"Execute read_sticky_notes",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let data = state
|
||||
.sticky
|
||||
.read_with(|s| serde_json::to_string(s).unwrap_or_else(|_| "[]".to_string()));
|
||||
Ok(data.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DeleteStickyNoteHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for DeleteStickyNoteHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"delete_sticky_note"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<DeleteStickyNoteTool>(
|
||||
"delete_sticky_note",
|
||||
"Execute delete_sticky_note",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: DeleteStickyNoteTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let mut success = false;
|
||||
state.sticky.modify(|notes| {
|
||||
if req.index > 0 && req.index <= notes.len() {
|
||||
notes.remove(req.index - 1);
|
||||
success = true;
|
||||
}
|
||||
});
|
||||
if success {
|
||||
Ok("Sticky note deleted.".to_string())
|
||||
} else {
|
||||
Err("Invalid sticky note index.".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ClearStickyNotesHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for ClearStickyNotesHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"clear_sticky_notes"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<ClearStickyNotesTool>(
|
||||
"clear_sticky_notes",
|
||||
"Execute clear_sticky_notes",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
state.sticky.modify(|notes| {
|
||||
notes.clear();
|
||||
});
|
||||
Ok("All sticky notes cleared.".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LeaveHandoffMemoHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for LeaveHandoffMemoHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"leave_handoff_memo"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<LeaveHandoffMemoTool>(
|
||||
"leave_handoff_memo",
|
||||
"Execute leave_handoff_memo",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: LeaveHandoffMemoTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
state.handoff_memos.modify(|memos| {
|
||||
memos.push(crate::models::HandoffMemo {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
author: "agy".to_string(),
|
||||
content: req.content,
|
||||
namespace: req.namespace,
|
||||
timestamp: crate::handlers::utils::now_secs(),
|
||||
})
|
||||
});
|
||||
Ok("Handoff memo left".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ReadHandoffMemosHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for ReadHandoffMemosHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"read_handoff_memos"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<ReadHandoffMemosTool>(
|
||||
"read_handoff_memos",
|
||||
"Execute read_handoff_memos",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: ReadHandoffMemosTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let data = state.handoff_memos.read_with(|items| {
|
||||
let filtered: Vec<_> = items
|
||||
.iter()
|
||||
.filter(|i| {
|
||||
if let Some(ns) = &req.namespace {
|
||||
&i.namespace == ns
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
serde_json::to_string(&filtered).unwrap_or_default()
|
||||
});
|
||||
Ok(data.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ClearHandoffMemosHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for ClearHandoffMemosHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"clear_handoff_memos"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<ClearHandoffMemosTool>(
|
||||
"clear_handoff_memos",
|
||||
"Execute clear_handoff_memos",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: ClearHandoffMemosTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let ids: HashSet<_> = req.ids.into_iter().collect();
|
||||
state
|
||||
.handoff_memos
|
||||
.modify(|memos| memos.retain(|m| !ids.contains(&m.id)));
|
||||
Ok("Handoff memos cleared".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AddSessionSummaryHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for AddSessionSummaryHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"add_session_summary"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<AddSessionSummaryTool>(
|
||||
"add_session_summary",
|
||||
"Execute add_session_summary",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: AddSessionSummaryTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
state.session_summaries.modify(|summaries| {
|
||||
summaries.push(crate::models::SessionSummary {
|
||||
summary: req.summary,
|
||||
namespace: req.namespace,
|
||||
timestamp: crate::handlers::utils::now_secs(),
|
||||
})
|
||||
});
|
||||
Ok("Session summary added".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GenerateStandupReportHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for GenerateStandupReportHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"generate_standup_report"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<GenerateStandupReportTool>(
|
||||
"generate_standup_report",
|
||||
"Execute generate_standup_report",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: GenerateStandupReportTool =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let cutoff =
|
||||
crate::handlers::utils::now_secs().saturating_sub(req.hours_lookback * 3600);
|
||||
|
||||
let report_str = state.tasks.read_with(|items| {
|
||||
state.ledger.read_with(|changes| {
|
||||
state.session_summaries.read_with(|summaries| {
|
||||
let filtered_tasks: Vec<_> = items.iter().filter(|t| t.updated_at >= cutoff).collect();
|
||||
let filtered_changes: Vec<_> = changes.iter().filter(|c| c.timestamp >= cutoff).collect();
|
||||
let filtered_summaries: Vec<_> = summaries.iter().filter(|s| s.namespace == req.namespace && s.timestamp >= cutoff).collect();
|
||||
let report = serde_json::json!({ "tasks_updated": filtered_tasks, "code_changes": filtered_changes, "session_summaries": filtered_summaries });
|
||||
report.to_string()
|
||||
})
|
||||
})
|
||||
});
|
||||
Ok(report_str)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,489 @@
|
||||
use crate::models::*;
|
||||
use crate::router::McpTool;
|
||||
use crate::state::MemoryState;
|
||||
use crate::tools::*;
|
||||
use async_trait::async_trait;
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct AddTaskHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for AddTaskHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"add_task"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<AddTaskTool>("add_task", "Execute add_task")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: AddTaskTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let now = crate::handlers::utils::now_secs();
|
||||
let task_id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
let deps = req.dependencies.unwrap_or_default();
|
||||
|
||||
let task = Task {
|
||||
id: task_id.clone(),
|
||||
title: req.title,
|
||||
status: "pending".to_string(),
|
||||
description: req.description,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
git_branch: req.git_branch,
|
||||
parent_id: req.parent_id,
|
||||
dependencies: deps,
|
||||
acceptance_criteria: vec![],
|
||||
};
|
||||
let idx = state
|
||||
.search_index
|
||||
.read()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clone();
|
||||
drop(idx.index_task(&task));
|
||||
state.tasks.modify(|tasks| {
|
||||
tasks.push(task);
|
||||
});
|
||||
Ok(format!("Task added with ID: {}", task_id).to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DeleteTaskHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for DeleteTaskHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"delete_task"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<DeleteTaskTool>("delete_task", "Execute delete_task")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: DeleteTaskTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let mut deleted_count = 0;
|
||||
let mut actually_deleted = Vec::new();
|
||||
state.tasks.modify(|tasks| {
|
||||
let initial_len = tasks.len();
|
||||
|
||||
// Build index-based children map
|
||||
let mut children_map: std::collections::HashMap<usize, Vec<usize>> =
|
||||
std::collections::HashMap::new();
|
||||
let mut id_to_index = std::collections::HashMap::new();
|
||||
for (idx, t) in tasks.iter().enumerate() {
|
||||
id_to_index.insert(t.id.as_str(), idx);
|
||||
}
|
||||
|
||||
for (idx, t) in tasks.iter().enumerate() {
|
||||
if let Some(pid) = &t.parent_id
|
||||
&& let Some(&parent_idx) = id_to_index.get(pid.as_str())
|
||||
{
|
||||
children_map.entry(parent_idx).or_default().push(idx);
|
||||
}
|
||||
}
|
||||
|
||||
let mut to_delete_idx = std::collections::HashSet::new();
|
||||
if let Some(&start_idx) = id_to_index.get(req.id.as_str()) {
|
||||
let mut queue = std::collections::VecDeque::new();
|
||||
queue.push_back(start_idx);
|
||||
|
||||
while let Some(curr) = queue.pop_front() {
|
||||
if to_delete_idx.insert(curr)
|
||||
&& let Some(children) = children_map.get(&curr)
|
||||
{
|
||||
queue.extend(children.iter().copied());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for &idx in &to_delete_idx {
|
||||
actually_deleted.push(tasks[idx].id.clone());
|
||||
}
|
||||
|
||||
tasks.retain(|t| !actually_deleted.contains(&t.id));
|
||||
deleted_count = initial_len - tasks.len();
|
||||
});
|
||||
|
||||
if deleted_count > 0 {
|
||||
let idx = state
|
||||
.search_index
|
||||
.read()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clone();
|
||||
for id in actually_deleted {
|
||||
drop(idx.delete_document(&id));
|
||||
}
|
||||
Ok(vec![
|
||||
format!("Deleted task and its children ({} total).", deleted_count).to_string(),
|
||||
][0]
|
||||
.clone())
|
||||
} else {
|
||||
Ok("Task not found.".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UpdateTaskStatusHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for UpdateTaskStatusHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"update_task_status"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<UpdateTaskStatusTool>(
|
||||
"update_task_status",
|
||||
"Execute update_task_status",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: UpdateTaskStatusTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let mut found = false;
|
||||
let mut blocked = false;
|
||||
let mut blocker_details = String::new();
|
||||
let target_status = req.status.to_lowercase();
|
||||
|
||||
state.tasks.modify(|tasks| {
|
||||
// Find target task
|
||||
let target_idx = tasks
|
||||
.iter()
|
||||
.position(|t| t.id == req.id || t.title == req.id);
|
||||
let target_idx = match target_idx {
|
||||
Some(idx) => idx,
|
||||
None => return,
|
||||
};
|
||||
|
||||
found = true;
|
||||
|
||||
if target_status == "done" || target_status == "completed" {
|
||||
// 1. Check Acceptance Criteria
|
||||
if tasks[target_idx]
|
||||
.acceptance_criteria
|
||||
.iter()
|
||||
.any(|c| !c.is_met)
|
||||
{
|
||||
blocked = true;
|
||||
blocker_details = "Unmet acceptance criteria exist.".to_string();
|
||||
}
|
||||
|
||||
// 2. Check dependencies
|
||||
if !blocked {
|
||||
let mut uncompleted_deps = Vec::new();
|
||||
for dep_id in &tasks[target_idx].dependencies {
|
||||
if let Some(dep_task) = tasks.iter().find(|dt| dt.id == *dep_id)
|
||||
&& dep_task.status != "completed"
|
||||
&& dep_task.status != "done"
|
||||
{
|
||||
uncompleted_deps.push(dep_task.title.as_str());
|
||||
}
|
||||
}
|
||||
if !uncompleted_deps.is_empty() {
|
||||
blocked = true;
|
||||
blocker_details =
|
||||
format!("Blocked by dependencies: {}", uncompleted_deps.join(", "));
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Check child tasks
|
||||
if !blocked {
|
||||
let target_id_ref = tasks[target_idx].id.as_str();
|
||||
let mut uncompleted_children = Vec::new();
|
||||
for child in tasks
|
||||
.iter()
|
||||
.filter(|t| t.parent_id.as_deref() == Some(target_id_ref))
|
||||
{
|
||||
if child.status != "completed" && child.status != "done" {
|
||||
uncompleted_children.push(child.title.as_str());
|
||||
}
|
||||
}
|
||||
if !uncompleted_children.is_empty() {
|
||||
blocked = true;
|
||||
blocker_details = format!(
|
||||
"Blocked by child tasks: {}",
|
||||
uncompleted_children.join(", ")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !blocked {
|
||||
// Apply update
|
||||
tasks[target_idx].status = target_status.clone();
|
||||
tasks[target_idx].updated_at = crate::handlers::utils::now_secs();
|
||||
|
||||
// Cascade cancellation to children
|
||||
if target_status == "cancelled" || target_status == "abandoned" {
|
||||
let mut children_map: std::collections::HashMap<usize, Vec<usize>> =
|
||||
std::collections::HashMap::new();
|
||||
|
||||
// First pass: map string ID to index to build the adjacency list by index
|
||||
let mut id_to_idx = std::collections::HashMap::new();
|
||||
for (idx, t) in tasks.iter().enumerate() {
|
||||
id_to_idx.insert(t.id.as_str(), idx);
|
||||
}
|
||||
|
||||
for (idx, t) in tasks.iter().enumerate() {
|
||||
if let Some(pid) = &t.parent_id
|
||||
&& let Some(&p_idx) = id_to_idx.get(pid.as_str())
|
||||
{
|
||||
children_map.entry(p_idx).or_default().push(idx);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(&start_idx) = id_to_idx.get(tasks[target_idx].id.as_str()) {
|
||||
let mut queue = std::collections::VecDeque::new();
|
||||
queue.push_back(start_idx);
|
||||
|
||||
while let Some(curr) = queue.pop_front() {
|
||||
if let Some(child_indices) = children_map.get(&curr) {
|
||||
for &idx in child_indices {
|
||||
if tasks[idx].status != "completed"
|
||||
&& tasks[idx].status != target_status
|
||||
{
|
||||
tasks[idx].status = target_status.clone();
|
||||
queue.push_back(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if blocked {
|
||||
Ok(format!(
|
||||
"Error: Cannot transition task. {}",
|
||||
blocker_details
|
||||
))
|
||||
} else if found {
|
||||
Ok("Task status updated.".to_string())
|
||||
} else {
|
||||
Ok("Task not found.".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ListActiveTasksHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for ListActiveTasksHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"list_active_tasks"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<ListActiveTasksTool>(
|
||||
"list_active_tasks",
|
||||
"Execute list_active_tasks",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: ListActiveTasksTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let data = state.tasks.read_with(|tasks| {
|
||||
let filtered: Vec<_> = tasks
|
||||
.iter()
|
||||
.filter(|t| {
|
||||
let status_match = t.status != "done";
|
||||
let branch_match = match &req.git_branch {
|
||||
Some(branch) => {
|
||||
t.git_branch.is_none()
|
||||
|| t.git_branch.as_deref() == Some(branch.as_str())
|
||||
}
|
||||
None => true,
|
||||
};
|
||||
status_match && branch_match
|
||||
})
|
||||
.collect();
|
||||
serde_json::to_string(&filtered).unwrap_or_default()
|
||||
});
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SetAcceptanceCriteriaHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for SetAcceptanceCriteriaHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"set_acceptance_criteria"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<SetAcceptanceCriteriaTool>(
|
||||
"set_acceptance_criteria",
|
||||
"Execute set_acceptance_criteria",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: SetAcceptanceCriteriaTool =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let mut success = false;
|
||||
state.tasks.modify(|tasks| {
|
||||
if let Some(task) = tasks.iter_mut().rev().find(|t| t.title == req.task_title) {
|
||||
task.acceptance_criteria = req
|
||||
.criteria
|
||||
.into_iter()
|
||||
.map(|desc| crate::models::AcceptanceCriteria {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
description: desc,
|
||||
is_met: false,
|
||||
})
|
||||
.collect();
|
||||
task.updated_at = crate::handlers::utils::now_secs();
|
||||
success = true;
|
||||
}
|
||||
});
|
||||
if success {
|
||||
Ok("Acceptance criteria set successfully.".to_string())
|
||||
} else {
|
||||
Ok("Task not found.".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VerifyAcceptanceCriteriaHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for VerifyAcceptanceCriteriaHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"verify_acceptance_criteria"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<VerifyAcceptanceCriteriaTool>(
|
||||
"verify_acceptance_criteria",
|
||||
"Execute verify_acceptance_criteria",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: VerifyAcceptanceCriteriaTool =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let mut success = false;
|
||||
let mut already_met = false;
|
||||
state.tasks.modify(|tasks| {
|
||||
if let Some(task) = tasks.iter_mut().find(|t| t.id == req.task_id)
|
||||
&& let Some(ac) = task
|
||||
.acceptance_criteria
|
||||
.iter_mut()
|
||||
.find(|c| c.id == req.criteria || c.description == req.criteria)
|
||||
{
|
||||
if ac.is_met {
|
||||
already_met = true;
|
||||
} else {
|
||||
ac.is_met = true;
|
||||
success = true;
|
||||
task.updated_at = crate::handlers::utils::now_secs();
|
||||
}
|
||||
}
|
||||
});
|
||||
if success {
|
||||
Ok(format!(
|
||||
"Acceptance criteria verified with proof: {}",
|
||||
req.proof
|
||||
))
|
||||
} else if already_met {
|
||||
Ok("Acceptance criteria was already met.".to_string())
|
||||
} else {
|
||||
Ok("Acceptance criteria or task not found.".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AddMilestoneHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for AddMilestoneHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"add_milestone"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<AddMilestoneTool>("add_milestone", "Execute add_milestone")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: AddMilestoneTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
state.milestones.modify(|ms| {
|
||||
ms.push(crate::models::Milestone {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
title: req.title,
|
||||
status: "pending".to_string(),
|
||||
namespace: req.namespace,
|
||||
target_date: None,
|
||||
})
|
||||
});
|
||||
Ok("Milestone added".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UpdateMilestoneHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for UpdateMilestoneHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"update_milestone"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<UpdateMilestoneTool>("update_milestone", "Execute update_milestone")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: UpdateMilestoneTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let mut found = false;
|
||||
state.milestones.modify(|ms| {
|
||||
for m in ms.iter_mut() {
|
||||
if m.id == req.id {
|
||||
m.status = req.status.clone();
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
if found {
|
||||
Ok("Milestone updated".to_string())
|
||||
} else {
|
||||
Ok("Milestone not found".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ListMilestonesHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for ListMilestonesHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"list_milestones"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<ListMilestonesTool>("list_milestones", "Execute list_milestones")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: ListMilestonesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let data = state.milestones.read_with(|items| {
|
||||
let filtered: Vec<_> = items
|
||||
.iter()
|
||||
.filter(|i| {
|
||||
if let Some(ns) = &req.namespace {
|
||||
&i.namespace == ns
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
serde_json::to_string(&filtered).unwrap_or_default()
|
||||
});
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
pub fn contains_ignore_ascii_case(haystack: &str, needle: &str) -> bool {
|
||||
if needle.is_empty() {
|
||||
return true;
|
||||
}
|
||||
haystack
|
||||
.as_bytes()
|
||||
.windows(needle.len())
|
||||
.any(|w| w.eq_ignore_ascii_case(needle.as_bytes()))
|
||||
}
|
||||
pub fn now_secs() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
use crate::models::*;
|
||||
use crate::router::McpTool;
|
||||
use crate::state::MemoryState;
|
||||
use crate::tools::*;
|
||||
use async_trait::async_trait;
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct PinFileHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for PinFileHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"pin_file"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<PinFileTool>("pin_file", "Execute pin_file")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: PinFileTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
state.pinned_files.modify(|pinned| {
|
||||
pinned.retain(|p| p.namespace != req.namespace || p.file_path != req.file_path);
|
||||
pinned.push(crate::models::PinnedFile {
|
||||
namespace: req.namespace,
|
||||
file_path: req.file_path,
|
||||
timestamp: crate::handlers::utils::now_secs(),
|
||||
git_branch: req.git_branch,
|
||||
});
|
||||
});
|
||||
Ok("File pinned".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UnpinFileHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for UnpinFileHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"unpin_file"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<UnpinFileTool>("unpin_file", "Execute unpin_file")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: UnpinFileTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
state.pinned_files.modify(|pinned| {
|
||||
pinned.retain(|p| p.namespace != req.namespace || p.file_path != req.file_path)
|
||||
});
|
||||
Ok("File unpinned".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ListPinnedFilesHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for ListPinnedFilesHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"list_pinned_files"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<ListPinnedFilesTool>(
|
||||
"list_pinned_files",
|
||||
"Execute list_pinned_files",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: ListPinnedFilesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let data = state.pinned_files.read_with(|pinned| {
|
||||
let filtered: Vec<_> = pinned
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
let ns_match = match &req.namespace {
|
||||
Some(ns) => &p.namespace == ns,
|
||||
std::option::Option::None => true,
|
||||
};
|
||||
let branch_match = match &req.git_branch {
|
||||
Some(branch) => {
|
||||
p.git_branch.is_none()
|
||||
|| p.git_branch.as_deref() == Some(branch.as_str())
|
||||
}
|
||||
std::option::Option::None => true,
|
||||
};
|
||||
ns_match && branch_match
|
||||
})
|
||||
.collect();
|
||||
serde_json::to_string(&filtered).unwrap_or_default()
|
||||
});
|
||||
Ok(data.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StoreSnippetHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for StoreSnippetHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"store_snippet"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<StoreSnippetTool>("store_snippet", "Execute store_snippet")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: StoreSnippetTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let req_name = req.name.clone(); // Keep for the OK message and retain closure
|
||||
let snippet = Snippet {
|
||||
name: req.name,
|
||||
language: req.language,
|
||||
code: req.code,
|
||||
description: req.description,
|
||||
updated_at: crate::handlers::utils::now_secs(),
|
||||
};
|
||||
|
||||
let idx = state
|
||||
.search_index
|
||||
.read()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clone();
|
||||
drop(idx.index_snippet(&snippet));
|
||||
|
||||
state.snippets.modify(|snippets| {
|
||||
snippets.retain(|s| s.name != req_name);
|
||||
snippets.push(snippet);
|
||||
});
|
||||
|
||||
Ok(format!("Snippet '{}' stored.", req_name).to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SearchSnippetsHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for SearchSnippetsHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"search_snippets"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<SearchSnippetsTool>("search_snippets", "Execute search_snippets")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: SearchSnippetsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let query = req.query;
|
||||
let data = state.snippets.read_with(|snippets| {
|
||||
let results: Vec<_> = snippets
|
||||
.iter()
|
||||
.filter(|s| {
|
||||
contains_ignore_ascii_case(&s.name, &query)
|
||||
|| contains_ignore_ascii_case(&s.description, &query)
|
||||
|| contains_ignore_ascii_case(&s.language, &query)
|
||||
})
|
||||
.collect();
|
||||
serde_json::to_string(&results).unwrap_or_default()
|
||||
});
|
||||
Ok(data.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DeleteSnippetHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for DeleteSnippetHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"delete_snippet"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<DeleteSnippetTool>("delete_snippet", "Execute delete_snippet")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: DeleteSnippetTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let mut deleted = false;
|
||||
state.snippets.modify(|snippets| {
|
||||
let orig = snippets.len();
|
||||
snippets.retain(|s| s.name != req.name);
|
||||
deleted = snippets.len() < orig;
|
||||
});
|
||||
if deleted {
|
||||
let idx = state
|
||||
.search_index
|
||||
.read()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clone();
|
||||
drop(idx.delete_document(&req.name));
|
||||
Ok("Snippet deleted.".to_string())
|
||||
} else {
|
||||
Ok("Snippet not found.".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SaveContextWorkspaceHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for SaveContextWorkspaceHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"save_context_workspace"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<SaveContextWorkspaceTool>(
|
||||
"save_context_workspace",
|
||||
"Execute save_context_workspace",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: SaveContextWorkspaceTool =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
state.context_workspaces.modify(|ws| {
|
||||
ws.retain(|w| w.namespace != req.namespace || w.name != req.name);
|
||||
ws.push(crate::models::ContextWorkspace {
|
||||
namespace: req.namespace,
|
||||
name: req.name,
|
||||
pinned_files: req.pinned_files,
|
||||
active_task_ids: req.active_task_ids,
|
||||
saved_at: crate::handlers::utils::now_secs(),
|
||||
});
|
||||
});
|
||||
Ok("Context workspace saved".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LoadContextWorkspaceHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for LoadContextWorkspaceHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"load_context_workspace"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<LoadContextWorkspaceTool>(
|
||||
"load_context_workspace",
|
||||
"Execute load_context_workspace",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: LoadContextWorkspaceTool =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let data = state.context_workspaces.read_with(|ws| {
|
||||
let filtered: Vec<_> = ws
|
||||
.iter()
|
||||
.filter(|w| w.namespace == req.namespace && w.name == req.name)
|
||||
.collect();
|
||||
serde_json::to_string(&filtered.first()).unwrap_or_default()
|
||||
});
|
||||
Ok(data.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ListContextWorkspacesHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for ListContextWorkspacesHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"list_context_workspaces"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<ListContextWorkspacesTool>(
|
||||
"list_context_workspaces",
|
||||
"Execute list_context_workspaces",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: ListContextWorkspacesTool =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let data = state.context_workspaces.read_with(|ws| {
|
||||
let filtered: Vec<_> = ws.iter().filter(|w| w.namespace == req.namespace).collect();
|
||||
serde_json::to_string(&filtered).unwrap_or_default()
|
||||
});
|
||||
Ok(data.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AddPrChecklistItemHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for AddPrChecklistItemHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"add_pr_checklist_item"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<AddPrChecklistItemTool>(
|
||||
"add_pr_checklist_item",
|
||||
"Execute add_pr_checklist_item",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: AddPrChecklistItemTool =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
state.pr_checklists.modify(|items| {
|
||||
items.push(crate::models::PrChecklistItem {
|
||||
namespace: req.namespace,
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
description: req.description,
|
||||
})
|
||||
});
|
||||
Ok("PR checklist item added".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GetPrChecklistHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for GetPrChecklistHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"get_pr_checklist"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<GetPrChecklistTool>("get_pr_checklist", "Execute get_pr_checklist")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: GetPrChecklistTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let data = state.pr_checklists.read_with(|items| {
|
||||
let filtered: Vec<_> = items
|
||||
.iter()
|
||||
.filter(|i| i.namespace == req.namespace)
|
||||
.collect();
|
||||
serde_json::to_string(&filtered).unwrap_or_default()
|
||||
});
|
||||
Ok(data.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ClearPrChecklistHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for ClearPrChecklistHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"clear_pr_checklist"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<ClearPrChecklistTool>(
|
||||
"clear_pr_checklist",
|
||||
"Execute clear_pr_checklist",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||
let req: ClearPrChecklistTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
state
|
||||
.pr_checklists
|
||||
.modify(|items| items.retain(|i| i.namespace != req.namespace));
|
||||
Ok("PR checklist cleared".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
use crate::handlers::utils::*;
|
||||
Reference in new issue
Block a user