1use serde_json::{json, Value};
11
12pub struct MemoryMcpServer;
19
20impl MemoryMcpServer {
21 pub fn new() -> Self {
22 Self
23 }
24}
25
26impl Default for MemoryMcpServer {
27 fn default() -> Self {
28 Self::new()
29 }
30}
31
32pub fn tool_definitions() -> Value {
43 tool_definitions_with(false)
44}
45
46pub fn tool_definitions_with(has_default: bool) -> Value {
57 let memory_remember_required: Vec<&str> = if has_default {
58 vec!["text"]
59 } else {
60 vec!["palace", "text"]
61 };
62 let memory_recall_required: Vec<&str> = if has_default {
63 vec!["query"]
64 } else {
65 vec!["palace", "query"]
66 };
67 let kg_assert_required: Vec<&str> = if has_default {
68 vec!["subject", "predicate", "object"]
69 } else {
70 vec!["palace", "subject", "predicate", "object"]
71 };
72 let kg_query_required: Vec<&str> = if has_default {
73 vec!["subject"]
74 } else {
75 vec!["palace", "subject"]
76 };
77 let memory_list_required: Vec<&str> = if has_default { vec![] } else { vec!["palace"] };
78 let memory_forget_required: Vec<&str> = if has_default {
79 vec!["drawer_id"]
80 } else {
81 vec!["palace", "drawer_id"]
82 };
83 let palace_info_required: Vec<&str> = if has_default { vec![] } else { vec!["palace"] };
84 let palace_compact_required: Vec<&str> = if has_default { vec![] } else { vec!["palace"] };
85 let memory_note_required: Vec<&str> = if has_default {
86 vec!["content"]
87 } else {
88 vec!["palace", "content"]
89 };
90 let add_alias_required: Vec<&str> = if has_default {
94 vec!["short", "full"]
95 } else {
96 vec!["palace", "short", "full"]
97 };
98 let discover_aliases_required: Vec<&str> = if has_default { vec![] } else { vec!["palace"] };
99
100 json!({
101 "tools": [
102 {
103 "name": "memory_remember",
104 "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.",
105 "inputSchema": {
106 "type": "object",
107 "properties": {
108 "palace": {"type": "string", "description": "Palace ID (optional if server started with --palace)"},
109 "text": {"type": "string", "description": "Memory content"},
110 "room": {"type": "string", "description": "Room type (optional)"},
111 "tags": {"type": "array", "items": {"type": "string"}},
112 "force": {"type": "boolean", "description": "Bypass the signal/noise filter. Use sparingly — intended for explicit operator overrides.", "default": false},
113 "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)."}
114 },
115 "required": memory_remember_required,
116 }
117 },
118 {
119 "name": "memory_note",
120 "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.",
121 "inputSchema": {
122 "type": "object",
123 "properties": {
124 "palace": {"type": "string"},
125 "content": {"type": "string", "description": "Brief fact to remember"},
126 "tags": {"type": "array", "items": {"type": "string"}},
127 "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)."}
128 },
129 "required": memory_note_required,
130 }
131 },
132 {
133 "name": "memory_recall",
134 "description": "Recall memories using L0+L1+L2 progressive retrieval.",
135 "inputSchema": {
136 "type": "object",
137 "properties": {
138 "palace": {"type": "string"},
139 "query": {"type": "string"},
140 "top_k": {"type": "integer", "default": 10}
141 },
142 "required": memory_recall_required,
143 }
144 },
145 {
146 "name": "memory_recall_deep",
147 "description": "Deep recall using L3 full HNSW search.",
148 "inputSchema": {
149 "type": "object",
150 "properties": {
151 "palace": {"type": "string"},
152 "query": {"type": "string"},
153 "top_k": {"type": "integer", "default": 10}
154 },
155 "required": memory_recall_required,
156 }
157 },
158 {
159 "name": "palace_create",
160 "description": "Create a new memory palace.",
161 "inputSchema": {
162 "type": "object",
163 "properties": {
164 "name": {"type": "string"},
165 "description": {"type": "string"},
166 "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)."}
167 },
168 "required": ["name"]
169 }
170 },
171 {
172 "name": "palace_list",
173 "description": "List all palaces on this machine.",
174 "inputSchema": {"type": "object", "properties": {}}
175 },
176 {
177 "name": "palace_delete",
178 "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.",
179 "inputSchema": {
180 "type": "object",
181 "properties": {
182 "palace_id": {"type": "string", "description": "Id of the palace to delete."},
183 "force": {"type": "boolean", "description": "Required when the palace still has drawers; defaults to false.", "default": false}
184 },
185 "required": ["palace_id"]
186 }
187 },
188 {
189 "name": "palace_update",
190 "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.",
191 "inputSchema": {
192 "type": "object",
193 "properties": {
194 "palace_id": {"type": "string", "description": "Id of the palace to rename."},
195 "name": {"type": "string", "description": "New display name. Trimmed; must be non-empty."}
196 },
197 "required": ["palace_id", "name"]
198 }
199 },
200 {
201 "name": "kg_assert",
202 "description": "Assert a fact in the temporal knowledge graph.",
203 "inputSchema": {
204 "type": "object",
205 "properties": {
206 "palace": {"type": "string"},
207 "subject": {"type": "string"},
208 "predicate": {"type": "string"},
209 "object": {"type": "string"},
210 "confidence": {"type": "number", "default": 1.0},
211 "provenance": {"type": "string"}
212 },
213 "required": kg_assert_required,
214 }
215 },
216 {
217 "name": "kg_query",
218 "description": "Query active knowledge-graph triples for a subject.",
219 "inputSchema": {
220 "type": "object",
221 "properties": {
222 "palace": {"type": "string"},
223 "subject": {"type": "string"}
224 },
225 "required": kg_query_required,
226 }
227 },
228 {
229 "name": "memory_list",
230 "description": "List drawers in a palace, optionally filtered by room type or tag.",
231 "inputSchema": {
232 "type": "object",
233 "properties": {
234 "palace": {"type": "string"},
235 "room": {"type": "string", "description": "Filter by room type (Frontend, Backend, Testing, Planning, Documentation, Research, Configuration, Meetings, General, or custom)"},
236 "tag": {"type": "string", "description": "Filter by tag"},
237 "limit": {"type": "integer", "description": "Max results (default 50)"}
238 },
239 "required": memory_list_required,
240 }
241 },
242 {
243 "name": "memory_forget",
244 "description": "Delete a drawer from a palace by its UUID.",
245 "inputSchema": {
246 "type": "object",
247 "properties": {
248 "palace": {"type": "string"},
249 "drawer_id": {"type": "string", "description": "UUID of the drawer to delete"}
250 },
251 "required": memory_forget_required,
252 }
253 },
254 {
255 "name": "palace_info",
256 "description": "Get metadata and stats for a single palace.",
257 "inputSchema": {
258 "type": "object",
259 "properties": {
260 "palace": {"type": "string"}
261 },
262 "required": palace_info_required,
263 }
264 },
265 {
266 "name": "palace_compact",
267 "description": "Remove orphaned vector index entries (vectors with no matching drawer row). See issue #49.",
268 "inputSchema": {
269 "type": "object",
270 "properties": {
271 "palace": {"type": "string"}
272 },
273 "required": palace_compact_required,
274 }
275 },
276 {
277 "name": "add_alias",
278 "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.",
279 "inputSchema": {
280 "type": "object",
281 "properties": {
282 "palace": {"type": "string", "description": "Palace ID (optional if server started with --palace)"},
283 "short": {"type": "string", "description": "Short name / alias (subject)"},
284 "full": {"type": "string", "description": "Full / canonical name (object)"},
285 "extra": {"type": "string", "description": "Optional extra context appended to the full name"}
286 },
287 "required": add_alias_required,
288 }
289 },
290 {
291 "name": "list_prompt_facts",
292 "description": "List every active prompt-fact triple (aliases, conventions, facts, shorthands) across all palaces.",
293 "inputSchema": {"type": "object", "properties": {}}
294 },
295 {
296 "name": "remove_prompt_fact",
297 "description": "Retract the active triple for a (subject, predicate) pair from the prompt-facts surface. Closes the interval without inserting a replacement.",
298 "inputSchema": {
299 "type": "object",
300 "properties": {
301 "subject": {"type": "string"},
302 "predicate": {"type": "string", "description": "One of is_alias_for, has_convention, is_fact, is_shorthand_for"}
303 },
304 "required": ["subject", "predicate"],
305 }
306 },
307 {
308 "name": "get_prompt_context",
309 "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).",
310 "inputSchema": {
311 "type": "object",
312 "properties": {
313 "query": {
314 "type": "string",
315 "description": "Optional filter — only return facts whose subject or object contains this string (case-insensitive). Omit to return all hot facts."
316 }
317 }
318 }
319 },
320 {
321 "name": "discover_aliases",
322 "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.",
323 "inputSchema": {
324 "type": "object",
325 "properties": {
326 "palace": {"type": "string", "description": "Palace ID (optional if server started with --palace)"},
327 "project_root": {"type": "string", "description": "Optional filesystem path to scan. Defaults to the process cwd."}
328 },
329 "required": discover_aliases_required,
330 }
331 },
332 {
333 "name": "kg_gaps",
334 "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.",
335 "inputSchema": {
336 "type": "object",
337 "properties": {
338 "palace": {"type": "string", "description": "Palace name (optional, defaults to the active palace)"}
339 }
340 }
341 },
342 {
343 "name": "kg_bootstrap",
344 "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.",
345 "inputSchema": {
346 "type": "object",
347 "properties": {
348 "palace": {"type": "string", "description": "Palace ID (optional if server started with --palace)"},
349 "project_path": {"type": "string", "description": "Filesystem path to scan. Omit to scan the palace's own data dir (temporal metadata only)."}
350 }
351 }
352 },
353 {
354 "name": "memory_recall_all",
355 "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.",
356 "inputSchema": {
357 "type": "object",
358 "properties": {
359 "q": {"type": "string", "description": "Free-text query"},
360 "top_k": {"type": "integer", "default": 10},
361 "deep": {"type": "boolean", "default": false}
362 },
363 "required": ["q"],
364 }
365 },
366 {
367 "name": "memory_send_message",
368 "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.",
369 "inputSchema": {
370 "type": "object",
371 "properties": {
372 "to_palace": {"type": "string", "description": "Recipient palace id (repo slug)."},
373 "purpose": {"type": "string", "description": "Free-text purpose / category (e.g. `task`, `notify`, `reply`)."},
374 "content": {"type": "string", "description": "Message body — plain text, no length limit. Rendered into the recipient session as a Markdown block."},
375 "from_palace": {"type": "string", "description": "Sender palace id (optional, defaults to cwd-derived slug)."}
376 },
377 "required": ["to_palace", "purpose", "content"],
378 }
379 },
380 {
381 "name": "upgrade",
382 "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.",
383 "inputSchema": {
384 "type": "object",
385 "properties": {
386 "check": {"type": "boolean", "description": "Report current and available versions only. No install. Default: true when confirm is absent.", "default": true},
387 "confirm": {"type": "boolean", "description": "Set to true to install the new version. NEVER set automatically — the operator must explicitly pass confirm=true.", "default": false}
388 },
389 "required": []
390 }
391 },
392 crate::console_metrics::descriptor()
393 ]
394 })
395}