docs: update architecture and design docs with proxy reconnect and deployment strategies

This commit is contained in:
Riz Ashraf committed 2026-09-14 04:07:52 +01:00
1 parent a9a861abe1
commit 1da413568e
18 files changed
+2010 -722

No files matched your search

+979 -2
View File
@@ -2,7 +2,7 @@ use crate::models::*;
use crate::state::MemoryState;
use crate::tools::*;
use serde::{Deserialize, de::DeserializeOwned};
use serde::de::DeserializeOwned;
use std::collections::HashSet;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
@@ -26,7 +26,9 @@ impl MemoryHandler {
id,
serde_json::json!({
"protocolVersion": "2024-11-05",
"capabilities": {},
"capabilities": {
"tools": {}
},
"serverInfo": {
"name": "gemini-mcp-memory",
"version": "3.0.0"
@@ -1608,6 +1610,981 @@ crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Create new entiti
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use crate::state::MemoryState;
use serde_json::json;
#[tokio::test]
async fn test_handle_initialize() {
let store_dir = std::env::temp_dir().join(format!("mcp_test_handlers_{}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs()));
std::fs::create_dir_all(&store_dir).unwrap();
let redb_path = store_dir.join("mcp_store.redb");
let db = Arc::new(redb::Database::create(&redb_path).unwrap());
{
let write_txn = db.begin_write().unwrap();
let _ = write_txn.open_table(crate::store::STORE_TABLE);
write_txn.commit().unwrap();
}
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)),
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()),
tasks: crate::store::Store::new("tasks", db.clone()),
snippets: crate::store::Store::new("snippets", db.clone()),
adrs: crate::store::Store::new("adrs", db.clone()),
prefs: crate::store::Store::new("preferences", db.clone()),
error_fixes: crate::store::Store::new("error_fixes", db.clone()),
pinned_files: crate::store::Store::new("pinned_files", db.clone()),
session_summaries: crate::store::Store::new("session_summaries", db.clone()),
handoff_memos: crate::store::Store::new("handoff_memos", db.clone()),
env_fingerprints: crate::store::Store::new("env_fingerprints", db.clone()),
env_requirements: crate::store::Store::new("env_requirements", db.clone()),
milestones: crate::store::Store::new("milestones", db.clone()),
environments: crate::store::Store::new("environments", db.clone()),
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()),
});
let handler = MemoryHandler { state };
let req = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {
"name": "test-client",
"version": "1.0.0"
}
}
});
let response = handler.handle_request(req).await.expect("Expected a response");
assert_eq!(response["id"], 1);
assert!(response.get("result").is_some());
let result = &response["result"];
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["serverInfo"]["name"], "gemini-mcp-memory");
}
fn setup_test_handler(test_name: &str) -> MemoryHandler {
let store_dir = std::env::temp_dir().join(format!("mcp_test_handlers_{}_{}", test_name, uuid::Uuid::new_v4()));
std::fs::create_dir_all(&store_dir).unwrap();
let redb_path = store_dir.join("mcp_store.redb");
let db = Arc::new(redb::Database::create(&redb_path).unwrap());
{
let write_txn = db.begin_write().unwrap();
let _ = write_txn.open_table(crate::store::STORE_TABLE);
write_txn.commit().unwrap();
}
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)),
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()),
tasks: crate::store::Store::new("tasks", db.clone()),
snippets: crate::store::Store::new("snippets", db.clone()),
adrs: crate::store::Store::new("adrs", db.clone()),
prefs: crate::store::Store::new("preferences", db.clone()),
error_fixes: crate::store::Store::new("error_fixes", db.clone()),
pinned_files: crate::store::Store::new("pinned_files", db.clone()),
session_summaries: crate::store::Store::new("session_summaries", db.clone()),
handoff_memos: crate::store::Store::new("handoff_memos", db.clone()),
env_fingerprints: crate::store::Store::new("env_fingerprints", db.clone()),
env_requirements: crate::store::Store::new("env_requirements", db.clone()),
milestones: crate::store::Store::new("milestones", db.clone()),
environments: crate::store::Store::new("environments", db.clone()),
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()),
});
MemoryHandler { state }
}
#[tokio::test]
async fn test_handle_tools_list() {
let handler = setup_test_handler("tools_list");
let req = json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
});
let response = handler.handle_request(req).await.expect("Expected a response");
assert_eq!(response["id"], 2);
let tools = response["result"]["tools"].as_array().expect("Tools must be an array");
assert!(!tools.is_empty());
// Verify a specific tool is registered
let add_task_tool = tools.iter().find(|t| t["name"] == "add_task").expect("add_task tool missing");
assert_eq!(add_task_tool["description"], "Add a new task to the task tracker.");
}
#[tokio::test]
async fn test_handle_add_task() {
let handler = setup_test_handler("add_task");
let req = json!({
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "add_task",
"arguments": {
"title": "Fix bug in handlers",
"description": "The proxy drops capabilities.",
"git_branch": "master"
}
}
});
let response = handler.handle_request(req).await.expect("Expected a response");
assert_eq!(response["id"], 3);
let content = &response["result"]["content"][0];
assert_eq!(content["type"], "text");
assert!(content["text"].as_str().unwrap().starts_with("Task added with ID: "));
// Verify task was actually added to store
let tasks = handler.state.tasks.read();
assert_eq!(tasks.len(), 1);
assert_eq!(tasks[0].title, "Fix bug in handlers");
assert_eq!(tasks[0].status, "pending");
}
#[tokio::test]
async fn test_handle_create_entities() {
let handler = setup_test_handler("create_entities");
let req = json!({
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "create_entities",
"arguments": {
"entities": [
{
"name": "MemoryHandler",
"entityType": "struct",
"observations": ["Handles MCP requests natively"],
"namespace": "core"
}
]
}
}
});
let response = handler.handle_request(req).await.expect("Expected a response");
assert_eq!(response["id"], 4);
let content = &response["result"]["content"][0];
assert_eq!(content["text"], "Entities created");
// Verify entity was actually added to state
let session_graph = handler.state.session_graph.read().unwrap();
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"]);
assert_eq!(entity.namespace, "core".to_string());
}
#[tokio::test]
async fn test_handle_store_snippet() {
let handler = setup_test_handler("store_snippet");
let req = json!({
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "store_snippet",
"arguments": {
"name": "Test Snippet",
"description": "A snippet used for testing",
"language": "rust",
"code": "fn main() { println!(\"Hello, World!\"); }"
}
}
});
let response = handler.handle_request(req).await.expect("Expected a response");
assert_eq!(response["id"], 5);
let snippets = handler.state.snippets.read();
assert_eq!(snippets.len(), 1);
assert_eq!(snippets[0].name, "Test Snippet");
assert_eq!(snippets[0].language, "rust");
}
#[tokio::test]
async fn test_handle_add_sticky_note() {
let handler = setup_test_handler("add_sticky_note");
let req = json!({
"jsonrpc": "2.0",
"id": 6,
"method": "tools/call",
"params": {
"name": "add_sticky_note",
"arguments": {
"content": "Don't forget to check coverage!"
}
}
});
let response = handler.handle_request(req).await.expect("Expected a response");
assert_eq!(response["id"], 6);
let notes = handler.state.sticky.read();
assert_eq!(notes.len(), 1);
assert_eq!(notes[0].content, "Don't forget to check coverage!");
}
#[tokio::test]
async fn test_handle_create_relations() {
let handler = setup_test_handler("create_relations");
let req = json!({
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "create_relations",
"arguments": {
"relations": [
{
"from": "NodeA",
"to": "NodeB",
"relationType": "depends_on",
"namespace": "core"
}
]
}
}
});
let response = handler.handle_request(req).await.unwrap();
assert_eq!(response["id"], 7);
let session = handler.state.session_graph.read().unwrap();
assert_eq!(session.relations.len(), 1);
assert_eq!(session.relations[0].from, "NodeA");
assert_eq!(session.relations[0].to, "NodeB");
}
#[tokio::test]
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();
session.entities.insert("NodeA".to_string(), crate::models::Entity {
name: "NodeA".to_string(),
entity_type: "class".to_string(),
observations: vec!["Initial".to_string()],
namespace: "".to_string(),
git_branch: None,
});
}
let req = json!({
"jsonrpc": "2.0",
"id": 8,
"method": "tools/call",
"params": {
"name": "add_observations",
"arguments": {
"observations": [
{
"entityName": "NodeA",
"contents": ["New observation"]
}
]
}
}
});
let _ = handler.handle_request(req).await.unwrap();
let session = handler.state.session_graph.read().unwrap();
let entity = session.entities.get("NodeA").unwrap();
assert_eq!(entity.observations, vec!["Initial", "New observation"]);
}
#[tokio::test]
async fn test_handle_delete_entities() {
let handler = setup_test_handler("delete_entities");
{
let mut session = handler.state.session_graph.write().unwrap();
session.entities.insert("ToDelete".to_string(), crate::models::Entity {
name: "ToDelete".to_string(),
entity_type: "var".to_string(),
observations: vec![],
namespace: "".to_string(),
git_branch: None,
});
}
// Force flush session to master
handler.state.apply_sync_write(|_| {}).await;
let req = json!({
"jsonrpc": "2.0",
"id": 9,
"method": "tools/call",
"params": {
"name": "delete_entities",
"arguments": {
"entityNames": ["ToDelete"]
}
}
});
let _ = handler.handle_request(req).await.unwrap();
let full_graph = handler.state.get_full_graph();
assert!(full_graph.entities.get("ToDelete").is_none());
}
#[tokio::test]
async fn test_handle_delete_observations() {
let handler = setup_test_handler("delete_observations");
{
let mut session = handler.state.session_graph.write().unwrap();
session.entities.insert("NodeA".to_string(), crate::models::Entity {
name: "NodeA".to_string(),
entity_type: "class".to_string(),
observations: vec!["Keep".to_string(), "Drop".to_string()],
namespace: "".to_string(),
git_branch: None,
});
}
handler.state.apply_sync_write(|_| {}).await;
let req = json!({
"jsonrpc": "2.0",
"id": 10,
"method": "tools/call",
"params": {
"name": "delete_observations",
"arguments": {
"deletions": [
{
"entityName": "NodeA",
"observations": ["Drop"]
}
]
}
}
});
let _ = handler.handle_request(req).await.unwrap();
let full = handler.state.get_full_graph();
let entity = full.entities.get("NodeA").unwrap();
assert_eq!(entity.observations, vec!["Keep"]);
}
#[tokio::test]
async fn test_handle_log_code_change() {
let handler = setup_test_handler("log_code_change");
let req = json!({
"jsonrpc": "2.0",
"id": 11,
"method": "tools/call",
"params": {
"name": "log_code_change",
"arguments": {
"filePath": "server/src/handlers.rs",
"description": "Added some unit tests",
"git_commit": "1234567"
}
}
});
let response = handler.handle_request(req).await.unwrap();
assert_eq!(response["id"], 11);
let ledger = handler.state.ledger.read();
assert_eq!(ledger.len(), 1);
assert_eq!(ledger[0].file_path, "server/src/handlers.rs");
assert_eq!(ledger[0].git_commit.as_deref(), Some("1234567"));
}
#[tokio::test]
async fn test_handle_list_active_tasks() {
let handler = setup_test_handler("list_active_tasks");
handler.state.tasks.modify(|tasks| {
tasks.push(crate::models::Task {
id: "1".to_string(),
title: "Active Task".to_string(),
status: "pending".to_string(),
description: "".to_string(),
created_at: 0,
updated_at: 0,
git_branch: None,
});
tasks.push(crate::models::Task {
id: "2".to_string(),
title: "Completed Task".to_string(),
status: "done".to_string(),
description: "".to_string(),
created_at: 0,
updated_at: 0,
git_branch: None,
});
});
let req = json!({
"jsonrpc": "2.0",
"id": 12,
"method": "tools/call",
"params": {
"name": "list_active_tasks",
"arguments": {}
}
});
let response = handler.handle_request(req).await.unwrap();
let content = response["result"]["content"][0]["text"].as_str().unwrap();
assert!(content.contains("Active Task"));
assert!(!content.contains("Completed Task"));
}
#[tokio::test]
async fn test_handle_search_snippets() {
let handler = setup_test_handler("search_snippets");
handler.state.snippets.modify(|snippets| {
snippets.push(crate::models::Snippet {
name: "React hook".to_string(),
language: "typescript".to_string(),
code: "useMemo(() => {}, [])".to_string(),
description: "React memoization".to_string(),
updated_at: 0,
});
snippets.push(crate::models::Snippet {
name: "Rust struct".to_string(),
language: "rust".to_string(),
code: "struct A {}".to_string(),
description: "Rust code".to_string(),
updated_at: 0,
});
});
let req = json!({
"jsonrpc": "2.0",
"id": 13,
"method": "tools/call",
"params": {
"name": "search_snippets",
"arguments": {
"query": "React"
}
}
});
let response = handler.handle_request(req).await.unwrap();
let content = response["result"]["content"][0]["text"].as_str().unwrap();
assert!(content.contains("React hook"));
assert!(!content.contains("Rust struct"));
}
#[tokio::test]
async fn test_handle_read_sticky_notes() {
let handler = setup_test_handler("read_sticky_notes");
handler.state.sticky.modify(|sticky| {
sticky.push(crate::models::StickyNote {
content: "Remember to commit".to_string(),
timestamp: 0,
});
});
let req = json!({
"jsonrpc": "2.0",
"id": 14,
"method": "tools/call",
"params": {
"name": "read_sticky_notes",
"arguments": {}
}
});
let response = handler.handle_request(req).await.unwrap();
let content = response["result"]["content"][0]["text"].as_str().unwrap();
assert!(content.contains("Remember to commit"));
}
#[tokio::test]
async fn test_handle_delete_relations() {
let handler = setup_test_handler("delete_relations");
{
let mut session = handler.state.session_graph.write().unwrap();
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;
let req = json!({
"jsonrpc": "2.0",
"id": 15,
"method": "tools/call",
"params": {
"name": "delete_relations",
"arguments": {
"relations": [
{
"from": "A",
"to": "B",
"relationType": "calls",
"namespace": ""
}
]
}
}
});
let _ = handler.handle_request(req).await.unwrap();
let full = handler.state.get_full_graph();
assert!(full.relations.is_empty());
}
#[tokio::test]
async fn test_handle_read_graph() {
let handler = setup_test_handler("read_graph");
{
let mut session = handler.state.session_graph.write().unwrap();
session.entities.insert("NodeA".to_string(), crate::models::Entity {
name: "NodeA".to_string(),
entity_type: "var".to_string(),
observations: vec![],
namespace: "".to_string(),
git_branch: None,
});
}
handler.state.apply_sync_write(|_| {}).await;
let req = json!({
"jsonrpc": "2.0",
"id": 16,
"method": "tools/call",
"params": {
"name": "read_graph",
"arguments": {}
}
});
let response = handler.handle_request(req).await.unwrap();
let content = response["result"]["content"][0]["text"].as_str().unwrap();
assert!(content.contains("NodeA"));
}
#[tokio::test]
async fn test_handle_open_nodes() {
let handler = setup_test_handler("open_nodes");
let entity = crate::models::Entity {
name: "UserRepository".to_string(),
entity_type: "class".to_string(),
observations: vec!["Handles user data".to_string()],
namespace: "".to_string(),
git_branch: None,
};
{
let mut session = handler.state.session_graph.write().unwrap();
session.entities.insert("UserRepository".to_string(), entity);
}
handler.state.apply_sync_write(|_| {}).await;
let req = json!({
"jsonrpc": "2.0",
"id": 17,
"method": "tools/call",
"params": {
"name": "open_nodes",
"arguments": {
"names": ["UserRepository"]
}
}
});
let response = handler.handle_request(req).await.unwrap();
let content = response["result"]["content"][0]["text"].as_str().unwrap();
assert!(content.contains("UserRepository"));
assert!(content.contains("Handles user data"));
}
#[tokio::test]
async fn test_handle_log_decision() {
let handler = setup_test_handler("log_decision");
let req = json!({
"jsonrpc": "2.0",
"id": 20,
"method": "tools/call",
"params": {
"name": "log_decision",
"arguments": {
"title": "Use async I/O",
"context": "Need better throughput",
"decision": "Use tokio",
"consequence": "Requires async all the way down"
}
}
});
let _ = handler.handle_request(req).await.unwrap();
let adrs = handler.state.adrs.read();
assert_eq!(adrs.len(), 1);
assert_eq!(adrs[0].title, "Use async I/O");
}
#[tokio::test]
async fn test_handle_query_decisions() {
let handler = setup_test_handler("query_decisions");
handler.state.adrs.modify(|adrs| {
adrs.push(crate::models::Adr {
id: "adr-1".to_string(),
title: "Use PostgreSQL".to_string(),
context: "Need relational data".to_string(),
decision: "Use pg".to_string(),
consequence: "Maintenance overhead".to_string(),
timestamp: 0,
});
});
let req = json!({
"jsonrpc": "2.0",
"id": 21,
"method": "tools/call",
"params": {
"name": "query_decisions",
"arguments": {
"query": "Postgre"
}
}
});
let response = handler.handle_request(req).await.unwrap();
let content = response["result"]["content"][0]["text"].as_str().unwrap();
assert!(content.contains("PostgreSQL"));
}
#[tokio::test]
async fn test_handle_log_error_fix() {
let handler = setup_test_handler("log_error_fix");
let req = json!({
"jsonrpc": "2.0",
"id": 22,
"method": "tools/call",
"params": {
"name": "log_error_fix",
"arguments": {
"signature": "IndexOutOfBounds",
"solution": "Check array length"
}
}
});
let _ = handler.handle_request(req).await.unwrap();
let fixes = handler.state.error_fixes.read();
assert_eq!(fixes.len(), 1);
assert_eq!(fixes[0].signature, "IndexOutOfBounds");
}
#[tokio::test]
async fn test_handle_search_error_fixes() {
let handler = setup_test_handler("search_error_fixes");
handler.state.error_fixes.modify(|fixes| {
fixes.push(crate::models::ErrorFix {
signature: "NullPointerException".to_string(),
solution: "Initialize the pointer".to_string(),
timestamp: 0,
git_branch: None,
git_commit: None,
});
});
let req = json!({
"jsonrpc": "2.0",
"id": 23,
"method": "tools/call",
"params": {
"name": "search_error_fixes",
"arguments": {
"query": "NullPointer"
}
}
});
let response = handler.handle_request(req).await.unwrap();
let content = response["result"]["content"][0]["text"].as_str().unwrap();
assert!(content.contains("Initialize the pointer"));
}
#[tokio::test]
async fn test_handle_list_pinned_files() {
let handler = setup_test_handler("list_pinned_files");
handler.state.pinned_files.modify(|files| {
files.push(crate::models::PinnedFile {
file_path: "src/important.rs".to_string(),
timestamp: 0,
namespace: "".to_string(),
git_branch: None,
});
});
let req = json!({
"jsonrpc": "2.0",
"id": 24,
"method": "tools/call",
"params": {
"name": "list_pinned_files",
"arguments": {}
}
});
let response = handler.handle_request(req).await.unwrap();
let content = response["result"]["content"][0]["text"].as_str().unwrap();
assert!(content.contains("important.rs"));
}
#[tokio::test]
async fn test_handle_add_session_summary() {
let handler = setup_test_handler("add_session_summary");
let req = json!({
"jsonrpc": "2.0",
"id": 25,
"method": "tools/call",
"params": {
"name": "add_session_summary",
"arguments": {
"namespace": "",
"summary": "Finished writing tests"
}
}
});
let _ = handler.handle_request(req).await.unwrap();
let summaries = handler.state.session_summaries.read();
assert_eq!(summaries.len(), 1);
assert_eq!(summaries[0].summary, "Finished writing tests");
}
#[tokio::test]
async fn test_handle_get_project_timeline() {
let handler = setup_test_handler("get_project_timeline");
handler.state.session_summaries.modify(|summaries| {
summaries.push(crate::models::SessionSummary {
summary: "Day 1: Setup project".to_string(),
namespace: "".to_string(),
timestamp: 0,
});
});
let req = json!({
"jsonrpc": "2.0",
"id": 26,
"method": "tools/call",
"params": {
"name": "get_project_timeline",
"arguments": {
"namespace": ""
}
}
});
let response = handler.handle_request(req).await.unwrap();
let content = response["result"]["content"][0]["text"].as_str().unwrap();
assert!(content.contains("Day 1: Setup project"));
}
#[tokio::test]
async fn test_handle_log_tech_debt() {
let handler = setup_test_handler("log_tech_debt");
let req = json!({
"jsonrpc": "2.0",
"id": 27,
"method": "tools/call",
"params": {
"name": "log_tech_debt",
"arguments": {
"namespace": "",
"description": "Hardcoded values",
"ideal_solution": "Remove magic numbers"
}
}
});
let _ = handler.handle_request(req).await.unwrap();
let debt = handler.state.tech_debts.read();
assert_eq!(debt.len(), 1);
assert_eq!(debt[0].description, "Hardcoded values");
}
#[tokio::test]
async fn test_handle_list_tech_debt() {
let handler = setup_test_handler("list_tech_debt");
handler.state.tech_debts.modify(|debts| {
debts.push(crate::models::TechDebt {
id: "debt-1".to_string(),
description: "Bad naming".to_string(),
ideal_solution: "Rename x to num_elements".to_string(),
namespace: "".to_string(),
is_resolved: false,
created_at: 0,
git_branch: None,
git_commit: None,
});
});
let req = json!({
"jsonrpc": "2.0",
"id": 28,
"method": "tools/call",
"params": {
"name": "list_tech_debt",
"arguments": {
"namespace": "",
"include_resolved": false
}
}
});
let response = handler.handle_request(req).await.unwrap();
let content = response["result"]["content"][0]["text"].as_str().unwrap();
assert!(content.contains("Bad naming"));
}
#[tokio::test]
async fn test_handle_get_project_health() {
let handler = setup_test_handler("get_project_health");
let req = json!({
"jsonrpc": "2.0",
"id": 29,
"method": "tools/call",
"params": {
"name": "get_project_health",
"arguments": {
"namespace": ""
}
}
});
let response = handler.handle_request(req).await.unwrap();
let content = response["result"]["content"][0]["text"].as_str().unwrap();
assert!(content.contains("\"active_tasks\""));
assert!(content.contains("\"unresolved_tech_debt\""));
}
#[tokio::test]
async fn test_handle_resolve_tech_debt() {
let handler = setup_test_handler("resolve_tech_debt");
handler.state.tech_debts.modify(|debts| {
debts.push(crate::models::TechDebt {
id: "debt-2".to_string(),
description: "Old api".to_string(),
ideal_solution: "Use new api".to_string(),
namespace: "".to_string(),
is_resolved: false,
created_at: 0,
git_branch: None,
git_commit: None,
});
});
let req = json!({
"jsonrpc": "2.0",
"id": 30,
"method": "tools/call",
"params": {
"name": "resolve_tech_debt",
"arguments": {
"id": "debt-2"
}
}
});
let _ = handler.handle_request(req).await.unwrap();
let debts = handler.state.tech_debts.read();
assert!(debts[0].is_resolved);
}
#[tokio::test]
async fn test_handle_leave_handoff_memo() {
let handler = setup_test_handler("leave_handoff_memo");
let req = json!({
"jsonrpc": "2.0",
"id": 31,
"method": "tools/call",
"params": {
"name": "leave_handoff_memo",
"arguments": {
"namespace": "",
"content": "Make sure to check the logs.",
"author": "Riz"
}
}
});
let _ = handler.handle_request(req).await.unwrap();
let memos = handler.state.handoff_memos.read();
assert_eq!(memos.len(), 1);
assert_eq!(memos[0].content, "Make sure to check the logs.");
}
#[tokio::test]
async fn test_handle_query_recent_changes() {
let handler = setup_test_handler("query_recent_changes");
handler.state.ledger.modify(|ledger| {
ledger.push(crate::models::CodeChange {
timestamp: 0,
file_path: "src/main.rs".to_string(),
description: "Fix bug".to_string(),
git_commit: None,
git_branch: None,
});
});
let req = json!({
"jsonrpc": "2.0",
"id": 18,
"method": "tools/call",
"params": {
"name": "query_recent_changes",
"arguments": {}
}
});
let response = handler.handle_request(req).await.unwrap();
let content = response["result"]["content"][0]["text"].as_str().unwrap();
assert!(content.contains("Fix bug"));
assert!(content.contains("src/main.rs"));
}
#[tokio::test]
async fn test_handle_update_task_status() {
let handler = setup_test_handler("update_task_status");
handler.state.tasks.modify(|tasks| {
tasks.push(crate::models::Task {
id: "test-task-123".to_string(),
title: "In progress task".to_string(),
status: "pending".to_string(),
description: "".to_string(),
created_at: 0,
updated_at: 0,
git_branch: None,
});
});
let req = json!({
"jsonrpc": "2.0",
"id": 19,
"method": "tools/call",
"params": {
"name": "update_task_status",
"arguments": {
"id": "test-task-123",
"status": "in_progress"
}
}
});
let _ = handler.handle_request(req).await.unwrap();
let tasks = handler.state.tasks.read();
assert_eq!(tasks[0].status, "in_progress");
}
}
+99 -83
View File
@@ -1,3 +1,5 @@
#![cfg_attr(not(target_os = "windows"), allow(dead_code, unused_imports, unreachable_code))]
mod handlers;
mod mcp;
mod models;
@@ -22,7 +24,7 @@ use clap::{Parser, Subcommand};
use std::collections::HashMap;
#[derive(Parser)]
#[command(author, version, about = "Antigravity MCP Memory Server", long_about = None)]
#[command(author, version = env!("APP_VERSION"), about = "Antigravity MCP Memory Server", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
@@ -320,7 +322,7 @@ fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::Error>>
let app = Router::new()
.route("/api/version", get(|| async move {
axum::Json(serde_json::json!({
"version": env!("BUILD_DATE"),
"version": env!("APP_VERSION"),
"git_hash": option_env!("GIT_HASH").unwrap_or("unknown")
}))
}))
@@ -546,6 +548,17 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, client_type: Str
}
});
if client_type == "proxy" {
let tx_clone = tx.clone();
tokio::spawn(async move {
let notify = serde_json::json!({
"jsonrpc": "2.0",
"method": "notifications/tools/list_changed"
});
let _ = tx_clone.send(notify.to_string()).await;
});
}
let handler = Arc::clone(&state.handler);
let state_clone = Arc::clone(&state);
let session_id_clone = session_id.clone();
@@ -707,99 +720,102 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
#[cfg(not(target_os = "windows"))]
#[cfg(target_os = "windows")]
{
// Linux no longer executes server logic natively due to workspace split
return Ok(());
}
let base_dir = std::env::var("MCP_MEMORY_STORE_DIR").unwrap_or_else(|_| {
dirs::home_dir()
.map(|mut h| {
h.push(".gemini/mcp_memory");
h.to_string_lossy().into_owned()
})
.unwrap_or_else(|| ".gemini/mcp_memory".into())
});
let base = PathBuf::from(base_dir);
fs::create_dir_all(&base).expect("Failed to create store dir");
let base_dir = std::env::var("MCP_MEMORY_STORE_DIR").unwrap_or_else(|_| {
dirs::home_dir()
.map(|mut h| {
h.push(".gemini/mcp_memory");
h.to_string_lossy().into_owned()
})
.unwrap_or_else(|| ".gemini/mcp_memory".into())
});
let base = PathBuf::from(base_dir);
fs::create_dir_all(&base).expect("Failed to create store dir");
let redb_path = base.join("mcp_store.redb");
let db = Arc::new(redb::Database::create(&redb_path).unwrap());
// Ensure table exists and migrate old JSON files
{
let write_txn = db.begin_write().unwrap();
let redb_path = base.join("mcp_store.redb");
let db = Arc::new(redb::Database::create(&redb_path).unwrap());
// Ensure table exists and migrate old JSON files
{
let mut table = write_txn.open_table(crate::store::STORE_TABLE).unwrap();
let stores = [
("audit_ledger", "audit_ledger.json"),
("sticky_notes", "sticky_notes.json"),
("tasks", "tasks.json"),
("snippets", "snippets.json"),
("adrs", "adrs.json"),
("preferences", "preferences.json"),
("error_fixes", "error_fixes.json"),
("pinned_files", "pinned_files.json"),
("session_summaries", "session_summaries.json"),
("handoff_memos", "handoff_memos.json"),
("env_fingerprints", "env_fingerprints.json"),
("env_requirements", "env_requirements.json"),
("milestones", "milestones.json"),
("environments", "environments.json"),
("pr_checklists", "pr_checklists.json"),
("tech_debts", "tech_debts.json"),
("gates", "gates.json"),
("context_workspaces", "context_workspaces.json"),
];
let write_txn = db.begin_write().unwrap();
{
let mut table = write_txn.open_table(crate::store::STORE_TABLE).unwrap();
let stores = [
("audit_ledger", "audit_ledger.json"),
("sticky_notes", "sticky_notes.json"),
("tasks", "tasks.json"),
("snippets", "snippets.json"),
("adrs", "adrs.json"),
("preferences", "preferences.json"),
("error_fixes", "error_fixes.json"),
("pinned_files", "pinned_files.json"),
("session_summaries", "session_summaries.json"),
("handoff_memos", "handoff_memos.json"),
("env_fingerprints", "env_fingerprints.json"),
("env_requirements", "env_requirements.json"),
("milestones", "milestones.json"),
("environments", "environments.json"),
("pr_checklists", "pr_checklists.json"),
("tech_debts", "tech_debts.json"),
("gates", "gates.json"),
("context_workspaces", "context_workspaces.json"),
];
for (key, file_name) in stores.iter() {
if table.get(*key).unwrap().is_none() {
let json_path = base.join(file_name);
if json_path.exists() {
if let Ok(data) = fs::read(&json_path) {
if serde_json::from_slice::<serde_json::Value>(&data).is_ok() {
table.insert(*key, data.as_slice()).unwrap();
for (key, file_name) in stores.iter() {
if table.get(*key).unwrap().is_none() {
let json_path = base.join(file_name);
if json_path.exists() {
if let Ok(data) = fs::read(&json_path) {
if serde_json::from_slice::<serde_json::Value>(&data).is_ok() {
table.insert(*key, data.as_slice()).unwrap();
}
}
}
}
}
}
write_txn.commit().unwrap();
}
write_txn.commit().unwrap();
let state = Arc::new(MemoryState {
master_path: base.join("knowledge_graph_master.json"),
session_graph: RwLock::new(KnowledgeGraph::default()),
base_dir: base.clone(),
master_cache: RwLock::new((KnowledgeGraph::default(), SystemTime::UNIX_EPOCH)),
search_index: RwLock::new(crate::search::MemoryIndex::new(&base).unwrap()),
ledger: Store::new("audit_ledger", db.clone()),
sticky: Store::new("sticky_notes", db.clone()),
tasks: Store::new("tasks", db.clone()),
snippets: Store::new("snippets", db.clone()),
adrs: Store::new("adrs", db.clone()),
prefs: Store::new("preferences", db.clone()),
error_fixes: Store::new("error_fixes", db.clone()),
pinned_files: Store::new("pinned_files", db.clone()),
session_summaries: Store::new("session_summaries", db.clone()),
handoff_memos: Store::new("handoff_memos", db.clone()),
env_fingerprints: Store::new("env_fingerprints", db.clone()),
env_requirements: Store::new("env_requirements", db.clone()),
milestones: Store::new("milestones", db.clone()),
environments: Store::new("environments", db.clone()),
pr_checklists: Store::new("pr_checklists", db.clone()),
tech_debts: Store::new("tech_debts", db.clone()),
gates: Store::new("gates", db.clone()),
context_workspaces: Store::new("context_workspaces", db.clone()),
});
state.recover_wal();
state.rebuild_index();
run_server(state)
}
let state = Arc::new(MemoryState {
master_path: base.join("knowledge_graph_master.json"),
session_graph: RwLock::new(KnowledgeGraph::default()),
base_dir: base.clone(),
master_cache: RwLock::new((KnowledgeGraph::default(), SystemTime::UNIX_EPOCH)),
search_index: RwLock::new(crate::search::MemoryIndex::new(&base).unwrap()),
ledger: Store::new("audit_ledger", db.clone()),
sticky: Store::new("sticky_notes", db.clone()),
tasks: Store::new("tasks", db.clone()),
snippets: Store::new("snippets", db.clone()),
adrs: Store::new("adrs", db.clone()),
prefs: Store::new("preferences", db.clone()),
error_fixes: Store::new("error_fixes", db.clone()),
pinned_files: Store::new("pinned_files", db.clone()),
session_summaries: Store::new("session_summaries", db.clone()),
handoff_memos: Store::new("handoff_memos", db.clone()),
env_fingerprints: Store::new("env_fingerprints", db.clone()),
env_requirements: Store::new("env_requirements", db.clone()),
milestones: Store::new("milestones", db.clone()),
environments: Store::new("environments", db.clone()),
pr_checklists: Store::new("pr_checklists", db.clone()),
tech_debts: Store::new("tech_debts", db.clone()),
gates: Store::new("gates", db.clone()),
context_workspaces: Store::new("context_workspaces", db.clone()),
});
state.recover_wal();
state.rebuild_index();
run_server(state)
#[cfg(not(target_os = "windows"))]
{
// Linux no longer executes server logic natively due to workspace split
Ok(())
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
use crate::models::*;
use crate::search::MemoryIndex;
use crate::store::Store;
use std::collections::{HashMap, HashSet};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::sync::RwLock;