Skip to main content

trusty_memory/tools/
mod.rs

1//! MCP tool surface for trusty-memory.
2//!
3//! Why: Concentrates the public tool contract in one file so changes are
4//! auditable and the MCP schema stays in sync with the implementation.
5//! What: Defines `MemoryMcpServer`, `tool_definitions()` (the MCP
6//! `tools/list` payload), and the in-process tool dispatcher wired to the
7//! real `PalaceRegistry` + retrieval / KG APIs.
8//! Test: `cargo test -p trusty-memory-mcp` validates the schema and dispatch.
9//!
10//! Tools exposed:
11//! - `memory_remember(palace, text, room?, tags?)` -> drawer_id
12//! - `memory_recall(palace, query, top_k?)`        -> Vec<Drawer> (L0+L1+L2)
13//! - `memory_recall_deep(palace, query, top_k?)`   -> Vec<Drawer> (L3 deep)
14//! - `memory_list(palace, room?, tag?, limit?)`    -> Vec<Drawer>
15//! - `memory_forget(palace, drawer_id)`            -> status: deleted|not_found
16//! - `palace_create(name, description?)`           -> PalaceId
17//! - `palace_list()`                                -> Vec<PalaceId>
18//! - `palace_info(palace)`                          -> palace metadata + stats
19//! - `room_list(palace)`                            -> Vec<RoomSummary>
20//! - `room_create(palace, label, description?)`     -> room_id (idempotent)
21//! - `room_rename(palace, room, new_label)`         -> renamed room
22//! - `kg_assert(palace, subject, predicate, object, confidence?, provenance?)` -> ()
23//! - `kg_retract_triple(palace, subject, predicate, object)` -> closed count
24//! - `kg_query(palace, subject)`                    -> Vec<Triple>
25//! - `kg_list_subjects(palace, limit?, with_counts?)` -> subjects (#4776)
26//! - `wing_list(palace)`                            -> Vec<WingSummary>
27//! - `wing_create(palace, label)`                   -> wing_id (idempotent)
28//! - `wing_rename(palace, wing, new_label)`         -> WingSummary
29
30pub mod bm25;
31pub mod chat_definitions;
32pub mod chat_ops;
33pub mod definitions;
34pub mod dream_ops;
35// #5000 / #4786: answer "is this findable?" per id and per estate.
36pub mod embed_audit;
37pub mod embed_audit_definitions;
38pub mod helpers;
39pub mod kg_ops;
40pub mod memory_ops;
41pub mod palace_ops;
42pub mod room_definitions;
43pub mod room_ops;
44pub mod task_definitions;
45pub mod task_ops;
46// ADR-0027 T9 (#4809): the wing surface ships WITH the wing entity — a level
47// nobody reads is the defect the ADR exists to correct.
48pub mod wing_definitions;
49pub mod wing_ops;
50
51// Re-export the public + cross-module surface so external call sites
52// (`crate::tools::X`) and the `super::*` glob in `tools::tests` keep
53// resolving exactly as they did against the former monolithic module.
54pub use bm25::{spawn_bm25_index_worker, Bm25IndexRequest, BM25_INDEX_QUEUE_CAPACITY};
55pub use definitions::{tool_definitions, tool_definitions_with, MemoryMcpServer};
56pub(crate) use helpers::{auto_extract_and_assert, room_label};
57
58// Re-exports used only by the in-crate test module (`super::*`).
59#[cfg(test)]
60pub(crate) use bm25::{bm25_hits_to_recall_results, bm25_index_enqueue};
61#[cfg(test)]
62pub(crate) use helpers::{blocklist_gate, content_gate, dedup_gate, open_palace_handle};
63
64use crate::AppState;
65use anyhow::Result;
66use serde_json::Value;
67
68use chat_ops::{
69    handle_chat_session_add_turn, handle_chat_session_create, handle_chat_session_delete,
70    handle_chat_session_get, handle_chat_session_list, handle_chat_session_recall,
71    handle_chat_turn_append,
72};
73use dream_ops::{handle_dream_consolidate_room, handle_palace_dream};
74use kg_ops::{
75    handle_add_alias, handle_discover_aliases, handle_get_prompt_context, handle_kg_assert,
76    handle_kg_bootstrap, handle_kg_gaps, handle_kg_list_subjects, handle_kg_query,
77    handle_kg_retract_triple, handle_list_prompt_facts, handle_remove_prompt_fact,
78    handle_upgrade_tool,
79};
80use memory_ops::{
81    handle_memory_forget, handle_memory_list, handle_memory_note, handle_memory_recall,
82    handle_memory_recall_all, handle_memory_recall_deep, handle_memory_remember,
83    handle_memory_send_message,
84};
85use palace_ops::{
86    handle_palace_compact, handle_palace_create, handle_palace_delete, handle_palace_info,
87    handle_palace_list, handle_palace_reembed, handle_palace_unalias, handle_palace_update,
88};
89use room_ops::{handle_room_create, handle_room_list, handle_room_rename};
90use task_ops::{handle_task_add, handle_task_complete, handle_task_list};
91use wing_ops::{handle_wing_create, handle_wing_list, handle_wing_rename};
92
93/// Dispatch a tool call by name to its real handler.
94///
95/// Why: Centralises the name → handler mapping; every handler now performs a
96/// real read/write against the live `PalaceRegistry` instead of returning a
97/// stub. After issue #227 the body is a thin router — every tool's logic
98/// lives in its own `handle_*` function above so the dispatcher itself is
99/// auditable at a glance.
100/// What: Returns `Ok(Value)` on success, `Err` on unknown tool / bad args /
101/// underlying failure.
102/// Test: `dispatch_palace_create_persists`, `dispatch_remember_then_recall`,
103/// `dispatch_kg_assert_then_query`, `dispatch_unknown_tool_errors`.
104pub async fn dispatch_tool(state: &AppState, name: &str, args: Value) -> Result<Value> {
105    match name {
106        "memory_remember" => handle_memory_remember(state, args).await,
107        "memory_note" => handle_memory_note(state, args).await,
108        "memory_recall" => handle_memory_recall(state, args).await,
109        "memory_recall_deep" => handle_memory_recall_deep(state, args).await,
110        "palace_create" => handle_palace_create(state, args).await,
111        "palace_list" => handle_palace_list(state, args).await,
112        "palace_delete" => handle_palace_delete(state, args).await,
113        "palace_update" => handle_palace_update(state, args).await,
114        "kg_assert" => handle_kg_assert(state, args).await,
115        // The inverse of `kg_assert`: closes one (subject, predicate, object)
116        // and leaves the pair's other objects live.
117        "kg_retract_triple" => handle_kg_retract_triple(state, args).await,
118        "add_alias" => handle_add_alias(state, args).await,
119        "list_prompt_facts" => handle_list_prompt_facts(state, args).await,
120        "remove_prompt_fact" => handle_remove_prompt_fact(state, args).await,
121        "kg_query" => handle_kg_query(state, args).await,
122        // #4776: subject discovery — the read that makes `kg_query` usable
123        // without already knowing a subject.
124        "kg_list_subjects" => handle_kg_list_subjects(state, args).await,
125        "memory_list" => handle_memory_list(state, args).await,
126        "memory_forget" => handle_memory_forget(state, args).await,
127        "palace_info" => handle_palace_info(state, args).await,
128        "palace_compact" => handle_palace_compact(state, args).await,
129        // #4906: report / repair drawers that have no vector.
130        "palace_reembed" => handle_palace_reembed(state, args).await,
131        // #5005: free drawers destroyed by a vector-id collision.
132        "palace_unalias" => handle_palace_unalias(state, args).await,
133        // #5000: verify a caller's OWN drawer ids, not the whole missing set.
134        "palace_verify_embedded" => embed_audit::handle_palace_verify_embedded(state, args).await,
135        // #5000 / #4786: every palace on disk, uncapped — the console report is
136        // capped at 20 and shows an uncached palace as 0/0, i.e. healthy.
137        "palace_embed_sweep" => embed_audit::handle_palace_embed_sweep(state, args).await,
138        "kg_gaps" => handle_kg_gaps(state, args).await,
139        "memory_recall_all" => handle_memory_recall_all(state, args).await,
140        "get_prompt_context" => handle_get_prompt_context(state, args).await,
141        "discover_aliases" => handle_discover_aliases(state, args).await,
142        "kg_bootstrap" => handle_kg_bootstrap(state, args).await,
143        "memory_send_message" => handle_memory_send_message(state, args).await,
144        "upgrade" => handle_upgrade_tool(state, args).await,
145        "console_metrics" => crate::console_metrics::handle_console_metrics(state, args).await,
146        "chat_session_create" => handle_chat_session_create(state, args).await,
147        "chat_session_add_turn" => handle_chat_session_add_turn(state, args).await,
148        "chat_session_get" => handle_chat_session_get(state, args).await,
149        "chat_session_recall" => handle_chat_session_recall(state, args).await,
150        "chat_session_list" => handle_chat_session_list(state, args).await,
151        "chat_session_delete" => handle_chat_session_delete(state, args).await,
152        "chat_turn_append" => handle_chat_turn_append(state, args).await,
153        "dream_consolidate_room" => handle_dream_consolidate_room(state, args).await,
154        "palace_dream" => handle_palace_dream(state, args).await,
155        "task_add" => handle_task_add(state, args).await,
156        "task_list" => handle_task_list(state, args).await,
157        "task_complete" => handle_task_complete(state, args).await,
158        // ADR-0027 T6 (#4805): the room surface — discovery, idempotent
159        // creation, and the rename that repairs an `unresolved-*` label.
160        "room_list" => handle_room_list(state, args).await,
161        "room_create" => handle_room_create(state, args).await,
162        "room_rename" => handle_room_rename(state, args).await,
163        // ADR-0027 T9 (#4809): the wing surface — the scope axis over rooms.
164        "wing_list" => handle_wing_list(state, args).await,
165        "wing_create" => handle_wing_create(state, args).await,
166        "wing_rename" => handle_wing_rename(state, args).await,
167        other => anyhow::bail!("unknown tool: {other}"),
168    }
169}
170
171#[cfg(test)]
172mod tests;