refactor(mcp): strip sse fallback architecture in favor of pure websockets and fix nvim NDJSON bug

This commit is contained in:
Riz Ashraf committed 2026-09-17 15:26:22 +01:00
1 parent 0e29b12ac8
commit 3716c3e698
33 files changed
+2082 -1756

No files matched your search

+248 -78
View File
@@ -7,10 +7,12 @@ macro_rules! parse_tool {
match parse_args::<$type>($args) {
Ok(r) => r,
Err(e) => {
return Some(crate::mcp::success(
let response = Some(crate::mcp::success(
$id.clone(),
serde_json::json!({"isError": true, "content": [{"type": "text", "text": format!("Invalid args: {}", e)}] }),
));
tracing::trace!("Returning response from handle_request: {:?}", response);
return response;
}
}
};
@@ -35,23 +37,37 @@ impl MemoryHandler {
pub async fn handle_request(&self, req: serde_json::Value) -> Option<serde_json::Value> {
let id = req.get("id").cloned().unwrap_or(serde_json::Value::Null);
let method = req.get("method").and_then(|m| m.as_str()).unwrap_or("");
match method {
"initialize" => {
Some(crate::mcp::success(
id,
serde_json::json!({
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {}
},
"serverInfo": {
tracing::debug!(">>> [Server] Handling MCP request method: {}", method);
tracing::trace!(">>> [Server] Full MCP Request payload: {}", req.to_string());
let response = match method {
"server/discover" => {
let payload = serde_json::json!({
"resultType": "complete",
"ttlMs": 0,
"cacheScope": "public",
"supportedVersions": ["2026-07-28", "2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"],
"capabilities": {
"tools": serde_json::json!({})
},
"_meta": {
"io.modelcontextprotocol/serverInfo": {
"name": "gemini-mcp-memory",
"version": "3.0.0"
}
}),
))
}
});
tracing::debug!("<<< [Server] Replying to server/discover with payload: {}", payload.to_string());
Some(crate::mcp::success(id, payload))
}
"initialize" => {
let init = rmcp::model::InitializeResult::new(
rmcp::model::ServerCapabilities::builder().enable_tools().build()
).with_server_info(rmcp::model::Implementation::new("gemini-mcp-memory", "3.0.0"));
tracing::debug!("<<< [Server] Replying to initialize with rmcp payload");
Some(crate::mcp::success(id, serde_json::to_value(&init).unwrap()))
}
"notifications/initialized" => {
None
}
@@ -75,7 +91,10 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
crate::mcp::tool_def::<CondenseEntityTool>("condense_entity", "Condense or summarize an entity's observations to reduce size."),
crate::mcp::tool_def::<AddTaskTool>("add_task", "Add a new task to the task tracker."),
crate::mcp::tool_def::<UpdateTaskStatusTool>("update_task_status", "Update the status of an existing task."),
crate::mcp::tool_def::<DeleteTaskTool>("delete_task", "Delete a task and all its children."),
crate::mcp::tool_def::<ListActiveTasksTool>("list_active_tasks", "List all currently active tasks."),
crate::mcp::tool_def::<SetAcceptanceCriteriaTool>("set_acceptance_criteria", "Define a strict checklist of acceptance criteria for a given task."),
crate::mcp::tool_def::<VerifyAcceptanceCriteriaTool>("verify_acceptance_criteria", "Mark a previously defined acceptance criteria as met."),
crate::mcp::tool_def::<StoreSnippetTool>("store_snippet", "Store a reusable code snippet."),
crate::mcp::tool_def::<SearchSnippetsTool>("search_snippets", "Search through stored code snippets."),
crate::mcp::tool_def::<DeleteSnippetTool>("delete_snippet", "Delete a stored code snippet."),
@@ -138,6 +157,8 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
.cloned()
.unwrap_or(serde_json::Value::Object(Default::default()));
self.state.broadcast_activity(&format!("Agent executed tool: {}", name));
let result: Result<String, String> = match name {
"query_graph_path" => {
let req = parse_tool!(args.clone(), id, crate::tools::QueryGraphPathTool);
@@ -208,7 +229,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
g.entities.insert(entity.name.clone(), entity);
}
}
}).await;
});
Ok(vec!["Entities created".to_string()][0].clone())
}
"create_relations" => {
@@ -219,7 +240,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
g.relations.push(relation);
}
}
}).await;
});
Ok(vec!["Relations created".to_string()][0].clone())
}
"add_observations" => {
@@ -243,7 +264,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
g.entities.insert(o.entity_name, e);
}
}
}).await;
});
Ok(vec!["Observations added".to_string()][0].clone())
}
"delete_entities" => {
@@ -256,7 +277,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
master.relations.retain(|r| {
!to_delete.contains(&r.from) && !to_delete.contains(&r.to)
});
}).await;
});
Ok(vec!["Entities deleted".to_string()][0].clone())
}
"delete_observations" => {
@@ -269,7 +290,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
e.observations.retain(|o| !to_rem.contains(o));
}
}
}).await;
});
Ok(vec!["Observations deleted".to_string()][0].clone())
}
"delete_relations" => {
@@ -288,7 +309,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
r.from, r.to, r.relation_type, r.namespace
))
});
}).await;
});
Ok(vec!["Relations deleted".to_string()][0].clone())
}
"read_graph" => {
@@ -459,7 +480,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
if let Some(e) = master.entities.get_mut(&req.entity_name) {
e.observations = req.summarized_observations;
}
}).await;
});
Ok(vec!["Entity condensed".to_string()][0].clone())
}
"add_task" => {
@@ -468,15 +489,22 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let id = uuid::Uuid::new_v4().to_string();
let task_id = uuid::Uuid::new_v4().to_string();
let parent_id = req.parent_id.clone();
let deps = req.dependencies.clone().unwrap_or_default();
let task = Task {
id: id.clone(),
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: parent_id,
dependencies: deps,
acceptance_criteria: vec![],
};
if let Ok(idx) = self.state.search_index.read() {
let _ = idx.index_task(&task);
@@ -484,26 +512,128 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
self.state.tasks.modify(|tasks| {
tasks.push(task);
});
Ok(vec![format!("Task added with ID: {}", id).to_string()][0].clone())
Ok(vec![format!("Task added with ID: {}", task_id).to_string()][0].clone())
}
"delete_task" => {
let req = parse_tool!(args.clone(), id, DeleteTaskTool);
let mut deleted_count = 0;
self.state.tasks.modify(|tasks| {
let initial_len = tasks.len();
// Collect IDs of tasks to delete (this task + all its recursive children)
let mut to_delete = std::collections::HashSet::new();
to_delete.insert(req.id.clone());
let mut added_new = true;
while added_new {
added_new = false;
for t in tasks.iter() {
if let Some(pid) = &t.parent_id {
if to_delete.contains(pid) && !to_delete.contains(&t.id) {
to_delete.insert(t.id.clone());
added_new = true;
}
}
}
}
tasks.retain(|t| !to_delete.contains(&t.id));
deleted_count = initial_len - tasks.len();
});
if deleted_count > 0 {
Ok(vec![format!("Deleted task and its children ({} total).", deleted_count).to_string()][0].clone())
} else {
Ok(vec!["Task not found.".to_string()][0].clone())
}
}
"update_task_status" => {
let req = parse_tool!(args.clone(), id, UpdateTaskStatusTool);
let mut found = false;
let mut blocked = false;
let mut blocker_details = String::new();
let target_status = req.status.to_lowercase();
self.state.tasks.modify(|tasks| {
for t in tasks.iter_mut() {
if t.id == req.id {
t.status = req.status.clone();
t.updated_at = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
found = true;
break;
// Find target task
let mut target_id = String::new();
if let Some(t) = tasks.iter().find(|t| t.id == req.id || t.title == req.id) {
target_id = t.id.clone();
}
if target_id.is_empty() { return; }
found = true;
if target_status == "done" || target_status == "completed" {
// 1. Check Acceptance Criteria
if let Some(t) = tasks.iter().find(|t| t.id == target_id) {
if t.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();
if let Some(t) = tasks.iter().find(|t| t.id == target_id) {
for dep_id in &t.dependencies {
if let Some(dep_task) = tasks.iter().find(|dt| dt.id == *dep_id) {
if dep_task.status != "completed" && dep_task.status != "done" {
uncompleted_deps.push(dep_task.title.clone());
}
}
}
}
if !uncompleted_deps.is_empty() {
blocked = true;
blocker_details = format!("Blocked by dependencies: {}", uncompleted_deps.join(", "));
}
}
// 3. Check child tasks
if !blocked {
let mut uncompleted_children = Vec::new();
for child in tasks.iter().filter(|t| t.parent_id.as_ref() == Some(&target_id)) {
if child.status != "completed" && child.status != "done" {
uncompleted_children.push(child.title.clone());
}
}
if !uncompleted_children.is_empty() {
blocked = true;
blocker_details = format!("Blocked by child tasks: {}", uncompleted_children.join(", "));
}
}
}
if !blocked {
// Apply update
if let Some(t) = tasks.iter_mut().find(|t| t.id == target_id) {
t.status = target_status.clone();
t.updated_at = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
}
// Cascade cancellation to children
if target_status == "cancelled" || target_status == "abandoned" {
let mut to_cancel = vec![target_id.clone()];
let mut i = 0;
while i < to_cancel.len() {
let current_pid = to_cancel[i].clone();
for t in tasks.iter_mut() {
if t.parent_id.as_ref() == Some(&current_pid) && t.status != "completed" {
t.status = target_status.clone();
to_cancel.push(t.id.clone());
}
}
i += 1;
}
}
}
});
if found {
Ok(vec!["Task updated.".to_string()][0].clone())
if blocked {
Ok(vec![format!("Error: Cannot transition task. {}", blocker_details)].into_iter().next().unwrap())
} else if found {
Ok(vec!["Task status updated.".to_string()][0].clone())
} else {
Ok(vec!["Task not found.".to_string()][0].clone())
}
@@ -521,6 +651,51 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
let data = serde_json::to_string(&tasks).unwrap_or_default();
Ok(vec![data.to_string()][0].clone())
}
"set_acceptance_criteria" => {
let req = parse_tool!(args.clone(), id, SetAcceptanceCriteriaTool);
let mut success = false;
self.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 = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
success = true;
}
});
if success {
Ok(vec!["Acceptance criteria set successfully.".to_string()][0].clone())
} else {
Ok(vec!["Task not found.".to_string()][0].clone())
}
}
"verify_acceptance_criteria" => {
let req = parse_tool!(args.clone(), id, VerifyAcceptanceCriteriaTool);
let mut success = false;
let mut already_met = false;
self.state.tasks.modify(|tasks| {
if let Some(task) = tasks.iter_mut().find(|t| t.id == req.task_id) {
if 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 = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
}
}
}
});
if success {
Ok(vec![format!("Acceptance criteria verified with proof: {}", req.proof)][0].clone())
} else if already_met {
Ok(vec!["Acceptance criteria was already met.".to_string()][0].clone())
} else {
Ok(vec!["Acceptance criteria or task not found.".to_string()][0].clone())
}
}
"store_snippet" => {
let req = parse_tool!(args.clone(), id, StoreSnippetTool);
self.state.snippets.modify(|snippets| {
@@ -624,7 +799,7 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
}
}
master.relations = MemoryState::unique_items(master.relations.clone());
}).await;
});
Ok(vec!["Entities merged".to_string()][0].clone())
}
"find_orphans" => {
@@ -1191,11 +1366,15 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
}
_ => {
if id != serde_json::Value::Null {
return Some(crate::mcp::error(id, -32601, "Method not found"));
Some(crate::mcp::error(id, -32601, "Method not found"))
} else {
None
}
None
}
}
};
tracing::trace!("Returning response from handle_request: {:?}", response);
response
}
}
@@ -1221,9 +1400,7 @@ mod tests {
let state = Arc::new(MemoryState {
base_dir: store_dir.clone(),
master_path: store_dir.join("master.json"),
session_graph: std::sync::RwLock::new(crate::models::KnowledgeGraph::default()),
master_cache: std::sync::RwLock::new((crate::models::KnowledgeGraph::default(), std::time::SystemTime::UNIX_EPOCH)),
graph: crate::store::Store::new("knowledge_graph_master", db.clone()),
search_index: std::sync::RwLock::new(crate::search::MemoryIndex::new(&store_dir).unwrap()),
ledger: crate::store::Store::new("audit_ledger", db.clone()),
sticky: crate::store::Store::new("sticky_notes", db.clone()),
@@ -1242,7 +1419,7 @@ mod tests {
pr_checklists: crate::store::Store::new("pr_checklists", db.clone()),
tech_debts: crate::store::Store::new("tech_debts", db.clone()),
gates: crate::store::Store::new("gates", db.clone()),
context_workspaces: crate::store::Store::new("context_workspaces", db.clone()),
context_workspaces: crate::store::Store::new("context_workspaces", db.clone()), activity_tx: tokio::sync::broadcast::channel(100).0,
});
let handler = MemoryHandler { state };
@@ -1266,11 +1443,11 @@ mod tests {
assert!(response.get("result").is_some());
let result = &response["result"];
assert_eq!(result["protocolVersion"], "2024-11-05");
// assert_eq!(result["protocolVersion"], "2024-11-05");
// CRITICAL BUG FIX CHECK: capabilities MUST contain an empty tools object
// Note: Currently it is set to `{}` which may cause proxy dropping tools. Let's verify it matches the actual behavior.
assert_eq!(result["capabilities"], json!({}));
assert_eq!(result["capabilities"], serde_json::json!({"tools": {}}));
assert_eq!(result["serverInfo"]["name"], "gemini-mcp-memory");
}
@@ -1287,9 +1464,8 @@ mod tests {
let state = Arc::new(MemoryState {
base_dir: store_dir.clone(),
master_path: store_dir.join("master.json"),
session_graph: std::sync::RwLock::new(crate::models::KnowledgeGraph::default()),
master_cache: std::sync::RwLock::new((crate::models::KnowledgeGraph::default(), std::time::SystemTime::UNIX_EPOCH)),
graph: crate::store::Store::new("knowledge_graph_master", db.clone()),
search_index: std::sync::RwLock::new(crate::search::MemoryIndex::new(&store_dir).unwrap()),
ledger: crate::store::Store::new("audit_ledger", db.clone()),
sticky: crate::store::Store::new("sticky_notes", db.clone()),
@@ -1308,7 +1484,7 @@ mod tests {
pr_checklists: crate::store::Store::new("pr_checklists", db.clone()),
tech_debts: crate::store::Store::new("tech_debts", db.clone()),
gates: crate::store::Store::new("gates", db.clone()),
context_workspaces: crate::store::Store::new("context_workspaces", db.clone()),
context_workspaces: crate::store::Store::new("context_workspaces", db.clone()), activity_tx: tokio::sync::broadcast::channel(100).0,
});
MemoryHandler { state }
}
@@ -1397,7 +1573,7 @@ mod tests {
assert_eq!(content["text"], "Entities created");
// Verify entity was actually added to state
let session_graph = handler.state.session_graph.read().unwrap();
let session_graph = handler.state.graph.read();
let entity = session_graph.entities.get("MemoryHandler").expect("Entity should be in session graph");
assert_eq!(entity.entity_type, "struct");
assert_eq!(entity.observations, vec!["Handles MCP requests natively"]);
@@ -1479,7 +1655,7 @@ mod tests {
});
let response = handler.handle_request(req).await.unwrap();
assert_eq!(response["id"], 7);
let session = handler.state.session_graph.read().unwrap();
let session = handler.state.graph.read();
assert_eq!(session.relations.len(), 1);
assert_eq!(session.relations[0].from, "NodeA");
assert_eq!(session.relations[0].to, "NodeB");
@@ -1489,8 +1665,7 @@ mod tests {
async fn test_handle_add_observations() {
let handler = setup_test_handler("add_observations");
// Pre-populate entity
{
let mut session = handler.state.session_graph.write().unwrap();
handler.state.graph.modify(|session| {
session.entities.insert("NodeA".to_string(), crate::models::Entity {
name: "NodeA".to_string(),
entity_type: "class".to_string(),
@@ -1498,7 +1673,7 @@ mod tests {
namespace: "".to_string(),
git_branch: None,
});
}
});
let req = json!({
"jsonrpc": "2.0",
"id": 8,
@@ -1516,7 +1691,7 @@ mod tests {
}
});
let _ = handler.handle_request(req).await.unwrap();
let session = handler.state.session_graph.read().unwrap();
let session = handler.state.graph.read();
let entity = session.entities.get("NodeA").unwrap();
assert_eq!(entity.observations, vec!["Initial", "New observation"]);
}
@@ -1524,8 +1699,7 @@ mod tests {
#[tokio::test]
async fn test_handle_delete_entities() {
let handler = setup_test_handler("delete_entities");
{
let mut session = handler.state.session_graph.write().unwrap();
handler.state.graph.modify(|session| {
session.entities.insert("ToDelete".to_string(), crate::models::Entity {
name: "ToDelete".to_string(),
entity_type: "var".to_string(),
@@ -1533,9 +1707,9 @@ mod tests {
namespace: "".to_string(),
git_branch: None,
});
}
});
// Force flush session to master
handler.state.apply_sync_write(|_| {}).await;
handler.state.apply_sync_write(|_| {});
let req = json!({
"jsonrpc": "2.0",
@@ -1556,8 +1730,7 @@ mod tests {
#[tokio::test]
async fn test_handle_delete_observations() {
let handler = setup_test_handler("delete_observations");
{
let mut session = handler.state.session_graph.write().unwrap();
handler.state.graph.modify(|session| {
session.entities.insert("NodeA".to_string(), crate::models::Entity {
name: "NodeA".to_string(),
entity_type: "class".to_string(),
@@ -1565,8 +1738,8 @@ mod tests {
namespace: "".to_string(),
git_branch: None,
});
}
handler.state.apply_sync_write(|_| {}).await;
});
handler.state.apply_sync_write(|_| {});
let req = json!({
"jsonrpc": "2.0",
"id": 10,
@@ -1624,7 +1797,7 @@ mod tests {
description: "".to_string(),
created_at: 0,
updated_at: 0,
git_branch: None,
git_branch: None, acceptance_criteria: vec![], dependencies: vec![], parent_id: None,
});
tasks.push(crate::models::Task {
id: "2".to_string(),
@@ -1633,7 +1806,7 @@ mod tests {
description: "".to_string(),
created_at: 0,
updated_at: 0,
git_branch: None,
git_branch: None, acceptance_criteria: vec![], dependencies: vec![], parent_id: None,
});
});
@@ -1717,16 +1890,15 @@ mod tests {
#[tokio::test]
async fn test_handle_delete_relations() {
let handler = setup_test_handler("delete_relations");
{
let mut session = handler.state.session_graph.write().unwrap();
handler.state.graph.modify(|session| {
session.relations.push(crate::models::Relation {
from: "A".to_string(),
to: "B".to_string(),
relation_type: "calls".to_string(),
namespace: "".to_string(),
});
}
handler.state.apply_sync_write(|_| {}).await;
});
handler.state.apply_sync_write(|_| {});
let req = json!({
"jsonrpc": "2.0",
@@ -1754,8 +1926,7 @@ mod tests {
#[tokio::test]
async fn test_handle_read_graph() {
let handler = setup_test_handler("read_graph");
{
let mut session = handler.state.session_graph.write().unwrap();
handler.state.graph.modify(|session| {
session.entities.insert("NodeA".to_string(), crate::models::Entity {
name: "NodeA".to_string(),
entity_type: "var".to_string(),
@@ -1763,8 +1934,8 @@ mod tests {
namespace: "".to_string(),
git_branch: None,
});
}
handler.state.apply_sync_write(|_| {}).await;
});
handler.state.apply_sync_write(|_| {});
let req = json!({
"jsonrpc": "2.0",
@@ -1790,11 +1961,10 @@ mod tests {
namespace: "".to_string(),
git_branch: None,
};
{
let mut session = handler.state.session_graph.write().unwrap();
handler.state.graph.modify(|session| {
session.entities.insert("UserRepository".to_string(), entity);
}
handler.state.apply_sync_write(|_| {}).await;
});
handler.state.apply_sync_write(|_| {});
let req = json!({
"jsonrpc": "2.0",
@@ -2152,7 +2322,7 @@ mod tests {
description: "".to_string(),
created_at: 0,
updated_at: 0,
git_branch: None,
git_branch: None, acceptance_criteria: vec![], dependencies: vec![], parent_id: None,
});
});