1use crate::chunking::{ChunkingStrategy, 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
11pub struct MCPHandlers {
13 storage: Arc<Storage>,
14}
15
16impl MCPHandlers {
17 pub fn new(storage: Arc<Storage>) -> Self {
19 Self { storage }
20 }
21
22 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::MethodNotFound(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::InvalidParams("Missing content parameter".to_string()))?;
42
43 if content.len() > 1024 * 1024 {
45 return Err(crate::error::Error::InvalidParams(format!(
46 "Content size {} bytes exceeds maximum limit of 1MB (1048576 bytes)",
47 content.len()
48 )));
49 }
50
51 let context = params["context"]
53 .as_str()
54 .ok_or_else(|| {
55 crate::error::Error::InvalidParams("Missing required context parameter".to_string())
56 })?
57 .to_string();
58
59 if context.len() > 1000 {
61 return Err(crate::error::Error::InvalidParams(format!(
62 "Context length {} characters exceeds maximum limit of 1000 characters",
63 context.len()
64 )));
65 }
66
67 let summary = params["summary"]
69 .as_str()
70 .ok_or_else(|| {
71 crate::error::Error::InvalidParams("Missing required summary parameter".to_string())
72 })?
73 .to_string();
74
75 if summary.len() > 500 {
77 return Err(crate::error::Error::InvalidParams(format!(
78 "Summary length {} characters exceeds maximum limit of 500 characters",
79 summary.len()
80 )));
81 }
82
83 let tags = params["tags"]
85 .as_array()
86 .ok_or_else(|| {
87 crate::error::Error::InvalidParams("Missing required tags parameter".to_string())
88 })?
89 .iter()
90 .filter_map(|v| v.as_str().map(String::from))
91 .collect::<Vec<_>>();
92
93 if tags.len() > 50 {
95 return Err(crate::error::Error::InvalidParams(format!(
96 "Tags count {} exceeds maximum limit of 50 tags",
97 tags.len()
98 )));
99 }
100
101 let id = self
102 .storage
103 .store(content, context, summary, Some(tags))
104 .await?;
105
106 Ok(json!({
107 "id": id.to_string(),
108 "message": "Memory stored successfully"
109 }))
110 }
111
112 async fn handle_get_memory(&self, params: Value) -> Result<Value> {
113 let id_str = params["id"]
114 .as_str()
115 .ok_or_else(|| crate::error::Error::InvalidParams("Missing id parameter".to_string()))?;
116
117 let id = Uuid::parse_str(id_str)
118 .map_err(|e| crate::error::Error::InvalidParams(format!("Invalid UUID: {}", e)))?;
119
120 match self.storage.get(id).await? {
121 Some(memory) => Ok(serde_json::to_value(memory)?),
122 None => Err(crate::error::Error::InvalidParams(format!(
123 "Memory not found: {}",
124 id
125 ))),
126 }
127 }
128
129 async fn handle_delete_memory(&self, params: Value) -> Result<Value> {
130 let id_str = params["id"]
131 .as_str()
132 .ok_or_else(|| crate::error::Error::InvalidParams("Missing id parameter".to_string()))?;
133
134 let id = Uuid::parse_str(id_str)
135 .map_err(|e| crate::error::Error::InvalidParams(format!("Invalid UUID: {}", e)))?;
136
137 let deleted = self.storage.delete(id).await?;
138
139 Ok(json!({
140 "deleted": deleted,
141 "message": if deleted { "Memory deleted successfully" } else { "Memory not found" }
142 }))
143 }
144
145 async fn handle_get_statistics(&self) -> Result<Value> {
146 let stats = self.storage.stats().await?;
147 Ok(serde_json::to_value(stats)?)
148 }
149
150 async fn handle_store_file(&self, params: Value) -> Result<Value> {
151 let file_path = params["file_path"]
152 .as_str()
153 .ok_or_else(|| crate::error::Error::InvalidParams("Missing file_path parameter".to_string()))?;
154
155 if tokio::fs::metadata(file_path).await.is_err() {
157 return Err(crate::error::Error::InvalidParams(format!(
158 "File not found or not readable: {}",
159 file_path
160 )));
161 }
162
163 let chunk_size = params
164 .get("chunk_size")
165 .and_then(|v| v.as_u64())
166 .unwrap_or(8000) as usize;
167
168 if chunk_size < 1024 || chunk_size > 102400 {
170 return Err(crate::error::Error::InvalidParams(format!(
171 "Chunk size {} must be between 1024 and 102400 characters",
172 chunk_size
173 )));
174 }
175
176 let overlap = params
177 .get("overlap")
178 .and_then(|v| v.as_u64())
179 .unwrap_or(200) as usize;
180
181 if overlap >= chunk_size / 2 {
183 return Err(crate::error::Error::InvalidParams(format!(
184 "Overlap size {} must be less than half of chunk size ({})",
185 overlap,
186 chunk_size / 2
187 )));
188 }
189
190 let chunking_strategy: ChunkingStrategy = params
192 .get("chunking_strategy")
193 .and_then(|v| v.as_str())
194 .and_then(|s| s.parse().ok())
195 .unwrap_or_default();
196
197 let tags = params.get("tags").and_then(|v| v.as_array()).map(|arr| {
198 arr.iter()
199 .filter_map(|v| v.as_str().map(String::from))
200 .collect::<Vec<_>>()
201 });
202
203 const MAX_FILE_SIZE: u64 = 50 * 1024 * 1024; let file_metadata = tokio::fs::metadata(file_path)
206 .await
207 .map_err(|e| crate::error::Error::InternalError(format!("Failed to get file metadata: {}", e)))?;
208
209 if file_metadata.len() > MAX_FILE_SIZE {
210 return Err(crate::error::Error::InvalidParams(format!(
211 "File size {} bytes exceeds maximum limit of 50MB ({})",
212 file_metadata.len(),
213 MAX_FILE_SIZE
214 )));
215 }
216
217 let content = if file_metadata.len() > 1024 * 1024 {
219 self.read_file_streaming(file_path).await?
221 } else {
222 tokio::fs::read_to_string(file_path)
224 .await
225 .map_err(|e| crate::error::Error::InternalError(format!("Failed to read file: {}", e)))?
226 };
227
228 let filename = Path::new(file_path)
230 .file_name()
231 .and_then(|n| n.to_str())
232 .unwrap_or("unknown");
233
234 let content_len = content.len();
236 let mut stored_ids = Vec::new();
237
238 let chunker = FileChunker::with_strategy(chunk_size, overlap, chunking_strategy.clone());
240 let chunks = chunker.chunk_content(&content)?;
241
242 if chunks.len() == 1 {
243 let context = format!("Content from file: {}", filename);
245 let summary = format!(
246 "Complete content of {} ({} characters)",
247 filename, content_len
248 );
249
250 let id = self
251 .storage
252 .store(&content, context, summary, tags.clone())
253 .await?;
254
255 stored_ids.push(id.to_string());
256 } else {
257 let parent_id = Uuid::new_v4();
259 let total_chunks = chunks.len();
260
261 for (index, chunk) in chunks.into_iter().enumerate() {
262 let chunk_num = index + 1;
263
264 let context = format!(
265 "Chunk {} of {} from file: {}",
266 chunk_num, total_chunks, filename
267 );
268
269 let summary = format!(
270 "Part {} of {} from {} (bytes {}-{} of {})",
271 chunk_num,
272 total_chunks,
273 filename,
274 chunk.start_byte,
275 chunk.end_byte,
276 content_len
277 );
278
279 let mut chunk_tags = tags.clone().unwrap_or_default();
280 chunk_tags.push(format!("chunk_{}", chunk_num));
281 chunk_tags.push(format!("file_{}", filename));
282 chunk_tags.push(format!("strategy_{:?}", chunking_strategy).to_lowercase());
283
284 let id = self
285 .storage
286 .store_chunk(
287 &chunk.content,
288 context,
289 summary,
290 Some(chunk_tags),
291 chunk_num as i32,
292 total_chunks as i32,
293 parent_id,
294 )
295 .await?;
296
297 stored_ids.push(id.to_string());
298 }
299 }
300
301 Ok(json!({
302 "file_path": file_path,
303 "file_size": content_len,
304 "chunks_created": stored_ids.len(),
305 "chunk_ids": stored_ids,
306 "chunking_strategy": format!("{:?}", chunking_strategy),
307 "chunk_size": chunk_size,
308 "overlap": overlap,
309 "message": format!("Successfully ingested {} as {} chunk(s) using {:?} strategy", filename, stored_ids.len(), chunking_strategy)
310 }))
311 }
312
313 async fn handle_search_memory(&self, params: Value) -> Result<Value> {
314 let query = params["query"]
315 .as_str()
316 .ok_or_else(|| crate::error::Error::InvalidParams("Missing query parameter".to_string()))?
317 .to_string();
318
319 let tag_filter = params
321 .get("tag_filter")
322 .and_then(|v| v.as_array())
323 .map(|arr| {
324 arr.iter()
325 .filter_map(|v| v.as_str().map(String::from))
326 .collect::<Vec<_>>()
327 });
328
329 let use_tag_embedding = params
330 .get("use_tag_embedding")
331 .and_then(|v| v.as_bool())
332 .unwrap_or(true);
333
334 let use_content_embedding = params
335 .get("use_content_embedding")
336 .and_then(|v| v.as_bool())
337 .unwrap_or(true);
338
339 let similarity_threshold = params
340 .get("similarity_threshold")
341 .and_then(|v| v.as_f64())
342 .unwrap_or(0.7)
343 .clamp(0.0, 1.0);
344
345 let max_results = params
346 .get("max_results")
347 .and_then(|v| v.as_u64())
348 .unwrap_or(10)
349 .clamp(1, 100) as usize;
350
351 let search_strategy = params
352 .get("search_strategy")
353 .and_then(|v| v.as_str())
354 .map(|s| match s {
355 "tags_first" => SearchStrategy::TagsFirst,
356 "content_first" => SearchStrategy::ContentFirst,
357 _ => SearchStrategy::Hybrid,
358 })
359 .unwrap_or(SearchStrategy::Hybrid);
360
361 let boost_recent = params
362 .get("boost_recent")
363 .and_then(|v| v.as_bool())
364 .unwrap_or(false);
365
366 let tag_weight = params
367 .get("tag_weight")
368 .and_then(|v| v.as_f64())
369 .unwrap_or(0.4)
370 .clamp(0.0, 1.0);
371
372 let content_weight = params
373 .get("content_weight")
374 .and_then(|v| v.as_f64())
375 .unwrap_or(0.6)
376 .clamp(0.0, 1.0);
377
378 let search_params = SearchParams {
380 query: query.clone(),
381 tag_filter: tag_filter.clone(),
382 use_tag_embedding,
383 use_content_embedding,
384 similarity_threshold,
385 max_results,
386 search_strategy: search_strategy.clone(),
387 boost_recent,
388 tag_weight,
389 content_weight,
390 };
391
392 let search_start = std::time::Instant::now();
394 let search_result_with_metadata = self
395 .storage
396 .search_memories_progressive_with_metadata(search_params.clone())
397 .await?;
398 let _search_duration = search_start.elapsed();
399
400 let formatted_results: Vec<Value> = search_result_with_metadata
402 .results
403 .iter()
404 .map(|result| {
405 json!({
406 "id": result.memory.id,
407 "content": result.memory.content,
408 "context": result.memory.context,
409 "summary": result.memory.summary,
410 "tags": result.memory.tags,
411 "chunk_index": result.memory.chunk_index,
412 "total_chunks": result.memory.total_chunks,
413 "parent_id": result.memory.parent_id,
414 "created_at": result.memory.created_at,
415 "updated_at": result.memory.updated_at,
416 "tag_similarity": result.tag_similarity,
417 "content_similarity": result.content_similarity,
418 "combined_score": result.combined_score,
419 "semantic_cluster": result.semantic_cluster
420 })
421 })
422 .collect();
423
424 if cfg!(test) {
427 let result_count = formatted_results.len();
429 Ok(json!({
430 "results": formatted_results,
431 "search_metadata": {
432 "query": query.clone(),
433 "total_results": result_count,
434 "similarity_threshold": similarity_threshold,
435 "max_results": max_results,
436 "search_strategy": format!("{:?}", search_strategy).to_lowercase(),
437 "boost_recent": boost_recent,
438 "tag_weight": tag_weight,
439 "content_weight": content_weight,
440 "use_tag_embedding": use_tag_embedding,
441 "use_content_embedding": use_content_embedding,
442 "tag_filter": tag_filter.clone(),
443 "search_time_ms": 0, "progressive_search": {},
445 "average_score": 0.0 }
447 }))
448 } else {
449 Ok(json!(formatted_results))
451 }
452 }
453
454 async fn read_file_streaming(&self, file_path: &str) -> Result<String> {
457 use tokio::io::{AsyncReadExt, BufReader};
458
459 const STREAM_BUFFER_SIZE: usize = 8192; const MAX_CONTENT_SIZE: usize = 50 * 1024 * 1024; let file = tokio::fs::File::open(file_path)
463 .await
464 .map_err(|e| crate::error::Error::InternalError(format!("Failed to open file: {}", e)))?;
465
466 let mut reader = BufReader::with_capacity(STREAM_BUFFER_SIZE, file);
467 let mut content = String::new();
468 let mut buffer = vec![0u8; STREAM_BUFFER_SIZE];
469 let mut total_read = 0;
470
471 loop {
472 let bytes_read = reader
473 .read(&mut buffer)
474 .await
475 .map_err(|e| crate::error::Error::InternalError(format!("Failed to read file chunk: {}", e)))?;
476
477 if bytes_read == 0 {
478 break; }
480
481 total_read += bytes_read;
482
483 if total_read > MAX_CONTENT_SIZE {
485 return Err(crate::error::Error::InvalidParams(format!(
486 "File content exceeds maximum size limit of {} bytes during streaming",
487 MAX_CONTENT_SIZE
488 )));
489 }
490
491 let chunk_str = std::str::from_utf8(&buffer[..bytes_read])
493 .map_err(|e| crate::error::Error::InternalError(format!("Invalid UTF-8 in file: {}", e)))?;
494
495 content.push_str(chunk_str);
496 }
497
498 Ok(content)
499 }
500}