use crate::mcp::registry::McpTool;
use crate::models::Tier;
use crate::{db, validate};
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::{Value, json};
#[derive(Debug, Clone, Default, Deserialize, JsonSchema)]
#[allow(dead_code)]
pub struct ListRequest {
#[serde(default)]
pub namespace: Option<String>,
#[serde(default)]
pub tier: Option<String>,
#[serde(default)]
pub limit: Option<i64>,
#[serde(default)]
pub agent_id: Option<String>,
#[serde(default)]
pub format: Option<String>,
}
#[allow(dead_code)]
pub struct ListTool;
impl McpTool for ListTool {
fn name() -> &'static str {
crate::mcp::registry::tool_names::MEMORY_LIST
}
fn description() -> &'static str {
"List memories, optionally filtered by namespace or tier."
}
fn docs() -> &'static str {
"Browse memories. Filters: namespace, tier, agent_id. Limit caps at 200."
}
fn input_schema() -> Value {
crate::mcp::registry::input_schema_for::<ListRequest>()
}
fn family() -> &'static str {
crate::profile::Family::Core.name()
}
}
pub(super) fn handle_list(
conn: &rusqlite::Connection,
params: &Value,
caller: Option<&str>,
) -> Result<Value, String> {
let namespace = params["namespace"].as_str();
let tier = params["tier"].as_str().and_then(Tier::from_str);
let limit = usize::try_from(params["limit"].as_u64().unwrap_or(20)).unwrap_or(usize::MAX);
let agent_id = params["agent_id"].as_str();
if let Some(aid) = agent_id {
validate::validate_agent_id(aid).map_err(|e| e.to_string())?;
}
let results = db::list(
conn,
namespace,
tier.as_ref(),
limit.min(200),
0,
None,
None,
None,
None,
agent_id,
)
.map_err(|e| e.to_string())?;
let results = match caller {
Some(c) => results
.into_iter()
.filter(|m| crate::visibility::is_visible_to_caller(m, c))
.collect::<Vec<_>>(),
None => results,
};
Ok(json!({"memories": results, "count": results.len()}))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::{Memory, Tier as MTier};
use crate::storage as db;
fn fresh_conn() -> rusqlite::Connection {
db::open(std::path::Path::new(":memory:")).expect("open in-memory db")
}
fn make_mem(title: &str, ns: &str, tier: MTier, agent: &str) -> Memory {
let now = chrono::Utc::now().to_rfc3339();
Memory {
id: uuid::Uuid::new_v4().to_string(),
tier,
namespace: ns.to_string(),
title: title.to_string(),
content: format!("content for {title}"),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".to_string(),
access_count: 0,
created_at: now.clone(),
updated_at: now,
last_accessed_at: None,
expires_at: None,
metadata: json!({"agent_id": agent}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
}
}
#[test]
fn empty_db_returns_empty_list() {
let conn = fresh_conn();
let out = handle_list(&conn, &json!({}), None).expect("ok");
assert_eq!(out["count"].as_u64(), Some(0));
assert!(out["memories"].as_array().unwrap().is_empty());
}
#[test]
fn returns_all_memories_with_default_limit() {
let conn = fresh_conn();
db::insert(&conn, &make_mem("a", "test", MTier::Mid, "ai:a")).expect("ins");
db::insert(&conn, &make_mem("b", "test", MTier::Mid, "ai:b")).expect("ins");
let out = handle_list(&conn, &json!({}), None).expect("ok");
assert_eq!(out["count"].as_u64(), Some(2));
}
#[test]
fn filters_by_namespace() {
let conn = fresh_conn();
db::insert(&conn, &make_mem("a", "ns1", MTier::Mid, "ai:a")).expect("ins");
db::insert(&conn, &make_mem("b", "ns2", MTier::Mid, "ai:b")).expect("ins");
let out = handle_list(&conn, &json!({"namespace": "ns1"}), None).expect("ok");
assert_eq!(out["count"].as_u64(), Some(1));
}
#[test]
fn filters_by_tier() {
let conn = fresh_conn();
db::insert(&conn, &make_mem("a", "ns", MTier::Short, "ai:a")).expect("ins");
db::insert(&conn, &make_mem("b", "ns", MTier::Long, "ai:b")).expect("ins");
let out = handle_list(&conn, &json!({"tier": MTier::Long.as_str()}), None).expect("ok");
assert_eq!(out["count"].as_u64(), Some(1));
let out_bad = handle_list(&conn, &json!({"tier": "nonsense"}), None).expect("ok");
assert_eq!(out_bad["count"].as_u64(), Some(2));
}
#[test]
fn filters_by_agent_id() {
let conn = fresh_conn();
db::insert(&conn, &make_mem("a", "ns", MTier::Mid, "ai:alice")).expect("ins");
db::insert(&conn, &make_mem("b", "ns", MTier::Mid, "ai:bob")).expect("ins");
let out = handle_list(&conn, &json!({"agent_id": "ai:alice"}), None).expect("ok");
assert_eq!(out["count"].as_u64(), Some(1));
}
#[test]
fn invalid_agent_id_rejected() {
let conn = fresh_conn();
let err = handle_list(&conn, &json!({"agent_id": "has space"}), None).unwrap_err();
assert!(!err.is_empty(), "expected validation err, got {err}");
}
#[test]
fn limit_overflow_saturates_and_caps() {
let conn = fresh_conn();
db::insert(&conn, &make_mem("a", "ns", MTier::Mid, "ai:a")).expect("ins");
let out = handle_list(&conn, &json!({"limit": u64::MAX}), None).expect("ok");
assert_eq!(out["count"].as_u64(), Some(1));
}
#[test]
fn idempotent_listing() {
let conn = fresh_conn();
db::insert(&conn, &make_mem("a", "ns", MTier::Mid, "ai:a")).expect("ins");
let one = handle_list(&conn, &json!({"namespace": "ns"}), None).expect("ok");
let two = handle_list(&conn, &json!({"namespace": "ns"}), None).expect("ok");
assert_eq!(one["count"], two["count"]);
}
fn private_mem(title: &str, ns: &str, agent: &str) -> Memory {
make_mem(title, ns, MTier::Mid, agent)
}
fn shared_mem(title: &str, ns: &str, agent: &str) -> Memory {
use crate::models::namespace::MemoryScope;
let mut m = make_mem(title, ns, MTier::Mid, agent);
m.metadata = json!({
crate::META_KEY_AGENT_ID: agent,
crate::META_KEY_SCOPE: MemoryScope::Collective.as_str(),
});
m
}
#[test]
fn caller_none_lists_all_including_cross_agent_private() {
let conn = fresh_conn();
db::insert(&conn, &private_mem("p", "ns", "ai:alice")).expect("ins");
db::insert(&conn, &shared_mem("s", "ns", "ai:bob")).expect("ins");
let out = handle_list(&conn, &json!({"namespace": "ns"}), None).expect("ok");
assert_eq!(out["count"].as_u64(), Some(2));
}
#[test]
fn caller_non_owner_excludes_cross_agent_private() {
let conn = fresh_conn();
db::insert(&conn, &private_mem("p", "ns", "ai:alice")).expect("ins");
db::insert(&conn, &shared_mem("s", "ns", "ai:bob")).expect("ins");
let out = handle_list(&conn, &json!({"namespace": "ns"}), Some("ai:carol")).expect("ok");
assert_eq!(
out["count"].as_u64(),
Some(1),
"only the shared row is visible"
);
assert_eq!(out["memories"][0]["title"], "s");
}
#[test]
fn caller_owner_sees_own_private_and_shared() {
let conn = fresh_conn();
db::insert(&conn, &private_mem("p", "ns", "ai:alice")).expect("ins");
db::insert(&conn, &shared_mem("s", "ns", "ai:bob")).expect("ins");
let out = handle_list(&conn, &json!({"namespace": "ns"}), Some("ai:alice")).expect("ok");
assert_eq!(out["count"].as_u64(), Some(2));
}
}
#[cfg(test)]
mod d1_4_985_tests {
use super::*;
use crate::mcp::d1_4_985_helpers::{
assert_descriptions_match, assert_property_set_parity, derived_props_for,
};
#[test]
fn memory_list_parity_985() {
let derived = derived_props_for::<ListRequest>();
assert_property_set_parity("memory_list", &derived);
assert_descriptions_match("memory_list", &derived);
}
#[test]
fn memory_list_tool_metadata_985() {
assert_eq!(ListTool::name(), "memory_list");
assert_eq!(ListTool::family(), "core");
}
}