mcp_memory/actions/
memory.rs1use serde_json::{Value, json};
2
3use crate::errors::{MCSError, Result};
4use crate::kg::{GraphHandle, push_json_str};
5
6const MAX_NAME_BYTES: usize = 1024;
7const MAX_OBSERVATION_BYTES: usize = 65536;
8const MAX_ENTITIES_PER_REQUEST: usize = 1000;
9const MAX_RELATIONS_PER_REQUEST: usize = 1000;
10const MAX_OBSERVATIONS_PER_ENTITY: usize = 1000;
11const MAX_NEIGHBOR_DEPTH: usize = 16;
12const MAX_NAMES_PER_REQUEST: usize = 1000;
13const MAX_SEARCH_LIMIT: usize = 1000;
14const MAX_RELATION_SEARCH_RESULTS: usize = 1000;
15const MAX_FIND_ALL_PATHS_DEPTH: usize = 10;
16const MAX_FIND_ALL_PATHS_RESULTS: usize = 100;
17const MAX_EXPORT_ROWS: i64 = 1_000_000;
21const DEFAULT_SEARCH_LIMIT: usize = 20;
23
24fn validate_name(name: &str) -> Result<()> {
25 if name.is_empty() {
26 return Err(MCSError::InvalidParams("Name must not be empty".into()));
27 }
28 if name.len() > MAX_NAME_BYTES {
29 return Err(MCSError::InvalidParams(format!(
30 "Name too long (max {MAX_NAME_BYTES} bytes)"
31 )));
32 }
33 Ok(())
34}
35
36fn validate_observation(content: &str) -> Result<()> {
37 if content.len() > MAX_OBSERVATION_BYTES {
38 return Err(MCSError::InvalidParams(format!(
39 "Observation too long (max {MAX_OBSERVATION_BYTES} bytes)"
40 )));
41 }
42 Ok(())
43}
44
45macro_rules! text_content {
46 ($text:expr) => {
47 json!({
48 "content": [{
49 "type": "text",
50 "text": $text
51 }]
52 })
53 };
54}
55
56fn build_content_response(inner_json: &str) -> String {
57 let mut out = String::with_capacity(64 + inner_json.len() + (inner_json.len() / 8));
58 out.push_str(r#"{"content":[{"type":"text","text":"#);
59 push_json_str(&mut out, inner_json);
60 out.push_str(r#"}]}"#);
61 out
62}
63
64pub fn handle_read_graph(kg: &GraphHandle, args: Option<&Value>) -> Result<String> {
65 let params = args.unwrap_or(&Value::Null);
66 let filter_type = params
67 .get("entityType")
68 .and_then(|v| v.as_str())
69 .filter(|s| !s.is_empty());
70 let offset = opt_usize(params, "offset", 0)?;
71 let limit = opt_usize(params, "limit", MAX_SEARCH_LIMIT)?.min(MAX_SEARCH_LIMIT);
72
73 let text = kg.read_graph_filtered(filter_type, offset, limit)?;
74 Ok(build_content_response(&text))
75}
76
77pub fn handle_create_entities(kg: &GraphHandle, args: Option<&Value>) -> Result<Value> {
78 let params = args.ok_or_else(|| MCSError::InvalidParams("Missing parameters".into()))?;
79 let entities_val = params
80 .get("entities")
81 .ok_or_else(|| MCSError::InvalidParams("Missing 'entities' parameter".into()))?;
82
83 let input_entities: Vec<crate::types::Entity> = serde_json::from_value(entities_val.clone())
84 .map_err(|e| MCSError::InvalidParams(format!("Invalid entity: {e}")))?;
85
86 if input_entities.len() > MAX_ENTITIES_PER_REQUEST {
87 return Err(MCSError::InvalidParams(format!(
88 "Too many entities (max {MAX_ENTITIES_PER_REQUEST})"
89 )));
90 }
91 for entity in &input_entities {
92 validate_name(&entity.name)?;
93 validate_name(&entity.entity_type)?;
94 if entity.observations.len() > MAX_OBSERVATIONS_PER_ENTITY {
95 return Err(MCSError::InvalidParams(format!(
96 "Too many observations per entity (max {MAX_OBSERVATIONS_PER_ENTITY})"
97 )));
98 }
99 for obs in &entity.observations {
100 validate_observation(obs)?;
101 }
102 }
103
104 let result = kg.create_entities(&input_entities)?;
105 let text = serde_json::to_string(&result).map_err(MCSError::JsonError)?;
106 Ok(text_content!(text))
107}
108
109pub fn handle_create_relations(kg: &GraphHandle, args: Option<&Value>) -> Result<Value> {
110 let params = args.ok_or_else(|| MCSError::InvalidParams("Missing parameters".into()))?;
111 let relations_val = params
112 .get("relations")
113 .ok_or_else(|| MCSError::InvalidParams("Missing 'relations' parameter".into()))?;
114
115 let input_relations: Vec<crate::types::Relation> =
116 serde_json::from_value(relations_val.clone())
117 .map_err(|e| MCSError::InvalidParams(format!("Invalid relation: {e}")))?;
118
119 if input_relations.len() > MAX_RELATIONS_PER_REQUEST {
120 return Err(MCSError::InvalidParams(format!(
121 "Too many relations (max {MAX_RELATIONS_PER_REQUEST})"
122 )));
123 }
124 for rel in &input_relations {
125 validate_name(&rel.from)?;
126 validate_name(&rel.to)?;
127 validate_name(&rel.relation_type)?;
128 }
129
130 let result = kg.create_relations(&input_relations)?;
131 let text = serde_json::to_string(&result).map_err(MCSError::JsonError)?;
132 Ok(text_content!(text))
133}
134
135pub fn handle_add_observations(kg: &GraphHandle, args: Option<&Value>) -> Result<Value> {
136 let params = args.ok_or_else(|| MCSError::InvalidParams("Missing parameters".into()))?;
137 let observations_val = params
138 .get("observations")
139 .ok_or_else(|| MCSError::InvalidParams("Missing 'observations' parameter".into()))?;
140
141 let observations: Vec<Value> = serde_json::from_value(observations_val.clone())
142 .map_err(|e| MCSError::InvalidParams(format!("Invalid observations: {e}")))?;
143
144 let mut results = Vec::new();
145
146 for obs in &observations {
147 let entity_name = obs
148 .get("entityName")
149 .and_then(|v| v.as_str())
150 .ok_or_else(|| MCSError::InvalidParams("Missing 'entityName' in observation".into()))?;
151
152 let contents: Vec<String> = obs
153 .get("contents")
154 .and_then(|v| v.as_array())
155 .map(|arr| {
156 arr.iter()
157 .filter_map(|v| v.as_str().map(String::from))
158 .collect()
159 })
160 .unwrap_or_default();
161
162 validate_name(entity_name)?;
163 if contents.len() > MAX_OBSERVATIONS_PER_ENTITY {
164 return Err(MCSError::InvalidParams(format!(
165 "Too many observations per entity (max {MAX_OBSERVATIONS_PER_ENTITY})"
166 )));
167 }
168 for content in &contents {
169 validate_observation(content)?;
170 }
171
172 let added = kg.add_observations(entity_name, &contents)?;
173
174 results.push(json!({
175 "entityName": entity_name,
176 "addedObservations": added
177 }));
178 }
179 let text = serde_json::to_string(&json!({"results": results})).map_err(MCSError::JsonError)?;
180 Ok(text_content!(text))
181}
182
183pub fn handle_delete_entities(kg: &GraphHandle, args: Option<&Value>) -> Result<Value> {
184 let params = args.ok_or_else(|| MCSError::InvalidParams("Missing parameters".into()))?;
185 let mut entity_names: Vec<String> = params
186 .get("entityNames")
187 .and_then(|v| v.as_array())
188 .map(|arr| {
189 arr.iter()
190 .filter_map(|v| v.as_str().map(String::from))
191 .collect()
192 })
193 .ok_or_else(|| {
194 MCSError::InvalidParams("Missing or invalid 'entityNames' parameter".into())
195 })?;
196 entity_names.truncate(MAX_NAMES_PER_REQUEST);
197
198 kg.delete_entities(&entity_names)?;
199
200 Ok(text_content!("Entities deleted successfully"))
201}
202
203pub fn handle_delete_observations(kg: &GraphHandle, args: Option<&Value>) -> Result<Value> {
204 let params = args.ok_or_else(|| MCSError::InvalidParams("Missing parameters".into()))?;
205 let deletions = params
206 .get("deletions")
207 .and_then(|v| v.as_array())
208 .ok_or_else(|| {
209 MCSError::InvalidParams("Missing or invalid 'deletions' parameter".into())
210 })?;
211
212 for deletion in deletions.iter().take(MAX_NAMES_PER_REQUEST) {
213 let entity_name = deletion
214 .get("entityName")
215 .and_then(|v| v.as_str())
216 .ok_or_else(|| MCSError::InvalidParams("Missing 'entityName' in deletion".into()))?;
217 let observations: Vec<String> = deletion
218 .get("observations")
219 .and_then(|v| v.as_array())
220 .map(|arr| {
221 arr.iter()
222 .filter_map(|v| v.as_str().map(String::from))
223 .collect()
224 })
225 .unwrap_or_default();
226
227 kg.delete_observations(entity_name, &observations)?;
228 }
229
230 Ok(text_content!("Observations deleted successfully"))
231}
232
233pub fn handle_delete_relations(kg: &GraphHandle, args: Option<&Value>) -> Result<Value> {
234 let params = args.ok_or_else(|| MCSError::InvalidParams("Missing parameters".into()))?;
235 let relations_val = params
236 .get("relations")
237 .ok_or_else(|| MCSError::InvalidParams("Missing 'relations' parameter".into()))?;
238
239 let mut input_relations: Vec<crate::types::Relation> =
240 serde_json::from_value(relations_val.clone())
241 .map_err(|e| MCSError::InvalidParams(format!("Invalid relation: {e}")))?;
242 input_relations.truncate(MAX_RELATIONS_PER_REQUEST);
243
244 kg.delete_relations(&input_relations)?;
245
246 Ok(text_content!("Relations deleted successfully"))
247}
248
249pub fn handle_search_nodes(kg: &GraphHandle, args: Option<&Value>) -> Result<String> {
250 let params = args.ok_or_else(|| MCSError::InvalidParams("Missing parameters".into()))?;
251 let query = params
252 .get("query")
253 .and_then(|v| v.as_str())
254 .ok_or_else(|| MCSError::InvalidParams("Missing 'query' parameter".into()))?;
255 let filter_type = params
256 .get("entityType")
257 .and_then(|v| v.as_str())
258 .filter(|s| !s.is_empty());
259 let offset = opt_usize(params, "offset", 0)?;
260 let limit = opt_usize(params, "limit", DEFAULT_SEARCH_LIMIT)?.min(MAX_SEARCH_LIMIT);
261
262 let matching = kg.search_nodes_filtered(query, filter_type, offset, limit);
263 let text = serde_json::to_string(&matching).map_err(MCSError::JsonError)?;
264 Ok(build_content_response(&text))
265}
266
267pub fn handle_open_nodes(kg: &GraphHandle, args: Option<&Value>) -> Result<Value> {
268 let params = args.ok_or_else(|| MCSError::InvalidParams("Missing parameters".into()))?;
269 let mut names: Vec<String> = params
270 .get("names")
271 .and_then(|v| v.as_array())
272 .map(|arr| {
273 arr.iter()
274 .filter_map(|v| v.as_str().map(String::from))
275 .collect()
276 })
277 .ok_or_else(|| MCSError::InvalidParams("Missing or invalid 'names' parameter".into()))?;
278 names.truncate(MAX_NAMES_PER_REQUEST);
279
280 let text = kg.open_nodes(&names);
281 Ok(text_content!(text))
282}
283
284pub fn handle_entity_exists(kg: &GraphHandle, args: Option<&Value>) -> Result<Value> {
285 let params = args.ok_or_else(|| MCSError::InvalidParams("Missing parameters".into()))?;
286 let mut names: Vec<String> = params
287 .get("names")
288 .and_then(|v| v.as_array())
289 .map(|arr| {
290 arr.iter()
291 .filter_map(|v| v.as_str().map(String::from))
292 .collect()
293 })
294 .ok_or_else(|| MCSError::InvalidParams("Missing or invalid 'names' parameter".into()))?;
295 names.truncate(MAX_NAMES_PER_REQUEST);
296
297 let results = kg.entities_exist(&names)?;
298 let text = serde_json::to_string(&results).map_err(MCSError::JsonError)?;
299 Ok(text_content!(text))
300}
301
302pub fn handle_degree(kg: &GraphHandle, args: Option<&Value>) -> Result<Value> {
303 let params = args.ok_or_else(|| MCSError::InvalidParams("Missing parameters".into()))?;
304 let name = params
305 .get("name")
306 .and_then(|v| v.as_str())
307 .ok_or_else(|| MCSError::InvalidParams("Missing 'name' parameter".into()))?;
308 validate_name(name)?;
309 let direction = crate::kg::Direction::parse(params.get("direction").and_then(|v| v.as_str()));
310
311 let degree = kg.degree(name, direction)?;
312 let text = serde_json::to_string(&json!({ "name": name, "degree": degree }))
313 .map_err(MCSError::JsonError)?;
314 Ok(text_content!(text))
315}
316
317pub fn handle_get_entity(kg: &GraphHandle, args: Option<&Value>) -> Result<Value> {
318 let params = args.ok_or_else(|| MCSError::InvalidParams("Missing parameters".into()))?;
319 let name = params
320 .get("name")
321 .and_then(|v| v.as_str())
322 .ok_or_else(|| MCSError::InvalidParams("Missing 'name' parameter".into()))?;
323
324 match kg.get_entity(name)? {
325 Some(entity) => {
326 let text = serde_json::to_string(&entity).map_err(MCSError::JsonError)?;
327 Ok(text_content!(text))
328 }
329 None => Err(MCSError::InvalidParams(format!(
330 "Entity '{name}' not found"
331 ))),
332 }
333}
334
335pub fn handle_graph_stats(kg: &GraphHandle) -> Result<Value> {
336 let entity_count = kg.get_entity_count().unwrap_or(0);
337 let relation_count = kg.get_relation_count().unwrap_or(0);
338 let text = serde_json::to_string(&json!({
339 "entities": entity_count,
340 "relations": relation_count
341 }))
342 .map_err(MCSError::JsonError)?;
343 Ok(text_content!(text))
344}
345
346pub fn handle_search_relations(kg: &GraphHandle, args: Option<&Value>) -> Result<Value> {
347 let params = args.unwrap_or(&serde_json::Value::Null);
348
349 let from = params.get("from").and_then(|v| v.as_str());
350 let to = params.get("to").and_then(|v| v.as_str());
351 let rtype = params.get("relationType").and_then(|v| v.as_str());
352
353 let mut results = kg.search_relations(from, to, rtype);
354 results.truncate(MAX_RELATION_SEARCH_RESULTS);
355 let text = serde_json::to_string(&results).map_err(MCSError::JsonError)?;
356 Ok(text_content!(text))
357}
358
359pub fn handle_find_path(kg: &GraphHandle, args: Option<&Value>) -> Result<Value> {
360 let params = args.ok_or_else(|| MCSError::InvalidParams("Missing parameters".into()))?;
361 let from = params
362 .get("from")
363 .and_then(|v| v.as_str())
364 .ok_or_else(|| MCSError::InvalidParams("Missing 'from' parameter".into()))?;
365 let to = params
366 .get("to")
367 .and_then(|v| v.as_str())
368 .ok_or_else(|| MCSError::InvalidParams("Missing 'to' parameter".into()))?;
369
370 let path = kg.find_path(from, to)?;
371 let text = serde_json::to_string(&path).map_err(MCSError::JsonError)?;
372 Ok(text_content!(text))
373}
374
375pub fn handle_compact(kg: &GraphHandle) -> Result<Value> {
376 kg.compact()?;
377 Ok(text_content!("Log compacted successfully"))
378}
379
380fn opt_usize(params: &Value, key: &str, default: usize) -> Result<usize> {
381 match params.get(key) {
382 None | Some(Value::Null) => Ok(default),
383 Some(v) => v.as_u64().map(|n| n as usize).ok_or_else(|| {
384 MCSError::InvalidParams(format!("'{key}' must be a non-negative integer"))
385 }),
386 }
387}
388
389pub fn handle_get_neighbors(kg: &GraphHandle, args: Option<&Value>) -> Result<Value> {
390 let params = args.ok_or_else(|| MCSError::InvalidParams("Missing parameters".into()))?;
391 let name = params
392 .get("name")
393 .and_then(|v| v.as_str())
394 .ok_or_else(|| MCSError::InvalidParams("Missing 'name' parameter".into()))?;
395 validate_name(name)?;
396
397 let direction = crate::kg::Direction::parse(params.get("direction").and_then(|v| v.as_str()));
398 let rtype = params
399 .get("relationType")
400 .and_then(|v| v.as_str())
401 .filter(|s| !s.is_empty());
402 let depth = opt_usize(params, "depth", 1)?;
403 if depth > MAX_NEIGHBOR_DEPTH {
404 return Err(MCSError::InvalidParams(format!(
405 "depth too large (max {MAX_NEIGHBOR_DEPTH})"
406 )));
407 }
408
409 let text = kg.neighbors(name, direction, rtype, depth as u32)?;
410 Ok(text_content!(text))
411}
412
413pub fn handle_describe_entity(kg: &GraphHandle, args: Option<&Value>) -> Result<Value> {
414 let params = args.ok_or_else(|| MCSError::InvalidParams("Missing parameters".into()))?;
415 let name = params
416 .get("name")
417 .and_then(|v| v.as_str())
418 .ok_or_else(|| MCSError::InvalidParams("Missing 'name' parameter".into()))?;
419 validate_name(name)?;
420
421 let result = kg.describe_entity(name)?;
422 let text = serde_json::to_string(&result).map_err(MCSError::JsonError)?;
423 Ok(text_content!(text))
424}
425
426pub fn handle_list_entity_types(kg: &GraphHandle) -> Result<Value> {
427 let counts = kg.entity_type_counts();
428 let arr: Vec<Value> = counts
429 .into_iter()
430 .map(|(t, c)| json!({ "type": t, "count": c }))
431 .collect();
432 let text = serde_json::to_string(&arr).map_err(MCSError::JsonError)?;
433 Ok(text_content!(text))
434}
435
436pub fn handle_list_relation_types(kg: &GraphHandle) -> Result<Value> {
437 let counts = kg.relation_type_counts();
438 let arr: Vec<Value> = counts
439 .into_iter()
440 .map(|(t, c)| json!({ "type": t, "count": c }))
441 .collect();
442 let text = serde_json::to_string(&arr).map_err(MCSError::JsonError)?;
443 Ok(text_content!(text))
444}
445
446pub fn handle_upsert_entities(kg: &GraphHandle, args: Option<&Value>) -> Result<Value> {
447 let params = args.ok_or_else(|| MCSError::InvalidParams("Missing parameters".into()))?;
448 let entities_val = params
449 .get("entities")
450 .ok_or_else(|| MCSError::InvalidParams("Missing 'entities' parameter".into()))?;
451
452 let input_entities: Vec<crate::types::Entity> = serde_json::from_value(entities_val.clone())
453 .map_err(|e| MCSError::InvalidParams(format!("Invalid entity: {e}")))?;
454
455 if input_entities.len() > MAX_ENTITIES_PER_REQUEST {
456 return Err(MCSError::InvalidParams(format!(
457 "Too many entities (max {MAX_ENTITIES_PER_REQUEST})"
458 )));
459 }
460 for entity in &input_entities {
461 validate_name(&entity.name)?;
462 validate_name(&entity.entity_type)?;
463 if entity.observations.len() > MAX_OBSERVATIONS_PER_ENTITY {
464 return Err(MCSError::InvalidParams(format!(
465 "Too many observations per entity (max {MAX_OBSERVATIONS_PER_ENTITY})"
466 )));
467 }
468 for obs in &entity.observations {
469 validate_observation(obs)?;
470 }
471 }
472
473 let results = kg.upsert_entities(&input_entities)?;
474 let text =
475 serde_json::to_string(&json!({ "results": results })).map_err(MCSError::JsonError)?;
476 Ok(text_content!(text))
477}
478
479pub fn handle_merge_entities(kg: &GraphHandle, args: Option<&Value>) -> Result<Value> {
480 let params = args.ok_or_else(|| MCSError::InvalidParams("Missing parameters".into()))?;
481 let source = params
482 .get("source")
483 .and_then(|v| v.as_str())
484 .ok_or_else(|| MCSError::InvalidParams("Missing 'source' parameter".into()))?;
485 let target = params
486 .get("target")
487 .and_then(|v| v.as_str())
488 .ok_or_else(|| MCSError::InvalidParams("Missing 'target' parameter".into()))?;
489 validate_name(source)?;
490 validate_name(target)?;
491
492 let result = kg.merge_entities(source, target)?;
493 let text = serde_json::to_string(&result).map_err(MCSError::JsonError)?;
494 Ok(text_content!(text))
495}
496
497pub fn handle_extract_subgraph(kg: &GraphHandle, args: Option<&Value>) -> Result<Value> {
498 let params = args.ok_or_else(|| MCSError::InvalidParams("Missing parameters".into()))?;
499 let names: Vec<String> = params
500 .get("names")
501 .and_then(|v| v.as_array())
502 .map(|arr| {
503 arr.iter()
504 .filter_map(|v| v.as_str().map(String::from))
505 .collect()
506 })
507 .ok_or_else(|| MCSError::InvalidParams("Missing or invalid 'names' parameter".into()))?;
508 let depth = opt_usize(params, "depth", 1)? as u32;
509 if depth > MAX_NEIGHBOR_DEPTH as u32 {
510 return Err(MCSError::InvalidParams(format!(
511 "depth too large (max {MAX_NEIGHBOR_DEPTH})"
512 )));
513 }
514
515 let text = kg.extract_subgraph(&names, depth)?;
516 Ok(text_content!(text))
517}
518
519pub fn handle_batch_get_entities(kg: &GraphHandle, args: Option<&Value>) -> Result<Value> {
520 let params = args.ok_or_else(|| MCSError::InvalidParams("Missing parameters".into()))?;
521 let mut names: Vec<String> = params
522 .get("names")
523 .and_then(|v| v.as_array())
524 .map(|arr| {
525 arr.iter()
526 .filter_map(|v| v.as_str().map(String::from))
527 .collect()
528 })
529 .ok_or_else(|| MCSError::InvalidParams("Missing or invalid 'names' parameter".into()))?;
530 names.truncate(MAX_NAMES_PER_REQUEST);
531
532 let results = kg.batch_get_entities(&names);
533 let arr: Vec<Value> = results
534 .into_iter()
535 .map(|opt| match opt {
536 Some(entity) => serde_json::to_value(entity).unwrap_or(Value::Null),
537 None => Value::Null,
538 })
539 .collect();
540 let text = serde_json::to_string(&arr).map_err(MCSError::JsonError)?;
541 Ok(text_content!(text))
542}
543
544pub fn handle_find_all_paths(kg: &GraphHandle, args: Option<&Value>) -> Result<Value> {
545 let params = args.ok_or_else(|| MCSError::InvalidParams("Missing parameters".into()))?;
546 let from = params
547 .get("from")
548 .and_then(|v| v.as_str())
549 .ok_or_else(|| MCSError::InvalidParams("Missing 'from' parameter".into()))?;
550 let to = params
551 .get("to")
552 .and_then(|v| v.as_str())
553 .ok_or_else(|| MCSError::InvalidParams("Missing 'to' parameter".into()))?;
554 let max_depth = opt_usize(params, "maxDepth", 6)?.min(MAX_FIND_ALL_PATHS_DEPTH);
555 let max_paths = opt_usize(params, "maxPaths", 50)?.min(MAX_FIND_ALL_PATHS_RESULTS);
556
557 let paths = kg.find_all_paths(from, to, max_depth, max_paths)?;
558 let text = serde_json::to_string(&paths).map_err(MCSError::JsonError)?;
559 Ok(text_content!(text))
560}
561
562pub fn handle_export_graph(kg: &GraphHandle, args: Option<&Value>) -> Result<Value> {
563 let format = args
564 .and_then(|p| p.get("format"))
565 .and_then(|v| v.as_str())
566 .unwrap_or("json");
567
568 let text = kg.export(format, MAX_EXPORT_ROWS)?;
569 Ok(text_content!(text))
570}