codex_memory/mcp_server/
handlers.rs

1//! Simple MCP request handlers
2use crate::chunking::FileChunker;
3use crate::error::Result;
4use crate::storage::Storage;
5use serde_json::{json, Value};
6use std::path::Path;
7use std::sync::Arc;
8use uuid::Uuid;
9
10/// Minimal MCP request handlers
11pub struct MCPHandlers {
12    storage: Arc<Storage>,
13}
14
15impl MCPHandlers {
16    /// Create new handlers with storage backend
17    pub fn new(storage: Arc<Storage>) -> Self {
18        Self { storage }
19    }
20
21    /// Handle tool calls
22    pub async fn handle_tool_call(&self, tool_name: &str, params: Value) -> Result<Value> {
23        match tool_name {
24            "store_memory" => self.handle_store_memory(params).await,
25            "get_memory" => self.handle_get_memory(params).await,
26            "delete_memory" => self.handle_delete_memory(params).await,
27            "get_statistics" => self.handle_get_statistics().await,
28            "store_file" => self.handle_store_file(params).await,
29            _ => Err(crate::error::Error::Other(format!(
30                "Unknown tool: {}",
31                tool_name
32            ))),
33        }
34    }
35
36    async fn handle_store_memory(&self, params: Value) -> Result<Value> {
37        let content = params["content"]
38            .as_str()
39            .ok_or_else(|| crate::error::Error::Other("Missing content parameter".to_string()))?;
40
41        // Context is required
42        let context = params["context"]
43            .as_str()
44            .ok_or_else(|| {
45                crate::error::Error::Other("Missing required context parameter".to_string())
46            })?
47            .to_string();
48
49        // Summary is required
50        let summary = params["summary"]
51            .as_str()
52            .ok_or_else(|| {
53                crate::error::Error::Other("Missing required summary parameter".to_string())
54            })?
55            .to_string();
56
57        // Tags are required
58        let tags = params["tags"]
59            .as_array()
60            .ok_or_else(|| {
61                crate::error::Error::Other("Missing required tags parameter".to_string())
62            })?
63            .iter()
64            .filter_map(|v| v.as_str().map(String::from))
65            .collect::<Vec<_>>();
66
67        let id = self
68            .storage
69            .store(content, context, summary, Some(tags))
70            .await?;
71
72        Ok(json!({
73            "id": id.to_string(),
74            "message": "Memory stored successfully"
75        }))
76    }
77
78    async fn handle_get_memory(&self, params: Value) -> Result<Value> {
79        let id_str = params["id"]
80            .as_str()
81            .ok_or_else(|| crate::error::Error::Other("Missing id parameter".to_string()))?;
82
83        let id = Uuid::parse_str(id_str)
84            .map_err(|e| crate::error::Error::Other(format!("Invalid UUID: {}", e)))?;
85
86        match self.storage.get(id).await? {
87            Some(memory) => Ok(serde_json::to_value(memory)?),
88            None => Err(crate::error::Error::Other(format!(
89                "Memory not found: {}",
90                id
91            ))),
92        }
93    }
94
95    async fn handle_delete_memory(&self, params: Value) -> Result<Value> {
96        let id_str = params["id"]
97            .as_str()
98            .ok_or_else(|| crate::error::Error::Other("Missing id parameter".to_string()))?;
99
100        let id = Uuid::parse_str(id_str)
101            .map_err(|e| crate::error::Error::Other(format!("Invalid UUID: {}", e)))?;
102
103        let deleted = self.storage.delete(id).await?;
104
105        Ok(json!({
106            "deleted": deleted,
107            "message": if deleted { "Memory deleted successfully" } else { "Memory not found" }
108        }))
109    }
110
111    async fn handle_get_statistics(&self) -> Result<Value> {
112        let stats = self.storage.stats().await?;
113        Ok(serde_json::to_value(stats)?)
114    }
115
116    async fn handle_store_file(&self, params: Value) -> Result<Value> {
117        let file_path = params["file_path"]
118            .as_str()
119            .ok_or_else(|| crate::error::Error::Other("Missing file_path parameter".to_string()))?;
120
121        let chunk_size = params
122            .get("chunk_size")
123            .and_then(|v| v.as_u64())
124            .unwrap_or(8000) as usize;
125
126        let overlap = params
127            .get("overlap")
128            .and_then(|v| v.as_u64())
129            .unwrap_or(200) as usize;
130
131        // Parse chunking strategy
132        // Note: Semantic chunking strategies have been moved to codex-dreams
133        // This now uses simple byte-based chunking with overlap
134
135        let tags = params.get("tags").and_then(|v| v.as_array()).map(|arr| {
136            arr.iter()
137                .filter_map(|v| v.as_str().map(String::from))
138                .collect::<Vec<_>>()
139        });
140
141        // Read the file
142        let content = tokio::fs::read_to_string(file_path)
143            .await
144            .map_err(|e| crate::error::Error::Other(format!("Failed to read file: {}", e)))?;
145
146        // Extract filename for context
147        let filename = Path::new(file_path)
148            .file_name()
149            .and_then(|n| n.to_str())
150            .unwrap_or("unknown");
151
152        // Use semantic chunking to preserve meaning boundaries
153        let content_len = content.len();
154        let mut stored_ids = Vec::new();
155
156        // Create simple file chunker
157        let chunker = FileChunker::new(chunk_size, overlap);
158        let chunks = chunker.chunk_content(&content)?;
159
160        if chunks.len() == 1 {
161            // File fits in a single chunk
162            let context = format!("Content from file: {}", filename);
163            let summary = format!(
164                "Complete content of {} ({} characters)",
165                filename, content_len
166            );
167
168            let id = self
169                .storage
170                .store(&content, context, summary, tags.clone())
171                .await?;
172
173            stored_ids.push(id.to_string());
174        } else {
175            // Multiple semantic chunks needed
176            let parent_id = Uuid::new_v4();
177            let total_chunks = chunks.len();
178
179            for (index, chunk) in chunks.into_iter().enumerate() {
180                let chunk_num = index + 1;
181
182                let context = format!(
183                    "Chunk {} of {} from file: {}",
184                    chunk_num, total_chunks, filename
185                );
186
187                let summary = format!(
188                    "Part {} of {} from {} (bytes {}-{} of {})",
189                    chunk_num,
190                    total_chunks,
191                    filename,
192                    chunk.start_byte,
193                    chunk.end_byte,
194                    content_len
195                );
196
197                let mut chunk_tags = tags.clone().unwrap_or_default();
198                chunk_tags.push(format!("chunk_{}", chunk_num));
199                chunk_tags.push(format!("file_{}", filename));
200                chunk_tags.push("byte_chunked".to_string());
201
202                let id = self
203                    .storage
204                    .store_chunk(
205                        &chunk.content,
206                        context,
207                        summary,
208                        Some(chunk_tags),
209                        chunk_num as i32,
210                        total_chunks as i32,
211                        parent_id,
212                    )
213                    .await?;
214
215                stored_ids.push(id.to_string());
216            }
217        }
218
219        Ok(json!({
220            "file_path": file_path,
221            "file_size": content_len,
222            "chunks_created": stored_ids.len(),
223            "chunk_ids": stored_ids,
224            "message": format!("Successfully ingested {} as {} chunk(s)", filename, stored_ids.len())
225        }))
226    }
227}