Skip to main content

mcp_memory/actions/
memory.rs

1use 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;
17/// Upper bound on rows returned per array by `export_graph`. A guard against an
18/// unbounded in-memory JSON string, not a functional limit — realistic graphs
19/// are far smaller.
20const MAX_EXPORT_ROWS: i64 = 1_000_000;
21/// Default page size for `search_nodes` when the caller omits `limit`.
22const 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}