use std::sync::{Arc, Mutex};
use rmcp::{
handler::server::{router::tool::ToolRouter, wrapper::Parameters},
model::{ServerCapabilities, ServerInfo},
tool, tool_handler, tool_router, ServerHandler,
};
use memnite_cli::App;
use crate::handlers::{self, Ctx};
use crate::params::{
AddArgs, CheckArgs, ConflictsArgs, ContextArgs, DeleteArgs, GetArgs, RelateArgs, SearchArgs,
SessionSummaryArgs, TimelineArgs, UpdateArgs,
};
fn ctx() -> Ctx {
let machine = std::env::var("COMPUTERNAME")
.or_else(|_| std::env::var("HOSTNAME"))
.unwrap_or_else(|_| "unknown".to_string());
Ctx {
ts: chrono::Utc::now().to_rfc3339(),
engine: "mcp".to_string(),
machine,
}
}
#[derive(Clone)]
pub struct MemniteServer {
app: Arc<Mutex<App>>,
tool_router: ToolRouter<Self>,
}
#[tool_router]
impl MemniteServer {
pub fn new(app: App) -> Self {
Self {
app: Arc::new(Mutex::new(app)),
tool_router: Self::tool_router(),
}
}
#[tool(
description = "Create a memory (title, body, optional type/scope/project/topic/tags/anchors)."
)]
fn mem_add(&self, Parameters(a): Parameters<AddArgs>) -> Result<String, String> {
let app = self.app.lock().map_err(|e| e.to_string())?;
handlers::op_add(&app, a, &ctx()).map_err(|e| e.to_string())
}
#[tool(
description = "Full-text search memories by plain text, optionally filtered by type/project/scope, with match_any for OR-mode."
)]
fn mem_search(&self, Parameters(a): Parameters<SearchArgs>) -> Result<String, String> {
let app = self.app.lock().map_err(|e| e.to_string())?;
handlers::op_search(&app, a).map_err(|e| e.to_string())
}
#[tool(description = "Fetch one memory by id.")]
fn mem_get(&self, Parameters(a): Parameters<GetArgs>) -> Result<String, String> {
let app = self.app.lock().map_err(|e| e.to_string())?;
handlers::op_get(&app, a).map_err(|e| e.to_string())
}
#[tool(description = "Re-check anchored memories for staleness; updates statuses.")]
fn mem_check(&self, Parameters(a): Parameters<CheckArgs>) -> Result<String, String> {
let app = self.app.lock().map_err(|e| e.to_string())?;
handlers::op_check(&app, a, &ctx()).map_err(|e| e.to_string())
}
#[tool(description = "List memories currently marked stale.")]
fn mem_stale(&self) -> Result<String, String> {
let app = self.app.lock().map_err(|e| e.to_string())?;
handlers::op_stale(&app).map_err(|e| e.to_string())
}
#[tool(description = "Rebuild the read projection from the event log.")]
fn mem_rebuild(&self) -> Result<String, String> {
let app = self.app.lock().map_err(|e| e.to_string())?;
handlers::op_rebuild(&app).map_err(|e| e.to_string())
}
#[tool(description = "Update fields of an existing memory; only the fields you pass change.")]
fn mem_update(&self, Parameters(a): Parameters<UpdateArgs>) -> Result<String, String> {
let app = self.app.lock().map_err(|e| e.to_string())?;
handlers::op_update(&app, a, &ctx()).map_err(|e| e.to_string())
}
#[tool(description = "Delete (tombstone) a memory; history is retained.")]
fn mem_delete(&self, Parameters(a): Parameters<DeleteArgs>) -> Result<String, String> {
let app = self.app.lock().map_err(|e| e.to_string())?;
handlers::op_delete(&app, a, &ctx()).map_err(|e| e.to_string())
}
#[tool(description = "Recent memories for a project (newest first).")]
fn mem_context(&self, Parameters(a): Parameters<ContextArgs>) -> Result<String, String> {
let app = self.app.lock().map_err(|e| e.to_string())?;
handlers::op_context(&app, a).map_err(|e| e.to_string())
}
#[tool(description = "Event history of one memory (oldest first).")]
fn mem_timeline(&self, Parameters(a): Parameters<TimelineArgs>) -> Result<String, String> {
let app = self.app.lock().map_err(|e| e.to_string())?;
handlers::op_timeline(&app, a).map_err(|e| e.to_string())
}
#[tool(description = "Check projection integrity against the event log.")]
fn mem_doctor(&self) -> Result<String, String> {
let app = self.app.lock().map_err(|e| e.to_string())?;
handlers::op_doctor(&app).map_err(|e| e.to_string())
}
#[tool(
description = "Persist a distilled session summary as a memory (type=session_summary). Call at compaction or session close."
)]
fn mem_session_summary(
&self,
Parameters(a): Parameters<SessionSummaryArgs>,
) -> Result<String, String> {
let app = self.app.lock().map_err(|e| e.to_string())?;
handlers::op_session_summary(&app, a, &ctx()).map_err(|e| e.to_string())
}
#[tool(
description = "Assert a relation between two memories: conflicts_with | supersedes | scoped | related | compatible | not_conflict. Use after mem_add reports candidates."
)]
fn mem_relate(&self, Parameters(a): Parameters<RelateArgs>) -> Result<String, String> {
let app = self.app.lock().map_err(|e| e.to_string())?;
handlers::op_relate(&app, a, &ctx()).map_err(|e| e.to_string())
}
#[tool(description = "List relation edges touching a memory (conflict/supersession graph).")]
fn mem_conflicts(&self, Parameters(a): Parameters<ConflictsArgs>) -> Result<String, String> {
let app = self.app.lock().map_err(|e| e.to_string())?;
handlers::op_conflicts(&app, a).map_err(|e| e.to_string())
}
}
#[tool_handler]
impl ServerHandler for MemniteServer {
fn get_info(&self) -> ServerInfo {
ServerInfo {
capabilities: ServerCapabilities::builder().enable_tools().build(),
instructions: Some(
"Memnite: event-sourced memory. mem_add to remember (it surfaces \
candidate memories to relate), mem_search/mem_context to recall, \
mem_relate to record a conflict/supersession, mem_conflicts to inspect \
the graph, mem_update/mem_delete to revise, mem_timeline for history, \
mem_check to flag stale anchors, mem_doctor for integrity."
.to_string(),
),
..Default::default()
}
}
}