memnite-mcp 0.2.1

MCP stdio server for memnite: exposes the event-sourced memory engine as 14 tools to AI agents.
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()
        }
    }
}