feat: implement dual-transport WAL architecture with permanent background leader and lightweight stubs
This commit is contained in:
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
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
Reference in new issue
Block a user