cortex_mcp/tools/
memory_list.rs1use std::sync::{Arc, Mutex};
14
15use cortex_store::repo::MemoryRepo;
16use serde_json::{json, Value};
17
18use crate::{GateId, ToolError, ToolHandler};
19
20const DEFAULT_LIMIT: usize = 20;
22const MAX_LIMIT: usize = 100;
24
25#[derive(Debug)]
40pub struct CortexMemoryListTool {
41 pool: Arc<Mutex<cortex_store::Pool>>,
42}
43
44impl CortexMemoryListTool {
45 #[must_use]
47 pub fn new(pool: Arc<Mutex<cortex_store::Pool>>) -> Self {
48 Self { pool }
49 }
50}
51
52impl ToolHandler for CortexMemoryListTool {
53 fn name(&self) -> &'static str {
54 "cortex_memory_list"
55 }
56
57 fn gate_set(&self) -> &'static [GateId] {
58 &[GateId::FtsRead]
59 }
60
61 fn call(&self, params: Value) -> Result<Value, ToolError> {
62 let domains: Vec<String> = match params.get("domains") {
64 None | Some(Value::Null) => Vec::new(),
65 Some(Value::Array(arr)) => {
66 let mut tags = Vec::with_capacity(arr.len());
67 for (i, v) in arr.iter().enumerate() {
68 match v.as_str() {
69 Some(s) => tags.push(s.to_owned()),
70 None => {
71 return Err(ToolError::InvalidParams(format!(
72 "domains[{i}] must be a string"
73 )));
74 }
75 }
76 }
77 tags
78 }
79 Some(other) => {
80 return Err(ToolError::InvalidParams(format!(
81 "domains must be an array of strings, got {other}"
82 )));
83 }
84 };
85
86 let limit: usize = match params.get("limit") {
88 None | Some(Value::Null) => DEFAULT_LIMIT,
89 Some(v) => {
90 let n = v.as_u64().ok_or_else(|| {
91 ToolError::InvalidParams("limit must be a non-negative integer".into())
92 })?;
93 let n = usize::try_from(n).unwrap_or(MAX_LIMIT);
94 n.min(MAX_LIMIT)
95 }
96 };
97
98 let offset: usize = match params.get("offset") {
100 None | Some(Value::Null) => 0,
101 Some(v) => {
102 let n = v.as_u64().ok_or_else(|| {
103 ToolError::InvalidParams("offset must be a non-negative integer".into())
104 })?;
105 usize::try_from(n).unwrap_or(0)
106 }
107 };
108
109 let pool = self
110 .pool
111 .lock()
112 .map_err(|err| ToolError::Internal(format!("failed to acquire store lock: {err}")))?;
113
114 let repo = MemoryRepo::new(&pool);
115
116 let all_memories = if domains.is_empty() {
118 repo.list_by_status("active").map_err(|err| {
119 ToolError::Internal(format!("failed to read active memories: {err}"))
120 })?
121 } else {
122 repo.list_by_status_with_tags("active", &domains)
123 .map_err(|err| {
124 ToolError::Internal(format!(
125 "failed to read tag-filtered active memories: {err}"
126 ))
127 })?
128 };
129
130 let total = all_memories.len();
131
132 let page: Vec<Value> = all_memories
134 .into_iter()
135 .skip(offset)
136 .take(limit)
137 .map(|m| {
138 let domains_list = string_array(&m.domains_json);
139 json!({
140 "id": m.id.to_string(),
141 "content": m.claim,
142 "domains": domains_list,
143 "confidence": m.confidence,
144 "created_at": m.created_at.to_rfc3339(),
145 })
146 })
147 .collect();
148
149 Ok(json!({
150 "memories": page,
151 "total": total,
152 }))
153 }
154}
155
156fn string_array(value: &Value) -> Vec<String> {
158 value
159 .as_array()
160 .into_iter()
161 .flatten()
162 .filter_map(|v| v.as_str().map(ToOwned::to_owned))
163 .collect()
164}