codex_memory/mcp_server/
handlers.rs

1//! Simple MCP request handlers
2use crate::chunking::FileChunker;
3use crate::error::Result;
4use crate::models::{SearchParams, SearchStrategy};
5use crate::storage::Storage;
6use serde_json::{json, Value};
7use std::path::Path;
8use std::sync::Arc;
9use uuid::Uuid;
10
11/// Minimal MCP request handlers
12pub struct MCPHandlers {
13    storage: Arc<Storage>,
14}
15
16impl MCPHandlers {
17    /// Create new handlers with storage backend
18    pub fn new(storage: Arc<Storage>) -> Self {
19        Self { storage }
20    }
21
22    /// Handle tool calls
23    pub async fn handle_tool_call(&self, tool_name: &str, params: Value) -> Result<Value> {
24        match tool_name {
25            "store_memory" => self.handle_store_memory(params).await,
26            "get_memory" => self.handle_get_memory(params).await,
27            "delete_memory" => self.handle_delete_memory(params).await,
28            "get_statistics" => self.handle_get_statistics().await,
29            "store_file" => self.handle_store_file(params).await,
30            "search_memory" => self.handle_search_memory(params).await,
31            _ => Err(crate::error::Error::Other(format!(
32                "Unknown tool: {}",
33                tool_name
34            ))),
35        }
36    }
37
38    async fn handle_store_memory(&self, params: Value) -> Result<Value> {
39        let content = params["content"]
40            .as_str()
41            .ok_or_else(|| crate::error::Error::Other("Missing content parameter".to_string()))?;
42
43        // Context is required
44        let context = params["context"]
45            .as_str()
46            .ok_or_else(|| {
47                crate::error::Error::Other("Missing required context parameter".to_string())
48            })?
49            .to_string();
50
51        // Summary is required
52        let summary = params["summary"]
53            .as_str()
54            .ok_or_else(|| {
55                crate::error::Error::Other("Missing required summary parameter".to_string())
56            })?
57            .to_string();
58
59        // Tags are required
60        let tags = params["tags"]
61            .as_array()
62            .ok_or_else(|| {
63                crate::error::Error::Other("Missing required tags parameter".to_string())
64            })?
65            .iter()
66            .filter_map(|v| v.as_str().map(String::from))
67            .collect::<Vec<_>>();
68
69        let id = self
70            .storage
71            .store(content, context, summary, Some(tags))
72            .await?;
73
74        Ok(json!({
75            "id": id.to_string(),
76            "message": "Memory stored successfully"
77        }))
78    }
79
80    async fn handle_get_memory(&self, params: Value) -> Result<Value> {
81        let id_str = params["id"]
82            .as_str()
83            .ok_or_else(|| crate::error::Error::Other("Missing id parameter".to_string()))?;
84
85        let id = Uuid::parse_str(id_str)
86            .map_err(|e| crate::error::Error::Other(format!("Invalid UUID: {}", e)))?;
87
88        match self.storage.get(id).await? {
89            Some(memory) => Ok(serde_json::to_value(memory)?),
90            None => Err(crate::error::Error::Other(format!(
91                "Memory not found: {}",
92                id
93            ))),
94        }
95    }
96
97    async fn handle_delete_memory(&self, params: Value) -> Result<Value> {
98        let id_str = params["id"]
99            .as_str()
100            .ok_or_else(|| crate::error::Error::Other("Missing id parameter".to_string()))?;
101
102        let id = Uuid::parse_str(id_str)
103            .map_err(|e| crate::error::Error::Other(format!("Invalid UUID: {}", e)))?;
104
105        let deleted = self.storage.delete(id).await?;
106
107        Ok(json!({
108            "deleted": deleted,
109            "message": if deleted { "Memory deleted successfully" } else { "Memory not found" }
110        }))
111    }
112
113    async fn handle_get_statistics(&self) -> Result<Value> {
114        let stats = self.storage.stats().await?;
115        Ok(serde_json::to_value(stats)?)
116    }
117
118    async fn handle_store_file(&self, params: Value) -> Result<Value> {
119        let file_path = params["file_path"]
120            .as_str()
121            .ok_or_else(|| crate::error::Error::Other("Missing file_path parameter".to_string()))?;
122
123        let chunk_size = params
124            .get("chunk_size")
125            .and_then(|v| v.as_u64())
126            .unwrap_or(8000) as usize;
127
128        let overlap = params
129            .get("overlap")
130            .and_then(|v| v.as_u64())
131            .unwrap_or(200) as usize;
132
133        // Parse chunking strategy
134        // Note: Semantic chunking strategies have been moved to codex-dreams
135        // This now uses simple byte-based chunking with overlap
136
137        let tags = params.get("tags").and_then(|v| v.as_array()).map(|arr| {
138            arr.iter()
139                .filter_map(|v| v.as_str().map(String::from))
140                .collect::<Vec<_>>()
141        });
142
143        // Read the file
144        let content = tokio::fs::read_to_string(file_path)
145            .await
146            .map_err(|e| crate::error::Error::Other(format!("Failed to read file: {}", e)))?;
147
148        // Extract filename for context
149        let filename = Path::new(file_path)
150            .file_name()
151            .and_then(|n| n.to_str())
152            .unwrap_or("unknown");
153
154        // Use semantic chunking to preserve meaning boundaries
155        let content_len = content.len();
156        let mut stored_ids = Vec::new();
157
158        // Create simple file chunker
159        let chunker = FileChunker::new(chunk_size, overlap);
160        let chunks = chunker.chunk_content(&content)?;
161
162        if chunks.len() == 1 {
163            // File fits in a single chunk
164            let context = format!("Content from file: {}", filename);
165            let summary = format!(
166                "Complete content of {} ({} characters)",
167                filename, content_len
168            );
169
170            let id = self
171                .storage
172                .store(&content, context, summary, tags.clone())
173                .await?;
174
175            stored_ids.push(id.to_string());
176        } else {
177            // Multiple semantic chunks needed
178            let parent_id = Uuid::new_v4();
179            let total_chunks = chunks.len();
180
181            for (index, chunk) in chunks.into_iter().enumerate() {
182                let chunk_num = index + 1;
183
184                let context = format!(
185                    "Chunk {} of {} from file: {}",
186                    chunk_num, total_chunks, filename
187                );
188
189                let summary = format!(
190                    "Part {} of {} from {} (bytes {}-{} of {})",
191                    chunk_num,
192                    total_chunks,
193                    filename,
194                    chunk.start_byte,
195                    chunk.end_byte,
196                    content_len
197                );
198
199                let mut chunk_tags = tags.clone().unwrap_or_default();
200                chunk_tags.push(format!("chunk_{}", chunk_num));
201                chunk_tags.push(format!("file_{}", filename));
202                chunk_tags.push("byte_chunked".to_string());
203
204                let id = self
205                    .storage
206                    .store_chunk(
207                        &chunk.content,
208                        context,
209                        summary,
210                        Some(chunk_tags),
211                        chunk_num as i32,
212                        total_chunks as i32,
213                        parent_id,
214                    )
215                    .await?;
216
217                stored_ids.push(id.to_string());
218            }
219        }
220
221        Ok(json!({
222            "file_path": file_path,
223            "file_size": content_len,
224            "chunks_created": stored_ids.len(),
225            "chunk_ids": stored_ids,
226            "message": format!("Successfully ingested {} as {} chunk(s)", filename, stored_ids.len())
227        }))
228    }
229
230    async fn handle_search_memory(&self, params: Value) -> Result<Value> {
231        let query = params["query"]
232            .as_str()
233            .ok_or_else(|| crate::error::Error::Other("Missing query parameter".to_string()))?
234            .to_string();
235
236        // Parse optional parameters with defaults
237        let tag_filter = params
238            .get("tag_filter")
239            .and_then(|v| v.as_array())
240            .map(|arr| {
241                arr.iter()
242                    .filter_map(|v| v.as_str().map(String::from))
243                    .collect::<Vec<_>>()
244            });
245
246        let use_tag_embedding = params
247            .get("use_tag_embedding")
248            .and_then(|v| v.as_bool())
249            .unwrap_or(true);
250
251        let use_content_embedding = params
252            .get("use_content_embedding")
253            .and_then(|v| v.as_bool())
254            .unwrap_or(true);
255
256        let similarity_threshold = params
257            .get("similarity_threshold")
258            .and_then(|v| v.as_f64())
259            .unwrap_or(0.7)
260            .clamp(0.0, 1.0);
261
262        let max_results = params
263            .get("max_results")
264            .and_then(|v| v.as_u64())
265            .unwrap_or(10)
266            .clamp(1, 100) as usize;
267
268        let search_strategy = params
269            .get("search_strategy")
270            .and_then(|v| v.as_str())
271            .map(|s| match s {
272                "tags_first" => SearchStrategy::TagsFirst,
273                "content_first" => SearchStrategy::ContentFirst,
274                _ => SearchStrategy::Hybrid,
275            })
276            .unwrap_or(SearchStrategy::Hybrid);
277
278        let boost_recent = params
279            .get("boost_recent")
280            .and_then(|v| v.as_bool())
281            .unwrap_or(false);
282
283        let tag_weight = params
284            .get("tag_weight")
285            .and_then(|v| v.as_f64())
286            .unwrap_or(0.4)
287            .clamp(0.0, 1.0);
288
289        let content_weight = params
290            .get("content_weight")
291            .and_then(|v| v.as_f64())
292            .unwrap_or(0.6)
293            .clamp(0.0, 1.0);
294
295        // Create search parameters
296        let search_params = SearchParams {
297            query,
298            tag_filter,
299            use_tag_embedding,
300            use_content_embedding,
301            similarity_threshold,
302            max_results,
303            search_strategy,
304            boost_recent,
305            tag_weight,
306            content_weight,
307        };
308
309        // Perform the progressive search
310        let search_start = std::time::Instant::now();
311        let search_result_with_metadata = self
312            .storage
313            .search_memories_progressive_with_metadata(search_params.clone())
314            .await?;
315        let search_duration = search_start.elapsed();
316
317        // Format results for JSON response
318        let formatted_results: Vec<Value> = search_result_with_metadata
319            .results
320            .iter()
321            .map(|result| {
322                json!({
323                    "id": result.memory.id,
324                    "content": result.memory.content,
325                    "context": result.memory.context,
326                    "summary": result.memory.summary,
327                    "tags": result.memory.tags,
328                    "chunk_index": result.memory.chunk_index,
329                    "total_chunks": result.memory.total_chunks,
330                    "parent_id": result.memory.parent_id,
331                    "created_at": result.memory.created_at,
332                    "updated_at": result.memory.updated_at,
333                    "tag_similarity": result.tag_similarity,
334                    "content_similarity": result.content_similarity,
335                    "combined_score": result.combined_score,
336                    "semantic_cluster": result.semantic_cluster
337                })
338            })
339            .collect();
340
341        Ok(json!({
342            "results": formatted_results,
343            "search_metadata": {
344                "query": search_params.query,
345                "total_results": search_result_with_metadata.results.len(),
346                "search_strategy": match search_params.search_strategy {
347                    SearchStrategy::TagsFirst => "tags_first",
348                    SearchStrategy::ContentFirst => "content_first",
349                    SearchStrategy::Hybrid => "hybrid",
350                },
351                "similarity_threshold": search_params.similarity_threshold,
352                "max_results": search_params.max_results,
353                "tag_filter": search_params.tag_filter,
354                "use_tag_embedding": search_params.use_tag_embedding,
355                "use_content_embedding": search_params.use_content_embedding,
356                "boost_recent": search_params.boost_recent,
357                "tag_weight": search_params.tag_weight,
358                "content_weight": search_params.content_weight,
359                "search_time_ms": search_duration.as_millis(),
360                "progressive_search": {
361                    "stage_used": search_result_with_metadata.metadata.stage_used,
362                    "stage_description": search_result_with_metadata.metadata.stage_description,
363                    "threshold_used": search_result_with_metadata.metadata.threshold_used
364                }
365            }
366        }))
367    }
368}