feat: implement dual-transport WAL architecture with permanent background leader and lightweight stubs

This commit is contained in:
Riz Ashraf committed 2026-09-08 09:07:56 +01:00
1 parent a2febd1b00
commit e4ff476b6d
16 files changed
+1910 -52

No files matched your search

File diff suppressed because it is too large. Load diff
+309
View File
@@ -0,0 +1,309 @@
mod handlers;
mod models;
mod state;
mod store;
mod tools;
use crate::handlers::MemoryHandler;
use crate::models::*;
use crate::state::MemoryState;
use crate::store::Store;
use rust_mcp_sdk::{
ServerDetails, ToMcpServerHandler,
error::SdkResult,
schema::{Implementation, ServerCapabilities, ServerCapabilitiesTools},
};
use std::fs;
use std::path::PathBuf;
use std::sync::{Arc, RwLock};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::time::sleep;
use clap::{Parser, Subcommand};
use std::collections::HashMap;
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
#[arg(long)]
target: Option<String>,
}
#[derive(Subcommand)]
enum Commands {
Gate {
#[command(subcommand)]
subcmd: GateCommands,
},
}
#[derive(Subcommand)]
enum GateCommands {
Set {
#[arg(long)]
action: String,
#[arg(long)]
target: String,
#[arg(long)]
namespace: Option<String>,
#[arg(short = 'p', long = "param")]
params: Vec<String>,
#[arg(long, conflicts_with = "block")]
authorize: bool,
#[arg(long, conflicts_with = "authorize")]
block: bool,
#[arg(long)]
reason: Option<String>,
},
Verify {
#[arg(long)]
action: String,
#[arg(long)]
target: String,
#[arg(long)]
namespace: Option<String>,
#[arg(short = 'p', long = "param")]
params: Vec<String>,
#[arg(long)]
consume: bool,
},
}
async fn reconcile_worker(state: Arc<MemoryState>) {
loop {
sleep(Duration::from_secs(5)).await;
let pattern = format!("{}/delta_*.json", state.base_dir.display());
let has_local = {
let session = state.session_graph.read().unwrap();
!session.entities.is_empty() || !session.relations.is_empty()
};
let has_files = glob::glob(&pattern).map(|p| p.count() > 0).unwrap_or(false);
if has_local || has_files {
state.apply_sync_write(|_master| {});
}
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
state.ledger.modify(|ledger| {
let seven_days = now.saturating_sub(7 * 24 * 60 * 60);
ledger.retain(|c| c.timestamp >= seven_days);
if ledger.len() > 1000 {
let excess = ledger.len() - 1000;
ledger.drain(0..excess);
}
});
state.sticky.modify(|notes| {
notes.retain(|note| note.timestamp >= now.saturating_sub(24 * 60 * 60));
});
}
}
use rust_mcp_axum::{create_axum_server, AxumServerOptions};
fn run_server(state: Arc<MemoryState>) -> SdkResult<()> {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let state_clone = state.clone();
tokio::spawn(async move {
reconcile_worker(state_clone).await;
});
let server_details = ServerDetails {
server_info: Implementation {
name: "gemini-mcp-memory".into(),
version: "3.0.0".into(),
title: Some("MCP Memory Server".into()),
description: Some("Persistent memory MCP server".into()),
icons: vec![],
website_url: None,
},
capabilities: ServerCapabilities {
tools: Some(ServerCapabilitiesTools {
list_changed: Some(true),
}),
..Default::default()
},
instructions: None,
meta: None,
};
let options = AxumServerOptions {
host: "0.0.0.0".to_string(),
port: 3000,
custom_sse_endpoint: Some("/sse".to_string()),
..Default::default()
};
let server = create_axum_server(
server_details,
MemoryHandler { state: state.clone() }.to_mcp_server_handler(),
options,
);
let server = server.with_route(
"/gate/verify",
axum::routing::get(|| async move {
axum::http::StatusCode::OK
}),
);
println!("MCP Memory Server running on http://0.0.0.0:3000/sse");
server.start().await.unwrap();
Ok(())
})
}
mod proxy;
fn main() -> SdkResult<()> {
let cli = Cli::parse();
#[cfg(target_os = "windows")]
{
loop {
if let Err(_) = std::net::TcpListener::bind("0.0.0.0:3000") {
// Port in use, become a stub proxy!
let target_url = cli.target.as_deref().unwrap_or("http://127.0.0.1:3000");
match proxy::run_proxy(target_url) {
Ok(true) => {
std::thread::sleep(std::time::Duration::from_millis(50));
continue; // Leader died, race to bind 3000
}
Ok(false) => return Ok(()), // Stdin closed, user exited
Err(_) => std::thread::sleep(std::time::Duration::from_millis(1000)),
}
} else {
break; // Proceed as Leader
}
}
}
#[cfg(not(target_os = "windows"))]
{
// This shouldn't be executed on linux natively anymore due to workspace split,
// but keeping it as a fallback.
let target_url = cli.target.as_deref().unwrap_or("http://host.docker.internal:3000");
let _ = proxy::run_proxy(target_url);
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 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)),
ledger: Store::new(base.join("audit_ledger.json")),
sticky: Store::new(base.join("sticky_notes.json")),
tasks: Store::new(base.join("tasks.json")),
snippets: Store::new(base.join("snippets.json")),
adrs: Store::new(base.join("adrs.json")),
prefs: Store::new(base.join("preferences.json")),
error_fixes: Store::new(base.join("error_fixes.json")),
pinned_files: Store::new(base.join("pinned_files.json")),
session_summaries: Store::new(base.join("session_summaries.json")),
handoff_memos: Store::new(base.join("handoff_memos.json")),
env_fingerprints: Store::new(base.join("env_fingerprints.json")),
env_requirements: Store::new(base.join("env_requirements.json")),
milestones: Store::new(base.join("milestones.json")),
environments: Store::new(base.join("environments.json")),
pr_checklists: Store::new(base.join("pr_checklists.json")),
tech_debts: Store::new(base.join("tech_debts.json")),
gates: Store::new(base.join("gates.json")),
context_workspaces: Store::new(base.join("context_workspaces.json")),
});
if let Some(command) = cli.command {
match command {
Commands::Gate { subcmd } => {
match subcmd {
GateCommands::Set { action, target, namespace, params, authorize, block, reason } => {
let status = if authorize { "authorized".to_string() } else if block { "blocked".to_string() } else { "pending".to_string() };
let mut param_map = HashMap::new();
for p in params {
if let Some((k, v)) = p.split_once('=') {
param_map.insert(k.to_string(), v.to_string());
}
}
let record = GateRecord {
id: uuid::Uuid::new_v4().to_string(),
action: action.clone(),
target: target.clone(),
namespace,
params: param_map,
status,
reason,
timestamp: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(),
};
state.gates.modify(|gates| {
gates.retain(|g| !(g.action == record.action && g.target == record.target));
gates.push(record);
});
println!("Gate state updated.");
std::process::exit(0);
}
GateCommands::Verify { action, target, namespace, params, consume } => {
let mut param_map = HashMap::new();
for p in params {
if let Some((k, v)) = p.split_once('=') {
param_map.insert(k.to_string(), v.to_string());
}
}
let mut found = None;
let mut to_remove = None;
state.gates.modify(|gates| {
if let Some(idx) = gates.iter().position(|g| g.action == action && g.target == target && g.namespace == namespace && g.params == param_map) {
found = Some(gates[idx].clone());
if consume {
to_remove = Some(idx);
}
}
if let Some(idx) = to_remove {
gates.remove(idx);
}
});
match found {
Some(record) => {
if record.status == "authorized" {
std::process::exit(0);
} else {
if let Some(r) = record.reason {
eprintln!("❌ Action blocked. Reason: {}", r);
} else {
eprintln!("❌ Action blocked.");
}
std::process::exit(1);
}
}
None => {
eprintln!("❌ Action not yet authorized (no gate record found).");
std::process::exit(2);
}
}
}
}
}
}
}
run_server(state)
}
+175
View File
@@ -0,0 +1,175 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CodeChange {
pub timestamp: u64,
pub file_path: String,
pub description: String,
pub git_commit: Option<String>,
pub git_branch: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct StickyNote {
pub timestamp: u64,
pub content: String,
}
pub fn default_namespace() -> String {
"global".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Entity {
pub name: String,
#[serde(rename = "entityType")]
pub entity_type: String,
#[serde(default)]
pub observations: Vec<String>,
#[serde(default = "default_namespace")]
pub namespace: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct Relation {
pub from: String,
pub to: String,
#[serde(rename = "relationType")]
pub relation_type: String,
#[serde(default = "default_namespace")]
pub namespace: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct KnowledgeGraph {
#[serde(default)]
pub entities: HashMap<String, Entity>,
#[serde(default)]
pub relations: Vec<Relation>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Task {
pub id: String,
pub title: String,
pub status: String,
pub description: String,
pub created_at: u64,
pub updated_at: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Snippet {
pub name: String,
pub language: String,
pub code: String,
pub description: String,
pub updated_at: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Adr {
pub id: String,
pub title: String,
pub context: String,
pub decision: String,
pub consequence: String,
pub timestamp: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Preference {
pub key: String,
pub value: String,
pub updated_at: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ErrorFix {
pub signature: String,
pub solution: String,
pub timestamp: u64,
pub git_commit: Option<String>,
pub git_branch: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PinnedFile {
pub namespace: String,
pub file_path: String,
pub timestamp: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SessionSummary {
pub timestamp: u64,
pub summary: String,
pub namespace: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct HandoffMemo {
pub id: String,
pub author: String,
pub content: String,
pub namespace: String,
pub timestamp: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct EnvFingerprint {
pub namespace: String,
pub os: String,
pub shell: String,
pub tool_versions: HashMap<String, String>,
pub updated_at: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct EnvRequirement {
pub namespace: String,
pub key: String,
pub description: String,
pub is_secret: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Milestone {
pub id: String,
pub title: String,
pub status: String,
pub namespace: String,
pub target_date: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct EnvironmentDetail {
pub namespace: String,
pub name: String,
pub url: String,
pub description: String,
pub requires_vpn: bool,
pub updated_at: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PrChecklistItem {
pub namespace: String,
pub id: String,
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TechDebt {
pub id: String,
pub namespace: String,
pub description: String,
pub ideal_solution: String,
pub is_resolved: bool,
pub created_at: u64,
pub git_commit: Option<String>,
pub git_branch: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ContextWorkspace {
pub name: String,
pub namespace: String,
pub pinned_files: Vec<String>,
pub active_task_ids: Vec<String>,
pub saved_at: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Default)]
pub struct GateRecord {
pub id: String,
pub action: String,
pub target: String,
pub namespace: Option<String>,
pub params: std::collections::HashMap<String, String>,
pub status: String,
pub reason: Option<String>,
pub timestamp: u64,
}
+93
View File
@@ -0,0 +1,93 @@
use rust_mcp_sdk::error::SdkResult;
use tokio_util::io::StreamReader;
use tokio::io::AsyncBufReadExt;
use futures_util::StreamExt;
use std::sync::Arc;
use tokio::sync::RwLock;
pub fn run_proxy(target_url: &str) -> SdkResult<bool> {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let client = reqwest::Client::builder().build().unwrap();
let sse_url = format!("{}/sse", target_url);
let resp = match client.get(&sse_url).send().await {
Ok(r) => r,
Err(_) => return Ok(true), // Connection failed (Leader is dead)
};
let post_url = Arc::new(RwLock::new(format!("{}/messages", target_url)));
let post_url_clone = Arc::clone(&post_url);
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
let tx_clone = tx.clone();
let client_clone = client.clone();
tokio::task::spawn_blocking(move || {
let stdin = std::io::stdin();
let mut handle = stdin.lock();
let mut buffer = String::new();
while let Ok(bytes) = std::io::BufRead::read_line(&mut handle, &mut buffer) {
if bytes == 0 { break; }
let body = buffer.clone();
buffer.clear();
let client = client_clone.clone();
let url_arc = Arc::clone(&post_url_clone);
tokio::spawn(async move {
let mut url = "".to_string();
for _ in 0..50 {
let u = url_arc.read().await.clone();
if u.contains("sessionId") {
url = u;
break;
}
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
if url.is_empty() {
url = url_arc.read().await.clone();
}
let _ = client.post(&url).header("Content-Type", "application/json").body(body).send().await;
});
}
let _ = tx_clone.blocking_send(false); // Stdin EOF
});
let target_url = target_url.to_string();
let tx_clone2 = tx.clone();
tokio::spawn(async move {
let stream = resp.bytes_stream().map(|res| res.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)));
let mut reader = tokio::io::BufReader::new(StreamReader::new(stream));
let mut line = String::new();
let mut is_message = false;
let mut is_endpoint = false;
while let Ok(bytes) = reader.read_line(&mut line).await {
if bytes == 0 { break; }
let trimmed = line.trim();
if trimmed.starts_with("event: message") {
is_message = true;
is_endpoint = false;
} else if trimmed.starts_with("event: endpoint") {
is_endpoint = true;
is_message = false;
} else if trimmed.starts_with("data: ") {
if is_message {
println!("{}", &trimmed[6..]);
is_message = false;
} else if is_endpoint {
let ep = &trimmed[6..];
let mut p = post_url.write().await;
*p = format!("{}{}", target_url, ep);
is_endpoint = false;
}
}
line.clear();
}
let _ = tx_clone2.send(true).await; // Stream dropped (Leader dead)
});
let dropped = rx.recv().await.unwrap_or(true);
Ok(dropped)
})
}
+140
View File
@@ -0,0 +1,140 @@
use crate::models::*;
use crate::store::{Store, read_json_file, write_json_atomic};
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::PathBuf;
use std::sync::RwLock;
use std::time::{Duration, SystemTime};
pub struct MemoryState {
pub base_dir: PathBuf,
pub master_path: PathBuf,
pub session_graph: RwLock<KnowledgeGraph>,
pub master_cache: RwLock<(KnowledgeGraph, SystemTime)>,
pub ledger: Store<Vec<CodeChange>>,
pub sticky: Store<Vec<StickyNote>>,
pub tasks: Store<Vec<Task>>,
pub snippets: Store<Vec<Snippet>>,
pub adrs: Store<Vec<Adr>>,
pub prefs: Store<HashMap<String, Preference>>,
pub error_fixes: Store<Vec<ErrorFix>>,
pub pinned_files: Store<Vec<PinnedFile>>,
pub session_summaries: Store<Vec<SessionSummary>>,
pub handoff_memos: Store<Vec<HandoffMemo>>,
pub env_fingerprints: Store<HashMap<String, EnvFingerprint>>,
pub env_requirements: Store<Vec<EnvRequirement>>,
pub milestones: Store<Vec<Milestone>>,
pub environments: Store<Vec<EnvironmentDetail>>,
pub pr_checklists: Store<Vec<PrChecklistItem>>,
pub tech_debts: Store<Vec<TechDebt>>,
pub gates: Store<Vec<GateRecord>>,
pub context_workspaces: Store<Vec<ContextWorkspace>>,
}
impl MemoryState {
fn master_mtime(&self) -> SystemTime {
fs::metadata(&self.master_path)
.and_then(|m| m.modified())
.unwrap_or(SystemTime::UNIX_EPOCH)
}
pub fn unique_items<T: Eq + std::hash::Hash + Clone>(input: Vec<T>) -> Vec<T> {
let mut keys = HashSet::new();
let mut list = Vec::new();
for entry in input {
if keys.insert(entry.clone()) {
list.push(entry);
}
}
list
}
pub fn merge_graphs(dest: &mut KnowledgeGraph, src: &KnowledgeGraph) {
for (name, src_ent) in &src.entities {
let dest_ent = dest
.entities
.entry(name.clone())
.or_insert_with(|| src_ent.clone());
if dest_ent.name == src_ent.name {
dest_ent.observations.extend(src_ent.observations.clone());
dest_ent.observations = Self::unique_items(dest_ent.observations.clone());
}
}
dest.relations.extend(src.relations.clone());
dest.relations = Self::unique_items(dest.relations.clone());
}
pub fn read_master_cached(&self) -> KnowledgeGraph {
let current_mtime = self.master_mtime();
{
let lock = self.master_cache.read().unwrap();
if lock.1 == current_mtime {
return lock.0.clone();
}
}
let mut lock = self.master_cache.write().unwrap();
let new_mtime = self.master_mtime();
if lock.1 != new_mtime {
lock.0 = read_json_file(&self.master_path);
lock.1 = new_mtime;
}
lock.0.clone()
}
pub fn get_full_graph(&self) -> KnowledgeGraph {
let mut master = self.read_master_cached();
let wal_path = self.base_dir.join("wal.jsonl");
if let Ok(content) = std::fs::read_to_string(&wal_path) {
for line in content.lines() {
if let Ok(d) = serde_json::from_str::<KnowledgeGraph>(line) {
Self::merge_graphs(&mut master, &d);
}
}
}
let session_graph = self.session_graph.read().unwrap();
Self::merge_graphs(&mut master, &session_graph);
master
}
pub fn write_to_local_delta<F: FnOnce(&mut KnowledgeGraph)>(&self, update_fn: F) {
let mut session_graph = self.session_graph.write().unwrap();
update_fn(&mut session_graph);
let wal_path = self.base_dir.join("wal.jsonl");
if let Ok(payload) = serde_json::to_string(&*session_graph) {
if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(&wal_path) {
use std::io::Write;
let _ = writeln!(file, "{}", payload);
}
}
}
pub fn apply_sync_write<F: FnOnce(&mut KnowledgeGraph)>(&self, update_fn: F) {
let lock_path = self.base_dir.join("master.lock");
let mut attempts = 0;
loop {
if fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(&lock_path)
.is_ok()
{
break;
}
if attempts > 100 {
let _ = fs::remove_file(&lock_path);
}
attempts += 1;
std::thread::sleep(Duration::from_millis(50));
}
let mut master = self.get_full_graph();
let wal_path = self.base_dir.join("wal.jsonl");
let _ = fs::remove_file(&wal_path);
*self.session_graph.write().unwrap() = KnowledgeGraph::default();
update_fn(&mut master);
let _ = write_json_atomic(&self.master_path, &master);
let mut cache_lock = self.master_cache.write().unwrap();
cache_lock.0 = master;
cache_lock.1 = self.master_mtime();
let _ = fs::remove_file(&lock_path);
}
}
+82
View File
@@ -0,0 +1,82 @@
use serde::{de::DeserializeOwned, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::RwLock;
use std::time::SystemTime;
pub fn read_json_file<T: DeserializeOwned + Default>(path: &Path) -> T {
if let Ok(data) = fs::read(path)
&& let Ok(parsed) = serde_json::from_slice(&data) {
return parsed;
}
let bak_path = path.with_extension("json.bak");
if let Ok(data) = fs::read(&bak_path)
&& let Ok(parsed) = serde_json::from_slice(&data) {
let _ = fs::write(path, data);
return parsed;
}
T::default()
}
pub fn write_json_atomic<T: Serialize>(path: &Path, data: &T) -> std::io::Result<()> {
if path.exists() {
let bak_path = path.with_extension("json.bak");
let _ = fs::copy(path, &bak_path);
}
let tmp_path = path.with_extension("json.tmp");
let json_data = serde_json::to_string_pretty(data)?;
fs::write(&tmp_path, json_data)?;
fs::rename(&tmp_path, path)
}
pub struct Store<T> {
pub path: PathBuf,
pub cache: RwLock<(T, SystemTime)>,
}
impl<T: DeserializeOwned + Default + Serialize + Clone> Store<T> {
fn mtime(&self) -> SystemTime {
fs::metadata(&self.path)
.and_then(|m| m.modified())
.unwrap_or(SystemTime::UNIX_EPOCH)
}
pub fn new(path: PathBuf) -> Self {
let mtime = fs::metadata(&path)
.and_then(|m| m.modified())
.unwrap_or(SystemTime::UNIX_EPOCH);
let initial_data = read_json_file(&path);
Self {
path,
cache: RwLock::new((initial_data, mtime)),
}
}
pub fn read(&self) -> T {
let current_mtime = self.mtime();
{
let lock = self.cache.read().unwrap();
if lock.1 == current_mtime {
return lock.0.clone();
}
}
let mut lock = self.cache.write().unwrap();
let new_mtime = self.mtime();
if lock.1 != new_mtime {
lock.0 = read_json_file(&self.path);
lock.1 = new_mtime;
}
lock.0.clone()
}
pub fn modify<F: FnOnce(&mut T)>(&self, f: F) {
let mut lock = self.cache.write().unwrap();
let current_mtime = self.mtime();
if lock.1 != current_mtime {
lock.0 = read_json_file(&self.path);
}
f(&mut lock.0);
let _ = write_json_atomic(&self.path, &lock.0);
lock.1 = self.mtime();
}
}
+364
View File
@@ -0,0 +1,364 @@
use rust_mcp_sdk::macros;
use serde::{Deserialize, Serialize};
#[macros::mcp_tool(name = "create_entities", description = "Create multiple new entities")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct CreateEntitiesTool {
pub entities: Vec<serde_json::Value>,
}
#[macros::mcp_tool(
name = "create_relations",
description = "Create multiple new relations"
)]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct CreateRelationsTool {
pub relations: Vec<serde_json::Value>,
}
#[macros::mcp_tool(
name = "add_observations",
description = "Add observations to entities"
)]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct AddObservationsTool {
pub observations: Vec<serde_json::Value>,
}
#[macros::mcp_tool(
name = "delete_entities",
description = "Delete entities and relations"
)]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct DeleteEntitiesTool {
#[serde(rename = "entityNames")]
pub entity_names: Vec<String>,
}
#[macros::mcp_tool(name = "delete_observations", description = "Delete observations")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct DeleteObservationsTool {
pub deletions: Vec<serde_json::Value>,
}
#[macros::mcp_tool(name = "delete_relations", description = "Delete relations")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct DeleteRelationsTool {
pub relations: Vec<serde_json::Value>,
}
#[macros::mcp_tool(name = "read_graph", description = "Read the entire knowledge graph")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct ReadGraphTool {
pub namespace: Option<String>,
}
#[macros::mcp_tool(name = "search_nodes", description = "Search for nodes")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct SearchNodesTool {
pub query: String,
pub namespace: Option<String>,
}
#[macros::mcp_tool(name = "open_nodes", description = "Open specific nodes")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct OpenNodesTool {
pub names: Vec<String>,
}
#[macros::mcp_tool(name = "log_code_change", description = "Log a code change")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct LogCodeChangeTool {
#[serde(rename = "filePath")]
pub file_path: String,
pub description: String,
pub git_commit: Option<String>,
pub git_branch: Option<String>,
}
#[macros::mcp_tool(name = "query_recent_changes", description = "Query the audit ledger")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct QueryRecentChangesTool {}
#[macros::mcp_tool(name = "visualize_graph", description = "Generate Mermaid diagram")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct VisualizeGraphTool {
pub query: Option<String>,
pub namespace: Option<String>,
}
#[macros::mcp_tool(name = "add_sticky_note", description = "Add a sticky note")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct AddStickyNoteTool {
pub content: String,
}
#[macros::mcp_tool(name = "read_sticky_notes", description = "Read sticky notes")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct ReadStickyNotesTool {}
#[macros::mcp_tool(name = "condense_entity", description = "Condense observations")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct CondenseEntityTool {
#[serde(rename = "entityName")]
pub entity_name: String,
pub summarized_observations: Vec<String>,
}
#[macros::mcp_tool(name = "add_task", description = "Add a task")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct AddTaskTool {
pub title: String,
pub description: String,
}
#[macros::mcp_tool(name = "update_task_status", description = "Update task status")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct UpdateTaskStatusTool {
pub id: String,
pub status: String,
}
#[macros::mcp_tool(name = "list_active_tasks", description = "List active tasks")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct ListActiveTasksTool {}
#[macros::mcp_tool(name = "store_snippet", description = "Store code snippet")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct StoreSnippetTool {
pub name: String,
pub language: String,
pub code: String,
pub description: String,
}
#[macros::mcp_tool(name = "search_snippets", description = "Search snippets")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct SearchSnippetsTool {
pub query: String,
}
#[macros::mcp_tool(name = "delete_snippet", description = "Delete snippet")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct DeleteSnippetTool {
pub name: String,
}
#[macros::mcp_tool(
name = "log_decision",
description = "Log architectural decision record"
)]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct LogDecisionTool {
pub title: String,
pub context: String,
pub decision: String,
pub consequence: String,
}
#[macros::mcp_tool(name = "query_decisions", description = "Query decision records")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct QueryDecisionsTool {
pub query: Option<String>,
}
#[macros::mcp_tool(
name = "merge_entities",
description = "Merge source entity into target entity"
)]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct MergeEntitiesTool {
#[serde(rename = "sourceEntity")]
pub source_entity: String,
#[serde(rename = "targetEntity")]
pub target_entity: String,
}
#[macros::mcp_tool(name = "find_orphans", description = "Find entities with no relations")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct FindOrphansTool {}
#[macros::mcp_tool(
name = "learn_preference",
description = "Store an AI behavioral preference"
)]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct LearnPreferenceTool {
pub key: String,
pub value: String,
}
#[macros::mcp_tool(
name = "read_preferences",
description = "Read all learned behavioral preferences"
)]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct ReadPreferencesTool {}
#[macros::mcp_tool(name = "log_error_fix", description = "Log an error fix")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct LogErrorFixTool {
pub signature: String,
pub solution: String,
pub git_commit: Option<String>,
pub git_branch: Option<String>,
}
#[macros::mcp_tool(name = "search_error_fixes", description = "Search error fixes")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct SearchErrorFixesTool {
pub query: String,
}
#[macros::mcp_tool(name = "pin_file", description = "Pin a file to working set")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct PinFileTool {
pub namespace: String,
pub file_path: String,
}
#[macros::mcp_tool(name = "unpin_file", description = "Unpin a file")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct UnpinFileTool {
pub namespace: String,
pub file_path: String,
}
#[macros::mcp_tool(name = "list_pinned_files", description = "List pinned files")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct ListPinnedFilesTool {
pub namespace: Option<String>,
}
#[macros::mcp_tool(name = "add_session_summary", description = "Add a session summary")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct AddSessionSummaryTool {
pub summary: String,
pub namespace: String,
}
#[macros::mcp_tool(name = "get_project_timeline", description = "Get project timeline")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct GetProjectTimelineTool {
pub namespace: Option<String>,
}
#[macros::mcp_tool(
name = "leave_handoff_memo",
description = "Leave memo for next session"
)]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct LeaveHandoffMemoTool {
pub content: String,
pub namespace: String,
}
#[macros::mcp_tool(name = "read_handoff_memos", description = "Read pending memos")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct ReadHandoffMemosTool {
pub namespace: Option<String>,
}
#[macros::mcp_tool(name = "clear_handoff_memos", description = "Clear read memos")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct ClearHandoffMemosTool {
pub ids: Vec<String>,
}
#[macros::mcp_tool(name = "update_env_fingerprint", description = "Update env versions")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct UpdateEnvFingerprintTool {
pub namespace: String,
pub tool_versions: std::collections::HashMap<String, String>,
}
#[macros::mcp_tool(name = "read_env_fingerprint", description = "Read env footprint")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct ReadEnvFingerprintTool {
pub namespace: String,
}
#[macros::mcp_tool(name = "log_env_requirement", description = "Log env var requirement")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct LogEnvRequirementTool {
pub namespace: String,
pub key: String,
pub description: String,
pub is_secret: bool,
}
#[macros::mcp_tool(name = "add_milestone", description = "Add project milestone")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct AddMilestoneTool {
pub title: String,
pub namespace: String,
}
#[macros::mcp_tool(name = "update_milestone", description = "Update milestone status")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct UpdateMilestoneTool {
pub id: String,
pub status: String,
}
#[macros::mcp_tool(name = "list_milestones", description = "List milestones")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct ListMilestonesTool {
pub namespace: Option<String>,
}
#[macros::mcp_tool(
name = "generate_standup_report",
description = "Generate a standup report"
)]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct GenerateStandupReportTool {
pub namespace: String,
pub hours_lookback: u64,
}
#[macros::mcp_tool(name = "register_environment", description = "Register an environment")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct RegisterEnvironmentTool {
pub namespace: String,
pub name: String,
pub url: String,
pub description: String,
pub requires_vpn: bool,
}
#[macros::mcp_tool(
name = "get_environment_details",
description = "Get environment details"
)]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct GetEnvironmentDetailsTool {
pub namespace: String,
}
#[macros::mcp_tool(
name = "add_pr_checklist_item",
description = "Add a PR checklist item"
)]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct AddPrChecklistItemTool {
pub namespace: String,
pub description: String,
}
#[macros::mcp_tool(name = "get_pr_checklist", description = "Get the PR checklist")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct GetPrChecklistTool {
pub namespace: String,
}
#[macros::mcp_tool(name = "clear_pr_checklist", description = "Clear the PR checklist")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct ClearPrChecklistTool {
pub namespace: String,
}
#[macros::mcp_tool(name = "log_tech_debt", description = "Log tech debt")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct LogTechDebtTool {
pub namespace: String,
pub description: String,
pub ideal_solution: String,
pub git_commit: Option<String>,
pub git_branch: Option<String>,
}
#[macros::mcp_tool(name = "resolve_tech_debt", description = "Resolve tech debt")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct ResolveTechDebtTool {
pub id: String,
}
#[macros::mcp_tool(name = "list_tech_debt", description = "List tech debt")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct ListTechDebtTool {
pub namespace: String,
pub include_resolved: bool,
}
#[macros::mcp_tool(name = "save_context_workspace", description = "Save current context")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct SaveContextWorkspaceTool {
pub namespace: String,
pub name: String,
pub pinned_files: Vec<String>,
pub active_task_ids: Vec<String>,
}
#[macros::mcp_tool(name = "load_context_workspace", description = "Load context")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct LoadContextWorkspaceTool {
pub namespace: String,
pub name: String,
}
#[macros::mcp_tool(name = "list_context_workspaces", description = "List contexts")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct ListContextWorkspacesTool {
pub namespace: String,
}
#[macros::mcp_tool(name = "omni_search", description = "Search across all memory vaults")]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct OmniSearchTool {
pub query: String,
pub namespace: Option<String>,
}
#[macros::mcp_tool(
name = "get_project_health",
description = "Get project health metrics"
)]
#[derive(Debug, Deserialize, Serialize, macros::JsonSchema)]
pub struct GetProjectHealthTool {
pub namespace: String,
}