@xultrax-web/agent-memory-mcp 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +38 -18
  3. package/dist/index.js +352 -4
  4. package/package.json +1 -1
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 xultrax-web
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 xultrax-web
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -185,18 +185,30 @@ Custom path:
185
185
 
186
186
  ## Tools
187
187
 
188
- | Tool | Purpose |
189
- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
190
- | `save_memory` | Create or update a memory. Atomic write + locked. Validates name + type. Updates the index. |
191
- | `search_memories` | Fuzzy search (Fuse.js · typo-tolerant, word-order tolerant, partial matches). Returns top N with relevance 0-100 + body snippet. |
192
- | `relevant_memories` | Same matching as search, but returns full memory bodies as one markdown doc. Built for LLM auto-context. |
193
- | `get_memory` | Fetch one memory by name. Returns frontmatter + body. |
194
- | `list_memories` | List memories. Optional `type` filter. Paginated (default 50/page). |
195
- | `delete_memory` | Soft delete: moves the memory to `.trash/<ts>-<name>.md`. Recoverable until you empty `.trash/` by hand. |
196
- | `restore_memory` | Restore a soft-deleted memory from `.trash/`. Picks the most recent trash entry for the name. |
197
- | `doctor` | Storage integrity check. Reports orphans, dangling index entries, unreadable files. Pass `rebuild-index=true` to repair `MEMORY.md` from disk. |
198
- | `stats` | Dashboard: counts per type, total size, largest memory, audit-log size, trash count. |
199
- | `log_events` | Read recent entries from the audit event log. Optional `tail` (default 20) + `action` filter. |
188
+ | Tool | Purpose |
189
+ | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
190
+ | `save_memory` | Create or update a memory. Atomic write + locked. Validates name + type. Updates the index. |
191
+ | `search_memories` | Fuzzy search (Fuse.js · typo-tolerant, word-order tolerant, partial matches). Returns top N with relevance 0-100 + body snippet. |
192
+ | `relevant_memories` | Same matching as search, but returns full memory bodies as one markdown doc. Built for LLM auto-context. |
193
+ | `get_memory` | Fetch one memory by name. Returns frontmatter + body. |
194
+ | `list_memories` | List memories. Optional `type` filter. Paginated (default 50/page). |
195
+ | `delete_memory` | Soft delete: moves the memory to `.trash/<ts>-<name>.md`. Recoverable until you empty `.trash/` by hand. |
196
+ | `restore_memory` | Restore a soft-deleted memory from `.trash/`. Picks the most recent trash entry for the name. |
197
+ | `doctor` | Storage integrity check. Reports orphans, dangling index entries, unreadable files. Pass `rebuild-index=true` to repair `MEMORY.md` from disk. |
198
+ | `stats` | Dashboard: counts per type, total size, largest memory, audit-log size, trash count. |
199
+ | `log_events` | Read recent entries from the audit event log. Optional `tail` (default 20) + `action` filter. |
200
+ | `verify_memory` | Re-evaluate a memory's claims. Extracts URLs/dates/file refs, flags stale-date signals, returns type-specific verification heuristics. Pairs with the `audit_stale` prompt. |
201
+
202
+ ### Prompts
203
+
204
+ The server exposes 4 built-in MCP prompts that clients (Claude Desktop, Cursor, etc.) surface as slash-commands. These turn memory into an active workflow layer, not just a passive store:
205
+
206
+ | Prompt | Arguments | What it does |
207
+ | ------------------ | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
208
+ | `extract_memories` | none | LLM scans the current conversation, proposes candidate memories, and calls `save_memory` for each one (with type + description chosen). |
209
+ | `summarize_topic` | `topic` | LLM pulls memories relevant to the topic via `relevant_memories` and synthesizes them into a single summary with citations. |
210
+ | `prepare_handoff` | `project` (optional) | LLM walks project-type memories matching the filter and assembles a structured handoff doc (current state, open items, watch-outs). |
211
+ | `audit_stale` | none | LLM evaluates project + reference memories for staleness and produces a triage list (likely stale / worth verifying / still fresh). |
200
212
 
201
213
  ### Memory types
202
214
 
@@ -229,6 +241,7 @@ agent-memory doctor --rebuild-index # repair MEMORY.md from disk
229
241
  agent-memory stats # dashboard: counts, sizes, audit/trash
230
242
  agent-memory log # last 20 entries from the audit log
231
243
  agent-memory log --tail 50 --action delete # filter by action, tail size
244
+ agent-memory verify deploy-process # extract URLs/dates/file refs + staleness heuristics
232
245
  ```
233
246
 
234
247
  ### Audit log + structured logging
@@ -326,15 +339,22 @@ This server is built to be used daily, not to demo well once.
326
339
 
327
340
  **Shipped in v0.6:**
328
341
 
329
- - **Vitest test suite** — 20+ blackbox tests covering CLI + MCP server paths.
330
- - **GitHub Actions CI** — runs tests on every push/PR across Node 18/20/22.
342
+ - **Vitest test suite** — 25+ blackbox tests covering CLI + MCP server paths.
343
+ - **GitHub Actions CI** — runs tests on every push/PR across Node 20/22/24.
331
344
  - **[COMPATIBILITY.md](COMPATIBILITY.md)** — known-working client matrix + quirks.
332
345
 
333
- **Landing in v0.7:**
346
+ **Shipped in v0.7 · the active context layer:**
334
347
 
335
- - Read-only mode (`AGENT_MEMORY_READ_ONLY=1`)
336
- - Live multi-client verification (Cursor, Cline, Claude Desktop, Continue)
337
- - Demo GIF / screencast
348
+ - **MCP Prompts capability** — 4 built-in workflows (`extract_memories`, `summarize_topic`, `prepare_handoff`, `audit_stale`) that the client surfaces as slash-commands.
349
+ - **`verify_memory` tool** — static analysis of a memory's URLs/dates/file refs with type-specific staleness heuristics. Plus the matching `agent-memory verify <name>` CLI.
350
+ - **Conflict detection on save** — fuzzy-matches new memories against existing ones; warns on near-duplicates without blocking the save (so the LLM can decide whether to merge, rename, or proceed).
351
+
352
+ **Landing in v0.8 - v0.9:**
353
+
354
+ - Tags + wiki-links + folder support + `find_related` (organization-at-scale)
355
+ - TUI / web UI for browsing + editing memories in a clean interface
356
+ - `agent-memory sync` for git-backed multi-machine memory (the moat)
357
+ - Memory packs for shareable curated bundles
338
358
 
339
359
  ---
340
360
 
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@
23
23
  */
24
24
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
25
25
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
26
- import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
26
+ import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
27
27
  import Fuse from "fuse.js";
28
28
  import matter from "gray-matter";
29
29
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync, } from "node:fs";
@@ -255,13 +255,89 @@ function toolSaveMemory(args) {
255
255
  `---\n\n`;
256
256
  const fp = memoryFilePath(name);
257
257
  const isUpdate = existsSync(fp);
258
+ // Conflict detection · only when creating a NEW memory (updates are
259
+ // intentional re-saves and don't need a similarity warning). Fuzzy
260
+ // match against existing names + descriptions; if anything scores
261
+ // above the conflict threshold, warn but don't block — the LLM can
262
+ // decide whether to merge, rename, or proceed.
263
+ let conflictWarning = "";
264
+ if (!isUpdate) {
265
+ const conflicts = detectConflicts({ name, description, type });
266
+ if (conflicts.length > 0) {
267
+ const list = conflicts
268
+ .map((c) => ` - ${c.name} [${c.type}] (${c.similarity}% similar)`)
269
+ .join("\n");
270
+ conflictWarning =
271
+ `\n\nWARNING · potentially similar existing memory(ies):\n${list}\n` +
272
+ `Consider merging or renaming. To proceed anyway, the save has already been completed.`;
273
+ }
274
+ }
258
275
  return withLock(() => {
259
276
  atomicWriteFile(fp, frontmatter + content + "\n");
260
277
  upsertIndexEntryUnlocked(name, description);
261
- logEvent("save", { name, type, update: isUpdate, bytes: content.length });
278
+ logEvent("save", {
279
+ name,
280
+ type,
281
+ update: isUpdate,
282
+ bytes: content.length,
283
+ conflicts: conflictWarning ? "warned" : undefined,
284
+ });
262
285
  log("debug", "save_memory", { name, type, update: isUpdate });
263
- return `${isUpdate ? "Updated" : "Saved"} memory "${name}" (${type}) at ${fp}`;
286
+ return `${isUpdate ? "Updated" : "Saved"} memory "${name}" (${type}) at ${fp}${conflictWarning}`;
287
+ });
288
+ }
289
+ function detectConflicts(candidate) {
290
+ const names = listMemoryFiles();
291
+ // Skip the candidate name itself (will be an update, not a conflict)
292
+ const others = names.filter((n) => n !== candidate.name);
293
+ if (others.length === 0)
294
+ return [];
295
+ const existing = others.map((n) => readMemory(n)).filter((m) => m !== null);
296
+ if (existing.length === 0)
297
+ return [];
298
+ // Two separate searches catch different conflict shapes:
299
+ // - Name-based: matches when the new slug is very close to an
300
+ // existing slug (e.g. "deploy-process" vs "deployment-strategy")
301
+ // - Description-based: matches when descriptions are paraphrases
302
+ // of each other regardless of name
303
+ // We merge results + dedupe. Threshold 0.5 here is intentionally
304
+ // looser than search (0.4) because we'd rather warn on a few false
305
+ // positives than miss an obvious duplicate.
306
+ // Threshold 0.6 catches paraphrases like "deployment-strategy" vs
307
+ // "deploy-process" (Fuse Bitap scores those around 0.5). The 45%
308
+ // similarity floor below then trims out the long tail.
309
+ const nameFuse = new Fuse(existing, {
310
+ includeScore: true,
311
+ threshold: 0.6,
312
+ ignoreLocation: true,
313
+ minMatchCharLength: 3,
314
+ keys: ["name"],
264
315
  });
316
+ const descFuse = new Fuse(existing, {
317
+ includeScore: true,
318
+ threshold: 0.6,
319
+ ignoreLocation: true,
320
+ minMatchCharLength: 3,
321
+ keys: ["description"],
322
+ });
323
+ const merged = new Map();
324
+ const addHit = (name, type, score) => {
325
+ const existingHit = merged.get(name);
326
+ const similarity = Math.round((1 - score) * 100);
327
+ if (!existingHit || similarity > existingHit.similarity) {
328
+ merged.set(name, { name, type, similarity });
329
+ }
330
+ };
331
+ for (const r of nameFuse.search(candidate.name, { limit: 3 })) {
332
+ addHit(r.item.name, r.item.type, r.score ?? 1);
333
+ }
334
+ for (const r of descFuse.search(candidate.description, { limit: 3 })) {
335
+ addHit(r.item.name, r.item.type, r.score ?? 1);
336
+ }
337
+ return Array.from(merged.values())
338
+ .filter((h) => h.similarity >= 45) // 45%+ similarity = worth surfacing
339
+ .sort((a, b) => b.similarity - a.similarity)
340
+ .slice(0, 3);
265
341
  }
266
342
  function toolGetMemory(args) {
267
343
  const name = String(args.name ?? "").trim();
@@ -558,6 +634,90 @@ function toolDoctor(args) {
558
634
  return formatDoctorReport(report, rebuild);
559
635
  }
560
636
  // -------------------------------------------------------------
637
+ // verify_memory · re-evaluate a memory's claims
638
+ // -------------------------------------------------------------
639
+ //
640
+ // Static analysis only (no network calls — MCP servers may run
641
+ // offline). Extracts URLs + dates + file paths from the body and
642
+ // returns a structured report the LLM can act on. Pairs with the
643
+ // audit_stale prompt to triage memory hygiene.
644
+ const URL_PATTERN = /\bhttps?:\/\/[^\s<>"')]+/g;
645
+ const DATE_PATTERN = /\b(20\d{2})-(\d{2})-(\d{2})\b/g;
646
+ const FILE_PATH_PATTERN = /\b(?:[A-Za-z]:\\|\/)?(?:[\w.-]+[\\/])+[\w.-]+\.\w+\b/g;
647
+ function toolVerifyMemory(args) {
648
+ const name = String(args.name ?? "").trim();
649
+ if (!SLUG_PATTERN.test(name))
650
+ throw new Error(`Invalid name "${name}".`);
651
+ const mem = readMemory(name);
652
+ if (!mem)
653
+ return `Memory "${name}" not found.`;
654
+ const urls = Array.from(new Set(mem.body.match(URL_PATTERN) ?? []));
655
+ const dates = Array.from(new Set((mem.body.match(DATE_PATTERN) ?? []).map((d) => d)));
656
+ const filePaths = Array.from(new Set(mem.body.match(FILE_PATH_PATTERN) ?? []));
657
+ // Staleness heuristic: if any date in the body is more than 60 days
658
+ // old AND the memory type is project, flag it for review.
659
+ const now = Date.now();
660
+ const SIXTY_DAYS = 60 * 24 * 3600 * 1000;
661
+ const oldDates = dates.filter((d) => {
662
+ const parsed = Date.parse(d);
663
+ return !isNaN(parsed) && now - parsed > SIXTY_DAYS;
664
+ });
665
+ const lines = [];
666
+ lines.push(c(ANSI.bold, `verify_memory · ${mem.name}`));
667
+ lines.push(`type : ${mem.type}`);
668
+ lines.push(`description : ${mem.description}`);
669
+ lines.push("");
670
+ lines.push(c(ANSI.bold, "Static signals:"));
671
+ lines.push(` URLs found : ${urls.length}`);
672
+ lines.push(` Dates referenced : ${dates.length}${oldDates.length > 0 ? ` (${oldDates.length} > 60 days old)` : ""}`);
673
+ lines.push(` File-path refs : ${filePaths.length}`);
674
+ if (urls.length > 0) {
675
+ lines.push("");
676
+ lines.push(c(ANSI.bold, "URLs to verify:"));
677
+ for (const u of urls.slice(0, 10))
678
+ lines.push(` - ${u}`);
679
+ if (urls.length > 10)
680
+ lines.push(` ... +${urls.length - 10} more`);
681
+ }
682
+ if (oldDates.length > 0) {
683
+ lines.push("");
684
+ lines.push(c(ANSI.yellow, "Stale-date signals (consider whether claims are still current):"));
685
+ for (const d of oldDates.slice(0, 5))
686
+ lines.push(` - ${d}`);
687
+ }
688
+ if (filePaths.length > 0 && filePaths.length <= 10) {
689
+ lines.push("");
690
+ lines.push(c(ANSI.bold, "File paths referenced:"));
691
+ for (const fp of filePaths)
692
+ lines.push(` - ${fp}`);
693
+ }
694
+ lines.push("");
695
+ lines.push(c(ANSI.bold, "Type-specific verification heuristics:"));
696
+ switch (mem.type) {
697
+ case "reference":
698
+ lines.push(" - HEAD-check each URL above (200 = alive, 404 = dead, 410 = gone)");
699
+ lines.push(" - Verify the resource still says what the memory claims it says");
700
+ break;
701
+ case "project":
702
+ lines.push(" - Check if any dates above are stale relative to current project state");
703
+ lines.push(" - Cross-reference any names/people mentioned against current org chart");
704
+ lines.push(" - Verify any deadlines or commitments haven't already passed");
705
+ break;
706
+ case "feedback":
707
+ lines.push(" - Confirm the rule still applies (operator hasn't changed their mind)");
708
+ lines.push(" - Check the **Why:** is still load-bearing — if the original reason is gone, the rule may be obsolete");
709
+ break;
710
+ case "user":
711
+ lines.push(" - User preferences drift over time; ask the operator if a 6+ month old memory still holds");
712
+ break;
713
+ }
714
+ lines.push("");
715
+ lines.push(c(ANSI.dim, "Memory body for review:"));
716
+ lines.push("");
717
+ lines.push(mem.body);
718
+ return lines.join("\n");
719
+ }
720
+ // -------------------------------------------------------------
561
721
  // Stats · operator dashboard
562
722
  // -------------------------------------------------------------
563
723
  function toolStats(_args) {
@@ -667,7 +827,7 @@ function actionColor(action) {
667
827
  // -------------------------------------------------------------
668
828
  // Server wiring
669
829
  // -------------------------------------------------------------
670
- const server = new Server({ name: "agent-memory", version: "0.2.0" }, { capabilities: { tools: {}, resources: {} } });
830
+ const server = new Server({ name: "agent-memory", version: "0.7.0" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
671
831
  // -------------------------------------------------------------
672
832
  // Resource URI scheme
673
833
  // -------------------------------------------------------------
@@ -727,6 +887,169 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
727
887
  }
728
888
  throw new Error(`Unknown resource URI: ${uri}. Supported: ${URI_INDEX}, ${URI_MEMORY_PREFIX}{name}`);
729
889
  });
890
+ const PROMPTS = [
891
+ {
892
+ name: "extract_memories",
893
+ description: "Scan the current conversation for things worth remembering. Returns a structured prompt asking the LLM to identify candidate memories and call save_memory for each one.",
894
+ },
895
+ {
896
+ name: "summarize_topic",
897
+ description: "Pull memories relevant to a topic and ask the LLM to synthesize them into a single coherent summary.",
898
+ arguments: [
899
+ { name: "topic", description: "What to summarize what's known about.", required: true },
900
+ ],
901
+ },
902
+ {
903
+ name: "prepare_handoff",
904
+ description: "Generate a project state snapshot from all project-type memories matching a filter. Useful for rotating on-call, end-of-day handoffs, or onboarding a collaborator.",
905
+ arguments: [
906
+ {
907
+ name: "project",
908
+ description: "Substring to filter project memories by (matches name + description + tags).",
909
+ required: false,
910
+ },
911
+ ],
912
+ },
913
+ {
914
+ name: "audit_stale",
915
+ description: "Walk recent project and reference memories and ask the LLM to flag which ones are likely stale or contradicted by current state. Pairs with verify_memory for follow-up.",
916
+ },
917
+ ];
918
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({
919
+ prompts: PROMPTS,
920
+ }));
921
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
922
+ const { name, arguments: args = {} } = request.params;
923
+ ensureStorage();
924
+ switch (name) {
925
+ case "extract_memories":
926
+ return {
927
+ description: PROMPTS[0].description,
928
+ messages: [
929
+ {
930
+ role: "user",
931
+ content: {
932
+ type: "text",
933
+ text: [
934
+ "Scan the current conversation for facts, rules, preferences, decisions, or context that would be useful to remember across future sessions. Focus on things that:",
935
+ "",
936
+ "- Reflect the operator's stable preferences or working style (type=user)",
937
+ "- Represent rules the assistant should follow going forward (type=feedback)",
938
+ "- Capture current-state context not derivable from code or docs (type=project)",
939
+ "- Point at external resources the operator references (type=reference)",
940
+ "",
941
+ "For each candidate, call the save_memory tool with:",
942
+ " - name: a short kebab-case slug (a-z, 0-9, -, _)",
943
+ " - type: one of user | feedback | project | reference",
944
+ " - description: a one-line summary used in the index",
945
+ " - content: the memory body in markdown. For feedback/project, include `**Why:**` and `**How to apply:**` lines.",
946
+ "",
947
+ "Before saving each one, briefly explain why it's worth remembering. Skip anything that's already obvious from code or that's only relevant to the current session.",
948
+ "",
949
+ "If there's nothing worth saving, say so plainly and stop.",
950
+ ].join("\n"),
951
+ },
952
+ },
953
+ ],
954
+ };
955
+ case "summarize_topic": {
956
+ const topic = String(args.topic ?? "").trim();
957
+ if (!topic)
958
+ throw new Error("summarize_topic requires a 'topic' argument");
959
+ return {
960
+ description: PROMPTS[1].description,
961
+ messages: [
962
+ {
963
+ role: "user",
964
+ content: {
965
+ type: "text",
966
+ text: [
967
+ `Call the relevant_memories tool with query="${topic}" (max=10).`,
968
+ "",
969
+ "Then synthesize the returned memories into a single coherent summary covering:",
970
+ "",
971
+ "1. What's established / known",
972
+ "2. Open questions or unresolved tensions across the memories",
973
+ "3. Any stale or contradicted claims worth flagging",
974
+ "",
975
+ "Keep it tight — aim for one paragraph per section unless the material is genuinely dense. Cite the source memory names inline as `[memory-name]`.",
976
+ ].join("\n"),
977
+ },
978
+ },
979
+ ],
980
+ };
981
+ }
982
+ case "prepare_handoff": {
983
+ const project = String(args.project ?? "").trim();
984
+ const filterClause = project
985
+ ? `filtered to project memories matching "${project}"`
986
+ : "across all project memories";
987
+ return {
988
+ description: PROMPTS[2].description,
989
+ messages: [
990
+ {
991
+ role: "user",
992
+ content: {
993
+ type: "text",
994
+ text: [
995
+ `Generate a project handoff document ${filterClause}.`,
996
+ "",
997
+ project
998
+ ? `Start by calling list_memories with type="project", then filter the results by substring match against "${project}".`
999
+ : `Start by calling list_memories with type="project" to get the full set.`,
1000
+ "",
1001
+ "For each relevant memory, call get_memory to load its full body. Then produce a single handoff document with these sections:",
1002
+ "",
1003
+ "## Current state",
1004
+ "What's in flight or recently shipped, distilled from project memories.",
1005
+ "",
1006
+ "## Open items",
1007
+ "Anything explicitly noted as pending, waiting, or unresolved.",
1008
+ "",
1009
+ "## Watch-outs",
1010
+ "Constraints, deadlines, or hidden gotchas captured in memory.",
1011
+ "",
1012
+ "## Reference material",
1013
+ "Links and external resources the next person should know about (from reference memories if they're relevant to the project).",
1014
+ "",
1015
+ "Cite source memories inline as `[memory-name]`. Keep prose dense; this is meant to be read by an experienced operator, not explained from scratch.",
1016
+ ].join("\n"),
1017
+ },
1018
+ },
1019
+ ],
1020
+ };
1021
+ }
1022
+ case "audit_stale":
1023
+ return {
1024
+ description: PROMPTS[3].description,
1025
+ messages: [
1026
+ {
1027
+ role: "user",
1028
+ content: {
1029
+ type: "text",
1030
+ text: [
1031
+ 'Call list_memories with type="project", then list_memories with type="reference". For each memory returned, evaluate whether its claims are likely still true based on:',
1032
+ "",
1033
+ "- Dates in the body (anything more than 30 days old in a `project` memory deserves scrutiny)",
1034
+ "- References to people, dependencies, or systems that may have changed",
1035
+ "- Claims about external state (URLs, dashboards, APIs) that you can't verify without external access",
1036
+ "",
1037
+ "Produce a triage list with three buckets:",
1038
+ "",
1039
+ "**Likely stale** (high confidence they're outdated) — explain why, suggest action.",
1040
+ "**Worth verifying** (claims you can't evaluate without external access) — suggest using the verify_memory tool.",
1041
+ "**Still fresh** (nothing in the content suggests staleness).",
1042
+ "",
1043
+ "Be conservative on the 'likely stale' bucket — false positives there create work for the operator.",
1044
+ ].join("\n"),
1045
+ },
1046
+ },
1047
+ ],
1048
+ };
1049
+ default:
1050
+ throw new Error(`Unknown prompt: ${name}`);
1051
+ }
1052
+ });
730
1053
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
731
1054
  tools: [
732
1055
  {
@@ -869,6 +1192,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
869
1192
  },
870
1193
  },
871
1194
  },
1195
+ {
1196
+ name: "verify_memory",
1197
+ description: "Re-evaluate a memory's claims against signals in its content. Extracts URLs, dates, and file paths from the body; flags stale-date signals on project-type memories; returns type-specific verification heuristics for the LLM or operator to act on. Pairs with the audit_stale prompt.",
1198
+ inputSchema: {
1199
+ type: "object",
1200
+ properties: {
1201
+ name: { type: "string", description: "The memory's name slug" },
1202
+ },
1203
+ required: ["name"],
1204
+ },
1205
+ },
872
1206
  ],
873
1207
  }));
874
1208
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -906,6 +1240,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
906
1240
  case "log_events":
907
1241
  result = toolLogEvents(args);
908
1242
  break;
1243
+ case "verify_memory":
1244
+ result = toolVerifyMemory(args);
1245
+ break;
909
1246
  default:
910
1247
  throw new Error(`Unknown tool: ${name}`);
911
1248
  }
@@ -938,6 +1275,7 @@ const CLI_COMMANDS = new Set([
938
1275
  "doctor",
939
1276
  "stats",
940
1277
  "log",
1278
+ "verify",
941
1279
  "import-claude-code",
942
1280
  "help",
943
1281
  "--help",
@@ -1067,6 +1405,13 @@ async function cliMain(command, rest) {
1067
1405
  process.stdout.write(toolStats({}) + "\n");
1068
1406
  return 0;
1069
1407
  }
1408
+ case "verify": {
1409
+ const name = positional[0];
1410
+ if (!name)
1411
+ throw new Error("Usage: agent-memory verify <name>");
1412
+ process.stdout.write(toolVerifyMemory({ name }) + "\n");
1413
+ return 0;
1414
+ }
1070
1415
  case "log": {
1071
1416
  process.stdout.write(toolLogEvents({
1072
1417
  tail: flags.tail ? Number(flags.tail) : undefined,
@@ -1117,6 +1462,9 @@ COMMANDS
1117
1462
  stats Dashboard: counts per type, sizes, audit/trash counts.
1118
1463
  log [--tail N] [--action save|delete|restore]
1119
1464
  Recent audit-log entries.
1465
+ verify <name> Re-evaluate a memory's claims (URLs, dates, file refs,
1466
+ type-specific staleness heuristics). Static analysis;
1467
+ no network calls.
1120
1468
  import-claude-code [--source <path>] [--project <pat>] [--overwrite] [--dry-run]
1121
1469
  Walk ~/.claude/projects/*/memory/ and
1122
1470
  import each memory into the current store.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xultrax-web/agent-memory-mcp",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "mcpName": "io.github.xultrax-web/agent-memory-mcp",
5
5
  "description": "Markdown memory for AI agents. Plain files you can read, edit, grep, and commit. The only MCP memory server that isn't a database.",
6
6
  "type": "module",