Skip to main content

remembrall_server/
lib.rs

1//! RemembrallMCP server - exposes memory and graph tools over the Model Context Protocol.
2
3use std::collections::HashSet;
4use std::path::PathBuf;
5use std::sync::Arc;
6
7use tokio::sync::Mutex;
8
9pub mod tools;
10pub mod watcher;
11
12use rmcp::{
13    ErrorData as McpError, ServerHandler,
14    handler::server::{router::tool::ToolRouter, wrapper::Parameters},
15    model::*,
16    tool, tool_handler, tool_router,
17};
18use sqlx::postgres::PgPoolOptions;
19
20use remembrall_core::{
21    embed::{Embedder, FastEmbedder},
22    graph::store::GraphStore,
23    memory::store::MemoryStore,
24};
25
26use tools::{
27    graph::{ImpactParams, IndexParams, LookupParams, TourParams},
28    ingest::{IngestDocsParams, IngestGithubParams},
29    memory::{DeleteParams, RecallParams, StoreParams, UpdateParams},
30};
31
32// ---------------------------------------------------------------------------
33// Server struct
34// ---------------------------------------------------------------------------
35
36#[derive(Clone)]
37pub struct RemembrallServer {
38    memory: Arc<MemoryStore>,
39    graph: Arc<GraphStore>,
40    embedder: Arc<dyn Embedder>,
41    tool_router: ToolRouter<Self>,
42    /// Directories that have already had a background watcher spawned.
43    /// Guarded by a mutex so concurrent `remembrall_index` calls don't race.
44    watched_dirs: Arc<Mutex<HashSet<PathBuf>>>,
45}
46
47// ---------------------------------------------------------------------------
48// Tool wrappers - thin delegates into the tools:: modules
49// ---------------------------------------------------------------------------
50//
51// All `#[tool]` methods must live in this single `#[tool_router]` impl block
52// because the proc-macro scans the block to build the router. Logic lives in
53// the sub-modules; these methods are intentionally one-liners.
54
55#[tool_router]
56impl RemembrallServer {
57    #[tool(description = "Store knowledge or a decision for future reference. Use this whenever you learn something important about a codebase, make an architectural decision, observe a pattern, or encounter an error worth remembering.")]
58    async fn remembrall_store(
59        &self,
60        Parameters(params): Parameters<StoreParams>,
61    ) -> Result<CallToolResult, McpError> {
62        tools::memory::store_impl(&self.memory, &self.embedder, params).await
63    }
64
65    #[tool(description = "Search organizational memory for relevant knowledge - decisions, patterns, errors, and context from past sessions. Use this before making significant decisions or when you need context about how something works or why it was built a certain way.")]
66    async fn remembrall_recall(
67        &self,
68        Parameters(params): Parameters<RecallParams>,
69    ) -> Result<CallToolResult, McpError> {
70        tools::memory::recall_impl(&self.memory, &self.embedder, params).await
71    }
72
73    #[tool(description = "Update an existing memory. Only the fields you provide will be changed. If content is updated, a new embedding is generated automatically.")]
74    async fn remembrall_update(
75        &self,
76        Parameters(params): Parameters<UpdateParams>,
77    ) -> Result<CallToolResult, McpError> {
78        tools::memory::update_impl(&self.memory, &self.embedder, params).await
79    }
80
81    #[tool(description = "Delete a stored memory by its UUID. Use this to remove outdated or incorrect knowledge.")]
82    async fn remembrall_delete(
83        &self,
84        Parameters(params): Parameters<DeleteParams>,
85    ) -> Result<CallToolResult, McpError> {
86        tools::memory::delete_impl(&self.memory, params).await
87    }
88
89    #[tool(description = "Index a project directory to build the code graph. Must be run before impact analysis or symbol lookup. Captures functions, classes, methods, and data fields plus their call, import, defines, inherits, and field-reference relationships. Supports Python, TypeScript, JavaScript, Rust, Go, Ruby, Java, and Kotlin.")]
90    async fn remembrall_index(
91        &self,
92        Parameters(params): Parameters<IndexParams>,
93    ) -> Result<CallToolResult, McpError> {
94        tools::graph::index_impl(&self.graph, &self.watched_dirs, params).await
95    }
96
97    #[tool(description = "Analyze the blast radius of changing a code symbol. Returns all callers (upstream) or all callees (downstream) up to max_depth levels. Works on functions, classes, methods, and fields - ask 'who references the amount field?' and get the methods that read it. Use this before refactoring to understand what will break.")]
98    async fn remembrall_impact(
99        &self,
100        Parameters(params): Parameters<ImpactParams>,
101    ) -> Result<CallToolResult, McpError> {
102        tools::graph::impact_impl(&self.graph, params).await
103    }
104
105    #[tool(description = "Look up a code symbol by name. Returns its file location, type, and line numbers. Works for functions, classes, methods, and fields. Use this to find where a symbol is defined.")]
106    async fn remembrall_lookup_symbol(
107        &self,
108        Parameters(params): Parameters<LookupParams>,
109    ) -> Result<CallToolResult, McpError> {
110        tools::graph::lookup_symbol_impl(&self.graph, params).await
111    }
112
113    #[tool(description = "Generate a guided onboarding tour of an indexed codebase. Returns files in recommended reading order, starting from entry points and following the dependency graph. Use this to understand an unfamiliar project.")]
114    async fn remembrall_tour(
115        &self,
116        Parameters(params): Parameters<TourParams>,
117    ) -> Result<CallToolResult, McpError> {
118        tools::graph::tour_impl(&self.graph, params).await
119    }
120
121    #[tool(description = "Ingest merged pull request descriptions from a GitHub repository as memories. Solves the cold-start problem by bulk-importing architectural decisions, rationale, and context from your PR history. Requires the GitHub CLI (gh) to be installed and authenticated.")]
122    async fn remembrall_ingest_github(
123        &self,
124        Parameters(params): Parameters<IngestGithubParams>,
125    ) -> Result<CallToolResult, McpError> {
126        tools::ingest::ingest_github_impl(&self.memory, &self.embedder, params).await
127    }
128
129    #[tool(description = "Ingest markdown files from a project directory as memories. Walks the directory tree, splits files by H2 section headers, and stores each section as a searchable memory. Solves the cold-start problem - run this once per project to immediately populate RemembrallMCP with knowledge from README, ARCHITECTURE, ADRs, and docs.")]
130    async fn remembrall_ingest_docs(
131        &self,
132        Parameters(params): Parameters<IngestDocsParams>,
133    ) -> Result<CallToolResult, McpError> {
134        tools::ingest::ingest_docs_impl(&self.memory, &self.embedder, params).await
135    }
136}
137
138// ---------------------------------------------------------------------------
139// ServerHandler implementation
140// ---------------------------------------------------------------------------
141
142#[tool_handler]
143impl ServerHandler for RemembrallServer {
144    fn get_info(&self) -> ServerInfo {
145        ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
146            .with_instructions(
147                "RemembrallMCP gives an AI coding agent whole-codebase knowledge: a field-aware code graph plus persistent memory. \
148                 Use remembrall_index to build the code graph from a project directory (functions, classes, methods, and fields across 8 languages). \
149                 Use remembrall_impact to analyze what code would break if you change a symbol - works on functions, classes, methods, and fields, so you can ask who references a given struct or model field. \
150                 Use remembrall_lookup_symbol to find where a function, class, method, or field is defined. \
151                 Use remembrall_tour to get a guided reading-order tour of an indexed project - start here when onboarding to an unfamiliar codebase. \
152                 Use remembrall_recall to search for past decisions, patterns, errors, and knowledge before starting work. \
153                 Use remembrall_store to save decisions, patterns, and context. \
154                 Use remembrall_update to edit an existing memory (content, summary, tags, or importance) without deleting and re-creating it. \
155                 Use remembrall_delete to remove stale memories. \
156                 Use remembrall_ingest_github to bulk-import merged PR descriptions from a GitHub repo - run this once per project to solve the cold-start problem. \
157                 Use remembrall_ingest_docs to scan a project directory for markdown files and ingest them as memories - run this once per project to immediately populate RemembrallMCP from README, ARCHITECTURE, ADRs, and docs."
158                    .to_string(),
159            )
160    }
161}
162
163// ---------------------------------------------------------------------------
164// Constructor
165// ---------------------------------------------------------------------------
166
167impl RemembrallServer {
168    /// Build from explicit parameters.
169    pub async fn from_config(database_url: &str, schema: &str) -> anyhow::Result<Self> {
170        let safe_url = database_url.split('@').last().unwrap_or("<url>");
171        tracing::info!("Connecting to database: {safe_url}");
172
173        let pool = PgPoolOptions::new()
174            .max_connections(10)
175            .acquire_timeout(std::time::Duration::from_secs(5))
176            .connect(database_url)
177            .await
178            .map_err(|e| anyhow::anyhow!(
179                "Cannot connect to database at {}. Is Postgres running? \
180                 Try 'remembrall doctor' to diagnose.\nError: {}",
181                safe_url,
182                e
183            ))?;
184
185        let memory = Arc::new(MemoryStore::new(pool.clone(), schema.to_string())?);
186        let graph = Arc::new(GraphStore::new(pool.clone(), schema.to_string())?);
187
188        tracing::info!("Initializing stores...");
189        memory.init().await?;
190        graph.init().await?;
191
192        tracing::info!("Loading embedding model...");
193        let embedder: Arc<dyn Embedder> = Arc::new(FastEmbedder::new()?);
194
195        Ok(Self {
196            memory,
197            graph,
198            embedder,
199            tool_router: Self::tool_router(),
200            watched_dirs: Arc::new(Mutex::new(HashSet::new())),
201        })
202    }
203
204    /// Build from environment. Reads DATABASE_URL / REMEMBRALL_DATABASE_URL.
205    /// Delegates to from_config.
206    pub async fn from_env() -> anyhow::Result<Self> {
207        let database_url = std::env::var("REMEMBRALL_DATABASE_URL")
208            .or_else(|_| std::env::var("DATABASE_URL"))
209            .unwrap_or_else(|_| {
210                "postgres://postgres:postgres@localhost:5450/remembrall".to_string()
211            });
212
213        let schema = std::env::var("REMEMBRALL_SCHEMA")
214            .unwrap_or_else(|_| "remembrall".to_string());
215
216        Self::from_config(&database_url, &schema).await
217    }
218}