@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
@@ -14,7 +14,6 @@ const TAG_SCHEMA = z
14
14
  .describe('Tag (no whitespace, max 50 chars)');
15
15
  const TAGS_ARRAY_SCHEMA = z
16
16
  .array(TAG_SCHEMA)
17
- .min(1, { error: 'At least one tag is required' })
18
17
  .max(100, { error: 'Maximum 100 tags allowed' })
19
18
  .describe('Memory tags');
20
19
  const MEMORY_TYPE_SCHEMA = z
@@ -67,6 +66,18 @@ const SEARCH_MEMORY_TYPE_DESCRIPTION = 'Memory type filter';
67
66
  const RECALL_MIN_IMPORTANCE_DESCRIPTION = 'Min importance filter';
68
67
  const RECALL_MAX_IMPORTANCE_DESCRIPTION = 'Max importance filter';
69
68
  const RECALL_MEMORY_TYPE_DESCRIPTION = 'Memory type filter';
69
+ function describeHash(label) {
70
+ return HASH_SCHEMA.describe(label);
71
+ }
72
+ function createPrefaultIntField(config) {
73
+ return z
74
+ .int()
75
+ .min(config.min)
76
+ .max(config.max)
77
+ .optional()
78
+ .prefault(config.prefault)
79
+ .describe(config.description);
80
+ }
70
81
  function describeImportanceFilter(description) {
71
82
  return IMPORTANCE_FILTER_SCHEMA.clone().describe(description);
72
83
  }
@@ -114,19 +125,22 @@ export const StoreMemoriesInputSchema = z
114
125
  .describe('Store multiple memories');
115
126
  export const GetMemoryInputSchema = z
116
127
  .strictObject({
117
- hash: HASH_SCHEMA,
128
+ hash: describeHash('SHA-256 hash'),
118
129
  })
119
130
  .describe('Get memory by hash');
120
131
  export const UpdateMemoryInputSchema = z
121
132
  .strictObject({
122
- hash: HASH_SCHEMA,
123
- content: CONTENT_SCHEMA,
133
+ hash: describeHash('SHA-256 hash'),
134
+ content: CONTENT_SCHEMA.optional(),
124
135
  tags: TAGS_ARRAY_SCHEMA.optional(),
136
+ })
137
+ .refine((data) => data.content !== undefined || data.tags !== undefined, {
138
+ error: 'At least one of content or tags must be provided',
125
139
  })
126
140
  .describe('Update memory');
127
141
  export const DeleteMemoryInputSchema = z
128
142
  .strictObject({
129
- hash: HASH_SCHEMA,
143
+ hash: describeHash('SHA-256 hash'),
130
144
  })
131
145
  .describe('Delete memory by hash');
132
146
  export const DeleteMemoriesInputSchema = z
@@ -138,16 +152,20 @@ export const DeleteMemoriesInputSchema = z
138
152
  .describe('Hashes to delete (1-50)'),
139
153
  })
140
154
  .describe('Delete multiple memories');
155
+ const RELATIONSHIP_ENDPOINT_FIELDS = {
156
+ from_hash: describeHash('Source hash'),
157
+ to_hash: describeHash('Target hash'),
158
+ relation_type: RELATION_TYPE_SCHEMA.describe('Relationship type'),
159
+ };
141
160
  export const SearchMemoriesInputSchema = z
142
161
  .strictObject({
143
162
  query: SEARCH_QUERY_SCHEMA.describe('Search query'),
144
- limit: z
145
- .int()
146
- .min(1)
147
- .max(100)
148
- .optional()
149
- .prefault(20)
150
- .describe('Max results (default 20)'),
163
+ limit: createPrefaultIntField({
164
+ min: 1,
165
+ max: 100,
166
+ prefault: 20,
167
+ description: 'Max results (default 20)',
168
+ }),
151
169
  cursor: CURSOR_SCHEMA.optional().describe(CURSOR_DESCRIPTION),
152
170
  ...SEARCH_FILTER_FIELDS,
153
171
  })
@@ -155,44 +173,47 @@ export const SearchMemoriesInputSchema = z
155
173
  export const RecallInputSchema = z
156
174
  .strictObject({
157
175
  query: SEARCH_QUERY_SCHEMA.describe('Search query'),
158
- depth: z
159
- .int()
160
- .min(0)
161
- .max(3)
162
- .optional()
163
- .prefault(1)
164
- .describe('Relationship hops (0-3)'),
165
- limit: z
166
- .int()
167
- .min(1)
168
- .max(50)
169
- .optional()
170
- .prefault(10)
171
- .describe('Max seed memories (default 10)'),
176
+ depth: createPrefaultIntField({
177
+ min: 0,
178
+ max: 3,
179
+ prefault: 1,
180
+ description: 'Relationship hops (0-3)',
181
+ }),
182
+ limit: createPrefaultIntField({
183
+ min: 1,
184
+ max: 50,
185
+ prefault: 10,
186
+ description: 'Max seed memories (default 10)',
187
+ }),
172
188
  cursor: CURSOR_SCHEMA.optional().describe(CURSOR_DESCRIPTION),
173
189
  ...RECALL_FILTER_FIELDS,
174
190
  })
175
191
  .describe('Recall memories via graph traversal');
192
+ const RETRIEVE_CONTEXT_FILTER_FIELDS = createSearchFilterFields({
193
+ min: 'Min importance filter',
194
+ max: 'Max importance filter',
195
+ type: 'Memory type filter',
196
+ });
176
197
  export const RetrieveContextInputSchema = z
177
198
  .strictObject({
178
199
  query: SEARCH_QUERY_SCHEMA.describe('Search query'),
179
- token_budget: z
180
- .int()
181
- .min(100)
182
- .max(200000)
183
- .optional()
184
- .prefault(4000)
185
- .describe('Max tokens (default 4000)'),
200
+ token_budget: createPrefaultIntField({
201
+ min: 100,
202
+ max: 200000,
203
+ prefault: 4000,
204
+ description: 'Max tokens (default 4000)',
205
+ }),
186
206
  strategy: z
187
207
  .enum(['importance', 'recency', 'relevance'])
188
208
  .optional()
189
209
  .prefault('relevance')
190
210
  .describe('Sort strategy'),
211
+ ...RETRIEVE_CONTEXT_FILTER_FIELDS,
191
212
  })
192
213
  .describe('Retrieve context within token budget');
193
214
  export const GetRelationshipsInputSchema = z
194
215
  .strictObject({
195
- hash: HASH_SCHEMA,
216
+ hash: describeHash('SHA-256 hash'),
196
217
  direction: z
197
218
  .enum(['outgoing', 'incoming', 'both'])
198
219
  .optional()
@@ -202,16 +223,12 @@ export const GetRelationshipsInputSchema = z
202
223
  .describe('Get relationships');
203
224
  export const CreateRelationshipInputSchema = z
204
225
  .strictObject({
205
- from_hash: HASH_SCHEMA.describe('Source hash'),
206
- to_hash: HASH_SCHEMA.describe('Target hash'),
207
- relation_type: RELATION_TYPE_SCHEMA.describe('Relationship type'),
226
+ ...RELATIONSHIP_ENDPOINT_FIELDS,
208
227
  })
209
228
  .describe('Create relationship');
210
229
  export const DeleteRelationshipInputSchema = z
211
230
  .strictObject({
212
- from_hash: HASH_SCHEMA.describe('Source hash'),
213
- to_hash: HASH_SCHEMA.describe('Target hash'),
214
- relation_type: RELATION_TYPE_SCHEMA.describe('Relationship type'),
231
+ ...RELATIONSHIP_ENDPOINT_FIELDS,
215
232
  })
216
233
  .describe('Delete relationship');
217
234
  export const MemoryStatsInputSchema = z
@@ -73,23 +73,23 @@ export declare const RelationshipEdgeSchema: z.ZodObject<{
73
73
  relation_type: z.ZodString;
74
74
  }, z.core.$strict>;
75
75
  export declare const RelationshipWithMemorySchema: z.ZodObject<{
76
- from_hash: z.ZodString;
77
- to_hash: z.ZodString;
78
- relation_type: z.ZodString;
79
76
  created_at: z.ZodString;
80
77
  linked_hash: z.ZodString;
81
78
  linked_content: z.ZodString;
82
79
  linked_tags: z.ZodArray<z.ZodString>;
80
+ from_hash: z.ZodString;
81
+ to_hash: z.ZodString;
82
+ relation_type: z.ZodString;
83
83
  }, z.core.$strict>;
84
84
  export declare const RelationshipResultSchema: z.ZodObject<{
85
85
  relationships: z.ZodArray<z.ZodObject<{
86
- from_hash: z.ZodString;
87
- to_hash: z.ZodString;
88
- relation_type: z.ZodString;
89
86
  created_at: z.ZodString;
90
87
  linked_hash: z.ZodString;
91
88
  linked_content: z.ZodString;
92
89
  linked_tags: z.ZodArray<z.ZodString>;
90
+ from_hash: z.ZodString;
91
+ to_hash: z.ZodString;
92
+ relation_type: z.ZodString;
93
93
  }, z.core.$strict>>;
94
94
  count: z.ZodNumber;
95
95
  }, z.core.$strict>;
@@ -3,6 +3,11 @@ const STRING_SCHEMA = z.string();
3
3
  const NUMBER_SCHEMA = z.number();
4
4
  const BOOLEAN_SCHEMA = z.boolean();
5
5
  const STRING_ARRAY_SCHEMA = z.array(STRING_SCHEMA);
6
+ const RELATIONSHIP_FIELDS = {
7
+ from_hash: STRING_SCHEMA.describe('Source hash'),
8
+ to_hash: STRING_SCHEMA.describe('Target hash'),
9
+ relation_type: STRING_SCHEMA.describe('Relationship type'),
10
+ };
6
11
  export const ErrorResultSchema = z
7
12
  .strictObject({
8
13
  code: STRING_SCHEMA.describe('Error code'),
@@ -65,16 +70,12 @@ export const SearchResultSchema = z
65
70
  .describe('Search result');
66
71
  export const RelationshipEdgeSchema = z
67
72
  .strictObject({
68
- from_hash: z.string().describe('Source hash'),
69
- to_hash: z.string().describe('Target hash'),
70
- relation_type: z.string().describe('Relationship type'),
73
+ ...RELATIONSHIP_FIELDS,
71
74
  })
72
75
  .describe('Relationship edge');
73
76
  export const RelationshipWithMemorySchema = z
74
77
  .strictObject({
75
- from_hash: z.string().describe('Source hash'),
76
- to_hash: z.string().describe('Target hash'),
77
- relation_type: z.string().describe('Relationship type'),
78
+ ...RELATIONSHIP_FIELDS,
78
79
  created_at: z.string().describe('Created at'),
79
80
  linked_hash: z.string().describe('Linked memory hash'),
80
81
  linked_content: z.string().describe('Linked memory content'),
package/dist/server.js CHANGED
@@ -63,15 +63,22 @@ function createIconDescriptors() {
63
63
  return undefined;
64
64
  return [{ src, mimeType: ICON_MIME, sizes: ICON_SIZES }];
65
65
  }
66
- export function createServer(db) {
66
+ function buildServerIdentity() {
67
67
  const { version } = loadPackageManifest();
68
68
  const icons = createIconDescriptors();
69
- const instructions = loadInstructions();
70
- const server = new McpServer({
69
+ return {
71
70
  name: SERVER_NAME,
72
71
  version,
73
72
  ...(icons ? { icons } : {}),
74
- }, {
73
+ };
74
+ }
75
+ function buildServerInstructions() {
76
+ return loadInstructions();
77
+ }
78
+ export function createServer(db) {
79
+ const identity = buildServerIdentity();
80
+ const instructions = buildServerInstructions();
81
+ const server = new McpServer(identity, {
75
82
  capabilities: SERVER_CAPABILITIES,
76
83
  instructions,
77
84
  });
@@ -1,10 +1,11 @@
1
- import { E_NOT_FOUND, E_UNKNOWN, getErrorMessage, rethrowMcpError, } from '../lib/errors.js';
1
+ import { E_NOT_FOUND } from '../lib/errors.js';
2
2
  import { logToolEvent } from '../lib/mcp-utils.js';
3
+ import { executeToolSafely } from '../lib/tool-execution.js';
3
4
  import { createErrorResponse, createToolResponse, } from '../lib/tool-response.js';
4
- import { CreateRelationshipInputSchema } from '../schemas/inputs.js';
5
- import { CreateRelationshipResultSchema } from '../schemas/outputs.js';
5
+ import {} from '../schemas/inputs.js';
6
6
  import { wrapToolHandler } from './progress.js';
7
7
  import { registerToolWithContract } from './register-contract.js';
8
+ import { formatRelationshipPreview } from './result.js';
8
9
  const INSERT_RELATIONSHIP_SQL = `INSERT OR IGNORE INTO relationships (from_hash, to_hash, relation_type, created_at)
9
10
  VALUES (?, ?, ?, ?)`;
10
11
  const SELECT_HASHES_SQL = 'SELECT hash FROM memories WHERE hash IN (?, ?) LIMIT 2';
@@ -42,25 +43,19 @@ function createRelationshipTx(db, params) {
42
43
  });
43
44
  }
44
45
  export function registerCreateRelationship(server, db) {
45
- registerToolWithContract(server, 'create_relationship', CreateRelationshipInputSchema, CreateRelationshipResultSchema, wrapToolHandler(async (params) => {
46
- try {
47
- const txResult = createRelationshipTx(db, params);
48
- if (!txResult.ok) {
49
- return createErrorResponse(txResult.code, txResult.message);
50
- }
51
- await logToolEvent(server, 'create_relationship', {
52
- fromHash: params.from_hash,
53
- toHash: params.to_hash,
54
- relationType: params.relation_type,
55
- created: txResult.created,
56
- });
57
- return createToolResponse({ created: txResult.created });
46
+ registerToolWithContract(server, 'create_relationship', wrapToolHandler(async (params) => executeToolSafely(async () => {
47
+ const txResult = createRelationshipTx(db, params);
48
+ if (!txResult.ok) {
49
+ return createErrorResponse(txResult.code, txResult.message);
58
50
  }
59
- catch (err) {
60
- rethrowMcpError(err);
61
- return createErrorResponse(E_UNKNOWN, getErrorMessage(err));
62
- }
63
- }, {
64
- progressMessage: (params) => `⊕ create_relationship: ${params.from_hash.slice(0, 8)}... -> ${params.to_hash.slice(0, 8)}... [${params.relation_type}]`,
51
+ await logToolEvent(server, 'create_relationship', {
52
+ fromHash: params.from_hash,
53
+ toHash: params.to_hash,
54
+ relationType: params.relation_type,
55
+ created: txResult.created,
56
+ });
57
+ return createToolResponse({ created: txResult.created });
58
+ }), {
59
+ progressMessage: (params) => `⊕ create_relationship: ${formatRelationshipPreview(params.from_hash, params.to_hash)} [${params.relation_type}]`,
65
60
  }));
66
61
  }
@@ -1,54 +1,45 @@
1
- import { E_UNKNOWN, getErrorMessage, rethrowMcpError } from '../lib/errors.js';
2
1
  import { logToolEvent, notifyMemoryResourceUpdated } from '../lib/mcp-utils.js';
3
2
  import { DELETE_MEMORY_SQL } from '../lib/sql.js';
4
- import { createErrorResponse, createToolResponse, } from '../lib/tool-response.js';
5
- import { DeleteMemoriesInputSchema } from '../schemas/inputs.js';
6
- import { BatchResultSchema } from '../schemas/outputs.js';
3
+ import { executeToolSafely, summarizeBatch } from '../lib/tool-execution.js';
4
+ import { createToolResponse } from '../lib/tool-response.js';
5
+ import {} from '../schemas/inputs.js';
7
6
  import { wrapToolHandler } from './progress.js';
8
7
  import { registerToolWithContract } from './register-contract.js';
9
8
  async function notifyDeletedResources(server, items) {
9
+ // MCP spec (v2025-11-25) has no 'deleted' resource notification;
10
+ // 'updated' is the closest available signal to inform clients.
10
11
  const notifications = items
11
12
  .filter((item) => item.deleted)
12
13
  .map((item) => notifyMemoryResourceUpdated(server, item.hash));
13
14
  await Promise.allSettled(notifications);
14
15
  }
15
16
  export function registerDeleteMemories(server, db) {
16
- registerToolWithContract(server, 'delete_memories', DeleteMemoriesInputSchema, BatchResultSchema, wrapToolHandler(async (params) => {
17
- try {
18
- const results = db.transaction(() => {
19
- const items = [];
20
- const stmt = db.prepareOnce(DELETE_MEMORY_SQL);
21
- for (const hash of params.hashes) {
22
- const result = stmt.run(hash);
23
- items.push({
24
- hash,
25
- ok: true,
26
- deleted: result.changes > 0,
27
- });
28
- }
29
- return items;
30
- });
31
- let deleted = 0;
32
- let succeeded = 0;
33
- for (const item of results) {
34
- if (item.ok)
35
- succeeded += 1;
36
- if (item.deleted)
37
- deleted += 1;
17
+ registerToolWithContract(server, 'delete_memories', wrapToolHandler(async (params) => executeToolSafely(async () => {
18
+ const results = db.transaction(() => {
19
+ const items = [];
20
+ const stmt = db.prepareOnce(DELETE_MEMORY_SQL);
21
+ for (const hash of params.hashes) {
22
+ const result = stmt.run(hash);
23
+ items.push({
24
+ hash,
25
+ ok: true,
26
+ deleted: result.changes > 0,
27
+ });
38
28
  }
39
- const failed = results.length - succeeded;
40
- await logToolEvent(server, 'delete_memories', {
41
- total: params.hashes.length,
42
- deleted,
43
- });
44
- await notifyDeletedResources(server, results);
45
- return createToolResponse({ items: results, succeeded, failed });
46
- }
47
- catch (err) {
48
- rethrowMcpError(err);
49
- return createErrorResponse(E_UNKNOWN, getErrorMessage(err));
50
- }
51
- }, {
29
+ return items;
30
+ });
31
+ const summary = summarizeBatch(results, (item) => item.deleted === true);
32
+ await logToolEvent(server, 'delete_memories', {
33
+ total: params.hashes.length,
34
+ deleted: summary.matched,
35
+ });
36
+ await notifyDeletedResources(server, results);
37
+ return createToolResponse({
38
+ items: results,
39
+ succeeded: summary.succeeded,
40
+ failed: summary.failed,
41
+ });
42
+ }), {
52
43
  progressMessage: (params) => `⊖ delete_memories: ${params.hashes.length} hashes [batch]`,
53
44
  }));
54
45
  }
@@ -1,29 +1,25 @@
1
- import { E_UNKNOWN, getErrorMessage, rethrowMcpError } from '../lib/errors.js';
2
1
  import { logToolEvent, notifyMemoryResourceUpdated } from '../lib/mcp-utils.js';
3
2
  import { DELETE_MEMORY_SQL } from '../lib/sql.js';
4
- import { createErrorResponse, createToolResponse, } from '../lib/tool-response.js';
5
- import { DeleteMemoryInputSchema } from '../schemas/inputs.js';
6
- import { DeleteResultSchema } from '../schemas/outputs.js';
3
+ import { executeToolSafely } from '../lib/tool-execution.js';
4
+ import { createToolResponse } from '../lib/tool-response.js';
5
+ import {} from '../schemas/inputs.js';
7
6
  import { wrapToolHandler } from './progress.js';
8
7
  import { registerToolWithContract } from './register-contract.js';
8
+ import { formatHashPreview } from './result.js';
9
9
  function deleteByHash(db, hash) {
10
10
  return db.prepareOnce(DELETE_MEMORY_SQL).run(hash).changes > 0;
11
11
  }
12
12
  export function registerDeleteMemory(server, db) {
13
- registerToolWithContract(server, 'delete_memory', DeleteMemoryInputSchema, DeleteResultSchema, wrapToolHandler(async (params) => {
14
- try {
15
- const deleted = deleteByHash(db, params.hash);
16
- if (deleted) {
17
- await logToolEvent(server, 'delete', { hash: params.hash });
18
- await notifyMemoryResourceUpdated(server, params.hash);
19
- }
20
- return createToolResponse({ deleted, hash: params.hash });
13
+ registerToolWithContract(server, 'delete_memory', wrapToolHandler(async (params) => executeToolSafely(async () => {
14
+ const deleted = deleteByHash(db, params.hash);
15
+ if (deleted) {
16
+ await logToolEvent(server, 'delete', { hash: params.hash });
17
+ // MCP spec (v2025-11-25) has no 'deleted' resource notification;
18
+ // 'updated' is the closest available signal to inform clients.
19
+ await notifyMemoryResourceUpdated(server, params.hash);
21
20
  }
22
- catch (err) {
23
- rethrowMcpError(err);
24
- return createErrorResponse(E_UNKNOWN, getErrorMessage(err));
25
- }
26
- }, {
27
- progressMessage: (params) => `⊖ delete_memory: ${params.hash.slice(0, 12)}... [single]`,
21
+ return createToolResponse({ deleted, hash: params.hash });
22
+ }), {
23
+ progressMessage: (params) => `⊖ delete_memory: ${formatHashPreview(params.hash)} [single]`,
28
24
  }));
29
25
  }
@@ -1,35 +1,20 @@
1
- import { E_NOT_FOUND, E_UNKNOWN, getErrorMessage, rethrowMcpError, } from '../lib/errors.js';
2
- import { createErrorResponse, createToolResponse, } from '../lib/tool-response.js';
3
- import { DeleteRelationshipInputSchema } from '../schemas/inputs.js';
4
- import { DeleteRelationshipResultSchema } from '../schemas/outputs.js';
1
+ import { executeToolSafely } from '../lib/tool-execution.js';
2
+ import { createToolResponse } from '../lib/tool-response.js';
3
+ import {} from '../schemas/inputs.js';
5
4
  import { wrapToolHandler } from './progress.js';
6
5
  import { registerToolWithContract } from './register-contract.js';
6
+ import { formatRelationshipPreview } from './result.js';
7
7
  const DELETE_RELATIONSHIP_SQL = 'DELETE FROM relationships WHERE from_hash = ? AND to_hash = ? AND relation_type = ?';
8
- const DELETE_RELATIONSHIP_RESULT = { deleted: true };
9
- function formatRelationship(params) {
10
- return `${params.from_hash} -[${params.relation_type}]-> ${params.to_hash}`;
11
- }
12
- function createNotFoundRelationshipMessage(params) {
13
- return `Relationship not found: ${formatRelationship(params)}`;
14
- }
15
8
  function deleteRelationship(db, params) {
16
9
  return (db
17
10
  .prepareOnce(DELETE_RELATIONSHIP_SQL)
18
11
  .run(params.from_hash, params.to_hash, params.relation_type).changes > 0);
19
12
  }
20
13
  export function registerDeleteRelationship(server, db) {
21
- registerToolWithContract(server, 'delete_relationship', DeleteRelationshipInputSchema, DeleteRelationshipResultSchema, wrapToolHandler((params) => {
22
- try {
23
- if (!deleteRelationship(db, params)) {
24
- return createErrorResponse(E_NOT_FOUND, createNotFoundRelationshipMessage(params));
25
- }
26
- return createToolResponse(DELETE_RELATIONSHIP_RESULT);
27
- }
28
- catch (err) {
29
- rethrowMcpError(err);
30
- return createErrorResponse(E_UNKNOWN, getErrorMessage(err));
31
- }
32
- }, {
33
- progressMessage: (params) => `⊖ delete_relationship: ${params.from_hash.slice(0, 8)}... -> ${params.to_hash.slice(0, 8)}... [${params.relation_type}]`,
14
+ registerToolWithContract(server, 'delete_relationship', wrapToolHandler((params) => executeToolSafely(() => {
15
+ const deleted = deleteRelationship(db, params);
16
+ return createToolResponse({ deleted });
17
+ }), {
18
+ progressMessage: (params) => `⊖ delete_relationship: ${formatRelationshipPreview(params.from_hash, params.to_hash)} [${params.relation_type}]`,
34
19
  }));
35
20
  }
@@ -1,11 +1,12 @@
1
- import { E_NOT_FOUND, E_UNKNOWN, getErrorMessage, rethrowMcpError, } from '../lib/errors.js';
1
+ import { E_NOT_FOUND } from '../lib/errors.js';
2
2
  import { SELECT_MEMORY_BY_HASH_SQL } from '../lib/sql.js';
3
+ import { executeToolSafely } from '../lib/tool-execution.js';
3
4
  import { createErrorResponse, createToolResponse, } from '../lib/tool-response.js';
4
5
  import { parseMemoryRow } from '../lib/types.js';
5
- import { GetMemoryInputSchema } from '../schemas/inputs.js';
6
- import { MemoryResultSchema } from '../schemas/outputs.js';
6
+ import {} from '../schemas/inputs.js';
7
7
  import { wrapToolHandler } from './progress.js';
8
8
  import { registerToolWithContract } from './register-contract.js';
9
+ import { formatHashPreview } from './result.js';
9
10
  function getMemoryRow(db, hash) {
10
11
  return db.prepareOnce(SELECT_MEMORY_BY_HASH_SQL).get(hash);
11
12
  }
@@ -13,20 +14,14 @@ function notFound(hash) {
13
14
  return createErrorResponse(E_NOT_FOUND, `Memory not found: ${hash}`);
14
15
  }
15
16
  export function registerGetMemory(server, db) {
16
- registerToolWithContract(server, 'get_memory', GetMemoryInputSchema, MemoryResultSchema, wrapToolHandler((params) => {
17
- try {
18
- const row = getMemoryRow(db, params.hash);
19
- if (!row) {
20
- return notFound(params.hash);
21
- }
22
- const memory = parseMemoryRow(row);
23
- return createToolResponse({ ...memory });
17
+ registerToolWithContract(server, 'get_memory', wrapToolHandler((params) => executeToolSafely(() => {
18
+ const row = getMemoryRow(db, params.hash);
19
+ if (!row) {
20
+ return notFound(params.hash);
24
21
  }
25
- catch (err) {
26
- rethrowMcpError(err);
27
- return createErrorResponse(E_UNKNOWN, getErrorMessage(err));
28
- }
29
- }, {
30
- progressMessage: (params) => `⊙ get_memory: ${params.hash.slice(0, 12)}... [single]`,
22
+ const memory = parseMemoryRow(row);
23
+ return createToolResponse({ ...memory });
24
+ }), {
25
+ progressMessage: (params) => `⊙ get_memory: ${formatHashPreview(params.hash)} [single]`,
31
26
  }));
32
27
  }
@@ -1,11 +1,12 @@
1
- import { E_NOT_FOUND, E_UNKNOWN, getErrorMessage, rethrowMcpError, } from '../lib/errors.js';
1
+ import { E_NOT_FOUND } from '../lib/errors.js';
2
2
  import { SELECT_MEMORY_HASH_SQL } from '../lib/sql.js';
3
+ import { executeToolSafely } from '../lib/tool-execution.js';
3
4
  import { createErrorResponse, createToolResponse, } from '../lib/tool-response.js';
4
5
  import { parseTags } from '../lib/types.js';
5
- import { GetRelationshipsInputSchema } from '../schemas/inputs.js';
6
- import { RelationshipResultSchema } from '../schemas/outputs.js';
6
+ import {} from '../schemas/inputs.js';
7
7
  import { wrapToolHandler } from './progress.js';
8
8
  import { registerToolWithContract } from './register-contract.js';
9
+ import { formatHashPreview } from './result.js';
9
10
  // Pre-defined SQL for each direction mode to maximise prepareOnce cache hits.
10
11
  const OUTGOING_SQL = `
11
12
  SELECT r.from_hash, r.to_hash, r.relation_type, r.created_at,
@@ -60,8 +61,10 @@ function toRelationshipWithMemory(row) {
60
61
  };
61
62
  }
62
63
  export function registerGetRelationships(server, db) {
63
- registerToolWithContract(server, 'get_relationships', GetRelationshipsInputSchema, RelationshipResultSchema, wrapToolHandler((params) => {
64
- try {
64
+ registerToolWithContract(server, 'get_relationships', wrapToolHandler((params) => executeToolSafely(() => {
65
+ // Transaction eliminates TOCTOU: memory cannot be deleted
66
+ // between the existence check and the relationship query.
67
+ return db.transaction(() => {
65
68
  if (!memoryExists(db, params.hash)) {
66
69
  return createErrorResponse(E_NOT_FOUND, `Memory not found: ${params.hash}`);
67
70
  }
@@ -71,12 +74,8 @@ export function registerGetRelationships(server, db) {
71
74
  relationships,
72
75
  count: relationships.length,
73
76
  });
74
- }
75
- catch (err) {
76
- rethrowMcpError(err);
77
- return createErrorResponse(E_UNKNOWN, getErrorMessage(err));
78
- }
79
- }, {
80
- progressMessage: (params) => `⊙ get_relationships: ${params.hash.slice(0, 12)}... [${params.direction}]`,
77
+ });
78
+ }), {
79
+ progressMessage: (params) => `⊙ get_relationships: ${formatHashPreview(params.hash)} [${params.direction}]`,
81
80
  }));
82
81
  }
@@ -1,8 +1,6 @@
1
- import { E_UNKNOWN, getErrorMessage, rethrowMcpError } from '../lib/errors.js';
2
1
  import { MEMORY_AGGREGATE_SQL, RELATIONSHIP_COUNT_SQL, TYPE_COUNTS_SQL, } from '../lib/sql.js';
3
- import { createErrorResponse, createToolResponse, } from '../lib/tool-response.js';
4
- import { MemoryStatsInputSchema } from '../schemas/inputs.js';
5
- import { StatsResultSchema } from '../schemas/outputs.js';
2
+ import { executeToolSafely } from '../lib/tool-execution.js';
3
+ import { createToolResponse } from '../lib/tool-response.js';
6
4
  import { wrapToolHandler } from './progress.js';
7
5
  import { registerToolWithContract } from './register-contract.js';
8
6
  function toTypeCounts(rows) {
@@ -13,30 +11,24 @@ function toTypeCounts(rows) {
13
11
  return byType;
14
12
  }
15
13
  export function registerMemoryStats(server, db) {
16
- registerToolWithContract(server, 'memory_stats', MemoryStatsInputSchema, StatsResultSchema, wrapToolHandler(() => {
17
- try {
18
- const aggregate = db
19
- .prepareOnce(MEMORY_AGGREGATE_SQL)
20
- .get();
21
- const totalRelationships = db.prepareOnce(RELATIONSHIP_COUNT_SQL).get()?.total ?? 0;
22
- const typeRows = db.prepareOnce(TYPE_COUNTS_SQL).all();
23
- const byType = toTypeCounts(typeRows);
24
- return createToolResponse({
25
- memories: {
26
- total: aggregate?.total ?? 0,
27
- oldest: aggregate?.oldest ?? null,
28
- newest: aggregate?.newest ?? null,
29
- avg_importance: aggregate?.avg_importance ?? null,
30
- },
31
- relationships: {
32
- total: totalRelationships,
33
- },
34
- by_type: byType,
35
- });
36
- }
37
- catch (err) {
38
- rethrowMcpError(err);
39
- return createErrorResponse(E_UNKNOWN, getErrorMessage(err));
40
- }
41
- }, { progressMessage: () => '⊙ memory_stats: [aggregate]' }));
14
+ registerToolWithContract(server, 'memory_stats', wrapToolHandler(() => executeToolSafely(() => {
15
+ const aggregate = db
16
+ .prepareOnce(MEMORY_AGGREGATE_SQL)
17
+ .get();
18
+ const totalRelationships = db.prepareOnce(RELATIONSHIP_COUNT_SQL).get()?.total ?? 0;
19
+ const typeRows = db.prepareOnce(TYPE_COUNTS_SQL).all();
20
+ const byType = toTypeCounts(typeRows);
21
+ return createToolResponse({
22
+ memories: {
23
+ total: aggregate?.total ?? 0,
24
+ oldest: aggregate?.oldest ?? null,
25
+ newest: aggregate?.newest ?? null,
26
+ avg_importance: aggregate?.avg_importance ?? null,
27
+ },
28
+ relationships: {
29
+ total: totalRelationships,
30
+ },
31
+ by_type: byType,
32
+ });
33
+ }), { progressMessage: () => '⊙ memory_stats: [aggregate]' }));
42
34
  }