use serde_json::{Value, json};
pub mod tool_names {
pub const MEMORY_AGENT_LIST: &str = "memory_agent_list";
pub const MEMORY_AGENT_REGISTER: &str = "memory_agent_register";
pub const MEMORY_ARCHIVE_LIST: &str = "memory_archive_list";
pub const MEMORY_ARCHIVE_PURGE: &str = "memory_archive_purge";
pub const MEMORY_ARCHIVE_RESTORE: &str = "memory_archive_restore";
pub const MEMORY_ARCHIVE_STATS: &str = "memory_archive_stats";
pub const MEMORY_ATOMISE: &str = "memory_atomise";
pub const MEMORY_AUTO_TAG: &str = "memory_auto_tag";
pub const MEMORY_CALIBRATE_CONFIDENCE: &str = "memory_calibrate_confidence";
pub const MEMORY_CAPABILITIES: &str = "memory_capabilities";
pub const MEMORY_CAPTURE_TURN: &str = "memory_capture_turn";
pub const MEMORY_CHECK_AGENT_ACTION: &str = "memory_check_agent_action";
pub const MEMORY_CHECK_DUPLICATE: &str = "memory_check_duplicate";
pub const MEMORY_CONSOLIDATE: &str = "memory_consolidate";
pub const MEMORY_DELETE: &str = "memory_delete";
pub const MEMORY_DEPENDENTS_OF_INVALIDATED: &str = "memory_dependents_of_invalidated";
pub const MEMORY_DEREF: &str = "memory_deref";
pub const MEMORY_DETECT_CONTRADICTION: &str = "memory_detect_contradiction";
pub const MEMORY_ENTITY_GET_BY_ALIAS: &str = "memory_entity_get_by_alias";
pub const MEMORY_ENTITY_REGISTER: &str = "memory_entity_register";
pub const MEMORY_EXPAND_QUERY: &str = "memory_expand_query";
pub const MEMORY_EXPORT_REFLECTION: &str = "memory_export_reflection";
pub const MEMORY_FIND_PATHS: &str = "memory_find_paths";
pub const MEMORY_FORGET: &str = "memory_forget";
pub const MEMORY_GC: &str = "memory_gc";
pub const MEMORY_GET: &str = "memory_get";
pub const MEMORY_GET_LINKS: &str = "memory_get_links";
pub const MEMORY_GET_TAXONOMY: &str = "memory_get_taxonomy";
pub const MEMORY_INBOX: &str = "memory_inbox";
pub const MEMORY_INGEST_MULTISTEP: &str = "memory_ingest_multistep";
pub const MEMORY_KG_INVALIDATE: &str = "memory_kg_invalidate";
pub const MEMORY_KG_QUERY: &str = "memory_kg_query";
pub const MEMORY_KG_TIMELINE: &str = "memory_kg_timeline";
pub const MEMORY_LINK: &str = "memory_link";
pub const MEMORY_LIST: &str = "memory_list";
pub const MEMORY_LIST_SUBSCRIPTIONS: &str = "memory_list_subscriptions";
pub const MEMORY_LOAD_FAMILY: &str = "memory_load_family";
pub const MEMORY_NAMESPACE_CLEAR_STANDARD: &str = "memory_namespace_clear_standard";
pub const MEMORY_NAMESPACE_GET_STANDARD: &str = "memory_namespace_get_standard";
pub const MEMORY_NAMESPACE_SET_STANDARD: &str = "memory_namespace_set_standard";
pub const MEMORY_NOTIFY: &str = "memory_notify";
pub const MEMORY_OFFLOAD: &str = "memory_offload";
pub const MEMORY_PENDING_APPROVE: &str = "memory_pending_approve";
pub const MEMORY_PENDING_LIST: &str = "memory_pending_list";
pub const MEMORY_PENDING_REJECT: &str = "memory_pending_reject";
pub const MEMORY_PERSONA: &str = "memory_persona";
pub const MEMORY_PERSONA_GENERATE: &str = "memory_persona_generate";
pub const MEMORY_PROMOTE: &str = "memory_promote";
pub const MEMORY_QUOTA_STATUS: &str = "memory_quota_status";
pub const MEMORY_RECALL: &str = "memory_recall";
pub const MEMORY_RECALL_OBSERVATIONS: &str = "memory_recall_observations";
pub const MEMORY_REFLECT: &str = "memory_reflect";
pub const MEMORY_REFLECTION_ORIGIN: &str = "memory_reflection_origin";
pub const MEMORY_REPLAY: &str = "memory_replay";
pub const MEMORY_RULE_LIST: &str = "memory_rule_list";
pub const MEMORY_SEARCH: &str = "memory_search";
pub const MEMORY_SESSION_START: &str = "memory_session_start";
pub const MEMORY_SHARE: &str = "memory_share";
pub const MEMORY_SKILL_COMPOSITIONAL_CONTEXT: &str = "memory_skill_compositional_context";
pub const MEMORY_SKILL_EXPORT: &str = "memory_skill_export";
pub const MEMORY_SKILL_GET: &str = "memory_skill_get";
pub const MEMORY_SKILL_LIST: &str = "memory_skill_list";
pub const MEMORY_SKILL_PROMOTE_FROM_REFLECTION: &str = "memory_skill_promote_from_reflection";
pub const MEMORY_SKILL_REGISTER: &str = "memory_skill_register";
pub const MEMORY_SKILL_RESOURCE: &str = "memory_skill_resource";
pub const MEMORY_SMART_LOAD: &str = "memory_smart_load";
pub const MEMORY_STATS: &str = "memory_stats";
pub const MEMORY_STORE: &str = "memory_store";
pub const MEMORY_SUBSCRIBE: &str = "memory_subscribe";
pub const MEMORY_SUBSCRIPTION_DLQ_LIST: &str = "memory_subscription_dlq_list";
pub const MEMORY_SUBSCRIPTION_REPLAY: &str = "memory_subscription_replay";
pub const MEMORY_UNSUBSCRIBE: &str = "memory_unsubscribe";
pub const MEMORY_UPDATE: &str = "memory_update";
pub const MEMORY_VERIFY: &str = "memory_verify";
#[allow(dead_code)]
pub const ALL: &[&str] = &[
MEMORY_AGENT_LIST,
MEMORY_AGENT_REGISTER,
MEMORY_ARCHIVE_LIST,
MEMORY_ARCHIVE_PURGE,
MEMORY_ARCHIVE_RESTORE,
MEMORY_ARCHIVE_STATS,
MEMORY_ATOMISE,
MEMORY_AUTO_TAG,
MEMORY_CALIBRATE_CONFIDENCE,
MEMORY_CAPABILITIES,
MEMORY_CAPTURE_TURN,
MEMORY_CHECK_AGENT_ACTION,
MEMORY_CHECK_DUPLICATE,
MEMORY_CONSOLIDATE,
MEMORY_DELETE,
MEMORY_DEPENDENTS_OF_INVALIDATED,
MEMORY_DEREF,
MEMORY_DETECT_CONTRADICTION,
MEMORY_ENTITY_GET_BY_ALIAS,
MEMORY_ENTITY_REGISTER,
MEMORY_EXPAND_QUERY,
MEMORY_EXPORT_REFLECTION,
MEMORY_FIND_PATHS,
MEMORY_FORGET,
MEMORY_GC,
MEMORY_GET,
MEMORY_GET_LINKS,
MEMORY_GET_TAXONOMY,
MEMORY_INBOX,
MEMORY_INGEST_MULTISTEP,
MEMORY_KG_INVALIDATE,
MEMORY_KG_QUERY,
MEMORY_KG_TIMELINE,
MEMORY_LINK,
MEMORY_LIST,
MEMORY_LIST_SUBSCRIPTIONS,
MEMORY_LOAD_FAMILY,
MEMORY_NAMESPACE_CLEAR_STANDARD,
MEMORY_NAMESPACE_GET_STANDARD,
MEMORY_NAMESPACE_SET_STANDARD,
MEMORY_NOTIFY,
MEMORY_OFFLOAD,
MEMORY_PENDING_APPROVE,
MEMORY_PENDING_LIST,
MEMORY_PENDING_REJECT,
MEMORY_PERSONA,
MEMORY_PERSONA_GENERATE,
MEMORY_PROMOTE,
MEMORY_QUOTA_STATUS,
MEMORY_RECALL,
MEMORY_RECALL_OBSERVATIONS,
MEMORY_REFLECT,
MEMORY_REFLECTION_ORIGIN,
MEMORY_REPLAY,
MEMORY_RULE_LIST,
MEMORY_SEARCH,
MEMORY_SESSION_START,
MEMORY_SHARE,
MEMORY_SKILL_COMPOSITIONAL_CONTEXT,
MEMORY_SKILL_EXPORT,
MEMORY_SKILL_GET,
MEMORY_SKILL_LIST,
MEMORY_SKILL_PROMOTE_FROM_REFLECTION,
MEMORY_SKILL_REGISTER,
MEMORY_SKILL_RESOURCE,
MEMORY_SMART_LOAD,
MEMORY_STATS,
MEMORY_STORE,
MEMORY_SUBSCRIBE,
MEMORY_SUBSCRIPTION_DLQ_LIST,
MEMORY_SUBSCRIPTION_REPLAY,
MEMORY_UNSUBSCRIBE,
MEMORY_UPDATE,
MEMORY_VERIFY,
];
#[cfg(test)]
mod pin_tests {
use super::*;
#[test]
fn const_count_matches_full_profile() {
assert_eq!(
ALL.len(),
crate::profile::Profile::full().expected_tool_count(),
"tool_names::ALL must cover exactly the full-profile tool set"
);
}
#[test]
fn const_set_is_deduplicated() {
use std::collections::BTreeSet;
let unique: BTreeSet<&&str> = ALL.iter().collect();
assert_eq!(
unique.len(),
ALL.len(),
"tool_names::ALL has duplicate entries"
);
}
#[test]
fn const_values_lowercase_memory_prefix() {
for name in ALL {
assert!(
name.starts_with("memory_"),
"tool name {name:?} does not start with 'memory_'"
);
assert!(
name.chars().all(|c| c.is_ascii_lowercase() || c == '_'),
"tool name {name:?} contains non-[a-z_] character"
);
}
}
#[test]
fn const_values_byte_equal_to_historical_wire_strings() {
assert_eq!(MEMORY_AGENT_LIST, "memory_agent_list");
assert_eq!(MEMORY_AGENT_REGISTER, "memory_agent_register");
assert_eq!(MEMORY_ARCHIVE_LIST, "memory_archive_list");
assert_eq!(MEMORY_ARCHIVE_PURGE, "memory_archive_purge");
assert_eq!(MEMORY_ARCHIVE_RESTORE, "memory_archive_restore");
assert_eq!(MEMORY_ARCHIVE_STATS, "memory_archive_stats");
assert_eq!(MEMORY_ATOMISE, "memory_atomise");
assert_eq!(MEMORY_AUTO_TAG, "memory_auto_tag");
assert_eq!(MEMORY_CALIBRATE_CONFIDENCE, "memory_calibrate_confidence");
assert_eq!(MEMORY_CAPABILITIES, "memory_capabilities");
assert_eq!(MEMORY_CHECK_AGENT_ACTION, "memory_check_agent_action");
assert_eq!(MEMORY_CHECK_DUPLICATE, "memory_check_duplicate");
assert_eq!(MEMORY_CONSOLIDATE, "memory_consolidate");
assert_eq!(MEMORY_DELETE, "memory_delete");
assert_eq!(
MEMORY_DEPENDENTS_OF_INVALIDATED,
"memory_dependents_of_invalidated"
);
assert_eq!(MEMORY_DEREF, "memory_deref");
assert_eq!(MEMORY_DETECT_CONTRADICTION, "memory_detect_contradiction");
assert_eq!(MEMORY_ENTITY_GET_BY_ALIAS, "memory_entity_get_by_alias");
assert_eq!(MEMORY_ENTITY_REGISTER, "memory_entity_register");
assert_eq!(MEMORY_EXPAND_QUERY, "memory_expand_query");
assert_eq!(MEMORY_EXPORT_REFLECTION, "memory_export_reflection");
assert_eq!(MEMORY_FIND_PATHS, "memory_find_paths");
assert_eq!(MEMORY_FORGET, "memory_forget");
assert_eq!(MEMORY_GC, "memory_gc");
assert_eq!(MEMORY_GET, "memory_get");
assert_eq!(MEMORY_GET_LINKS, "memory_get_links");
assert_eq!(MEMORY_GET_TAXONOMY, "memory_get_taxonomy");
assert_eq!(MEMORY_INBOX, "memory_inbox");
assert_eq!(MEMORY_INGEST_MULTISTEP, "memory_ingest_multistep");
assert_eq!(MEMORY_KG_INVALIDATE, "memory_kg_invalidate");
assert_eq!(MEMORY_KG_QUERY, "memory_kg_query");
assert_eq!(MEMORY_KG_TIMELINE, "memory_kg_timeline");
assert_eq!(MEMORY_LINK, "memory_link");
assert_eq!(MEMORY_LIST, "memory_list");
assert_eq!(MEMORY_LIST_SUBSCRIPTIONS, "memory_list_subscriptions");
assert_eq!(MEMORY_LOAD_FAMILY, "memory_load_family");
assert_eq!(
MEMORY_NAMESPACE_CLEAR_STANDARD,
"memory_namespace_clear_standard"
);
assert_eq!(
MEMORY_NAMESPACE_GET_STANDARD,
"memory_namespace_get_standard"
);
assert_eq!(
MEMORY_NAMESPACE_SET_STANDARD,
"memory_namespace_set_standard"
);
assert_eq!(MEMORY_NOTIFY, "memory_notify");
assert_eq!(MEMORY_OFFLOAD, "memory_offload");
assert_eq!(MEMORY_PENDING_APPROVE, "memory_pending_approve");
assert_eq!(MEMORY_PENDING_LIST, "memory_pending_list");
assert_eq!(MEMORY_PENDING_REJECT, "memory_pending_reject");
assert_eq!(MEMORY_PERSONA, "memory_persona");
assert_eq!(MEMORY_PERSONA_GENERATE, "memory_persona_generate");
assert_eq!(MEMORY_PROMOTE, "memory_promote");
assert_eq!(MEMORY_QUOTA_STATUS, "memory_quota_status");
assert_eq!(MEMORY_RECALL, "memory_recall");
assert_eq!(MEMORY_RECALL_OBSERVATIONS, "memory_recall_observations");
assert_eq!(MEMORY_REFLECT, "memory_reflect");
assert_eq!(MEMORY_REFLECTION_ORIGIN, "memory_reflection_origin");
assert_eq!(MEMORY_REPLAY, "memory_replay");
assert_eq!(MEMORY_RULE_LIST, "memory_rule_list");
assert_eq!(MEMORY_SEARCH, "memory_search");
assert_eq!(MEMORY_SESSION_START, "memory_session_start");
assert_eq!(MEMORY_SHARE, "memory_share");
assert_eq!(
MEMORY_SKILL_COMPOSITIONAL_CONTEXT,
"memory_skill_compositional_context"
);
assert_eq!(MEMORY_SKILL_EXPORT, "memory_skill_export");
assert_eq!(MEMORY_SKILL_GET, "memory_skill_get");
assert_eq!(MEMORY_SKILL_LIST, "memory_skill_list");
assert_eq!(
MEMORY_SKILL_PROMOTE_FROM_REFLECTION,
"memory_skill_promote_from_reflection"
);
assert_eq!(MEMORY_SKILL_REGISTER, "memory_skill_register");
assert_eq!(MEMORY_SKILL_RESOURCE, "memory_skill_resource");
assert_eq!(MEMORY_SMART_LOAD, "memory_smart_load");
assert_eq!(MEMORY_STATS, "memory_stats");
assert_eq!(MEMORY_STORE, "memory_store");
assert_eq!(MEMORY_SUBSCRIBE, "memory_subscribe");
assert_eq!(MEMORY_SUBSCRIPTION_DLQ_LIST, "memory_subscription_dlq_list");
assert_eq!(MEMORY_SUBSCRIPTION_REPLAY, "memory_subscription_replay");
assert_eq!(MEMORY_UNSUBSCRIBE, "memory_unsubscribe");
assert_eq!(MEMORY_UPDATE, "memory_update");
assert_eq!(MEMORY_VERIFY, "memory_verify");
}
#[test]
fn consts_match_registered_tools() {
use std::collections::BTreeSet;
let from_consts: BTreeSet<&str> = ALL.iter().copied().collect();
let from_registry: BTreeSet<&str> = crate::mcp::registry::registered_tools()
.iter()
.map(|r| r.name)
.collect();
let only_in_consts: Vec<&&str> = from_consts.difference(&from_registry).collect();
let only_in_registry: Vec<&&str> = from_registry.difference(&from_consts).collect();
assert!(
only_in_consts.is_empty() && only_in_registry.is_empty(),
"tool_names::ALL and registered_tools() drifted; \
only_in_consts = {only_in_consts:?}, \
only_in_registry = {only_in_registry:?}"
);
}
}
}
#[allow(dead_code)]
pub trait McpTool {
fn name() -> &'static str;
fn description() -> &'static str;
fn docs() -> &'static str;
fn input_schema() -> Value;
fn family() -> &'static str;
}
const SCHEMA_TO_VALUE_EXPECT: &str = "schemars schema must serialize to Value";
#[must_use]
pub fn input_schema_for<T: schemars::JsonSchema>() -> Value {
serde_json::to_value(schemars::schema_for!(T)).expect(SCHEMA_TO_VALUE_EXPECT)
}
pub struct RegisteredTool {
pub name: &'static str,
pub description: &'static str,
pub docs: &'static str,
#[allow(dead_code)]
pub family: &'static str,
pub input_schema: Value,
}
impl RegisteredTool {
#[must_use]
pub fn of<T: McpTool>() -> Self {
Self {
name: T::name(),
description: T::description(),
docs: T::docs(),
family: T::family(),
input_schema: T::input_schema(),
}
}
#[must_use]
pub fn to_value(&self) -> Value {
let mut input_schema = self.input_schema.clone();
if let Some(obj) = input_schema.as_object_mut()
&& !obj.contains_key("properties")
{
obj.insert(
"properties".to_string(),
Value::Object(serde_json::Map::new()),
);
}
json!({
"name": self.name,
"description": self.description,
"docs": self.docs,
"inputSchema": input_schema,
})
}
}
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn registered_tools() -> Vec<RegisteredTool> {
vec![
RegisteredTool::of::<crate::mcp::store::StoreTool>(),
RegisteredTool::of::<crate::mcp::recall::RecallTool>(),
RegisteredTool::of::<crate::mcp::recall_observations::RecallObservationsTool>(),
RegisteredTool::of::<crate::mcp::search::SearchTool>(),
RegisteredTool::of::<crate::mcp::list::ListTool>(),
RegisteredTool::of::<crate::mcp::load_family::LoadFamilyTool>(),
RegisteredTool::of::<crate::mcp::load_family::SmartLoadTool>(),
RegisteredTool::of::<crate::mcp::get_taxonomy::GetTaxonomyTool>(),
RegisteredTool::of::<crate::mcp::check_duplicate::CheckDuplicateTool>(),
RegisteredTool::of::<crate::mcp::entity_register::EntityRegisterTool>(),
RegisteredTool::of::<crate::mcp::entity_get_by_alias::EntityGetByAliasTool>(),
RegisteredTool::of::<crate::mcp::kg_timeline::KgTimelineTool>(),
RegisteredTool::of::<crate::mcp::kg_invalidate::KgInvalidateTool>(),
RegisteredTool::of::<crate::mcp::kg_query::KgQueryTool>(),
RegisteredTool::of::<crate::mcp::find_paths::FindPathsTool>(),
RegisteredTool::of::<crate::mcp::delete::DeleteTool>(),
RegisteredTool::of::<crate::mcp::promote::PromoteTool>(),
RegisteredTool::of::<crate::mcp::forget::ForgetTool>(),
RegisteredTool::of::<crate::mcp::forget::StatsTool>(),
RegisteredTool::of::<crate::mcp::update::UpdateTool>(),
RegisteredTool::of::<crate::mcp::get::GetTool>(),
RegisteredTool::of::<crate::mcp::link::LinkTool>(),
RegisteredTool::of::<crate::mcp::link::GetLinksTool>(),
RegisteredTool::of::<crate::mcp::verify::VerifyTool>(),
RegisteredTool::of::<crate::mcp::replay::ReplayTool>(),
RegisteredTool::of::<crate::mcp::reflect::ReflectTool>(),
RegisteredTool::of::<crate::mcp::export_reflection::ExportReflectionTool>(),
RegisteredTool::of::<crate::mcp::persona::PersonaTool>(),
RegisteredTool::of::<crate::mcp::persona::PersonaGenerateTool>(),
RegisteredTool::of::<crate::mcp::reflection_origin::ReflectionOriginTool>(),
RegisteredTool::of::<crate::mcp::dependents_of_invalidated::DependentsOfInvalidatedTool>(),
RegisteredTool::of::<crate::mcp::consolidate::ConsolidateTool>(),
RegisteredTool::of::<crate::mcp::ingest_multistep::IngestMultistepTool>(),
RegisteredTool::of::<crate::mcp::atomise::AtomiseTool>(),
RegisteredTool::of::<crate::mcp::share::ShareTool>(),
RegisteredTool::of::<crate::mcp::calibrate_confidence::CalibrateConfidenceTool>(),
RegisteredTool::of::<crate::mcp::capabilities::CapabilitiesTool>(),
RegisteredTool::of::<crate::mcp::capture_turn::MemoryCaptureTurnTool>(),
RegisteredTool::of::<crate::mcp::expand_query::ExpandQueryTool>(),
RegisteredTool::of::<crate::mcp::auto_tag::AutoTagTool>(),
RegisteredTool::of::<crate::mcp::detect_contradiction::DetectContradictionTool>(),
RegisteredTool::of::<crate::mcp::archive::ArchiveListTool>(),
RegisteredTool::of::<crate::mcp::archive::ArchiveRestoreTool>(),
RegisteredTool::of::<crate::mcp::archive::ArchivePurgeTool>(),
RegisteredTool::of::<crate::mcp::archive::ArchiveStatsTool>(),
RegisteredTool::of::<crate::mcp::archive::GcTool>(),
RegisteredTool::of::<crate::mcp::session_start::SessionStartTool>(),
RegisteredTool::of::<crate::mcp::namespace::NamespaceSetStandardTool>(),
RegisteredTool::of::<crate::mcp::namespace::NamespaceGetStandardTool>(),
RegisteredTool::of::<crate::mcp::namespace::NamespaceClearStandardTool>(),
RegisteredTool::of::<crate::mcp::pending::PendingListTool>(),
RegisteredTool::of::<crate::mcp::pending::PendingApproveTool>(),
RegisteredTool::of::<crate::mcp::pending::PendingRejectTool>(),
RegisteredTool::of::<crate::mcp::agent::AgentRegisterTool>(),
RegisteredTool::of::<crate::mcp::agent::AgentListTool>(),
RegisteredTool::of::<crate::mcp::notify::NotifyTool>(),
RegisteredTool::of::<crate::mcp::notify::InboxTool>(),
RegisteredTool::of::<crate::mcp::subscribe::SubscribeTool>(),
RegisteredTool::of::<crate::mcp::subscribe::UnsubscribeTool>(),
RegisteredTool::of::<crate::mcp::subscribe::ListSubscriptionsTool>(),
RegisteredTool::of::<crate::mcp::subscribe::SubscriptionReplayTool>(),
RegisteredTool::of::<crate::mcp::pending::SubscriptionDlqListTool>(),
RegisteredTool::of::<crate::mcp::quota_status::QuotaStatusTool>(),
RegisteredTool::of::<crate::mcp::check_agent_action::CheckAgentActionTool>(),
RegisteredTool::of::<crate::mcp::rule_list::RuleListTool>(),
RegisteredTool::of::<crate::mcp::skill_register::SkillRegisterTool>(),
RegisteredTool::of::<crate::mcp::skill_list::SkillListTool>(),
RegisteredTool::of::<crate::mcp::skill_get::SkillGetTool>(),
RegisteredTool::of::<crate::mcp::skill_resource::SkillResourceTool>(),
RegisteredTool::of::<crate::mcp::skill_export::SkillExportTool>(),
RegisteredTool::of::<crate::mcp::skill_promote::SkillPromoteFromReflectionTool>(),
RegisteredTool::of::<crate::mcp::skill_compositional_context::SkillCompositionalContextTool>(
),
RegisteredTool::of::<crate::mcp::offload::OffloadTool>(),
RegisteredTool::of::<crate::mcp::offload::DerefTool>(),
]
}
const TOOLS_VERSION: &str = "2026-05-06";
#[allow(dead_code)]
const C4_KEEP_OPTIONAL_PARAMS: &[&str] = &["namespace", "format"];
pub(crate) fn trim_optional_params(defs: &mut Value) -> usize {
let Some(tools) = defs.get_mut("tools").and_then(Value::as_array_mut) else {
return 0;
};
let mut stripped = 0_usize;
for tool in tools.iter_mut() {
let Some(input_schema) = tool.get_mut("inputSchema") else {
continue;
};
let Some(properties) = input_schema
.get_mut("properties")
.and_then(Value::as_object_mut)
else {
continue;
};
for (_param_name, prop_value) in properties.iter_mut() {
let had_desc = prop_value
.as_object()
.is_some_and(|o| o.contains_key("description"));
strip_description_recursively(prop_value);
if had_desc {
stripped += 1;
}
}
}
stripped
}
pub(crate) fn families_overview(profile: &crate::profile::Profile) -> Value {
use crate::profile::Family;
let defs = tool_definitions();
let all_tools = defs
.get("tools")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let entries: Vec<Value> = Family::all()
.iter()
.map(|fam| {
let tools_in_family: Vec<&str> = all_tools
.iter()
.filter_map(|t| t.get("name").and_then(Value::as_str))
.filter(|n| Family::for_tool(n) == Some(*fam))
.collect();
json!({
"name": fam.name(),
"tool_count": tools_in_family.len(),
"loaded": profile.includes(*fam),
"tools": tools_in_family,
})
})
.collect();
json!({
"schema_version": "v0.6.4-families-1",
"always_on": crate::profile::ALWAYS_ON_TOOLS,
"families": entries,
})
}
pub fn handle_capabilities_family(
family_name: &str,
include_schema: bool,
verbose: bool,
profile: &crate::profile::Profile,
allowlist_cfg: Option<&crate::config::McpConfig>,
agent_id: Option<&str>,
audit_conn: Option<&rusqlite::Connection>,
) -> Result<Value, String> {
use crate::profile::Family;
if family_name.is_empty() {
return Err("memory_capabilities: 'family' must not be empty".to_string());
}
let family = Family::all()
.iter()
.find(|f| f.name() == family_name)
.copied()
.ok_or_else(|| {
let valid: Vec<&str> = Family::all().iter().map(|f| f.name()).collect();
format!(
"unknown family '{family_name}'. Valid families: {}.",
valid.join(", ")
)
})?;
if include_schema && let Some(mcp_cfg) = allowlist_cfg {
use crate::config::AllowlistDecision;
match mcp_cfg.allowlist_decision(agent_id, family.name()) {
AllowlistDecision::Disabled | AllowlistDecision::Allow => {}
AllowlistDecision::Deny => {
if let Some(conn) = audit_conn {
crate::db::record_capability_expansion(
conn,
agent_id,
family.name(),
false,
None,
);
}
return Err(format!(
"agent '{}' is not permitted to expand family '{}' under \
[mcp.allowlist]. Ask an operator to add a matching rule \
to config.toml or pass an allowed agent_id.",
agent_id.unwrap_or("<anonymous>"),
family.name()
));
}
}
}
if include_schema && let Some(conn) = audit_conn {
crate::db::record_capability_expansion(conn, agent_id, family.name(), true, None);
}
let mut defs = tool_definitions();
if !verbose {
trim_optional_params(&mut defs);
}
let all_tools = defs
.get("tools")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let mut in_family: Vec<Value> = all_tools
.into_iter()
.filter(|t| {
t.get("name")
.and_then(Value::as_str)
.and_then(Family::for_tool)
== Some(family)
})
.collect();
if !verbose {
strip_docs_from_tools(&mut in_family);
}
if include_schema {
Ok(json!({
"schema_version": "v0.6.4-family-schemas-1",
"family": family.name(),
"loaded_under_active_profile": profile.includes(family),
"verbose": verbose,
"tools": in_family,
}))
} else {
let names: Vec<&str> = in_family
.iter()
.filter_map(|t| t.get("name").and_then(Value::as_str))
.collect();
Ok(json!({
"schema_version": "v0.6.4-family-list-1",
"family": family.name(),
"loaded_under_active_profile": profile.includes(family),
"tools": names,
}))
}
}
pub fn tool_definitions_for_profile(profile: &crate::profile::Profile) -> Value {
static CACHE: std::sync::OnceLock<
std::sync::RwLock<std::collections::HashMap<crate::profile::Profile, Value>>,
> = std::sync::OnceLock::new();
let cache = CACHE.get_or_init(|| std::sync::RwLock::new(std::collections::HashMap::new()));
if let Ok(read) = cache.read()
&& let Some(cached) = read.get(profile)
{
return cached.clone();
}
let defs = build_tool_definitions_for_profile(profile);
if let Ok(mut write) = cache.write() {
write.entry(profile.clone()).or_insert_with(|| defs.clone());
}
defs
}
fn build_tool_definitions_for_profile(profile: &crate::profile::Profile) -> Value {
let mut defs = tool_definitions_for_profile_verbose(profile);
if !tools_verbose_env_enabled() {
trim_optional_params(&mut defs);
wire_compact_descriptions(&mut defs);
}
defs
}
fn wire_compact_descriptions(defs: &mut Value) {
let Some(tools) = defs.get_mut("tools").and_then(Value::as_array_mut) else {
return;
};
for tool in tools.iter_mut() {
let Some(obj) = tool.as_object_mut() else {
continue;
};
let Some(desc) = obj.get("description").and_then(Value::as_str) else {
continue;
};
let compact = compact_description(desc);
if compact.len() != desc.len() {
obj.insert("description".to_string(), Value::String(compact));
}
}
}
fn compact_description(s: &str) -> String {
const MAX: usize = 32;
if s.len() <= MAX {
return s.to_string();
}
let slice = &s[..MAX.min(s.len())];
if let Some(idx) = slice.find(['.', ';']) {
return s[..idx].to_string();
}
if let Some(idx) = slice.rfind(char::is_whitespace) {
return s[..idx].to_string();
}
let mut end = MAX.min(s.len());
while !s.is_char_boundary(end) && end > 0 {
end -= 1;
}
s[..end].to_string()
}
fn tools_verbose_env_enabled() -> bool {
static CACHED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*CACHED.get_or_init(|| {
std::env::var("AI_MEMORY_TOOLS_VERBOSE")
.ok()
.is_some_and(|v| v == "1" || v.eq_ignore_ascii_case("true"))
})
}
pub fn tool_definitions_for_profile_verbose(profile: &crate::profile::Profile) -> Value {
let mut defs = tool_definitions();
if let Some(arr) = defs.get_mut("tools").and_then(|t| t.as_array_mut()) {
arr.retain(|tool| {
tool.get("name")
.and_then(Value::as_str)
.is_some_and(|name| profile.loads(name))
});
strip_docs_from_tools(arr);
}
defs
}
pub(crate) fn strip_docs_from_tools(tools: &mut Vec<Value>) {
for tool in tools.iter_mut() {
let Some(obj) = tool.as_object_mut() else {
continue;
};
obj.remove("docs");
if let Some(input_schema) = obj.get_mut("inputSchema").and_then(Value::as_object_mut) {
input_schema.remove("description");
input_schema.remove("$schema");
input_schema.remove("title");
if let Some(defs) = input_schema
.get_mut("definitions")
.and_then(Value::as_object_mut)
{
for (_name, def_value) in defs.iter_mut() {
strip_description_recursively(def_value);
}
}
if let Some(props) = input_schema
.get_mut("properties")
.and_then(Value::as_object_mut)
{
for (_param_name, prop_value) in props.iter_mut() {
strip_description_recursively(prop_value);
}
}
}
}
}
fn strip_description_recursively(value: &mut Value) {
match value {
Value::Object(map) => {
map.remove("description");
if let Some(Value::Null) = map.get("default") {
map.remove("default");
}
if let Some(default) = map.get("default")
&& default.as_str().is_some_and(|s| s.len() > 32)
{
map.remove("default");
}
for (_, child) in map.iter_mut() {
strip_description_recursively(child);
}
}
Value::Array(items) => {
for item in items.iter_mut() {
strip_description_recursively(item);
}
}
_ => {}
}
}
pub fn tool_definitions() -> Value {
static CACHE: std::sync::OnceLock<Value> = std::sync::OnceLock::new();
CACHE
.get_or_init(|| {
let tools: Vec<Value> = registered_tools()
.iter()
.map(RegisteredTool::to_value)
.collect();
json!({
"toolsVersion": TOOLS_VERSION,
"tools": tools,
})
})
.clone()
}
#[cfg(test)]
mod d1_6_987_tests {
use super::*;
use std::collections::BTreeSet;
fn load_snapshot() -> Value {
let path = "tests/snapshots/tool_definitions_pre_d1_6.json";
let raw = std::fs::read_to_string(path).unwrap_or_else(|e| {
panic!(
"missing pre-D1.6 snapshot at {path}: {e}; \
regenerate via the one-shot capture documented in #987 dispatch"
)
});
serde_json::from_str(&raw).expect("snapshot must be valid JSON")
}
#[test]
fn tool_definitions_byte_shape_v0_7_0_compat_987_count() {
let pre = load_snapshot();
let post = tool_definitions();
let pre_count = pre["tools"].as_array().map_or(0, Vec::len);
let post_count = post["tools"].as_array().map_or(0, Vec::len);
assert_eq!(
pre_count, post_count,
"post-D1.6 tool count must match pre-D1.6 snapshot ({pre_count}); \
a tool was added or removed."
);
}
#[test]
fn tool_definitions_byte_shape_v0_7_0_compat_987_names() {
let pre = load_snapshot();
let post = tool_definitions();
let names = |v: &Value| -> BTreeSet<String> {
v["tools"]
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|t| t.get("name").and_then(Value::as_str).map(String::from))
.collect()
})
.unwrap_or_default()
};
let pre_names = names(&pre);
let post_names = names(&post);
let added: Vec<&String> = post_names.difference(&pre_names).collect();
let removed: Vec<&String> = pre_names.difference(&post_names).collect();
assert!(
added.is_empty() && removed.is_empty(),
"post-D1.6 tool name set drifted: added = {added:?}, removed = {removed:?}"
);
}
#[test]
fn tool_definitions_byte_shape_v0_7_0_compat_987_descriptions() {
let pre = load_snapshot();
let post = tool_definitions();
let by_name = |v: &Value| -> std::collections::BTreeMap<String, String> {
v["tools"]
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|t| {
let name = t.get("name").and_then(Value::as_str)?.to_string();
let desc = t.get("description").and_then(Value::as_str)?.to_string();
Some((name, desc))
})
.collect()
})
.unwrap_or_default()
};
let pre_map = by_name(&pre);
let post_map = by_name(&post);
for (name, want) in &pre_map {
let got = post_map
.get(name)
.unwrap_or_else(|| panic!("post-D1.6 missing tool {name}"));
assert_eq!(
got, want,
"post-D1.6 {name}.description drifted from snapshot\n legacy: {want:?}\n post-D1.6: {got:?}"
);
}
}
#[test]
fn tool_definitions_byte_shape_v0_7_0_compat_987_docs() {
let pre = load_snapshot();
let post = tool_definitions();
let by_name = |v: &Value| -> std::collections::BTreeMap<String, String> {
v["tools"]
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|t| {
let name = t.get("name").and_then(Value::as_str)?.to_string();
let docs = t.get("docs").and_then(Value::as_str)?.to_string();
Some((name, docs))
})
.collect()
})
.unwrap_or_default()
};
let pre_map = by_name(&pre);
let post_map = by_name(&post);
for (name, want) in &pre_map {
let got = post_map
.get(name)
.unwrap_or_else(|| panic!("post-D1.6 missing tool {name}"));
assert_eq!(
got, want,
"post-D1.6 {name}.docs drifted from snapshot\n legacy: {want:?}\n post-D1.6: {got:?}"
);
}
}
#[test]
fn tool_definitions_byte_shape_v0_7_0_compat_987_property_keys() {
let pre = load_snapshot();
let post = tool_definitions();
let props_by_name = |v: &Value| -> std::collections::BTreeMap<String, BTreeSet<String>> {
v["tools"]
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|t| {
let name = t.get("name").and_then(Value::as_str)?.to_string();
let keys: BTreeSet<String> = t
.pointer("/inputSchema/properties")
.and_then(Value::as_object)
.map(|m| m.keys().cloned().collect())
.unwrap_or_default();
Some((name, keys))
})
.collect()
})
.unwrap_or_default()
};
let pre_props = props_by_name(&pre);
let post_props = props_by_name(&post);
for (name, want) in &pre_props {
let got = post_props
.get(name)
.unwrap_or_else(|| panic!("post-D1.6 missing tool {name}"));
let added: Vec<&String> = got.difference(want).collect();
let removed: Vec<&String> = want.difference(got).collect();
assert!(
added.is_empty() && removed.is_empty(),
"post-D1.6 {name}.inputSchema.properties drifted: added = {added:?}, removed = {removed:?}"
);
}
}
#[test]
fn tool_definitions_byte_shape_v0_7_0_compat_987_required() {
let pre = load_snapshot();
let post = tool_definitions();
let req_by_name = |v: &Value| -> std::collections::BTreeMap<String, BTreeSet<String>> {
v["tools"]
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|t| {
let name = t.get("name").and_then(Value::as_str)?.to_string();
let req: BTreeSet<String> = t
.pointer("/inputSchema/required")
.and_then(Value::as_array)
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
Some((name, req))
})
.collect()
})
.unwrap_or_default()
};
let pre_req = req_by_name(&pre);
let post_req = req_by_name(&post);
for (name, want) in &pre_req {
let got = post_req
.get(name)
.unwrap_or_else(|| panic!("post-D1.6 missing tool {name}"));
assert_eq!(
got, want,
"post-D1.6 {name}.inputSchema.required drifted; \
the D1.6 allowed-diffs do NOT permit required-set changes"
);
}
}
}