@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
package/dist/tools/progress.d.ts
CHANGED
|
@@ -26,9 +26,15 @@ interface WrappedHandlerOptions<TArgs> {
|
|
|
26
26
|
completionMessage?: (args: TArgs, result: CallToolResult) => string;
|
|
27
27
|
progressMessage: (args: TArgs) => string;
|
|
28
28
|
}
|
|
29
|
+
interface CompletionRunOptions {
|
|
30
|
+
reporter: Pick<ProgressReporter, 'flush'>;
|
|
31
|
+
completionCurrent: number | (() => number);
|
|
32
|
+
completionMessage: (result: CallToolResult) => string;
|
|
33
|
+
}
|
|
29
34
|
type ToolHandler<TArgs> = (args: TArgs, extra: ProgressContext) => Promise<CallToolResult> | CallToolResult;
|
|
30
35
|
export declare function notifyProgress(extra: ProgressContext, progress: ProgressUpdate): Promise<void>;
|
|
31
36
|
export declare function createProgressReporter(extra: ProgressContext, options?: ProgressReporterOptions): ProgressReporter;
|
|
32
37
|
export declare function progressWithMessage(reporter: ProgressReporter, getMessage: (progress: ProgressSnapshot) => string): ProgressReporter<ProgressSnapshot>;
|
|
33
38
|
export declare function wrapToolHandler<TArgs>(handler: ToolHandler<TArgs>, options: WrappedHandlerOptions<TArgs>): ToolHandler<TArgs>;
|
|
39
|
+
export declare function runWithProgressCompletion(extra: ProgressContext, run: () => CallToolResult | Promise<CallToolResult>, options: CompletionRunOptions): Promise<CallToolResult>;
|
|
34
40
|
export {};
|
package/dist/tools/progress.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { CancelledError, E_CANCELLED, E_UNKNOWN, getErrorMessage, rethrowMcpError, } from '../lib/errors.js';
|
|
3
|
+
import { createErrorResponse } from '../lib/tool-response.js';
|
|
2
4
|
import { getToolResultText } from './result.js';
|
|
5
|
+
function resolveCompletionCurrent(value) {
|
|
6
|
+
return typeof value === 'function' ? value() : value;
|
|
7
|
+
}
|
|
3
8
|
const DEFAULT_PROGRESS_INTERVAL_MS = 250;
|
|
9
|
+
const TOOL_HANDLER_PROGRESS_TOTAL = 1;
|
|
10
|
+
function hasProgressTransport(extra, progressToken) {
|
|
11
|
+
return (progressToken !== undefined && typeof extra.sendNotification === 'function');
|
|
12
|
+
}
|
|
4
13
|
function toProgressToken(value) {
|
|
5
14
|
if (typeof value === 'string' || typeof value === 'number') {
|
|
6
15
|
return value;
|
|
@@ -20,6 +29,19 @@ function toNotificationParams(progressToken, progress) {
|
|
|
20
29
|
}
|
|
21
30
|
return params;
|
|
22
31
|
}
|
|
32
|
+
function toProgressNotification(progressToken, progress) {
|
|
33
|
+
return {
|
|
34
|
+
method: 'notifications/progress',
|
|
35
|
+
params: toNotificationParams(progressToken, progress),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function toProgressPayload(progress, current) {
|
|
39
|
+
return {
|
|
40
|
+
current,
|
|
41
|
+
...(progress.total !== undefined ? { total: progress.total } : {}),
|
|
42
|
+
...(progress.message !== undefined ? { message: progress.message } : {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
23
45
|
function getResultOutcome(result) {
|
|
24
46
|
if (result.isError) {
|
|
25
47
|
const text = getToolResultText(result);
|
|
@@ -41,14 +63,11 @@ export async function notifyProgress(extra, progress) {
|
|
|
41
63
|
if (progressToken === undefined) {
|
|
42
64
|
return;
|
|
43
65
|
}
|
|
44
|
-
if (
|
|
66
|
+
if (!hasProgressTransport(extra, progressToken)) {
|
|
45
67
|
return;
|
|
46
68
|
}
|
|
47
69
|
try {
|
|
48
|
-
await extra.sendNotification(
|
|
49
|
-
method: 'notifications/progress',
|
|
50
|
-
params: toNotificationParams(progressToken, progress),
|
|
51
|
-
});
|
|
70
|
+
await extra.sendNotification(toProgressNotification(progressToken, progress));
|
|
52
71
|
}
|
|
53
72
|
catch {
|
|
54
73
|
// best-effort progress
|
|
@@ -75,13 +94,7 @@ export function createProgressReporter(extra, options = {}) {
|
|
|
75
94
|
lastReportedAt = now;
|
|
76
95
|
isCompleted =
|
|
77
96
|
progress.total !== undefined && monotonicCurrent >= progress.total;
|
|
78
|
-
const payload =
|
|
79
|
-
if (progress.total !== undefined) {
|
|
80
|
-
payload.total = progress.total;
|
|
81
|
-
}
|
|
82
|
-
if (progress.message !== undefined) {
|
|
83
|
-
payload.message = progress.message;
|
|
84
|
-
}
|
|
97
|
+
const payload = toProgressPayload(progress, monotonicCurrent);
|
|
85
98
|
notificationChain = notificationChain.then(() => notifyProgress(extra, payload));
|
|
86
99
|
});
|
|
87
100
|
reporter.flush = async () => {
|
|
@@ -102,11 +115,18 @@ export function progressWithMessage(reporter, getMessage) {
|
|
|
102
115
|
return wrapped;
|
|
103
116
|
}
|
|
104
117
|
export function wrapToolHandler(handler, options) {
|
|
118
|
+
const notifyTerminalProgress = async (extra, startMessage, outcome, completionMessage) => {
|
|
119
|
+
await notifyProgress(extra, {
|
|
120
|
+
current: TOOL_HANDLER_PROGRESS_TOTAL,
|
|
121
|
+
total: TOOL_HANDLER_PROGRESS_TOTAL,
|
|
122
|
+
message: completionMessage ?? `${startMessage} • ${outcome}`,
|
|
123
|
+
});
|
|
124
|
+
};
|
|
105
125
|
return async (args, extra) => {
|
|
106
126
|
const startMessage = options.progressMessage(args);
|
|
107
127
|
await notifyProgress(extra, {
|
|
108
128
|
current: 0,
|
|
109
|
-
total:
|
|
129
|
+
total: TOOL_HANDLER_PROGRESS_TOTAL,
|
|
110
130
|
message: startMessage,
|
|
111
131
|
});
|
|
112
132
|
let result;
|
|
@@ -114,21 +134,44 @@ export function wrapToolHandler(handler, options) {
|
|
|
114
134
|
result = await handler(args, extra);
|
|
115
135
|
}
|
|
116
136
|
catch (error) {
|
|
117
|
-
const isCancelled = error instanceof
|
|
118
|
-
await
|
|
119
|
-
current: 1,
|
|
120
|
-
total: 1,
|
|
121
|
-
message: `${startMessage} • ${isCancelled ? 'cancelled' : 'failed'}`,
|
|
122
|
-
});
|
|
137
|
+
const isCancelled = error instanceof CancelledError;
|
|
138
|
+
await notifyTerminalProgress(extra, startMessage, isCancelled ? 'cancelled' : 'failed');
|
|
123
139
|
throw error;
|
|
124
140
|
}
|
|
125
141
|
const completionMessage = options.completionMessage?.(args, result) ??
|
|
126
142
|
defaultCompletionMessage(startMessage, result);
|
|
127
|
-
await
|
|
128
|
-
current: 1,
|
|
129
|
-
total: 1,
|
|
130
|
-
message: completionMessage,
|
|
131
|
-
});
|
|
143
|
+
await notifyTerminalProgress(extra, startMessage, 'completed', completionMessage);
|
|
132
144
|
return result;
|
|
133
145
|
};
|
|
134
146
|
}
|
|
147
|
+
export async function runWithProgressCompletion(extra, run, options) {
|
|
148
|
+
let result;
|
|
149
|
+
let thrownError;
|
|
150
|
+
try {
|
|
151
|
+
result = await run();
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (error instanceof CancelledError) {
|
|
155
|
+
result = createErrorResponse(E_CANCELLED, 'Request cancelled');
|
|
156
|
+
}
|
|
157
|
+
else if (error instanceof McpError) {
|
|
158
|
+
thrownError = error;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
rethrowMcpError(error);
|
|
162
|
+
result = createErrorResponse(E_UNKNOWN, getErrorMessage(error));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
await options.reporter.flush();
|
|
166
|
+
const completionResult = result ?? createErrorResponse(E_UNKNOWN, getErrorMessage(thrownError));
|
|
167
|
+
const completionCurrent = resolveCompletionCurrent(options.completionCurrent);
|
|
168
|
+
await notifyProgress(extra, {
|
|
169
|
+
current: completionCurrent,
|
|
170
|
+
total: completionCurrent,
|
|
171
|
+
message: options.completionMessage(completionResult),
|
|
172
|
+
});
|
|
173
|
+
if (thrownError) {
|
|
174
|
+
throw thrownError;
|
|
175
|
+
}
|
|
176
|
+
return completionResult;
|
|
177
|
+
}
|
package/dist/tools/recall.js
CHANGED
|
@@ -1,121 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import { E_CANCELLED, E_UNKNOWN, getErrorMessage, rethrowMcpError, } from '../lib/errors.js';
|
|
1
|
+
import { throwIfAborted } from '../lib/errors.js';
|
|
2
|
+
import { traverseGraph, } from '../lib/graph-traversal.js';
|
|
4
3
|
import { logToolEvent } from '../lib/mcp-utils.js';
|
|
5
4
|
import { splitPage } from '../lib/pagination.js';
|
|
6
5
|
import { buildSearchCursorScope, decodeSearchCursor, encodeSearchCursor, } from '../lib/search-cursor.js';
|
|
7
6
|
import { loadRankedSearchRows, toMemoryFilters } from '../lib/search.js';
|
|
8
|
-
import {
|
|
7
|
+
import { createToolResponse } from '../lib/tool-response.js';
|
|
9
8
|
import { parseMemoryRow } from '../lib/types.js';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { createProgressReporter, notifyProgress, progressWithMessage, } from './progress.js';
|
|
9
|
+
import {} from '../schemas/inputs.js';
|
|
10
|
+
import { createProgressReporter, notifyProgress, progressWithMessage, runWithProgressCompletion, } from './progress.js';
|
|
13
11
|
import { registerToolWithContract } from './register-contract.js';
|
|
14
|
-
import { countPayloadArrayItems,
|
|
15
|
-
function throwIfAborted(signal) {
|
|
16
|
-
if (!signal?.aborted) {
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
throw new Error(E_CANCELLED);
|
|
20
|
-
}
|
|
21
|
-
function yieldToEventLoop() {
|
|
22
|
-
return new Promise((resolve) => setImmediate(resolve));
|
|
23
|
-
}
|
|
24
|
-
function parseEnvInt(name, fallback, min, max) {
|
|
25
|
-
const raw = process.env[name];
|
|
26
|
-
if (raw == null)
|
|
27
|
-
return fallback;
|
|
28
|
-
const parsed = parseInt(raw, 10);
|
|
29
|
-
if (Number.isNaN(parsed))
|
|
30
|
-
return fallback;
|
|
31
|
-
return Math.max(min, Math.min(max, parsed));
|
|
32
|
-
}
|
|
33
|
-
const MAX_FRONTIER_SIZE = parseEnvInt('RECALL_MAX_FRONTIER_SIZE', 1000, 100, 50000);
|
|
34
|
-
const MAX_EDGE_ROWS = parseEnvInt('RECALL_MAX_EDGE_ROWS', 5000, 100, 50000);
|
|
35
|
-
const MAX_VISITED_NODES = parseEnvInt('RECALL_MAX_VISITED_NODES', 5000, 100, 50000);
|
|
36
|
-
const EDGE_QUERY_SQL = `SELECT from_hash, to_hash, relation_type FROM relationships
|
|
37
|
-
WHERE from_hash IN (SELECT value FROM json_each(?))
|
|
38
|
-
OR to_hash IN (SELECT value FROM json_each(?))
|
|
39
|
-
LIMIT ?`;
|
|
12
|
+
import { countPayloadArrayItems, formatToolCompletionMessage, } from './result.js';
|
|
40
13
|
const MEMORIES_BY_HASH_SQL = 'SELECT * FROM memories WHERE hash IN (SELECT value FROM json_each(?))';
|
|
41
|
-
async function traverseGraph(db, seeds, depth, signal, onHop) {
|
|
42
|
-
const visited = new Set();
|
|
43
|
-
const frontier = [];
|
|
44
|
-
for (const seed of seeds) {
|
|
45
|
-
visited.add(seed.hash);
|
|
46
|
-
frontier.push(seed.hash);
|
|
47
|
-
}
|
|
48
|
-
const edges = [];
|
|
49
|
-
const seenEdges = new Set();
|
|
50
|
-
let depthReached = 0;
|
|
51
|
-
let aborted = false;
|
|
52
|
-
const edgeStmt = db.prepareOnce(EDGE_QUERY_SQL);
|
|
53
|
-
for (let hop = 0; hop < depth && frontier.length > 0; hop += 1) {
|
|
54
|
-
// Yield to event loop to allow progress notifications and cancellation
|
|
55
|
-
await yieldToEventLoop();
|
|
56
|
-
throwIfAborted(signal);
|
|
57
|
-
depthReached = hop + 1;
|
|
58
|
-
onHop?.(hop, depth);
|
|
59
|
-
if (frontier.length > MAX_FRONTIER_SIZE) {
|
|
60
|
-
frontier.length = MAX_FRONTIER_SIZE;
|
|
61
|
-
aborted = true;
|
|
62
|
-
}
|
|
63
|
-
const remainingEdgeBudget = MAX_EDGE_ROWS - edges.length;
|
|
64
|
-
const remainingNodeBudget = MAX_VISITED_NODES - visited.size;
|
|
65
|
-
if (remainingEdgeBudget <= 0 || remainingNodeBudget <= 0) {
|
|
66
|
-
aborted = true;
|
|
67
|
-
break;
|
|
68
|
-
}
|
|
69
|
-
const frontierJson = JSON.stringify(frontier);
|
|
70
|
-
const edgeRows = edgeStmt.all(frontierJson, frontierJson, remainingEdgeBudget + 1);
|
|
71
|
-
const rowsToProcess = edgeRows.length > remainingEdgeBudget
|
|
72
|
-
? remainingEdgeBudget
|
|
73
|
-
: edgeRows.length;
|
|
74
|
-
if (edgeRows.length > remainingEdgeBudget) {
|
|
75
|
-
aborted = true;
|
|
76
|
-
}
|
|
77
|
-
const nextHashes = [];
|
|
78
|
-
const queueVisitedHash = (hash) => {
|
|
79
|
-
if (visited.has(hash)) {
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
if (visited.size >= MAX_VISITED_NODES) {
|
|
83
|
-
aborted = true;
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
visited.add(hash);
|
|
87
|
-
if (nextHashes.length < MAX_FRONTIER_SIZE) {
|
|
88
|
-
nextHashes.push(hash);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
aborted = true;
|
|
92
|
-
};
|
|
93
|
-
for (let i = 0; i < rowsToProcess; i += 1) {
|
|
94
|
-
const edge = edgeRows[i];
|
|
95
|
-
if (edge === undefined) {
|
|
96
|
-
break;
|
|
97
|
-
}
|
|
98
|
-
const edgeKey = `${edge.from_hash}|${edge.to_hash}|${edge.relation_type}`;
|
|
99
|
-
if (!seenEdges.has(edgeKey)) {
|
|
100
|
-
seenEdges.add(edgeKey);
|
|
101
|
-
edges.push({
|
|
102
|
-
from_hash: edge.from_hash,
|
|
103
|
-
to_hash: edge.to_hash,
|
|
104
|
-
relation_type: edge.relation_type,
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
queueVisitedHash(edge.from_hash);
|
|
108
|
-
queueVisitedHash(edge.to_hash);
|
|
109
|
-
if (aborted &&
|
|
110
|
-
(edges.length >= MAX_EDGE_ROWS || visited.size >= MAX_VISITED_NODES)) {
|
|
111
|
-
break;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
frontier.length = 0;
|
|
115
|
-
frontier.push(...nextHashes);
|
|
116
|
-
}
|
|
117
|
-
return { edges, visited, depthReached, aborted };
|
|
118
|
-
}
|
|
119
14
|
function loadMemoriesByHashes(db, hashes) {
|
|
120
15
|
if (hashes.length === 0) {
|
|
121
16
|
return [];
|
|
@@ -125,29 +20,87 @@ function loadMemoriesByHashes(db, hashes) {
|
|
|
125
20
|
.all(JSON.stringify(hashes));
|
|
126
21
|
}
|
|
127
22
|
function formatRecallCompletionMessage(query, result) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
23
|
+
return formatToolCompletionMessage('recall', query, result, (payload) => {
|
|
24
|
+
const memoriesCount = countPayloadArrayItems(payload, 'memories');
|
|
25
|
+
const edgesCount = countPayloadArrayItems(payload, 'graph');
|
|
26
|
+
const aborted = 'aborted' in payload && payload.aborted === true;
|
|
27
|
+
return `${memoriesCount} memories, ${edgesCount} edges${aborted ? ' [aborted]' : ''}`;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function decodeCursorForRecall(cursor, scope) {
|
|
31
|
+
if (!cursor) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return decodeSearchCursor(cursor, scope);
|
|
35
|
+
}
|
|
36
|
+
function buildSeedRelevanceMap(seeds) {
|
|
37
|
+
const seedRelevance = new Map();
|
|
38
|
+
for (const seed of seeds) {
|
|
39
|
+
if (seed.rank != null) {
|
|
40
|
+
seedRelevance.set(seed.hash, -seed.rank);
|
|
133
41
|
}
|
|
134
|
-
return failedMessage;
|
|
135
42
|
}
|
|
136
|
-
|
|
137
|
-
|
|
43
|
+
return seedRelevance;
|
|
44
|
+
}
|
|
45
|
+
function toMemoriesWithRelevance(rows, seedRelevance) {
|
|
46
|
+
return rows.map((row) => {
|
|
47
|
+
const memory = parseMemoryRow(row);
|
|
48
|
+
const relevance = seedRelevance.get(memory.hash);
|
|
49
|
+
if (relevance != null) {
|
|
50
|
+
memory.relevance = relevance;
|
|
51
|
+
}
|
|
52
|
+
return memory;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function buildNextCursor(hasMore, pageSeeds, scope) {
|
|
56
|
+
if (!hasMore || pageSeeds.length === 0) {
|
|
57
|
+
return undefined;
|
|
138
58
|
}
|
|
139
|
-
const
|
|
140
|
-
if (!
|
|
141
|
-
return
|
|
59
|
+
const lastSeed = pageSeeds[pageSeeds.length - 1];
|
|
60
|
+
if (!lastSeed) {
|
|
61
|
+
return undefined;
|
|
142
62
|
}
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
63
|
+
const rank = lastSeed.rank ?? 0;
|
|
64
|
+
return encodeSearchCursor(scope, rank, lastSeed.hash);
|
|
65
|
+
}
|
|
66
|
+
function createHopReporter(extra, query, completionCurrent) {
|
|
67
|
+
const reporter = progressWithMessage(createProgressReporter(extra), ({ current, total }) => `⊙ recall: ${query} [hop ${current}/${Math.max((total ?? current) - 1, current)}]`);
|
|
68
|
+
const onHop = (hop) => {
|
|
69
|
+
reporter({ current: hop + 1, total: completionCurrent });
|
|
70
|
+
};
|
|
71
|
+
return { reporter, onHop };
|
|
72
|
+
}
|
|
73
|
+
function toRecallResponse(computation) {
|
|
74
|
+
return createToolResponse({
|
|
75
|
+
memories: computation.memories,
|
|
76
|
+
graph: computation.edges,
|
|
77
|
+
depth_reached: computation.depthReached,
|
|
78
|
+
...(computation.aborted ? { aborted: true } : {}),
|
|
79
|
+
...(computation.nextCursor ? { nextCursor: computation.nextCursor } : {}),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
async function computeRecall(db, params, scope, signal, onHop) {
|
|
83
|
+
const decodedCursor = decodeCursorForRecall(params.cursor, scope);
|
|
84
|
+
const seedRows = loadRankedSearchRows(db, params.query, params.limit, decodedCursor, toMemoryFilters(params));
|
|
85
|
+
const { page: pageSeeds, hasMore } = splitPage(seedRows, params.limit);
|
|
86
|
+
const traversal = await traverseGraph(db, pageSeeds, params.depth, signal, onHop);
|
|
87
|
+
const allHashes = Array.from(traversal.visited);
|
|
88
|
+
const seedRelevance = buildSeedRelevanceMap(pageSeeds);
|
|
89
|
+
const memoryRows = loadMemoriesByHashes(db, allHashes);
|
|
90
|
+
const nextCursor = buildNextCursor(hasMore, pageSeeds, scope);
|
|
91
|
+
return {
|
|
92
|
+
memories: toMemoriesWithRelevance(memoryRows, seedRelevance),
|
|
93
|
+
edges: traversal.edges,
|
|
94
|
+
depthReached: traversal.depthReached,
|
|
95
|
+
aborted: traversal.aborted,
|
|
96
|
+
...(nextCursor ? { nextCursor } : {}),
|
|
97
|
+
seedCount: pageSeeds.length,
|
|
98
|
+
visitedCount: traversal.visited.size,
|
|
99
|
+
};
|
|
147
100
|
}
|
|
148
101
|
export function registerRecall(server, db) {
|
|
149
|
-
registerToolWithContract(server, 'recall',
|
|
150
|
-
const { depth
|
|
102
|
+
registerToolWithContract(server, 'recall', async (params, extra) => {
|
|
103
|
+
const { depth } = params;
|
|
151
104
|
const filters = toMemoryFilters(params);
|
|
152
105
|
const scope = buildSearchCursorScope(params.query, filters);
|
|
153
106
|
const contextLabel = `⊙ recall: ${params.query} [depth ${depth}]`;
|
|
@@ -157,86 +110,24 @@ export function registerRecall(server, db) {
|
|
|
157
110
|
total: completionCurrent,
|
|
158
111
|
message: contextLabel,
|
|
159
112
|
});
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
hopReporter({ current: hop + 1, total: completionCurrent });
|
|
163
|
-
};
|
|
164
|
-
let result;
|
|
165
|
-
let thrownError;
|
|
166
|
-
try {
|
|
113
|
+
const { reporter: hopReporter, onHop } = createHopReporter(extra, params.query, completionCurrent);
|
|
114
|
+
return runWithProgressCompletion(extra, async () => {
|
|
167
115
|
throwIfAborted(extra.signal);
|
|
168
|
-
const
|
|
169
|
-
? decodeSearchCursor(cursor, scope)
|
|
170
|
-
: undefined;
|
|
171
|
-
// Step 1: FTS seed search
|
|
172
|
-
const seedRows = loadRankedSearchRows(db, params.query, limit, decodedCursor, filters);
|
|
116
|
+
const computation = await computeRecall(db, params, scope, extra.signal, onHop);
|
|
173
117
|
throwIfAborted(extra.signal);
|
|
174
|
-
const { page: pageSeeds, hasMore } = splitPage(seedRows, limit);
|
|
175
|
-
// Step 2: BFS traversal up to `depth` hops
|
|
176
|
-
const traversal = await traverseGraph(db, pageSeeds, depth, extra.signal, onHop);
|
|
177
|
-
throwIfAborted(extra.signal);
|
|
178
|
-
// Step 3: Load all discovered memory rows
|
|
179
|
-
const allHashes = Array.from(traversal.visited);
|
|
180
|
-
const seedRelevance = new Map();
|
|
181
|
-
for (const seed of pageSeeds) {
|
|
182
|
-
if (seed.rank != null)
|
|
183
|
-
seedRelevance.set(seed.hash, -seed.rank);
|
|
184
|
-
}
|
|
185
|
-
const memoryRows = loadMemoriesByHashes(db, allHashes);
|
|
186
|
-
const memories = memoryRows.map((row) => {
|
|
187
|
-
const memory = parseMemoryRow(row);
|
|
188
|
-
const rel = seedRelevance.get(memory.hash);
|
|
189
|
-
if (rel != null) {
|
|
190
|
-
memory.relevance = rel;
|
|
191
|
-
}
|
|
192
|
-
return memory;
|
|
193
|
-
});
|
|
194
|
-
let nextCursor;
|
|
195
|
-
if (hasMore && pageSeeds.length > 0) {
|
|
196
|
-
const lastSeed = pageSeeds[pageSeeds.length - 1];
|
|
197
|
-
if (lastSeed !== undefined) {
|
|
198
|
-
const rank = lastSeed.rank ?? 0;
|
|
199
|
-
nextCursor = encodeSearchCursor(scope, rank, lastSeed.hash);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
118
|
await logToolEvent(server, 'recall', {
|
|
203
119
|
depth,
|
|
204
|
-
depth_reached:
|
|
205
|
-
seed_count:
|
|
206
|
-
visited_nodes:
|
|
207
|
-
edge_count:
|
|
208
|
-
aborted:
|
|
120
|
+
depth_reached: computation.depthReached,
|
|
121
|
+
seed_count: computation.seedCount,
|
|
122
|
+
visited_nodes: computation.visitedCount,
|
|
123
|
+
edge_count: computation.edges.length,
|
|
124
|
+
aborted: computation.aborted,
|
|
209
125
|
});
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
...(nextCursor ? { nextCursor } : {}),
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
catch (err) {
|
|
219
|
-
if (err instanceof Error && err.message === E_CANCELLED) {
|
|
220
|
-
result = createErrorResponse(E_CANCELLED, 'Request cancelled');
|
|
221
|
-
}
|
|
222
|
-
else if (err instanceof McpError) {
|
|
223
|
-
thrownError = err;
|
|
224
|
-
}
|
|
225
|
-
else {
|
|
226
|
-
rethrowMcpError(err);
|
|
227
|
-
result = createErrorResponse(E_UNKNOWN, getErrorMessage(err));
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
await hopReporter.flush();
|
|
231
|
-
const completionResult = result ?? createErrorResponse(E_UNKNOWN, getErrorMessage(thrownError));
|
|
232
|
-
await notifyProgress(extra, {
|
|
233
|
-
current: completionCurrent,
|
|
234
|
-
total: completionCurrent,
|
|
235
|
-
message: formatRecallCompletionMessage(params.query, completionResult),
|
|
126
|
+
return toRecallResponse(computation);
|
|
127
|
+
}, {
|
|
128
|
+
reporter: hopReporter,
|
|
129
|
+
completionCurrent,
|
|
130
|
+
completionMessage: (result) => formatRecallCompletionMessage(params.query, result),
|
|
236
131
|
});
|
|
237
|
-
if (thrownError) {
|
|
238
|
-
throw thrownError;
|
|
239
|
-
}
|
|
240
|
-
return completionResult;
|
|
241
132
|
});
|
|
242
133
|
}
|
|
@@ -1,3 +1,2 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
|
|
3
|
-
export declare function registerToolWithContract(server: McpServer, toolName: string, _inputSchema: z.ZodType, _outputSchema: z.ZodType, handler: unknown): void;
|
|
2
|
+
export declare function registerToolWithContract(server: McpServer, toolName: string, handler: unknown): void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getToolContract } from '../lib/tool-contracts.js';
|
|
2
|
-
export function registerToolWithContract(server, toolName,
|
|
2
|
+
export function registerToolWithContract(server, toolName, handler) {
|
|
3
3
|
const contract = getToolContract(toolName);
|
|
4
4
|
server.registerTool(contract.name, {
|
|
5
5
|
title: contract.title,
|
|
@@ -7,5 +7,7 @@ export function registerToolWithContract(server, toolName, _inputSchema, _output
|
|
|
7
7
|
inputSchema: contract.inputSchema,
|
|
8
8
|
outputSchema: contract.outputSchema,
|
|
9
9
|
annotations: contract.annotations,
|
|
10
|
-
},
|
|
10
|
+
},
|
|
11
|
+
// The handler is validated at runtime and tested via contract verification
|
|
12
|
+
handler);
|
|
11
13
|
}
|
package/dist/tools/result.d.ts
CHANGED
|
@@ -5,3 +5,7 @@ export declare function getToolResultPayload(result: CallToolResult): ToolResult
|
|
|
5
5
|
export declare function getToolResultText(result: CallToolResult): string;
|
|
6
6
|
/** Count items in a named array field of a tool result payload. */
|
|
7
7
|
export declare function countPayloadArrayItems(payload: Record<string, unknown>, key: string): number;
|
|
8
|
+
/** Standardize format of tool completion messages. */
|
|
9
|
+
export declare function formatToolCompletionMessage(toolName: string, query: string, result: CallToolResult, getSuccessMessage: (payload: ToolResultPayload) => string): string;
|
|
10
|
+
export declare function formatHashPreview(hash: string, length?: number): string;
|
|
11
|
+
export declare function formatRelationshipPreview(fromHash: string, toHash: string, length?: number): string;
|
package/dist/tools/result.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { E_CANCELLED } from '../lib/errors.js';
|
|
1
2
|
function isRecord(value) {
|
|
2
3
|
return typeof value === 'object' && value !== null;
|
|
3
4
|
}
|
|
@@ -20,3 +21,29 @@ export function countPayloadArrayItems(payload, key) {
|
|
|
20
21
|
const value = payload[key];
|
|
21
22
|
return Array.isArray(value) ? value.length : 0;
|
|
22
23
|
}
|
|
24
|
+
/** Standardize format of tool completion messages. */
|
|
25
|
+
export function formatToolCompletionMessage(toolName, query, result, getSuccessMessage) {
|
|
26
|
+
const failedMessage = `⊙ ${toolName}: ${query} • failed`;
|
|
27
|
+
if (result.isError) {
|
|
28
|
+
const text = getToolResultText(result);
|
|
29
|
+
if (text.includes(E_CANCELLED)) {
|
|
30
|
+
return `⊙ ${toolName}: ${query} • cancelled`;
|
|
31
|
+
}
|
|
32
|
+
return failedMessage;
|
|
33
|
+
}
|
|
34
|
+
if (!isOkStructuredToolResult(result)) {
|
|
35
|
+
return failedMessage;
|
|
36
|
+
}
|
|
37
|
+
const payload = getToolResultPayload(result);
|
|
38
|
+
if (!payload) {
|
|
39
|
+
return `⊙ ${toolName}: ${query} • completed`;
|
|
40
|
+
}
|
|
41
|
+
const successSuffix = getSuccessMessage(payload);
|
|
42
|
+
return `⊙ ${toolName}: ${query} • ${successSuffix}`;
|
|
43
|
+
}
|
|
44
|
+
export function formatHashPreview(hash, length = 12) {
|
|
45
|
+
return `${hash.slice(0, length)}...`;
|
|
46
|
+
}
|
|
47
|
+
export function formatRelationshipPreview(fromHash, toHash, length = 8) {
|
|
48
|
+
return `${formatHashPreview(fromHash, length)} -> ${formatHashPreview(toHash, length)}`;
|
|
49
|
+
}
|