1use serde_json::{json, Value};
11
12use super::task_definitions::task_tool_definitions;
13
14pub struct MemoryMcpServer;
21
22impl MemoryMcpServer {
23 pub fn new() -> Self {
24 Self
25 }
26}
27
28impl Default for MemoryMcpServer {
29 fn default() -> Self {
30 Self::new()
31 }
32}
33
34pub fn tool_definitions() -> Value {
45 tool_definitions_with(false)
46}
47
48pub fn tool_definitions_with(has_default: bool) -> Value {
59 let memory_remember_required: Vec<&str> = if has_default {
60 vec!["text"]
61 } else {
62 vec!["palace", "text"]
63 };
64 let memory_recall_required: Vec<&str> = if has_default {
65 vec!["query"]
66 } else {
67 vec!["palace", "query"]
68 };
69 let kg_assert_required: Vec<&str> = if has_default {
70 vec!["subject", "predicate", "object"]
71 } else {
72 vec!["palace", "subject", "predicate", "object"]
73 };
74 let kg_query_required: Vec<&str> = if has_default {
75 vec!["subject"]
76 } else {
77 vec!["palace", "subject"]
78 };
79 let memory_list_required: Vec<&str> = if has_default { vec![] } else { vec!["palace"] };
80 let memory_forget_required: Vec<&str> = if has_default {
81 vec!["drawer_id"]
82 } else {
83 vec!["palace", "drawer_id"]
84 };
85 let palace_info_required: Vec<&str> = if has_default { vec![] } else { vec!["palace"] };
86 let palace_compact_required: Vec<&str> = if has_default { vec![] } else { vec!["palace"] };
87 let memory_note_required: Vec<&str> = if has_default {
88 vec!["content"]
89 } else {
90 vec!["palace", "content"]
91 };
92 let add_alias_required: Vec<&str> = if has_default {
96 vec!["short", "full"]
97 } else {
98 vec!["palace", "short", "full"]
99 };
100 let discover_aliases_required: Vec<&str> = if has_default { vec![] } else { vec!["palace"] };
101 let chat_session_palace_required: Vec<&str> = if has_default { vec![] } else { vec!["palace"] };
105 let chat_session_get_required: Vec<&str> = if has_default {
106 vec!["session_id"]
107 } else {
108 vec!["palace", "session_id"]
109 };
110 let chat_session_add_turn_required: Vec<&str> = if has_default {
111 vec!["session_id", "role", "content"]
112 } else {
113 vec!["palace", "session_id", "role", "content"]
114 };
115 let dream_consolidate_room_required: Vec<&str> =
116 if has_default { vec![] } else { vec!["palace"] };
117 let chat_turn_append_required: Vec<&str> = if has_default {
119 vec!["session_id", "prompt", "response"]
120 } else {
121 vec!["palace", "session_id", "prompt", "response"]
122 };
123 let chat_session_delete_required: Vec<&str> = if has_default {
124 vec!["session_id"]
125 } else {
126 vec!["palace", "session_id"]
127 };
128
129 let mut result = json!({
130 "tools": [
131 {
132 "name": "memory_remember",
133 "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 filtering, or use memory_note for short curated facts.",
134 "inputSchema": {
135 "type": "object",
136 "properties": {
137 "palace": {"type": "string", "description": "Palace ID (optional if server started with --palace)"},
138 "text": {"type": "string", "description": "Memory content"},
139 "room": {"type": "string", "description": "Room type (optional)"},
140 "tags": {"type": "array", "items": {"type": "string"}},
141 "force": {"type": "boolean", "description": "Bypass the signal/noise filter. Use sparingly — intended for explicit operator overrides.", "default": false},
142 "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)."}
143 },
144 "required": memory_remember_required,
145 }
146 },
147 {
148 "name": "memory_note",
149 "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.",
150 "inputSchema": {
151 "type": "object",
152 "properties": {
153 "palace": {"type": "string"},
154 "content": {"type": "string", "description": "Brief fact to remember"},
155 "tags": {"type": "array", "items": {"type": "string"}},
156 "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)."}
157 },
158 "required": memory_note_required,
159 }
160 },
161 {
162 "name": "memory_recall",
163 "description": "Recall memories using L0+L1+L2 progressive retrieval.",
164 "inputSchema": {
165 "type": "object",
166 "properties": {
167 "palace": {"type": "string"},
168 "query": {"type": "string"},
169 "top_k": {"type": "integer", "default": 10}
170 },
171 "required": memory_recall_required,
172 }
173 },
174 {
175 "name": "memory_recall_deep",
176 "description": "Deep recall using L3 full HNSW search.",
177 "inputSchema": {
178 "type": "object",
179 "properties": {
180 "palace": {"type": "string"},
181 "query": {"type": "string"},
182 "top_k": {"type": "integer", "default": 10}
183 },
184 "required": memory_recall_required,
185 }
186 },
187 {
188 "name": "palace_create",
189 "description": "Create a new memory palace.",
190 "inputSchema": {
191 "type": "object",
192 "properties": {
193 "name": {"type": "string"},
194 "description": {"type": "string"},
195 "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)."},
196 "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}
197 },
198 "required": ["name"]
199 }
200 },
201 {
202 "name": "palace_list",
203 "description": "List all palaces on this machine.",
204 "inputSchema": {"type": "object", "properties": {}}
205 },
206 {
207 "name": "palace_delete",
208 "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.",
209 "inputSchema": {
210 "type": "object",
211 "properties": {
212 "palace_id": {"type": "string", "description": "Id of the palace to delete."},
213 "force": {"type": "boolean", "description": "Required when the palace still has drawers; defaults to false.", "default": false}
214 },
215 "required": ["palace_id"]
216 }
217 },
218 {
219 "name": "palace_update",
220 "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.",
221 "inputSchema": {
222 "type": "object",
223 "properties": {
224 "palace_id": {"type": "string", "description": "Id of the palace to rename."},
225 "name": {"type": "string", "description": "New display name. Trimmed; must be non-empty."}
226 },
227 "required": ["palace_id", "name"]
228 }
229 },
230 {
231 "name": "kg_assert",
232 "description": "Assert a fact in the temporal knowledge graph.",
233 "inputSchema": {
234 "type": "object",
235 "properties": {
236 "palace": {"type": "string"},
237 "subject": {"type": "string"},
238 "predicate": {"type": "string"},
239 "object": {"type": "string"},
240 "confidence": {"type": "number", "default": 1.0},
241 "provenance": {"type": "string"}
242 },
243 "required": kg_assert_required,
244 }
245 },
246 {
247 "name": "kg_query",
248 "description": "Query active knowledge-graph triples for a subject.",
249 "inputSchema": {
250 "type": "object",
251 "properties": {
252 "palace": {"type": "string"},
253 "subject": {"type": "string"}
254 },
255 "required": kg_query_required,
256 }
257 },
258 {
259 "name": "memory_list",
260 "description": "List drawers in a palace, optionally filtered by room type or tag.",
261 "inputSchema": {
262 "type": "object",
263 "properties": {
264 "palace": {"type": "string"},
265 "room": {"type": "string", "description": "Filter by room type (Frontend, Backend, Testing, Planning, Documentation, Research, Configuration, Meetings, General, or custom)"},
266 "tag": {"type": "string", "description": "Filter by tag"},
267 "limit": {"type": "integer", "description": "Max results (default 50)"}
268 },
269 "required": memory_list_required,
270 }
271 },
272 {
273 "name": "memory_forget",
274 "description": "Delete a drawer from a palace by its UUID.",
275 "inputSchema": {
276 "type": "object",
277 "properties": {
278 "palace": {"type": "string"},
279 "drawer_id": {"type": "string", "description": "UUID of the drawer to delete"}
280 },
281 "required": memory_forget_required,
282 }
283 },
284 {
285 "name": "palace_info",
286 "description": "Get metadata and stats for a single palace.",
287 "inputSchema": {
288 "type": "object",
289 "properties": {
290 "palace": {"type": "string"}
291 },
292 "required": palace_info_required,
293 }
294 },
295 {
296 "name": "palace_compact",
297 "description": "Remove orphaned vector index entries (vectors with no matching drawer row). See issue #49.",
298 "inputSchema": {
299 "type": "object",
300 "properties": {
301 "palace": {"type": "string"}
302 },
303 "required": palace_compact_required,
304 }
305 },
306 {
307 "name": "add_alias",
308 "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.",
309 "inputSchema": {
310 "type": "object",
311 "properties": {
312 "palace": {"type": "string", "description": "Palace ID (optional if server started with --palace)"},
313 "short": {"type": "string", "description": "Short name / alias (subject)"},
314 "full": {"type": "string", "description": "Full / canonical name (object)"},
315 "extra": {"type": "string", "description": "Optional extra context appended to the full name"}
316 },
317 "required": add_alias_required,
318 }
319 },
320 {
321 "name": "list_prompt_facts",
322 "description": "List every active prompt-fact triple (aliases, conventions, facts, shorthands) across all palaces.",
323 "inputSchema": {"type": "object", "properties": {}}
324 },
325 {
326 "name": "remove_prompt_fact",
327 "description": "Retract the active triple for a (subject, predicate) pair from the prompt-facts surface. Closes the interval without inserting a replacement.",
328 "inputSchema": {
329 "type": "object",
330 "properties": {
331 "subject": {"type": "string"},
332 "predicate": {"type": "string", "description": "One of is_alias_for, has_convention, is_fact, is_shorthand_for"}
333 },
334 "required": ["subject", "predicate"],
335 }
336 },
337 {
338 "name": "get_prompt_context",
339 "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).",
340 "inputSchema": {
341 "type": "object",
342 "properties": {
343 "query": {
344 "type": "string",
345 "description": "Optional filter — only return facts whose subject or object contains this string (case-insensitive). Omit to return all hot facts."
346 }
347 }
348 }
349 },
350 {
351 "name": "discover_aliases",
352 "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.",
353 "inputSchema": {
354 "type": "object",
355 "properties": {
356 "palace": {"type": "string", "description": "Palace ID (optional if server started with --palace)"},
357 "project_root": {"type": "string", "description": "Optional filesystem path to scan. Defaults to the process cwd."}
358 },
359 "required": discover_aliases_required,
360 }
361 },
362 {
363 "name": "kg_gaps",
364 "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.",
365 "inputSchema": {
366 "type": "object",
367 "properties": {
368 "palace": {"type": "string", "description": "Palace name (optional, defaults to the active palace)"}
369 }
370 }
371 },
372 {
373 "name": "kg_bootstrap",
374 "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.",
375 "inputSchema": {
376 "type": "object",
377 "properties": {
378 "palace": {"type": "string", "description": "Palace ID (optional if server started with --palace)"},
379 "project_path": {"type": "string", "description": "Filesystem path to scan. Omit to scan the palace's own data dir (temporal metadata only)."}
380 }
381 }
382 },
383 {
384 "name": "memory_recall_all",
385 "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.",
386 "inputSchema": {
387 "type": "object",
388 "properties": {
389 "q": {"type": "string", "description": "Free-text query"},
390 "top_k": {"type": "integer", "default": 10},
391 "deep": {"type": "boolean", "default": false}
392 },
393 "required": ["q"],
394 }
395 },
396 {
397 "name": "memory_send_message",
398 "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.",
399 "inputSchema": {
400 "type": "object",
401 "properties": {
402 "to_palace": {"type": "string", "description": "Recipient palace id (repo slug)."},
403 "purpose": {"type": "string", "description": "Free-text purpose / category (e.g. `task`, `notify`, `reply`)."},
404 "content": {"type": "string", "description": "Message body — plain text, no length limit. Rendered into the recipient session as a Markdown block."},
405 "from_palace": {"type": "string", "description": "Sender palace id (optional, defaults to cwd-derived slug)."}
406 },
407 "required": ["to_palace", "purpose", "content"],
408 }
409 },
410 {
411 "name": "upgrade",
412 "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.",
413 "inputSchema": {
414 "type": "object",
415 "properties": {
416 "check": {"type": "boolean", "description": "Report current and available versions only. No install. Default: true when confirm is absent.", "default": true},
417 "confirm": {"type": "boolean", "description": "Set to true to install the new version. NEVER set automatically — the operator must explicitly pass confirm=true.", "default": false}
418 },
419 "required": []
420 }
421 },
422 {
423 "name": "chat_session_create",
424 "description": "Create a new chat session in a palace (spec-001 chat-session manager). Returns the session id, its creation timestamp, and the message count (0 for a fresh session). Pass an optional session_id to use a caller-chosen id (idempotent — an existing session is returned unchanged); pass an optional title to name a server-generated session. Sessions are stored in the palace's dedicated redb chat store, NOT the generic memory drawer surface.",
425 "inputSchema": {
426 "type": "object",
427 "properties": {
428 "palace": {"type": "string", "description": "Palace slug (optional if server started with --palace)"},
429 "session_id": {"type": "string", "description": "Optional caller-supplied session id; a UUID is generated when omitted."},
430 "title": {"type": "string", "description": "Optional session name (applied only when session_id is omitted)."}
431 },
432 "required": chat_session_palace_required,
433 }
434 },
435 {
436 "name": "chat_session_add_turn",
437 "description": "Append a message (prompt or response) to a chat session's history. Creates the session if it does not yet exist. Returns the new message_count and updated_at. Bypasses the memory_remember signal/noise + dedup gates so sequential conversational turns persist verbatim.",
438 "inputSchema": {
439 "type": "object",
440 "properties": {
441 "palace": {"type": "string"},
442 "session_id": {"type": "string"},
443 "role": {"type": "string", "enum": ["user", "assistant", "system"]},
444 "content": {"type": "string"}
445 },
446 "required": chat_session_add_turn_required,
447 }
448 },
449 {
450 "name": "chat_session_get",
451 "description": "Retrieve a full chat session: metadata plus every turn in chronological order. Errors if the session id is unknown.",
452 "inputSchema": {
453 "type": "object",
454 "properties": {
455 "palace": {"type": "string"},
456 "session_id": {"type": "string"}
457 },
458 "required": chat_session_get_required,
459 }
460 },
461 {
462 "name": "chat_session_list",
463 "description": "List chat sessions in a palace as paginated metadata (id, title, timestamps, message_count) ordered most-recently-updated first. Does not include message bodies. Returns { sessions, total_count }.",
464 "inputSchema": {
465 "type": "object",
466 "properties": {
467 "palace": {"type": "string"},
468 "limit": {"type": "integer", "default": 50},
469 "offset": {"type": "integer", "default": 0}
470 },
471 "required": chat_session_palace_required,
472 }
473 },
474 {
475 "name": "dream_consolidate_room",
476 "description": "Trigger LLM-driven semantic consolidation for one room (or all rooms) of a palace, on demand and synchronously (spec-001). Consolidates facts older than max_age_days into canonical summaries, then evicts the superseded originals so history shrinks. Task drawers are always skipped. No-op (zero counts) when no inference backend (OpenRouter key / local model) is configured. Returns { summary_facts_created, facts_evicted }.",
477 "inputSchema": {
478 "type": "object",
479 "properties": {
480 "palace": {"type": "string"},
481 "room": {"type": "string", "description": "Room to scope to (e.g. Backend, Planning, or a custom name). Omit or null to consolidate all rooms."},
482 "max_age_days": {"type": "integer", "default": 7, "description": "Only consolidate facts older than this many days."}
483 },
484 "required": dream_consolidate_room_required,
485 }
486 },
487 {
488 "name": "palace_dream",
489 "description": "On-demand LLM-driven consolidation for a palace (issue #1721). Alias for dream_consolidate_room with the same parameters; use this name when following the palace_* convention. Triggers a scoped dream/consolidation cycle immediately for the named palace, optionally filtered to one room. Task drawers are always skipped. Gracefully returns zero counts when no inference backend is configured. Returns { palace, room, summary_facts_created, facts_evicted }.",
490 "inputSchema": {
491 "type": "object",
492 "properties": {
493 "palace": {"type": "string"},
494 "room": {"type": "string", "description": "Room to scope to. Omit or null to consolidate all rooms."},
495 "max_age_days": {"type": "integer", "default": 7, "description": "Only consolidate facts older than this many days."}
496 },
497 "required": dream_consolidate_room_required,
498 }
499 },
500 {
501 "name": "chat_session_recall",
502 "description": "Retrieve a full chat session with all turns in order (alias for chat_session_get, preferred name for agent-facing recall). Errors if the session id is unknown.",
503 "inputSchema": {
504 "type": "object",
505 "properties": {
506 "palace": {"type": "string"},
507 "session_id": {"type": "string"}
508 },
509 "required": chat_session_get_required,
510 }
511 },
512 {
513 "name": "chat_session_delete",
514 "description": "Delete a chat session (and its full history) from a palace. Idempotent: deleting an unknown session id is a no-op, not an error. Returns { deleted: session_id }.",
515 "inputSchema": {
516 "type": "object",
517 "properties": {
518 "palace": {"type": "string"},
519 "session_id": {"type": "string"}
520 },
521 "required": chat_session_delete_required,
522 }
523 },
524 {
525 "name": "chat_turn_append",
526 "description": "Append a prompt/response PAIR to a chat session as two consecutive messages (user role then assistant role). Atomic at the session level — both messages are written together. Creates the session implicitly when it does not exist. Returns { message_count, updated_at }.",
527 "inputSchema": {
528 "type": "object",
529 "properties": {
530 "palace": {"type": "string"},
531 "session_id": {"type": "string"},
532 "prompt": {"type": "string", "description": "User-side message (stored with role=user)."},
533 "response": {"type": "string", "description": "Assistant-side message (stored with role=assistant)."}
534 },
535 "required": chat_turn_append_required,
536 }
537 },
538 crate::console_metrics::descriptor()
539 ]
540 });
541 let tools = result["tools"].as_array_mut().expect("tools is array");
544 let metrics = tools.pop().expect("console_metrics sentinel");
545 tools.extend(task_tool_definitions(has_default));
546 tools.push(metrics);
547 result
548}