@j0hanz/memory-mcp 1.5.0 → 1.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 (45) hide show
  1. package/dist/db/index.js +16 -13
  2. package/dist/lib/errors.d.ts +4 -0
  3. package/dist/lib/errors.js +11 -0
  4. package/dist/lib/graph-traversal.d.ts +12 -0
  5. package/dist/lib/graph-traversal.js +145 -0
  6. package/dist/lib/json-schema.d.ts +5 -0
  7. package/dist/lib/json-schema.js +19 -1
  8. package/dist/lib/pagination.d.ts +0 -2
  9. package/dist/lib/pagination.js +0 -43
  10. package/dist/lib/search.js +44 -23
  11. package/dist/lib/tool-contracts.js +50 -73
  12. package/dist/lib/tool-execution.d.ts +13 -0
  13. package/dist/lib/tool-execution.js +51 -0
  14. package/dist/prompts/index.js +12 -8
  15. package/dist/resources/index.js +67 -43
  16. package/dist/resources/instructions.js +44 -37
  17. package/dist/resources/server-config.js +33 -22
  18. package/dist/resources/tool-catalog.js +2 -6
  19. package/dist/resources/tool-info.js +9 -9
  20. package/dist/resources/workflows.js +69 -40
  21. package/dist/schemas/inputs.d.ts +8 -5
  22. package/dist/schemas/inputs.js +57 -40
  23. package/dist/schemas/outputs.d.ts +6 -6
  24. package/dist/schemas/outputs.js +7 -6
  25. package/dist/server.js +11 -4
  26. package/dist/tools/create-relationship.js +17 -22
  27. package/dist/tools/delete-memories.js +30 -39
  28. package/dist/tools/delete-memory.js +14 -18
  29. package/dist/tools/delete-relationship.js +9 -24
  30. package/dist/tools/get-memory.js +12 -17
  31. package/dist/tools/get-relationships.js +11 -12
  32. package/dist/tools/memory-stats.js +22 -30
  33. package/dist/tools/progress.d.ts +6 -0
  34. package/dist/tools/progress.js +68 -25
  35. package/dist/tools/recall.js +94 -203
  36. package/dist/tools/register-contract.d.ts +1 -2
  37. package/dist/tools/register-contract.js +4 -2
  38. package/dist/tools/result.d.ts +4 -0
  39. package/dist/tools/result.js +27 -0
  40. package/dist/tools/retrieve-context.js +80 -98
  41. package/dist/tools/search-memories.js +31 -34
  42. package/dist/tools/store-memories.js +33 -44
  43. package/dist/tools/store-memory.js +13 -20
  44. package/dist/tools/update-memory.js +45 -49
  45. package/package.json +1 -1
@@ -1,13 +1,11 @@
1
- import { McpError } from '@modelcontextprotocol/sdk/types.js';
2
- import { E_CANCELLED, E_UNKNOWN, getErrorMessage, rethrowMcpError, } from '../lib/errors.js';
3
- import { sanitizeFtsQuery } from '../lib/search.js';
4
- import { createErrorResponse, createToolResponse, } from '../lib/tool-response.js';
1
+ import { throwIfAborted } from '../lib/errors.js';
2
+ import { buildAndWhereClause, buildFilterClauses, sanitizeFtsQuery, toMemoryFilters, } from '../lib/search.js';
3
+ import { createToolResponse } from '../lib/tool-response.js';
5
4
  import { parseMemoryRow } from '../lib/types.js';
6
- import { RetrieveContextInputSchema } from '../schemas/inputs.js';
7
- import { RetrieveContextResultSchema } from '../schemas/outputs.js';
8
- import { createProgressReporter, notifyProgress, progressWithMessage, } from './progress.js';
5
+ import {} from '../schemas/inputs.js';
6
+ import { createProgressReporter, notifyProgress, progressWithMessage, runWithProgressCompletion, } from './progress.js';
9
7
  import { registerToolWithContract } from './register-contract.js';
10
- import { countPayloadArrayItems, getToolResultPayload, getToolResultText, isOkStructuredToolResult, } from './result.js';
8
+ import { countPayloadArrayItems, formatToolCompletionMessage, } from './result.js';
11
9
  const MIN_CANDIDATE_ROWS = 200;
12
10
  const MAX_CANDIDATE_ROWS = 2000;
13
11
  const ESTIMATED_CHARS_PER_TOKEN = 4;
@@ -25,15 +23,18 @@ function countPayloadMemories(payload) {
25
23
  function estimateTokens(content) {
26
24
  return Math.ceil(content.length / ESTIMATED_CHARS_PER_TOKEN);
27
25
  }
28
- function loadContextRows(db, query, orderBy, limit) {
26
+ function loadContextRows(db, query, strategy, limit, filters) {
29
27
  const ftsQuery = sanitizeFtsQuery(query);
28
+ const filter = buildFilterClauses(filters);
29
+ const whereExtra = buildAndWhereClause(filter.clauses);
30
+ const orderBy = ORDER_BY_MAP[strategy];
30
31
  return db
31
32
  .prepareOnce(`SELECT m.*, memories_fts.rank AS rank FROM memories m
32
33
  JOIN memories_fts ON memories_fts.rowid = m.rowid
33
- WHERE memories_fts MATCH ?
34
+ WHERE memories_fts MATCH ?${whereExtra}
34
35
  ORDER BY ${orderBy}
35
36
  LIMIT ?`)
36
- .all(ftsQuery, limit + 1);
37
+ .all(ftsQuery, ...filter.params, limit + 1);
37
38
  }
38
39
  function reportSelectionProgress(onProgress, current, total, force = false) {
39
40
  if (!onProgress || current === 0) {
@@ -45,108 +46,89 @@ function reportSelectionProgress(onProgress, current, total, force = false) {
45
46
  onProgress({ current, total });
46
47
  }
47
48
  function formatCompletionMessage(query, result) {
48
- const failedMessage = `⊙ retrieve_context: ${query} • failed`;
49
- if (result.isError) {
50
- const text = getToolResultText(result);
51
- if (text.includes(E_CANCELLED)) {
52
- return `⊙ retrieve_context: ${query} • cancelled`;
49
+ return formatToolCompletionMessage('retrieve_context', query, result, (payload) => {
50
+ const memoriesCount = countPayloadMemories(payload);
51
+ const estimatedTokens = 'estimated_tokens' in payload &&
52
+ typeof payload.estimated_tokens === 'number'
53
+ ? payload.estimated_tokens
54
+ : 0;
55
+ const truncated = 'truncated' in payload && payload.truncated === true
56
+ ? ' [truncated]'
57
+ : '';
58
+ return `${memoriesCount} memories, ${estimatedTokens} tokens${truncated}`;
59
+ });
60
+ }
61
+ function computeCandidateLimit(tokenBudget) {
62
+ const estimatedCandidates = Math.ceil(tokenBudget / ESTIMATED_TOKENS_PER_MEMORY);
63
+ return Math.min(Math.max(MIN_CANDIDATE_ROWS, estimatedCandidates), MAX_CANDIDATE_ROWS);
64
+ }
65
+ function selectMemoriesWithinBudget(rows, candidateCount, tokenBudget, completionCurrent, signal, onProgress) {
66
+ let estimatedTokens = 0;
67
+ let truncated = rows.length > candidateCount;
68
+ const selected = [];
69
+ let scanned = 0;
70
+ for (let i = 0; i < candidateCount; i += 1) {
71
+ throwIfAborted(signal);
72
+ const row = rows[i];
73
+ if (!row) {
74
+ break;
53
75
  }
54
- return failedMessage;
55
- }
56
- if (!isOkStructuredToolResult(result)) {
57
- return failedMessage;
58
- }
59
- const payload = getToolResultPayload(result);
60
- if (!payload) {
61
- return `⊙ retrieve_context: ${query} • completed`;
76
+ const memory = parseMemoryRow(row);
77
+ const tokens = estimateTokens(memory.content);
78
+ scanned += 1;
79
+ reportSelectionProgress(onProgress, scanned, completionCurrent, false);
80
+ if (estimatedTokens + tokens > tokenBudget) {
81
+ truncated = true;
82
+ break;
83
+ }
84
+ estimatedTokens += tokens;
85
+ selected.push(memory);
62
86
  }
63
- const memoriesCount = countPayloadMemories(payload);
64
- const estimatedTokens = 'estimated_tokens' in payload &&
65
- typeof payload.estimated_tokens === 'number'
66
- ? payload.estimated_tokens
67
- : 0;
68
- const truncated = 'truncated' in payload && payload.truncated === true ? ' [truncated]' : '';
69
- return `⊙ retrieve_context: ${query} • ${memoriesCount} memories, ${estimatedTokens} tokens${truncated}`;
87
+ reportSelectionProgress(onProgress, scanned, completionCurrent, true);
88
+ return { selected, estimatedTokens, truncated };
70
89
  }
71
- function throwIfAborted(signal) {
72
- if (!signal?.aborted) {
73
- return;
74
- }
75
- throw new Error(E_CANCELLED);
90
+ function toRetrieveContextResponse(computation) {
91
+ const { selection } = computation;
92
+ return createToolResponse({
93
+ memories: selection.selected,
94
+ estimated_tokens: selection.estimatedTokens,
95
+ truncated: selection.truncated,
96
+ });
97
+ }
98
+ function computeRetrieveContextResult(db, params, limit, signal, onProgress) {
99
+ const filters = toMemoryFilters(params);
100
+ const rows = loadContextRows(db, params.query, params.strategy, limit, filters);
101
+ const rowCapExceeded = rows.length > limit;
102
+ const candidateCount = rowCapExceeded ? limit : rows.length;
103
+ const completionCurrent = candidateCount + 1;
104
+ return {
105
+ selection: selectMemoriesWithinBudget(rows, candidateCount, params.token_budget, completionCurrent, signal, onProgress),
106
+ completionCurrent,
107
+ };
76
108
  }
77
109
  export function registerRetrieveContext(server, db) {
78
- registerToolWithContract(server, 'retrieve_context', RetrieveContextInputSchema, RetrieveContextResultSchema, async (params, extra) => {
110
+ registerToolWithContract(server, 'retrieve_context', async (params, extra) => {
79
111
  const { query, strategy } = params;
80
112
  const tokenBudget = params.token_budget;
81
113
  const contextLabel = `⊙ retrieve_context: ${query} [${strategy}]`;
82
114
  let completionCurrent = 1;
83
- // Heuristic: Load enough candidates to likely fill the budget, but cap to avoid massive queries
84
- const estimatedCandidates = Math.ceil(tokenBudget / ESTIMATED_TOKENS_PER_MEMORY);
85
- const limit = Math.min(Math.max(MIN_CANDIDATE_ROWS, estimatedCandidates), MAX_CANDIDATE_ROWS);
115
+ // Heuristic: Load enough candidates to likely fill the budget, but cap to avoid massive queries.
116
+ const limit = computeCandidateLimit(tokenBudget);
86
117
  await notifyProgress(extra, {
87
118
  current: 0,
88
119
  message: `${contextLabel} [budget ${tokenBudget}]`,
89
120
  });
90
121
  const loopProgress = progressWithMessage(createProgressReporter(extra), ({ current, total }) => `${contextLabel} [scan ${current}/${Math.max((total ?? current) - 1, current)}]`);
91
- let result;
92
- let thrownError;
93
- try {
122
+ return runWithProgressCompletion(extra, () => {
94
123
  throwIfAborted(extra.signal);
95
- const orderBy = ORDER_BY_MAP[strategy];
96
- const rows = loadContextRows(db, query, orderBy, limit);
97
- const rowCapExceeded = rows.length > limit;
98
- const candidateCount = rowCapExceeded ? limit : rows.length;
99
- completionCurrent = candidateCount + 1;
100
- let estimatedTokens = 0;
101
- let truncated = rowCapExceeded;
102
- const selected = [];
103
- let scanned = 0;
104
- for (let i = 0; i < candidateCount; i += 1) {
105
- throwIfAborted(extra.signal);
106
- const row = rows[i];
107
- if (row === undefined) {
108
- break;
109
- }
110
- const mem = parseMemoryRow(row);
111
- const tokens = estimateTokens(mem.content);
112
- scanned += 1;
113
- reportSelectionProgress(loopProgress, scanned, completionCurrent, false);
114
- if (estimatedTokens + tokens > tokenBudget) {
115
- truncated = true;
116
- break;
117
- }
118
- estimatedTokens += tokens;
119
- selected.push(mem);
120
- }
121
- reportSelectionProgress(loopProgress, scanned, completionCurrent, true);
122
- result = createToolResponse({
123
- memories: selected,
124
- estimated_tokens: estimatedTokens,
125
- truncated,
126
- });
127
- }
128
- catch (err) {
129
- if (err instanceof Error && err.message === E_CANCELLED) {
130
- result = createErrorResponse(E_CANCELLED, 'Request cancelled');
131
- }
132
- else if (err instanceof McpError) {
133
- thrownError = err;
134
- }
135
- else {
136
- rethrowMcpError(err);
137
- result = createErrorResponse(E_UNKNOWN, getErrorMessage(err));
138
- }
139
- }
140
- await loopProgress.flush();
141
- const completionResult = result ?? createErrorResponse(E_UNKNOWN, getErrorMessage(thrownError));
142
- await notifyProgress(extra, {
143
- current: completionCurrent,
144
- total: completionCurrent,
145
- message: formatCompletionMessage(query, completionResult),
124
+ const computation = computeRetrieveContextResult(db, params, limit, extra.signal, loopProgress);
125
+ const { completionCurrent: nextCompletionCurrent } = computation;
126
+ completionCurrent = nextCompletionCurrent;
127
+ return toRetrieveContextResponse(computation);
128
+ }, {
129
+ reporter: loopProgress,
130
+ completionCurrent: () => completionCurrent,
131
+ completionMessage: (result) => formatCompletionMessage(query, result),
146
132
  });
147
- if (thrownError) {
148
- throw thrownError;
149
- }
150
- return completionResult;
151
133
  });
152
134
  }
@@ -1,44 +1,41 @@
1
- import { E_UNKNOWN, getErrorMessage, rethrowMcpError } from '../lib/errors.js';
2
1
  import { splitPage } from '../lib/pagination.js';
3
2
  import { buildSearchCursorScope, decodeSearchCursor, encodeSearchCursor, } from '../lib/search-cursor.js';
4
3
  import { loadRankedSearchRows, toMemoryFilters } from '../lib/search.js';
5
- import { createErrorResponse, createToolResponse, } from '../lib/tool-response.js';
4
+ import { executeToolSafely } from '../lib/tool-execution.js';
5
+ import { createToolResponse } from '../lib/tool-response.js';
6
6
  import { parseMemoryRow } from '../lib/types.js';
7
- import { SearchMemoriesInputSchema } from '../schemas/inputs.js';
8
- import { SearchResultSchema } from '../schemas/outputs.js';
7
+ import {} from '../schemas/inputs.js';
9
8
  import { wrapToolHandler } from './progress.js';
10
9
  import { registerToolWithContract } from './register-contract.js';
10
+ function buildNextCursorFromRows(scope, hasMore, pageRows) {
11
+ if (!hasMore || pageRows.length === 0) {
12
+ return undefined;
13
+ }
14
+ const lastRow = pageRows[pageRows.length - 1];
15
+ if (!lastRow) {
16
+ return undefined;
17
+ }
18
+ const rank = lastRow.rank ?? 0;
19
+ return encodeSearchCursor(scope, rank, lastRow.hash);
20
+ }
11
21
  export function registerSearchMemories(server, db) {
12
- registerToolWithContract(server, 'search_memories', SearchMemoriesInputSchema, SearchResultSchema, wrapToolHandler((params) => {
13
- try {
14
- const { limit, cursor } = params;
15
- const filters = toMemoryFilters(params);
16
- const scope = buildSearchCursorScope(params.query, filters);
17
- const decodedCursor = cursor
18
- ? decodeSearchCursor(cursor, scope)
19
- : undefined;
20
- const rows = loadRankedSearchRows(db, params.query, limit, decodedCursor, filters);
21
- const { page: pageRows, hasMore } = splitPage(rows, limit);
22
- const memories = pageRows.map(parseMemoryRow);
23
- let nextCursor;
24
- if (hasMore && pageRows.length > 0) {
25
- const lastRow = pageRows[pageRows.length - 1];
26
- if (lastRow !== undefined) {
27
- const rank = lastRow.rank ?? 0;
28
- nextCursor = encodeSearchCursor(scope, rank, lastRow.hash);
29
- }
30
- }
31
- return createToolResponse({
32
- memories,
33
- total_returned: memories.length,
34
- ...(nextCursor ? { nextCursor } : {}),
35
- });
36
- }
37
- catch (err) {
38
- rethrowMcpError(err);
39
- return createErrorResponse(E_UNKNOWN, getErrorMessage(err));
40
- }
41
- }, {
22
+ registerToolWithContract(server, 'search_memories', wrapToolHandler((params) => executeToolSafely(() => {
23
+ const { limit, cursor } = params;
24
+ const filters = toMemoryFilters(params);
25
+ const scope = buildSearchCursorScope(params.query, filters);
26
+ const decodedCursor = cursor
27
+ ? decodeSearchCursor(cursor, scope)
28
+ : undefined;
29
+ const rows = loadRankedSearchRows(db, params.query, limit, decodedCursor, filters);
30
+ const { page: pageRows, hasMore } = splitPage(rows, limit);
31
+ const memories = pageRows.map(parseMemoryRow);
32
+ const nextCursor = buildNextCursorFromRows(scope, hasMore, pageRows);
33
+ return createToolResponse({
34
+ memories,
35
+ total_returned: memories.length,
36
+ ...(nextCursor ? { nextCursor } : {}),
37
+ });
38
+ }), {
42
39
  progressMessage: (params) => `⊙ search_memories: ${params.query} [limit ${params.limit}]`,
43
40
  }));
44
41
  }
@@ -1,10 +1,9 @@
1
- import { E_UNKNOWN, getErrorMessage, rethrowMcpError } from '../lib/errors.js';
2
1
  import { computeMemoryHash } from '../lib/hash.js';
3
2
  import { logToolEvent, notifyMemoryResourceUpdated } from '../lib/mcp-utils.js';
4
3
  import { INSERT_MEMORY_SQL } from '../lib/sql.js';
5
- import { createErrorResponse, createToolResponse, } from '../lib/tool-response.js';
6
- import { StoreMemoriesInputSchema } from '../schemas/inputs.js';
7
- import { BatchResultSchema } from '../schemas/outputs.js';
4
+ import { executeToolSafely, summarizeBatch } from '../lib/tool-execution.js';
5
+ import { createToolResponse } from '../lib/tool-response.js';
6
+ import {} from '../schemas/inputs.js';
8
7
  import { wrapToolHandler } from './progress.js';
9
8
  import { registerToolWithContract } from './register-contract.js';
10
9
  async function notifyCreatedResources(server, items) {
@@ -14,47 +13,37 @@ async function notifyCreatedResources(server, items) {
14
13
  await Promise.allSettled(notifications);
15
14
  }
16
15
  export function registerStoreMemories(server, db) {
17
- registerToolWithContract(server, 'store_memories', StoreMemoriesInputSchema, BatchResultSchema, wrapToolHandler(async (params) => {
18
- try {
19
- const now = new Date().toISOString();
20
- const results = db.transaction(() => {
21
- const items = [];
22
- const stmt = db.prepareOnce(INSERT_MEMORY_SQL);
23
- for (const item of params.items) {
24
- const { importance, memory_type: rawMemoryType } = item;
25
- const memoryType = rawMemoryType ?? 'general';
26
- const hash = computeMemoryHash(item.content, item.tags);
27
- const tagsJson = JSON.stringify(item.tags);
28
- const result = stmt.run(hash, item.content, tagsJson, memoryType, importance, now, now);
29
- items.push({
30
- hash,
31
- ok: true,
32
- created: result.changes > 0,
33
- });
34
- }
35
- return items;
36
- });
37
- let created = 0;
38
- let succeeded = 0;
39
- for (const item of results) {
40
- if (item.ok)
41
- succeeded += 1;
42
- if (item.created)
43
- created += 1;
16
+ registerToolWithContract(server, 'store_memories', wrapToolHandler(async (params) => executeToolSafely(async () => {
17
+ const now = new Date().toISOString();
18
+ const results = db.transaction(() => {
19
+ const items = [];
20
+ const stmt = db.prepareOnce(INSERT_MEMORY_SQL);
21
+ for (const item of params.items) {
22
+ const { importance, memory_type: rawMemoryType } = item;
23
+ const memoryType = rawMemoryType ?? 'general';
24
+ const hash = computeMemoryHash(item.content, item.tags);
25
+ const tagsJson = JSON.stringify(item.tags);
26
+ const result = stmt.run(hash, item.content, tagsJson, memoryType, importance, now, now);
27
+ items.push({
28
+ hash,
29
+ ok: true,
30
+ created: result.changes > 0,
31
+ });
44
32
  }
45
- const failed = results.length - succeeded;
46
- await logToolEvent(server, 'store_memories', {
47
- total: results.length,
48
- created,
49
- });
50
- await notifyCreatedResources(server, results);
51
- return createToolResponse({ items: results, succeeded, failed });
52
- }
53
- catch (err) {
54
- rethrowMcpError(err);
55
- return createErrorResponse(E_UNKNOWN, getErrorMessage(err));
56
- }
57
- }, {
33
+ return items;
34
+ });
35
+ const summary = summarizeBatch(results, (item) => item.created === true);
36
+ await logToolEvent(server, 'store_memories', {
37
+ total: results.length,
38
+ created: summary.matched,
39
+ });
40
+ await notifyCreatedResources(server, results);
41
+ return createToolResponse({
42
+ items: results,
43
+ succeeded: summary.succeeded,
44
+ failed: summary.failed,
45
+ });
46
+ }), {
58
47
  progressMessage: (params) => `⊕ store_memories: ${params.items.length} items [batch]`,
59
48
  }));
60
49
  }
@@ -1,10 +1,9 @@
1
- import { E_UNKNOWN, getErrorMessage, rethrowMcpError } from '../lib/errors.js';
2
1
  import { computeMemoryHash } from '../lib/hash.js';
3
2
  import { logToolEvent, notifyMemoryResourceUpdated } from '../lib/mcp-utils.js';
4
3
  import { INSERT_MEMORY_SQL } from '../lib/sql.js';
5
- import { createErrorResponse, createToolResponse, } from '../lib/tool-response.js';
6
- import { StoreMemoryInputSchema } from '../schemas/inputs.js';
7
- import { StoreResultSchema } from '../schemas/outputs.js';
4
+ import { executeToolSafely } from '../lib/tool-execution.js';
5
+ import { createToolResponse } from '../lib/tool-response.js';
6
+ import {} from '../schemas/inputs.js';
8
7
  import { wrapToolHandler } from './progress.js';
9
8
  import { registerToolWithContract } from './register-contract.js';
10
9
  function toInsertParams(params, hash, memoryType, now) {
@@ -25,23 +24,17 @@ function insertMemory(db, params, hash, memoryType, now) {
25
24
  return insertResult.changes > 0;
26
25
  }
27
26
  export function registerStoreMemory(server, db) {
28
- registerToolWithContract(server, 'store_memory', StoreMemoryInputSchema, StoreResultSchema, wrapToolHandler(async (params) => {
29
- try {
30
- const memoryType = params.memory_type ?? 'general';
31
- const hash = computeMemoryHash(params.content, params.tags);
32
- const now = new Date().toISOString();
33
- const created = insertMemory(db, params, hash, memoryType, now);
34
- await logToolEvent(server, 'store', { hash, created });
35
- if (created) {
36
- await notifyMemoryResourceUpdated(server, hash);
37
- }
38
- return createToolResponse({ hash, created });
27
+ registerToolWithContract(server, 'store_memory', wrapToolHandler(async (params) => executeToolSafely(async () => {
28
+ const memoryType = params.memory_type ?? 'general';
29
+ const hash = computeMemoryHash(params.content, params.tags);
30
+ const now = new Date().toISOString();
31
+ const created = insertMemory(db, params, hash, memoryType, now);
32
+ await logToolEvent(server, 'store', { hash, created });
33
+ if (created) {
34
+ await notifyMemoryResourceUpdated(server, hash);
39
35
  }
40
- catch (err) {
41
- rethrowMcpError(err);
42
- return createErrorResponse(E_UNKNOWN, getErrorMessage(err));
43
- }
44
- }, {
36
+ return createToolResponse({ hash, created });
37
+ }), {
45
38
  progressMessage: (params) => `⊕ store_memory: ${params.tags.length} tags [single]`,
46
39
  }));
47
40
  }
@@ -1,13 +1,14 @@
1
- import { E_CONFLICT, E_NOT_FOUND, E_UNKNOWN, getErrorMessage, rethrowMcpError, } from '../lib/errors.js';
1
+ import { E_CONFLICT, E_NOT_FOUND } from '../lib/errors.js';
2
2
  import { computeMemoryHash } from '../lib/hash.js';
3
3
  import { logToolEvent, notifyMemoryResourceUpdated } from '../lib/mcp-utils.js';
4
4
  import { SELECT_MEMORY_BY_HASH_SQL, SELECT_MEMORY_HASH_SQL, } from '../lib/sql.js';
5
+ import { executeToolSafely } from '../lib/tool-execution.js';
5
6
  import { createErrorResponse, createToolResponse, } from '../lib/tool-response.js';
6
7
  import { parseTags } from '../lib/types.js';
7
- import { UpdateMemoryInputSchema } from '../schemas/inputs.js';
8
- import { UpdateResultSchema } from '../schemas/outputs.js';
8
+ import {} from '../schemas/inputs.js';
9
9
  import { wrapToolHandler } from './progress.js';
10
10
  import { registerToolWithContract } from './register-contract.js';
11
+ import { formatHashPreview } from './result.js';
11
12
  const UPDATE_MEMORY_SQL = `UPDATE memories
12
13
  SET hash = ?, content = ?, tags = ?, updated_at = ?
13
14
  WHERE hash = ?`;
@@ -19,57 +20,52 @@ async function notifyUpdatedMemoryResources(server, oldHash, newHash) {
19
20
  await Promise.allSettled(notifications);
20
21
  }
21
22
  export function registerUpdateMemory(server, db) {
22
- registerToolWithContract(server, 'update_memory', UpdateMemoryInputSchema, UpdateResultSchema, wrapToolHandler(async (params) => {
23
- try {
24
- // All reads and the write are inside a single IMMEDIATE transaction
25
- // to prevent TOCTOU between existence/collision checks and UPDATE.
26
- const txResult = db.transaction(() => {
27
- const existing = db
28
- .prepareOnce(SELECT_MEMORY_BY_HASH_SQL)
29
- .get(params.hash);
30
- if (!existing) {
23
+ registerToolWithContract(server, 'update_memory', wrapToolHandler(async (params) => executeToolSafely(async () => {
24
+ // All reads and the write are inside a single IMMEDIATE transaction
25
+ // to prevent TOCTOU between existence/collision checks and UPDATE.
26
+ const txResult = db.transaction(() => {
27
+ const existing = db
28
+ .prepareOnce(SELECT_MEMORY_BY_HASH_SQL)
29
+ .get(params.hash);
30
+ if (!existing) {
31
+ return {
32
+ ok: false,
33
+ code: E_NOT_FOUND,
34
+ message: `Memory not found: ${params.hash}`,
35
+ };
36
+ }
37
+ const newContent = params.content ?? existing.content;
38
+ const newTags = params.tags ?? parseTags(existing.tags);
39
+ const newHash = computeMemoryHash(newContent, newTags);
40
+ if (newHash !== params.hash) {
41
+ const collision = db
42
+ .prepareOnce(SELECT_MEMORY_HASH_SQL)
43
+ .get(newHash);
44
+ if (collision) {
31
45
  return {
32
46
  ok: false,
33
- code: E_NOT_FOUND,
34
- message: `Memory not found: ${params.hash}`,
47
+ code: E_CONFLICT,
48
+ message: `Memory already exists for target content/tags: ${newHash}`,
35
49
  };
36
50
  }
37
- const newTags = params.tags ?? parseTags(existing.tags);
38
- const newHash = computeMemoryHash(params.content, newTags);
39
- if (newHash !== params.hash) {
40
- const collision = db
41
- .prepareOnce(SELECT_MEMORY_HASH_SQL)
42
- .get(newHash);
43
- if (collision) {
44
- return {
45
- ok: false,
46
- code: E_CONFLICT,
47
- message: `Memory already exists for target content/tags: ${newHash}`,
48
- };
49
- }
50
- }
51
- const now = new Date().toISOString();
52
- db.prepareOnce(UPDATE_MEMORY_SQL).run(newHash, params.content, JSON.stringify(newTags), now, params.hash);
53
- return { ok: true, oldHash: params.hash, newHash };
54
- });
55
- if (!txResult.ok) {
56
- return createErrorResponse(txResult.code, txResult.message);
57
51
  }
58
- await logToolEvent(server, 'update', {
59
- oldHash: txResult.oldHash,
60
- newHash: txResult.newHash,
61
- });
62
- await notifyUpdatedMemoryResources(server, txResult.oldHash, txResult.newHash);
63
- return createToolResponse({
64
- old_hash: txResult.oldHash,
65
- new_hash: txResult.newHash,
66
- });
67
- }
68
- catch (err) {
69
- rethrowMcpError(err);
70
- return createErrorResponse(E_UNKNOWN, getErrorMessage(err));
52
+ const now = new Date().toISOString();
53
+ db.prepareOnce(UPDATE_MEMORY_SQL).run(newHash, newContent, JSON.stringify(newTags), now, params.hash);
54
+ return { ok: true, oldHash: params.hash, newHash };
55
+ });
56
+ if (!txResult.ok) {
57
+ return createErrorResponse(txResult.code, txResult.message);
71
58
  }
72
- }, {
73
- progressMessage: (params) => `⊜ update_memory: ${params.hash.slice(0, 12)}... [replace content]`,
59
+ await logToolEvent(server, 'update', {
60
+ oldHash: txResult.oldHash,
61
+ newHash: txResult.newHash,
62
+ });
63
+ await notifyUpdatedMemoryResources(server, txResult.oldHash, txResult.newHash);
64
+ return createToolResponse({
65
+ old_hash: txResult.oldHash,
66
+ new_hash: txResult.newHash,
67
+ });
68
+ }), {
69
+ progressMessage: (params) => `⊜ update_memory: ${formatHashPreview(params.hash)} [replace content]`,
74
70
  }));
75
71
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/memory-mcp",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "mcpName": "io.github.j0hanz/memory-mcp",
5
5
  "author": "Johanz",
6
6
  "license": "MIT",