@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.
- package/dist/db/index.js +16 -13
- package/dist/lib/errors.d.ts +4 -0
- package/dist/lib/errors.js +11 -0
- package/dist/lib/graph-traversal.d.ts +12 -0
- package/dist/lib/graph-traversal.js +145 -0
- package/dist/lib/json-schema.d.ts +5 -0
- package/dist/lib/json-schema.js +19 -1
- package/dist/lib/pagination.d.ts +0 -2
- package/dist/lib/pagination.js +0 -43
- package/dist/lib/search.js +44 -23
- package/dist/lib/tool-contracts.js +50 -73
- package/dist/lib/tool-execution.d.ts +13 -0
- package/dist/lib/tool-execution.js +51 -0
- package/dist/prompts/index.js +12 -8
- package/dist/resources/index.js +67 -43
- package/dist/resources/instructions.js +44 -37
- package/dist/resources/server-config.js +33 -22
- package/dist/resources/tool-catalog.js +2 -6
- package/dist/resources/tool-info.js +9 -9
- package/dist/resources/workflows.js +69 -40
- package/dist/schemas/inputs.d.ts +8 -5
- package/dist/schemas/inputs.js +57 -40
- package/dist/schemas/outputs.d.ts +6 -6
- package/dist/schemas/outputs.js +7 -6
- package/dist/server.js +11 -4
- package/dist/tools/create-relationship.js +17 -22
- package/dist/tools/delete-memories.js +30 -39
- package/dist/tools/delete-memory.js +14 -18
- package/dist/tools/delete-relationship.js +9 -24
- package/dist/tools/get-memory.js +12 -17
- package/dist/tools/get-relationships.js +11 -12
- package/dist/tools/memory-stats.js +22 -30
- package/dist/tools/progress.d.ts +6 -0
- package/dist/tools/progress.js +68 -25
- package/dist/tools/recall.js +94 -203
- package/dist/tools/register-contract.d.ts +1 -2
- package/dist/tools/register-contract.js +4 -2
- package/dist/tools/result.d.ts +4 -0
- package/dist/tools/result.js +27 -0
- package/dist/tools/retrieve-context.js +80 -98
- package/dist/tools/search-memories.js +31 -34
- package/dist/tools/store-memories.js +33 -44
- package/dist/tools/store-memory.js +13 -20
- package/dist/tools/update-memory.js +45 -49
- package/package.json +1 -1
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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 {
|
|
7
|
-
import {
|
|
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,
|
|
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,
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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',
|
|
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
|
|
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
|
-
|
|
92
|
-
let thrownError;
|
|
93
|
-
try {
|
|
122
|
+
return runWithProgressCompletion(extra, () => {
|
|
94
123
|
throwIfAborted(extra.signal);
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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 {
|
|
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 {
|
|
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',
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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 {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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',
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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 {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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',
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
|
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 {
|
|
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',
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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:
|
|
34
|
-
message: `Memory
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return
|
|
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
|
-
|
|
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
|
}
|