@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
@@ -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 {};
@@ -1,6 +1,15 @@
1
- import { E_CANCELLED } from '../lib/errors.js';
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 (typeof extra.sendNotification !== 'function') {
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 = { current: monotonicCurrent };
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: 1,
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 Error && error.message === E_CANCELLED;
118
- await notifyProgress(extra, {
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 notifyProgress(extra, {
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
+ }
@@ -1,121 +1,16 @@
1
- import { McpError, } from '@modelcontextprotocol/sdk/types.js';
2
- import process from 'node:process';
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 { createErrorResponse, createToolResponse, } from '../lib/tool-response.js';
7
+ import { createToolResponse } from '../lib/tool-response.js';
9
8
  import { parseMemoryRow } from '../lib/types.js';
10
- import { RecallInputSchema } from '../schemas/inputs.js';
11
- import { RecallResultSchema } from '../schemas/outputs.js';
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, getToolResultPayload, getToolResultText, isOkStructuredToolResult, } from './result.js';
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
- const failedMessage = `⊙ recall: ${query} • failed`;
129
- if (result.isError) {
130
- const text = getToolResultText(result);
131
- if (text.includes(E_CANCELLED)) {
132
- return `⊙ recall: ${query} cancelled`;
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
- if (!isOkStructuredToolResult(result)) {
137
- return failedMessage;
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 payload = getToolResultPayload(result);
140
- if (!payload) {
141
- return `⊙ recall: ${query} • completed`;
59
+ const lastSeed = pageSeeds[pageSeeds.length - 1];
60
+ if (!lastSeed) {
61
+ return undefined;
142
62
  }
143
- const memoriesCount = countPayloadArrayItems(payload, 'memories');
144
- const edgesCount = countPayloadArrayItems(payload, 'graph');
145
- const aborted = 'aborted' in payload && payload.aborted === true;
146
- return `⊙ recall: ${query} • ${memoriesCount} memories, ${edgesCount} edges${aborted ? ' [aborted]' : ''}`;
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', RecallInputSchema, RecallResultSchema, async (params, extra) => {
150
- const { depth, limit, cursor } = params;
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 hopReporter = progressWithMessage(createProgressReporter(extra), ({ current, total }) => `⊙ recall: ${params.query} [hop ${current}/${Math.max((total ?? current) - 1, current)}]`);
161
- const onHop = (hop) => {
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 decodedCursor = cursor
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: traversal.depthReached,
205
- seed_count: pageSeeds.length,
206
- visited_nodes: traversal.visited.size,
207
- edge_count: traversal.edges.length,
208
- aborted: traversal.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
- result = createToolResponse({
211
- memories,
212
- graph: traversal.edges,
213
- depth_reached: traversal.depthReached,
214
- ...(traversal.aborted ? { aborted: true } : {}),
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
- import type { z } from 'zod/v4';
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, _inputSchema, _outputSchema, handler) {
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
- }, handler);
10
+ },
11
+ // The handler is validated at runtime and tested via contract verification
12
+ handler);
11
13
  }
@@ -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;
@@ -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
+ }