Skip to main content

trusty_memory/tools/
definitions.rs

1//! MCP `tools/list` schema + server marker for trusty-memory.
2//!
3//! Why: Concentrates the public tool contract (the `tools/list` payload) in
4//! one place so the MCP schema stays auditable and in sync with the handlers.
5//! What: Defines `MemoryMcpServer` and the `tool_definitions{,_with}` schema
6//! builders moved out of the former monolithic `tools.rs` (issue #607).
7//! Test: `tool_definitions_lists_all_tools`,
8//! `tool_definitions_drops_palace_required_when_default_set` in `tools::tests`.
9
10use serde_json::{json, Value};
11
12use super::chat_definitions::chat_tool_definitions;
13use super::room_definitions::room_tool_definitions;
14use super::task_definitions::task_tool_definitions;
15use super::wing_definitions::wing_tool_definitions;
16
17/// Marker server type. Reserved for future stateful MCP server impls.
18///
19/// Why: Keep a stable type name while the protocol-loop is implemented at
20/// module level, so external callers can still depend on a server symbol.
21/// What: Zero-sized struct with `new` / `Default`.
22/// Test: `MemoryMcpServer::default()` constructs without panic.
23pub struct MemoryMcpServer;
24
25impl MemoryMcpServer {
26    pub fn new() -> Self {
27        Self
28    }
29}
30
31impl Default for MemoryMcpServer {
32    fn default() -> Self {
33        Self::new()
34    }
35}
36
37/// MCP `tools/list` response payload.
38///
39/// Why: Claude Code calls `tools/list` once on connect and uses the schema
40/// to drive the tool picker; the schema is the source of truth for arg names.
41/// `palace` is required only when the server has no `--palace` default
42/// configured — when a default is set, the schema omits `palace` from
43/// `required` so clients can drop it.
44/// What: Returns a JSON object `{ "tools": [...] }` with all 10 tool defs.
45/// Test: `tool_definitions_lists_all_tools`,
46/// `tool_definitions_drops_palace_required_when_default_set`.
47pub fn tool_definitions() -> Value {
48    tool_definitions_with(false)
49}
50
51/// Variant of `tool_definitions` aware of whether a default palace is
52/// configured. When `has_default` is true, the `palace` argument is moved
53/// out of the `required` list for every tool that takes it.
54///
55/// Why: Lets `handle_message` emit a schema that matches the running
56/// server's actual contract — clients reading the schema should see exactly
57/// what they need to send.
58/// What: Builds the same shape as `tool_definitions` but with conditional
59/// `required` arrays.
60/// Test: `tool_definitions_drops_palace_required_when_default_set`.
61pub fn tool_definitions_with(has_default: bool) -> Value {
62    let memory_remember_required: Vec<&str> = if has_default {
63        vec!["text"]
64    } else {
65        vec!["palace", "text"]
66    };
67    let memory_recall_required: Vec<&str> = if has_default {
68        vec!["query"]
69    } else {
70        vec!["palace", "query"]
71    };
72    let kg_assert_required: Vec<&str> = if has_default {
73        vec!["subject", "predicate", "object"]
74    } else {
75        vec!["palace", "subject", "predicate", "object"]
76    };
77    // Retraction takes the same full triple key as the assertion it undoes, so
78    // its `required` list is `kg_assert`'s by construction.
79    let kg_retract_triple_required: Vec<&str> = kg_assert_required.clone();
80    let kg_query_required: Vec<&str> = if has_default {
81        vec!["subject"]
82    } else {
83        vec!["palace", "subject"]
84    };
85    // #4776: subject enumeration takes no argument of its own, so `palace` is
86    // the whole `required` list when no server default is configured.
87    let kg_list_subjects_required: Vec<&str> = if has_default { vec![] } else { vec!["palace"] };
88    let memory_list_required: Vec<&str> = if has_default { vec![] } else { vec!["palace"] };
89    let memory_forget_required: Vec<&str> = if has_default {
90        vec!["drawer_id"]
91    } else {
92        vec!["palace", "drawer_id"]
93    };
94    let palace_info_required: Vec<&str> = if has_default { vec![] } else { vec!["palace"] };
95    let palace_compact_required: Vec<&str> = if has_default { vec![] } else { vec!["palace"] };
96    let memory_note_required: Vec<&str> = if has_default {
97        vec!["content"]
98    } else {
99        vec!["palace", "content"]
100    };
101    // Issue #664: add_alias and discover_aliases both call resolve_palace() but
102    // previously omitted `palace` from their schemas, making them uncallable
103    // without a server-side default. Now follow the memory_remember pattern.
104    let add_alias_required: Vec<&str> = if has_default {
105        vec!["short", "full"]
106    } else {
107        vec!["palace", "short", "full"]
108    };
109    let discover_aliases_required: Vec<&str> = if has_default { vec![] } else { vec!["palace"] };
110
111    let mut result = json!({
112        "tools": [
113            {
114                "name": "memory_remember",
115                "description": "Store a memory (drawer) in a palace room. Content is filtered for signal vs. noise (issue #61): rejects empty/very short content, raw tool/commit output, and code-only blobs. Issue #215: very short standalone content (< 4 words) is silently dropped unless a `context` is supplied, in which case the context is prepended so the stored memory has standalone value. Pass force=true to bypass content-QUALITY gates (blocklist, short-content, dedup, noise); this does NOT bypass secret detection — see allow_secret_like. Or use memory_note for short curated facts.",
116                "inputSchema": {
117                    "type": "object",
118                    "properties": {
119                        "palace":  {"type": "string", "description": "Palace ID (optional if server started with --palace)"},
120                        "text":    {"type": "string", "description": "Memory content"},
121                        "room":    {"type": "string", "description": "Room type (optional)"},
122                        "wing":    {"type": "string", "description": "ADR-0027: optional wing (scope/ownership) id or label that OWNS this room — e.g. an agent type such as `engineer`. Omit for the palace's default wing, which is the pre-wing behaviour. With it, `engineer`+`Planning` and `pm`+`Planning` are two distinct rooms. Errors if the wing does not exist; create it with wing_create or list wings with wing_list."},
123                        "tags":    {"type": "array", "items": {"type": "string"}},
124                        "force":   {"type": "boolean", "description": "Explicit operator override: bypasses the content-QUALITY gates for this write — the blocklist (auto-capture noise patterns), the short-content check, the dedup window, and the noise-pattern filter. Issue #2520: does NOT bypass secret/credential detection — a force=true write of secret-shaped content (API keys, tokens) is still rejected. Use sparingly; intended for app-managed writers (e.g. session/turn recorders) that need deterministic storage regardless of heuristic false positives.", "default": false},
125                        "allow_secret_like": {"type": "boolean", "description": "DANGEROUS, rarely needed: bypasses the secret/credential heuristic gate specifically, on top of whatever `force` already bypasses. Only set this when you are DELIBERATELY storing content that looks like a credential (e.g. a redacted example or test fixture) and have confirmed it contains no real secret. Automated writers (turn recorders, auto-capture hooks) must NOT set this — it exists for rare, explicit human/operator overrides only.", "default": false},
126                        "context": {"type": "string", "description": "Optional surrounding context. When supplied alongside very short content (< 4 words), the context is prepended (separated by `---`) so the stored memory has standalone meaning; without it, short content is dropped (issue #215)."},
127                        "fact_key":  {"type": "string", "description": "ADR-0028 Tier C: the slot this CURRENT fact occupies, as `<domain>:<id>/<aspect>` — e.g. `pr:4818/state`, `ws:tm-03/resume`, `daemon:trusty-search/install-state`. One slot holds one live fact: writing a slot that is already occupied atomically retires the prior occupant, which stays readable but stops being current. Use this for anything a later event makes FALSE (an in-flight PR's head SHA, a session resume target, a daemon's install state) so it retires itself instead of being asserted for weeks after it stopped being true. Do NOT use it for standing rules or historical records. A key that is not namespaced, or an `expires_at` that has already passed, is REFUSED — the memory is still stored, as an ordinary drawer with no slot, and the response says `tier: \"E\"` with `tier_c_refused`."},
128                        "expires_at": {"type": "string", "description": "ADR-0028 Tier C retirement condition: RFC 3339 timestamp (e.g. `2026-08-06T12:00:00Z`) after which this fact stops being current. With `fact_key` and omitted, a 24-hour default applies — a Tier C fact ALWAYS has a retirement condition. Without `fact_key` this is just an ordinary drawer TTL. A timestamp already in the past is refused rather than admitted."},
129                        "cwd":         {"type": "string", "description": "DOC-53: optional caller working directory, used to derive the writer's `creator:workstream=`/`ws:` attribution tags (via the `.worktrees/<name>` path segment). The MCP stdio bridge sets this automatically per-request; you normally do not need to pass it yourself. Never falls back to this shared daemon's own cwd."},
130                        "workstream":  {"type": "string", "description": "DOC-53: optional explicit workstream/session name for the `creator:workstream=`/`ws:` attribution tags — wins over any value derived from `cwd`. The MCP stdio bridge sets this automatically per-request; you normally do not need to pass it yourself."}
131                    },
132                    "required": memory_remember_required,
133                }
134            },
135            {
136                "name": "memory_note",
137                "description": "Curated shortcut for short, high-signal facts (\"User prefers snake_case\", \"Deploy target is prod-east\"). Bypasses the token-length filter but still rejects auto-capture noise. Stored as DrawerType::UserFact with importance 1.0. Issue #215: a `context` argument can be supplied to wrap an otherwise meaningless single-word response.",
138                "inputSchema": {
139                    "type": "object",
140                    "properties": {
141                        "palace":  {"type": "string"},
142                        "content": {"type": "string", "description": "Brief fact to remember"},
143                        "room":    {"type": "string", "description": "ADR-0027: room to file this note in (Frontend, Backend, Testing, Planning, Documentation, Research, Configuration, Meetings, General, or any custom name). Defaults to General; list a palace's rooms with room_list."},
144                        "tags":    {"type": "array", "items": {"type": "string"}},
145                        "context": {"type": "string", "description": "Optional surrounding context. Prepended to `content` (separated by `---`) when supplied; with very short content (< 4 words) and no context the write is skipped (issue #215)."},
146                        "fact_key":  {"type": "string", "description": "ADR-0028 Tier C slot, `<domain>:<id>/<aspect>` (e.g. `pr:4818/state`). Same semantics as memory_remember's: one slot, one live fact, and writing an occupied slot retires its prior occupant. Reach for it here whenever the note asserts something a later event makes false — memory_note pins importance 1.0, so a stale note here is the exact failure ADR-0028 exists to stop."},
147                        "expires_at": {"type": "string", "description": "ADR-0028 Tier C retirement condition: RFC 3339 timestamp after which the fact stops being current. With `fact_key` and omitted, a 24-hour default applies."},
148                        "cwd":         {"type": "string", "description": "DOC-53: optional caller working directory, used to derive the writer's `creator:workstream=`/`ws:` attribution tags. The MCP stdio bridge sets this automatically per-request."},
149                        "workstream":  {"type": "string", "description": "DOC-53: optional explicit workstream/session name for the `creator:workstream=`/`ws:` attribution tags — wins over any value derived from `cwd`. The MCP stdio bridge sets this automatically per-request."}
150                    },
151                    "required": memory_note_required,
152                }
153            },
154            {
155                "name": "memory_recall",
156                "description": "Recall memories using L0+L1+L2 progressive retrieval. Pass `room` to scope the search to one room, or `wing` to scope it to one owner's rooms (ADR-0027).",
157                "inputSchema": {
158                    "type": "object",
159                    "properties": {
160                        "palace": {"type": "string"},
161                        "query":  {"type": "string"},
162                        "room":   {"type": "string", "description": "ADR-0027: restrict the semantic layer to this room. The always-on identity/essential layers (L0/L1) are still returned — they are the palace's baseline grounding, not search results. Use room_list to discover a palace's rooms."},
163                        "top_k":  {"type": "integer", "default": 10},
164                        "wing":   {"type": "string", "description": "ADR-0027: optional wing (scope) id or label. Restricts the L2 search to the rooms that wing owns — 'recall everything the engineer wing has learned' in one query. Palace identity/essentials (L0/L1) are always included since they are not any one wing's property. Mutually exclusive with `room`. Omit for an unscoped recall. Errors if the wing does not exist (see wing_list)."}
165                    },
166                    "required": memory_recall_required,
167                }
168            },
169            {
170                "name": "memory_recall_deep",
171                "description": "Deep recall using L3 full HNSW search. Pass `room` to scope the search to one room (ADR-0027).",
172                "inputSchema": {
173                    "type": "object",
174                    "properties": {
175                        "palace": {"type": "string"},
176                        "query":  {"type": "string"},
177                        "room":   {"type": "string", "description": "ADR-0027: restrict the deep search to this room. Same semantics as memory_recall's `room`."},
178                        "top_k":  {"type": "integer", "default": 10}
179                    },
180                    "required": memory_recall_required,
181                }
182            },
183            {
184                "name": "palace_create",
185                "description": "Create a new memory palace.",
186                "inputSchema": {
187                    "type": "object",
188                    "properties": {
189                        "name":        {"type": "string"},
190                        "description": {"type": "string"},
191                        "cwd":         {"type": "string", "description": "Optional caller working directory used for palace-name enforcement. Pass the project root (or any path inside it) so the pin file at `.trusty-tools/trusty-memory.yaml` is honoured. When omitted, the daemon's own cwd is used (rarely meaningful for remote calls)."},
192                        "force":       {"type": "boolean", "description": "Bypass project-slug validation so an application can create a palace under an arbitrary slug (spec-001: chat-session manager, one palace per app/tenant). Defaults to false.", "default": false}
193                    },
194                    "required": ["name"]
195                }
196            },
197            {
198                "name": "palace_list",
199                "description": "List all palaces on this machine.",
200                "inputSchema": {"type": "object", "properties": {}}
201            },
202            {
203                "name": "palace_delete",
204                "description": "Delete an entire memory palace, including its drawers, vectors, and knowledge graph. Refuses to delete a non-empty palace unless `force=true` is set.",
205                "inputSchema": {
206                    "type": "object",
207                    "properties": {
208                        "palace_id": {"type": "string", "description": "Id of the palace to delete."},
209                        "force":     {"type": "boolean", "description": "Required when the palace still has drawers; defaults to false.", "default": false}
210                    },
211                    "required": ["palace_id"]
212                }
213            },
214            {
215                "name": "palace_update",
216                "description": "Update the display name of an existing palace. The palace's drawers, vectors, and knowledge graph are preserved; only the human-readable name changes.",
217                "inputSchema": {
218                    "type": "object",
219                    "properties": {
220                        "palace_id": {"type": "string", "description": "Id of the palace to rename."},
221                        "name":      {"type": "string", "description": "New display name. Trimmed; must be non-empty."}
222                    },
223                    "required": ["palace_id", "name"]
224                }
225            },
226            {
227                "name": "kg_assert",
228                "description": "Assert a fact in the temporal knowledge graph.",
229                "inputSchema": {
230                    "type": "object",
231                    "properties": {
232                        "palace":     {"type": "string"},
233                        "subject":    {"type": "string"},
234                        "predicate":  {"type": "string"},
235                        "object":     {"type": "string"},
236                        "confidence": {"type": "number", "default": 1.0},
237                        "provenance": {"type": "string"}
238                    },
239                    "required": kg_assert_required,
240                }
241            },
242            {
243                "name": "kg_retract_triple",
244                "description": "Retract one fact from the temporal knowledge graph — the inverse of kg_assert. Targets the FULL (subject, predicate, object) key, so every other object at the same (subject, predicate) pair stays active; use it to take back a single wrong assertion. Re-asserting is not a substitute: for predicates outside the functional set (is-a, works-at, uses, depends-on among them) a new object joins the wrong one rather than replacing it. Returns {closed, retracted}: `closed` is how many active triples were closed — 1 for a retraction, 0 when nothing matched that exact triple. A 0 is a real no-op, not an error, so calling twice is safe; the response carries `reason` to say so.",
245                "inputSchema": {
246                    "type": "object",
247                    "properties": {
248                        "palace":    {"type": "string", "description": "Palace ID (optional if server started with --palace)"},
249                        "subject":   {"type": "string"},
250                        "predicate": {"type": "string"},
251                        "object":    {"type": "string", "description": "The exact object to retract. Required: omitting it does NOT retract the whole (subject, predicate) pair, it is an error — pair-wide retraction is deliberately not on this tool."}
252                    },
253                    "required": kg_retract_triple_required,
254                }
255            },
256            {
257                "name": "kg_query",
258                "description": "Query active knowledge-graph triples for a subject.",
259                "inputSchema": {
260                    "type": "object",
261                    "properties": {
262                        "palace":  {"type": "string"},
263                        "subject": {"type": "string"}
264                    },
265                    "required": kg_query_required,
266                }
267            },
268            {
269                "name": "kg_list_subjects",
270                "description": "List the subjects this palace's knowledge graph actually holds, ordered by subject. Call this BEFORE kg_query instead of guessing a subject: kg_query needs a subject you already know, and a guessed name that misses costs a round trip that this call spends better. (A kg_query miss does say which miss it was — `graph_state: subject_not_found` vs `graph_empty` — so an empty result is never ambiguous.) Subjects are namespaced by kind — `tag:<name>`, `topic:<name>`, `drawer:<uuid>`, `room:<name>` — alongside bare entity names asserted by kg_assert. Returns {palace, subjects, with_counts, truncated}. `truncated: true` means a subject beyond this page was actually seen, so raising `limit` will show more; a page that exactly fills `limit` with nothing behind it reports `false`.",
271                "inputSchema": {
272                    "type": "object",
273                    "properties": {
274                        "palace":      {"type": "string", "description": "Palace ID (optional if server started with --palace)"},
275                        "limit":       {"type": "integer", "description": "Max subjects to return. Default 50, clamped to 1..=200.", "default": 50},
276                        "with_counts": {"type": "boolean", "description": "Return {subject, count} objects carrying each subject's active-triple count instead of bare subject strings. Use it to find the densest subjects to query first.", "default": false}
277                    },
278                    "required": kg_list_subjects_required,
279                }
280            },
281            {
282                "name": "memory_list",
283                "description": "List drawers in a palace, optionally filtered by wing, room type, or tag.",
284                "inputSchema": {
285                    "type": "object",
286                    "properties": {
287                        "palace": {"type": "string"},
288                        "room":   {"type": "string", "description": "Filter by room type (Frontend, Backend, Testing, Planning, Documentation, Research, Configuration, Meetings, General, or custom)"},
289                        "wing":   {"type": "string", "description": "ADR-0027: filter by wing (scope) id or label — every drawer in every room that wing owns. Mutually exclusive with `room` for now; passing both is an error rather than a silently-ignored filter. Errors if the wing does not exist (see wing_list)."},
290                        "tag":    {"type": "string", "description": "Filter by tag"},
291                        "limit":  {"type": "integer", "description": "Max results (default 50)"}
292                    },
293                    "required": memory_list_required,
294                }
295            },
296            {
297                "name": "memory_forget",
298                // #5231: the caller can only trust a delete if the tool says
299                // which of the two things happened, so the contract is in the
300                // description an LLM caller actually reads.
301                "description": "Delete a drawer from a palace by its UUID. Returns status='deleted' when a drawer was removed, or status='not_found' when no drawer with that id existed (nothing was deleted).",
302                "inputSchema": {
303                    "type": "object",
304                    "properties": {
305                        "palace":    {"type": "string"},
306                        "drawer_id": {"type": "string", "description": "UUID of the drawer to delete"}
307                    },
308                    "required": memory_forget_required,
309                }
310            },
311            {
312                "name": "palace_info",
313                "description": "Get metadata and stats for a single palace.",
314                "inputSchema": {
315                    "type": "object",
316                    "properties": {
317                        "palace": {"type": "string"}
318                    },
319                    "required": palace_info_required,
320                }
321            },
322            {
323                "name": "palace_compact",
324                "description": "Remove orphaned vector index entries (vectors with no matching drawer row). See issue #49.",
325                "inputSchema": {
326                    "type": "object",
327                    "properties": {
328                        "palace": {"type": "string"}
329                    },
330                    "required": palace_compact_required,
331                }
332            },
333            {
334                "name": "palace_reembed",
335                "description": "#4906: report drawers that have no vector (durable but unfindable), and optionally re-embed them. Defaults to a dry run. #5005: `missing: 0` does NOT mean every drawer is findable — a drawer lost to an id collision has a vector row and is still unreachable. Before treating this report as a complete account of what is retrievable — and ALWAYS before deleting a drawer on the strength of it — read `alias_audit`: act only on `is_clean: true`, and run `palace_unalias` first when it is false. Read `alias_audit.key_rows` vs `distinct_vector_ids` directly if you need the raw counts; they cannot be masked.",
336                "inputSchema": {
337                    "type": "object",
338                    "properties": {
339                        "palace":  {"type": "string"},
340                        "dry_run": {"type": "boolean", "description": "Report only; do not embed. Default true."},
341                        "limit":   {"type": "integer", "description": "Cap repairs per run."}
342                    },
343                    "required": palace_compact_required,
344                }
345            },
346            {
347                "name": "palace_unalias",
348                "description": "#5005: free drawers whose vector was destroyed by an id collision (`palace_reembed` reports these as `aliased`), so a re-embed can repair them. Defaults to a dry run. Branch on `outcome` (clean/planned/repaired/partial/unavailable), never on the id counts — `partial` and `unavailable` are not successes. Run `palace_reembed` afterwards to make the freed drawers findable again.",
349                "inputSchema": {
350                    "type": "object",
351                    "properties": {
352                        "palace":  {"type": "string"},
353                        "dry_run": {"type": "boolean", "description": "Name the drawer ids that would be freed; delete nothing. Default true."}
354                    },
355                    "required": palace_compact_required,
356                }
357            },
358            {
359                "name": "add_alias",
360                "description": "Add a short→full alias (e.g. tga → trusty-git-analytics) to the prompt-facts surface. Asserts the alias as a hot KG triple and refreshes the session-init prompt cache.",
361                "inputSchema": {
362                    "type": "object",
363                    "properties": {
364                        "palace": {"type": "string", "description": "Palace ID (optional if server started with --palace)"},
365                        "short": {"type": "string", "description": "Short name / alias (subject)"},
366                        "full":  {"type": "string", "description": "Full / canonical name (object)"},
367                        "extra": {"type": "string", "description": "Optional extra context appended to the full name"}
368                    },
369                    "required": add_alias_required,
370                }
371            },
372            {
373                "name": "list_prompt_facts",
374                "description": "List every active prompt-fact triple (aliases, conventions, facts, shorthands) across all palaces.",
375                "inputSchema": {"type": "object", "properties": {}}
376            },
377            {
378                "name": "remove_prompt_fact",
379                "description": "Retract the active triple for a (subject, predicate) pair from the prompt-facts surface. Closes the interval without inserting a replacement.",
380                "inputSchema": {
381                    "type": "object",
382                    "properties": {
383                        "subject":   {"type": "string"},
384                        "predicate": {"type": "string", "description": "One of is_alias_for, has_convention, is_fact, is_shorthand_for"}
385                    },
386                    "required": ["subject", "predicate"],
387                }
388            },
389            {
390                "name": "get_prompt_context",
391                "description": "Fetch the current project context (aliases, conventions, facts, shorthands) from the memory palace as a Markdown block ready to drop into the model's working context. Call at the start of each turn. Pass an optional `query` to filter to facts whose subject or object contains the query string (case-insensitive).",
392                "inputSchema": {
393                    "type": "object",
394                    "properties": {
395                        "query": {
396                            "type": "string",
397                            "description": "Optional filter — only return facts whose subject or object contains this string (case-insensitive). Omit to return all hot facts."
398                        }
399                    }
400                }
401            },
402            {
403                "name": "discover_aliases",
404                "description": "Auto-discover project aliases by scanning Cargo workspace members, binary names, first-letter abbreviations, and the git remote. Asserts any newly-discovered (short, is_alias_for, full) triples into the resolved palace and rebuilds the prompt cache. Skips triples that already exist active in the KG.",
405                "inputSchema": {
406                    "type": "object",
407                    "properties": {
408                        "palace": {"type": "string", "description": "Palace ID (optional if server started with --palace)"},
409                        "project_root": {"type": "string", "description": "Optional filesystem path to scan. Defaults to the process cwd."}
410                    },
411                    "required": discover_aliases_required,
412                }
413            },
414            {
415                "name": "kg_gaps",
416                "description": "List knowledge gaps detected in the memory palace graph. Returns communities (clusters of related entities) with low internal density that may benefit from additional knowledge. Populated by the dream cycle; an empty list means no cycle has run yet.",
417                "inputSchema": {
418                    "type": "object",
419                    "properties": {
420                        "palace": {"type": "string", "description": "Palace name (optional, defaults to the active palace)"}
421                    }
422                }
423            },
424            {
425                "name": "kg_bootstrap",
426                "description": "Seed the knowledge graph from well-known project files (Cargo.toml, package.json, pyproject.toml, go.mod, CLAUDE.md, .git/config). Asserts structured triples (has_language, has_version, source_repo, ...) plus temporal metadata (created_at, bootstrapped_at). Idempotent: re-running refreshes bootstrapped_at without disturbing created_at. See issue #60.",
427                "inputSchema": {
428                    "type": "object",
429                    "properties": {
430                        "palace":       {"type": "string", "description": "Palace ID (optional if server started with --palace)"},
431                        "project_path": {"type": "string", "description": "Filesystem path to scan. Omit to scan the palace's own data dir (temporal metadata only)."}
432                    }
433                }
434            },
435            {
436                "name": "memory_recall_all",
437                "description": "Semantic search across ALL palaces simultaneously. Returns the top-k most relevant drawers ranked by similarity, regardless of which palace they belong to. Each result includes a `palace_id` field identifying its source.",
438                "inputSchema": {
439                    "type": "object",
440                    "properties": {
441                        "q":     {"type": "string", "description": "Free-text query"},
442                        "top_k": {"type": "integer", "default": 10},
443                        "deep":  {"type": "boolean", "default": false}
444                    },
445                    "required": ["q"],
446                }
447            },
448            {
449                "name": "memory_send_message",
450                "description": "Send an inter-project message (issue #99). Writes a tagged drawer into the recipient palace; the recipient's SessionStart hook picks it up via `trusty-memory inbox-check`. `to_palace` is the recipient repo slug (e.g. `trusty-tools`, `claude-mpm`). `from_palace` defaults to the calling project's cwd-derived slug when omitted.",
451                "inputSchema": {
452                    "type": "object",
453                    "properties": {
454                        "to_palace":   {"type": "string", "description": "Recipient palace id (repo slug)."},
455                        "purpose":     {"type": "string", "description": "Free-text purpose / category (e.g. `task`, `notify`, `reply`)."},
456                        "content":     {"type": "string", "description": "Message body — plain text, no length limit. Rendered into the recipient session as a Markdown block."},
457                        "from_palace": {"type": "string", "description": "Sender palace id (optional, defaults to cwd-derived slug)."},
458                        "cwd":         {"type": "string", "description": "DOC-53: optional caller working directory, used to derive the sender's `creator:workstream=`/`ws:` attribution tags. The MCP stdio bridge sets this automatically per-request."},
459                        "workstream":  {"type": "string", "description": "DOC-53: optional explicit workstream/session name for the sender's `creator:workstream=`/`ws:` attribution tags — wins over any value derived from `cwd`. The MCP stdio bridge sets this automatically per-request."}
460                    },
461                    "required": ["to_palace", "purpose", "content"],
462                }
463            },
464            {
465                "name": "upgrade",
466                "description": "Check for or install a new version of trusty-memory (issue #537). With check=true (or without confirm): report current vs. available version only — NEVER installs. With confirm=true: install via `cargo install trusty-memory --locked`, run a binary health gate, then restart the daemon under launchd (or print a restart hint when not supervised). The MCP response is returned BEFORE the daemon exits so the client sees the result before reconnecting.",
467                "inputSchema": {
468                    "type": "object",
469                    "properties": {
470                        "check":   {"type": "boolean", "description": "Report current and available versions only. No install. Default: true when confirm is absent.", "default": true},
471                        "confirm": {"type": "boolean", "description": "Set to true to install the new version. NEVER set automatically — the operator must explicitly pass confirm=true.", "default": false}
472                    },
473                    "required": []
474                }
475            },
476            crate::console_metrics::descriptor()
477        ]
478    });
479    // spec-001 Phase 4 (issue #1722) + DOC-53 (2026-07-23): splice task and
480    // chat-session/dream tool schemas. Defined in sibling modules
481    // (task_definitions.rs, chat_definitions.rs) to respect the 500-SLOC
482    // production cap on this file.
483    let tools = result["tools"].as_array_mut().expect("tools is array");
484    let metrics = tools.pop().expect("console_metrics sentinel");
485    tools.extend(task_tool_definitions(has_default));
486    tools.extend(chat_tool_definitions(has_default));
487    // ADR-0027 T6 (#4805) / T9 (#4809): the room and wing surfaces, each in its
488    // own sibling module for the same 500-SLOC reason as the task and chat groups.
489    tools.extend(room_tool_definitions(has_default));
490    tools.extend(wing_tool_definitions(has_default));
491    tools.push(metrics);
492    result
493}