1use 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#[derive(Clone)]
37pub struct RemembrallServer {
38 memory: Arc<MemoryStore>,
39 graph: Arc<GraphStore>,
40 embedder: Arc<dyn Embedder>,
41 tool_router: ToolRouter<Self>,
42 watched_dirs: Arc<Mutex<HashSet<PathBuf>>>,
45}
46
47#[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#[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
163impl RemembrallServer {
168 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 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}