use crate::mcp::param_names;
use crate::mcp::registry::McpTool;
use crate::models::field_names;
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 GetTaxonomyRequest {
#[serde(default)]
pub namespace_prefix: Option<String>,
#[serde(default)]
pub depth: Option<i64>,
#[serde(default)]
pub limit: Option<i64>,
}
#[allow(dead_code)]
pub struct GetTaxonomyTool;
impl McpTool for GetTaxonomyTool {
fn name() -> &'static str {
crate::mcp::registry::tool_names::MEMORY_GET_TAXONOMY
}
fn description() -> &'static str {
"Return a hierarchical tree of namespaces with memory counts."
}
fn docs() -> &'static str {
"Pillar 1 / Stream A: namespace tree (live rows only). Each node has count + subtree_count. Response includes total_count and truncated flag."
}
fn input_schema() -> Value {
crate::mcp::registry::input_schema_for::<GetTaxonomyRequest>()
}
fn family() -> &'static str {
crate::profile::Family::Graph.name()
}
}
pub(super) fn handle_get_taxonomy(
conn: &rusqlite::Connection,
params: &Value,
) -> Result<Value, String> {
let prefix_raw = params
.get("namespace_prefix")
.and_then(|v| v.as_str())
.map(str::trim)
.filter(|s| !s.is_empty());
let prefix_owned: Option<String> = prefix_raw.map(|s| s.trim_end_matches('/').to_string());
if let Some(p) = prefix_owned.as_deref() {
validate::validate_namespace(p).map_err(|e| e.to_string())?;
}
let depth = usize::try_from(
params
.get(param_names::DEPTH)
.and_then(Value::as_u64)
.unwrap_or(8),
)
.unwrap_or(usize::MAX)
.min(crate::models::MAX_NAMESPACE_DEPTH);
let limit = params
.get(param_names::LIMIT)
.and_then(Value::as_u64)
.map_or(crate::storage::TAXONOMY_DEFAULT_LIMIT, |v| {
usize::try_from(v).unwrap_or(usize::MAX)
})
.clamp(1, crate::storage::TAXONOMY_MAX_LIMIT);
let tax =
db::get_taxonomy(conn, prefix_owned.as_deref(), depth, limit).map_err(|e| e.to_string())?;
Ok(json!({
"tree": tax.tree,
(field_names::TOTAL_COUNT): tax.total_count,
"truncated": tax.truncated,
}))
}
#[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_get_taxonomy_parity_985() {
let derived = derived_props_for::<GetTaxonomyRequest>();
assert_property_set_parity("memory_get_taxonomy", &derived);
assert_descriptions_match("memory_get_taxonomy", &derived);
}
#[test]
fn memory_get_taxonomy_tool_metadata_985() {
assert_eq!(GetTaxonomyTool::name(), "memory_get_taxonomy");
assert_eq!(GetTaxonomyTool::family(), "graph");
}
}