1use std::sync::Arc;
8
9use rmcp::{ErrorData as McpError, model::*, schemars};
10use serde_json::json;
11
12use remembrall_core::{
13 embed::Embedder,
14 memory::{
15 store::MemoryStore,
16 types::{CreateMemory, MatchType, MemoryQuery, MemoryType, Scope, Source},
17 },
18};
19
20pub(crate) const CONTRADICTION_THRESHOLD: f64 = 0.75;
24
25#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
30pub struct StoreParams {
31 #[schemars(description = "The knowledge to store. Be specific - include the why, not just the what.")]
32 pub content: String,
33 #[schemars(description = "One of: decision, pattern, error_pattern, preference, outcome, code_context, guideline, incident, architecture")]
34 pub memory_type: String,
35 #[schemars(description = "One-line summary")]
36 pub summary: Option<String>,
37 #[schemars(description = "Categorization tags")]
38 pub tags: Option<Vec<String>>,
39 #[schemars(description = "0.0 to 1.0, default 0.5. Use 0.8+ for architectural decisions.")]
40 pub importance: Option<f32>,
41 #[schemars(description = "Where this came from (PR URL, file path, etc.)")]
42 pub source_identifier: Option<String>,
43 #[schemars(description = "Tenant/organization scope for multi-tenant isolation. When set, only that tenant can retrieve this memory.")]
44 pub organization: Option<String>,
45 #[schemars(description = "Optional sub-project scope within the organization.")]
46 pub project: Option<String>,
47}
48
49#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
50pub struct RecallParams {
51 #[schemars(description = "What to search for. Can be a question ('how does auth work?') or keywords ('DATABASE_URL timeout').")]
52 pub query: String,
53 #[schemars(description = "Maximum number of results (default 10, max 25)")]
54 pub limit: Option<i64>,
55 #[schemars(description = "Filter by type: decision, pattern, error_pattern, preference, outcome, code_context, guideline, incident, architecture. Comma-separated for multiple.")]
56 pub memory_types: Option<String>,
57 #[schemars(description = "Filter by tags (comma-separated). Returns memories matching ALL supplied tags.")]
58 pub tags: Option<String>,
59 #[schemars(description = "Filter to memories about a specific project")]
60 pub project: Option<String>,
61 #[schemars(description = "Tenant/organization scope. When set, only memories stored under this organization are returned.")]
62 pub organization: Option<String>,
63}
64
65#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
66pub struct UpdateParams {
67 #[schemars(description = "UUID of the memory to update")]
68 pub id: String,
69 #[schemars(description = "New content (replaces existing). If provided, a new embedding will be generated.")]
70 pub content: Option<String>,
71 #[schemars(description = "New summary")]
72 pub summary: Option<String>,
73 #[schemars(description = "New tags (replaces existing)")]
74 pub tags: Option<Vec<String>>,
75 #[schemars(description = "New importance (0.0 to 1.0)")]
76 pub importance: Option<f32>,
77}
78
79#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
80pub struct DeleteParams {
81 #[schemars(description = "UUID of the memory to delete")]
82 pub id: String,
83}
84
85pub async fn store_impl(
90 memory: &Arc<MemoryStore>,
91 embedder: &Arc<dyn Embedder>,
92 params: StoreParams,
93) -> Result<CallToolResult, McpError> {
94 let StoreParams { content, memory_type, summary, tags, importance, source_identifier, organization, project } = params;
95
96 let mtype: MemoryType = memory_type.parse().map_err(|e: String| {
97 McpError::invalid_params(format!("invalid memory_type: {e}"), None)
98 })?;
99
100 let embedder = Arc::clone(embedder);
101 let content_clone = content.clone();
102 let embedding = tokio::task::spawn_blocking(move || embedder.embed(&content_clone))
103 .await
104 .map_err(|e| McpError::internal_error(format!("spawn_blocking failed: {e}"), None))?
105 .map_err(|e| McpError::internal_error(format!("embedding failed: {e}"), None))?;
106
107 let near_dupes = memory
108 .search_semantic(embedding.clone(), 3, CONTRADICTION_THRESHOLD, None)
109 .await
110 .unwrap_or_default();
111
112 let mut contradiction_list: Vec<serde_json::Value> = Vec::new();
113 for (dupe_id, similarity) in near_dupes.iter().take(3) {
114 if let Ok(mem) = memory.get_readonly(*dupe_id).await {
115 contradiction_list.push(json!({
116 "id": mem.id.to_string(),
117 "similarity": (similarity * 100.0).round() / 100.0,
118 "content": mem.content,
119 "memory_type": mem.memory_type.to_string(),
120 }));
121 }
122 }
123
124 let identifier = source_identifier.unwrap_or_else(|| "mcp".to_string());
125 let content_len = content.len();
126
127 let input = CreateMemory {
128 content,
129 summary,
130 memory_type: mtype,
131 source: Source {
132 system: "mcp".to_string(),
133 identifier,
134 author: None,
135 },
136 scope: Scope {
137 organization,
138 team: None,
139 project,
140 },
141 tags: tags.unwrap_or_default(),
142 metadata: None,
143 importance,
144 expires_at: None,
145 };
146
147 let id = memory
148 .store(input, embedding)
149 .await
150 .map_err(|e| McpError::internal_error(format!("store failed: {e}"), None))?;
151
152 let mut response = json!({ "id": id.to_string(), "stored": true });
153 if !contradiction_list.is_empty() {
154 response["contradictions"] = json!(contradiction_list);
155 }
156 if content_len > 2000 {
157 response["note"] = json!(
158 "Content exceeds 2000 characters. The semantic search embedding represents only the first ~500 words."
159 );
160 }
161
162 Ok(CallToolResult::success(vec![Content::text(response.to_string())]))
163}
164
165pub async fn recall_impl(
166 memory: &Arc<MemoryStore>,
167 embedder: &Arc<dyn Embedder>,
168 params: RecallParams,
169) -> Result<CallToolResult, McpError> {
170 let RecallParams { query, limit, memory_types, tags, project, organization } = params;
171
172 if query.trim().is_empty() {
173 let text = json!({
174 "query": "",
175 "total_results": 0,
176 "results": [],
177 "suggestion": "Please provide a search query."
178 })
179 .to_string();
180 return Ok(CallToolResult::success(vec![Content::text(text)]));
181 }
182
183 let limit = limit.unwrap_or(10).min(25).max(1);
184
185 let type_filter: Option<Vec<MemoryType>> = memory_types
186 .map(|s| {
187 s.split(',')
188 .map(|t| t.trim().parse::<MemoryType>())
189 .collect::<Result<Vec<_>, _>>()
190 })
191 .transpose()
192 .map_err(|e: String| McpError::invalid_params(format!("invalid memory_type: {e}"), None))?;
193
194 let tag_filter: Option<Vec<String>> = tags.map(|s| {
195 s.split(',').map(|t| t.trim().to_string()).collect()
196 });
197
198 let scope = if organization.is_some() || project.is_some() {
199 Some(Scope { organization, team: None, project })
200 } else {
201 None
202 };
203
204 let embedder = Arc::clone(embedder);
205 let query_clone = query.clone();
206 let embedding = tokio::task::spawn_blocking(move || embedder.embed(&query_clone))
207 .await
208 .map_err(|e| McpError::internal_error(format!("spawn_blocking failed: {e}"), None))?
209 .map_err(|e| McpError::internal_error(format!("embedding failed: {e}"), None))?;
210
211 let mq = MemoryQuery {
212 query: query.clone(),
213 memory_types: type_filter,
214 scope,
215 tags: tag_filter,
216 limit: Some(limit),
217 min_similarity: None,
218 };
219
220 let results = memory
221 .search_hybrid(embedding, &mq)
222 .await
223 .map_err(|e| McpError::internal_error(format!("recall failed: {e}"), None))?;
224
225 let result_json: Vec<serde_json::Value> = results
226 .iter()
227 .map(|r| {
228 let content = if r.memory.content.len() > 2000 {
229 let mut end = 2000;
230 while !r.memory.content.is_char_boundary(end) {
231 end -= 1;
232 }
233 format!("{}...[truncated]", &r.memory.content[..end])
234 } else {
235 r.memory.content.clone()
236 };
237
238 let match_type = match r.match_type {
239 MatchType::Semantic => "semantic",
240 MatchType::FullText => "fulltext",
241 MatchType::Hybrid => "hybrid",
242 };
243
244 json!({
245 "id": r.memory.id.to_string(),
246 "score": r.score,
247 "match_type": match_type,
248 "memory_type": r.memory.memory_type.to_string(),
249 "content": content,
250 "summary": r.memory.summary,
251 "tags": r.memory.tags,
252 "importance": r.memory.importance,
253 "source": r.memory.source.identifier,
254 "created_at": r.memory.created_at.to_rfc3339(),
255 "access_count": r.memory.access_count,
256 })
257 })
258 .collect();
259
260 let suggestion = if result_json.is_empty() {
261 Some("No memories found. Store context with remembrall_store as you work to build up searchable knowledge.")
262 } else {
263 None
264 };
265
266 let text = json!({
267 "query": query,
268 "total_results": result_json.len(),
269 "results": result_json,
270 "suggestion": suggestion,
271 })
272 .to_string();
273
274 Ok(CallToolResult::success(vec![Content::text(text)]))
275}
276
277pub async fn update_impl(
278 memory: &Arc<MemoryStore>,
279 embedder: &Arc<dyn Embedder>,
280 params: UpdateParams,
281) -> Result<CallToolResult, McpError> {
282 let UpdateParams { id, content, summary, tags, importance } = params;
283
284 let uid: uuid::Uuid = id.parse().map_err(|e| {
285 McpError::invalid_params(format!("invalid UUID: {e}"), None)
286 })?;
287
288 let embedding: Option<Vec<f32>> = if let Some(ref c) = content {
289 let embedder = Arc::clone(embedder);
290 let c_clone = c.clone();
291 let emb = tokio::task::spawn_blocking(move || embedder.embed(&c_clone))
292 .await
293 .map_err(|e| McpError::internal_error(format!("spawn_blocking failed: {e}"), None))?
294 .map_err(|e| McpError::internal_error(format!("embedding failed: {e}"), None))?;
295 Some(emb)
296 } else {
297 None
298 };
299
300 let updated = memory
301 .update(uid, content, summary, tags, importance, embedding)
302 .await
303 .map_err(|e| McpError::internal_error(format!("update failed: {e}"), None))?;
304
305 let text = if updated {
306 json!({ "id": id, "updated": true }).to_string()
307 } else {
308 json!({ "id": id, "updated": false, "reason": "not found" }).to_string()
309 };
310
311 Ok(CallToolResult::success(vec![Content::text(text)]))
312}
313
314pub async fn delete_impl(
315 memory: &Arc<MemoryStore>,
316 params: DeleteParams,
317) -> Result<CallToolResult, McpError> {
318 let DeleteParams { id } = params;
319
320 let uid: uuid::Uuid = id.parse().map_err(|e| {
321 McpError::invalid_params(format!("invalid UUID: {e}"), None)
322 })?;
323
324 let deleted = memory
325 .delete(uid)
326 .await
327 .map_err(|e| McpError::internal_error(format!("delete failed: {e}"), None))?;
328
329 let text = json!({ "id": id, "deleted": deleted }).to_string();
330 Ok(CallToolResult::success(vec![Content::text(text)]))
331}