1use 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
17pub 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
37pub fn tool_definitions() -> Value {
48 tool_definitions_with(false)
49}
50
51pub 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 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 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 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 "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 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 tools.extend(room_tool_definitions(has_default));
490 tools.extend(wing_tool_definitions(has_default));
491 tools.push(metrics);
492 result
493}