//! 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::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"]
};
let memory_recall_required: Vec<&str> = if has_default {
vec!["query"]
} else {
vec!["palace", "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> = if has_default {
vec!["subject"]
} else {
vec!["palace", "subject"]
};
// #4776: subject enumeration takes no argument of its own, so `palace` is
// the whole `required` list when no server default is configured.
let kg_list_subjects_required: Vec<&str> = if has_default { vec![] } else { vec!["palace"] };
let memory_list_required: Vec<&str> = if has_default { vec![] } else { vec!["palace"] };
let memory_forget_required: Vec<&str> = if has_default {
vec!["drawer_id"]
} else {
vec!["palace", "drawer_id"]
};
let palace_info_required: Vec<&str> = if has_default { vec![] } else { vec!["palace"] };
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).",
"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)."}
},
"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).",
"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}
},
"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.",
"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`.",
"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.",
"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.",
"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.",
"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.",
"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}
},
"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));
// 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));
tools.push(metrics);
result
}