trusty-memory 0.26.2

MCP server (stdio + Unix socket) for trusty-memory
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
//! MCP `tools/list` schema + server marker for trusty-memory.
//!
//! Why: Concentrates the public tool contract (the `tools/list` payload) in
//! one place so the MCP schema stays auditable and in sync with the handlers.
//! What: Defines `MemoryMcpServer` and the `tool_definitions{,_with}` schema
//! builders moved out of the former monolithic `tools.rs` (issue #607).
//! Test: `tool_definitions_lists_all_tools`,
//! `tool_definitions_drops_palace_required_when_default_set` in `tools::tests`.

use serde_json::{json, Value};

use super::chat_definitions::chat_tool_definitions;
use super::embed_audit_definitions::embed_audit_tool_definitions;
use super::room_definitions::room_tool_definitions;
use super::task_definitions::task_tool_definitions;
use super::wing_definitions::wing_tool_definitions;

/// Marker server type. Reserved for future stateful MCP server impls.
///
/// Why: Keep a stable type name while the protocol-loop is implemented at
/// module level, so external callers can still depend on a server symbol.
/// What: Zero-sized struct with `new` / `Default`.
/// Test: `MemoryMcpServer::default()` constructs without panic.
pub struct MemoryMcpServer;

impl MemoryMcpServer {
    pub fn new() -> Self {
        Self
    }
}

impl Default for MemoryMcpServer {
    fn default() -> Self {
        Self::new()
    }
}

/// MCP `tools/list` response payload.
///
/// Why: Claude Code calls `tools/list` once on connect and uses the schema
/// to drive the tool picker; the schema is the source of truth for arg names.
/// `palace` is required only when the server has no `--palace` default
/// configured — when a default is set, the schema omits `palace` from
/// `required` so clients can drop it.
/// What: Returns a JSON object `{ "tools": [...] }` with all 10 tool defs.
/// Test: `tool_definitions_lists_all_tools`,
/// `tool_definitions_drops_palace_required_when_default_set`.
pub fn tool_definitions() -> Value {
    tool_definitions_with(false)
}

/// Variant of `tool_definitions` aware of whether a default palace is
/// configured. When `has_default` is true, the `palace` argument is moved
/// out of the `required` list for every tool that takes it.
///
/// Why: Lets `handle_message` emit a schema that matches the running
/// server's actual contract — clients reading the schema should see exactly
/// what they need to send.
/// What: Builds the same shape as `tool_definitions` but with conditional
/// `required` arrays.
/// Test: `tool_definitions_drops_palace_required_when_default_set`.
pub fn tool_definitions_with(has_default: bool) -> Value {
    let memory_remember_required: Vec<&str> = if has_default {
        vec!["text"]
    } else {
        vec!["palace", "text"]
    };
    // #6318: `palace` is never required on a palace-scoped READ tool. With
    // neither an argument nor a `--palace` default the handler answers with an
    // index of the palaces on this host, so a client that cannot name one must
    // still be allowed to call. The write tools below keep the conditional.
    let memory_recall_required: Vec<&str> = vec!["query"];
    let kg_assert_required: Vec<&str> = if has_default {
        vec!["subject", "predicate", "object"]
    } else {
        vec!["palace", "subject", "predicate", "object"]
    };
    // Retraction takes the same full triple key as the assertion it undoes, so
    // its `required` list is `kg_assert`'s by construction.
    let kg_retract_triple_required: Vec<&str> = kg_assert_required.clone();
    let kg_query_required: Vec<&str> = vec!["subject"];
    // #4776: subject enumeration takes no argument of its own. #6318: and
    // `palace` is not required either, so the list is empty on both branches.
    let kg_list_subjects_required: Vec<&str> = vec![];
    let memory_list_required: Vec<&str> = vec![];
    let memory_forget_required: Vec<&str> = if has_default {
        vec!["drawer_id"]
    } else {
        vec!["palace", "drawer_id"]
    };
    let palace_info_required: Vec<&str> = vec![];
    let palace_compact_required: Vec<&str> = if has_default { vec![] } else { vec!["palace"] };
    let memory_note_required: Vec<&str> = if has_default {
        vec!["content"]
    } else {
        vec!["palace", "content"]
    };
    // Issue #664: add_alias and discover_aliases both call resolve_palace() but
    // previously omitted `palace` from their schemas, making them uncallable
    // without a server-side default. Now follow the memory_remember pattern.
    let add_alias_required: Vec<&str> = if has_default {
        vec!["short", "full"]
    } else {
        vec!["palace", "short", "full"]
    };
    let discover_aliases_required: Vec<&str> = if has_default { vec![] } else { vec!["palace"] };

    let mut result = json!({
        "tools": [
            {
                "name": "memory_remember",
                "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.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "palace":  {"type": "string", "description": "Palace ID (optional if server started with --palace)"},
                        "text":    {"type": "string", "description": "Memory content"},
                        "room":    {"type": "string", "description": "Room type (optional)"},
                        "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."},
                        "tags":    {"type": "array", "items": {"type": "string"}},
                        "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},
                        "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},
                        "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)."},
                        "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`."},
                        "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."},
                        "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."},
                        "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."}
                    },
                    "required": memory_remember_required,
                }
            },
            {
                "name": "memory_note",
                "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.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "palace":  {"type": "string"},
                        "content": {"type": "string", "description": "Brief fact to remember"},
                        "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."},
                        "tags":    {"type": "array", "items": {"type": "string"}},
                        "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)."},
                        "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."},
                        "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."},
                        "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."},
                        "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."}
                    },
                    "required": memory_note_required,
                }
            },
            {
                "name": "memory_recall",
                "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). #6318: with no `palace` and no server default this succeeds with a palace index — ids, drawer/room/wing counts, rooms, and a `hint` — instead of erroring, so call it to find out which palace to name.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "palace": {"type": "string"},
                        "query":  {"type": "string"},
                        "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."},
                        "top_k":  {"type": "integer", "default": 10},
                        "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)."},
                        "min_score": {"type": "number", "description": "Optional relevance floor. Hits scored by the query — L2, L3, and the lexical lane — are dropped below it before top_k is applied. Setting it makes the search fetch a wider candidate set (4x top_k, capped at 200) so removed hits are backfilled rather than leaving you short; when the corpus holds fewer than top_k qualifying drawers you still get fewer, which no widening can change. L0/L1 identity and essential drawers are never filtered: their scores are a flat 1.0 and an importance value, neither comparable to a similarity. Omit for no floor, which is the previous behaviour. Must be a number — a quoted value like \"0.4\" is rejected, not coerced. 0.4 is the recommended value for PM-context recall: the L2 lane returns loosely related drawers in the 0.38-0.46 band, so 0.4 cuts most of them while keeping hits that are genuinely on topic. The response reports `dropped_below_floor` so you can tell how many were removed and lower the floor if it was too aggressive."},
                        "include_creator_tags": {"type": "boolean", "default": false, "description": "Return the `creator:*` attribution tags (client, version, source, cwd) on each hit. They are hidden by default because they are provenance rather than topic, and they are roughly four tags on every drawer this daemon wrote — most of a recall response's tag bytes. Nothing is deleted: the tags stay in storage and stay queryable through memory_list's `tag` filter. Set true when you are auditing who wrote a memory."}
                    },
                    "required": memory_recall_required,
                }
            },
            {
                "name": "memory_recall_deep",
                "description": "Deep recall using L3 full HNSW search. Pass `room` to scope the search to one room (ADR-0027). #6318: with no `palace` and no server default this returns a palace index (ids, counts, rooms, `hint`) as a successful result instead of an error.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "palace": {"type": "string"},
                        "query":  {"type": "string"},
                        "room":   {"type": "string", "description": "ADR-0027: restrict the deep search to this room. Same semantics as memory_recall's `room`."},
                        "top_k":  {"type": "integer", "default": 10},
                        "min_score": {"type": "number", "description": "Optional relevance floor. Same semantics as memory_recall's `min_score`, applied to the L3 deep lane."},
                        "include_creator_tags": {"type": "boolean", "default": false, "description": "Same semantics as memory_recall's `include_creator_tags`."}
                    },
                    "required": memory_recall_required,
                }
            },
            {
                "name": "palace_create",
                "description": "Create a new memory palace.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "name":        {"type": "string"},
                        "description": {"type": "string"},
                        "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)."},
                        "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}
                    },
                    "required": ["name"]
                }
            },
            {
                "name": "palace_list",
                "description": "List all palaces on this machine.",
                "inputSchema": {"type": "object", "properties": {}}
            },
            {
                "name": "palace_delete",
                "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.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "palace_id": {"type": "string", "description": "Id of the palace to delete."},
                        "force":     {"type": "boolean", "description": "Required when the palace still has drawers; defaults to false.", "default": false}
                    },
                    "required": ["palace_id"]
                }
            },
            {
                "name": "palace_update",
                "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.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "palace_id": {"type": "string", "description": "Id of the palace to rename."},
                        "name":      {"type": "string", "description": "New display name. Trimmed; must be non-empty."}
                    },
                    "required": ["palace_id", "name"]
                }
            },
            {
                "name": "kg_assert",
                "description": "Assert a fact in the temporal knowledge graph.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "palace":     {"type": "string"},
                        "subject":    {"type": "string"},
                        "predicate":  {"type": "string"},
                        "object":     {"type": "string"},
                        "confidence": {"type": "number", "default": 1.0},
                        "provenance": {"type": "string"}
                    },
                    "required": kg_assert_required,
                }
            },
            {
                "name": "kg_retract_triple",
                "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.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "palace":    {"type": "string", "description": "Palace ID (optional if server started with --palace)"},
                        "subject":   {"type": "string"},
                        "predicate": {"type": "string"},
                        "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."}
                    },
                    "required": kg_retract_triple_required,
                }
            },
            {
                "name": "kg_query",
                "description": "Query active knowledge-graph triples for a subject. #6318: with no `palace` and no server default this returns a palace index (ids, counts, rooms, `hint`) as a successful result instead of an error.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "palace":  {"type": "string"},
                        "subject": {"type": "string"}
                    },
                    "required": kg_query_required,
                }
            },
            {
                "name": "kg_list_subjects",
                "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`. #6318: with no `palace` and no server default this returns a palace index (ids, counts, rooms, `hint`) as a successful result instead of an error.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "palace":      {"type": "string", "description": "Palace ID (optional if server started with --palace)"},
                        "limit":       {"type": "integer", "description": "Max subjects to return. Default 50, clamped to 1..=200.", "default": 50},
                        "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}
                    },
                    "required": kg_list_subjects_required,
                }
            },
            {
                "name": "memory_list",
                "description": "List drawers in a palace, optionally filtered by wing, room type, or tag. #6318: with no `palace` and no server default this returns a palace index (ids, counts, rooms, `hint`) as a successful result instead of an error.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "palace": {"type": "string"},
                        "room":   {"type": "string", "description": "Filter by room type (Frontend, Backend, Testing, Planning, Documentation, Research, Configuration, Meetings, General, or custom)"},
                        "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)."},
                        "tag":    {"type": "string", "description": "Filter by tag"},
                        "limit":  {"type": "integer", "description": "Max results (default 50)"}
                    },
                    "required": memory_list_required,
                }
            },
            {
                "name": "memory_forget",
                // #5231: the caller can only trust a delete if the tool says
                // which of the two things happened, so the contract is in the
                // description an LLM caller actually reads.
                "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).",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "palace":    {"type": "string"},
                        "drawer_id": {"type": "string", "description": "UUID of the drawer to delete"}
                    },
                    "required": memory_forget_required,
                }
            },
            {
                "name": "palace_info",
                "description": "Get metadata and stats for a single palace. #6318: with no `palace` and no server default this returns a palace index (ids, counts, rooms, `hint`) as a successful result instead of an error.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "palace": {"type": "string"}
                    },
                    "required": palace_info_required,
                }
            },
            {
                "name": "palace_compact",
                "description": "Remove orphaned vector index entries (vectors with no matching drawer row). See issue #49. VECTOR-INDEX ONLY: this touches index.usearch.redb and does not read, rewrite, prune, or shrink the knowledge-graph store kg.redb. To reclaim kg.redb disk, use palace_dream with compact=true, or the CLI `trusty-memory palace compact <name>` (#6652).",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "palace": {"type": "string"}
                    },
                    "required": palace_compact_required,
                }
            },
            {
                "name": "palace_reembed",
                "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.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "palace":  {"type": "string"},
                        "dry_run": {"type": "boolean", "description": "Report only; do not embed. Default true."},
                        "limit":   {"type": "integer", "description": "Cap repairs per run."}
                    },
                    "required": palace_compact_required,
                }
            },
            {
                "name": "palace_unalias",
                "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.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "palace":  {"type": "string"},
                        "dry_run": {"type": "boolean", "description": "Name the drawer ids that would be freed; delete nothing. Default true."}
                    },
                    "required": palace_compact_required,
                }
            },
            {
                "name": "add_alias",
                "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.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "palace": {"type": "string", "description": "Palace ID (optional if server started with --palace)"},
                        "short": {"type": "string", "description": "Short name / alias (subject)"},
                        "full":  {"type": "string", "description": "Full / canonical name (object)"},
                        "extra": {"type": "string", "description": "Optional extra context appended to the full name"}
                    },
                    "required": add_alias_required,
                }
            },
            {
                "name": "list_prompt_facts",
                "description": "List every active prompt-fact triple (aliases, conventions, facts, shorthands) across all palaces.",
                "inputSchema": {"type": "object", "properties": {}}
            },
            {
                "name": "remove_prompt_fact",
                "description": "Retract the active triple for a (subject, predicate) pair from the prompt-facts surface. Closes the interval without inserting a replacement.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "subject":   {"type": "string"},
                        "predicate": {"type": "string", "description": "One of is_alias_for, has_convention, is_fact, is_shorthand_for"}
                    },
                    "required": ["subject", "predicate"],
                }
            },
            {
                "name": "get_prompt_context",
                "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).",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "query": {
                            "type": "string",
                            "description": "Optional filter — only return facts whose subject or object contains this string (case-insensitive). Omit to return all hot facts."
                        }
                    }
                }
            },
            {
                "name": "discover_aliases",
                "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.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "palace": {"type": "string", "description": "Palace ID (optional if server started with --palace)"},
                        "project_root": {"type": "string", "description": "Optional filesystem path to scan. Defaults to the process cwd."}
                    },
                    "required": discover_aliases_required,
                }
            },
            {
                "name": "kg_gaps",
                "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. #6318: with no `palace` and no server default this returns a palace index (ids, counts, rooms, `hint`) as a successful result instead of an error.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "palace": {"type": "string", "description": "Palace name (optional, defaults to the active palace)"}
                    }
                }
            },
            {
                "name": "kg_bootstrap",
                "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.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "palace":       {"type": "string", "description": "Palace ID (optional if server started with --palace)"},
                        "project_path": {"type": "string", "description": "Filesystem path to scan. Omit to scan the palace's own data dir (temporal metadata only)."}
                    }
                }
            },
            {
                "name": "memory_recall_all",
                "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.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "q":     {"type": "string", "description": "Free-text query"},
                        "top_k": {"type": "integer", "default": 10},
                        "deep":  {"type": "boolean", "default": false},
                        "include_creator_tags": {"type": "boolean", "default": false, "description": "Same semantics as memory_recall's `include_creator_tags`. This tool takes no `min_score`: one floor across palaces would mean a different thing in each corpus."}
                    },
                    "required": ["q"],
                }
            },
            {
                "name": "memory_send_message",
                "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.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "to_palace":   {"type": "string", "description": "Recipient palace id (repo slug)."},
                        "purpose":     {"type": "string", "description": "Free-text purpose / category (e.g. `task`, `notify`, `reply`)."},
                        "content":     {"type": "string", "description": "Message body — plain text, no length limit. Rendered into the recipient session as a Markdown block."},
                        "from_palace": {"type": "string", "description": "Sender palace id (optional, defaults to cwd-derived slug)."},
                        "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."},
                        "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."}
                    },
                    "required": ["to_palace", "purpose", "content"],
                }
            },
            {
                "name": "upgrade",
                "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.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "check":   {"type": "boolean", "description": "Report current and available versions only. No install. Default: true when confirm is absent.", "default": true},
                        "confirm": {"type": "boolean", "description": "Set to true to install the new version. NEVER set automatically — the operator must explicitly pass confirm=true.", "default": false}
                    },
                    "required": []
                }
            },
            crate::console_metrics::descriptor()
        ]
    });
    // spec-001 Phase 4 (issue #1722) + DOC-53 (2026-07-23): splice task and
    // chat-session/dream tool schemas. Defined in sibling modules
    // (task_definitions.rs, chat_definitions.rs) to respect the 500-SLOC
    // production cap on this file.
    let tools = result["tools"].as_array_mut().expect("tools is array");
    let metrics = tools.pop().expect("console_metrics sentinel");
    tools.extend(task_tool_definitions(has_default));
    tools.extend(chat_tool_definitions(has_default));
    tools.extend(super::chat_assets::definitions());
    // ADR-0027 T6 (#4805) / T9 (#4809): the room and wing surfaces, each in its
    // own sibling module for the same 500-SLOC reason as the task and chat groups.
    tools.extend(room_tool_definitions(has_default));
    tools.extend(wing_tool_definitions(has_default));
    // #5000 / #4786: spliced for the recursion limit as much as the SLOC cap —
    // see `embed_audit_definitions`.
    tools.extend(embed_audit_tool_definitions(has_default));
    tools.push(metrics);
    // #7493: every result-returning tool advertises the `max_bytes` / `full`
    // knobs the dispatcher honours, from the same table the fold reads — a
    // tool cannot advertise a knob it does not honour, or honour one it never
    // advertised. Applied after the splices so a spliced group is covered too.
    super::byte_cap::annotate_capped_tools(&mut result["tools"]);
    result
}