@exaudeus/memory-mcp 1.7.0 → 1.9.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/config.d.ts +20 -1
- package/dist/config.js +70 -6
- package/dist/embedder.d.ts +22 -0
- package/dist/embedder.js +50 -0
- package/dist/formatters.d.ts +10 -1
- package/dist/formatters.js +27 -1
- package/dist/index.js +517 -117
- package/dist/store.d.ts +14 -1
- package/dist/store.js +56 -2
- package/dist/text-analyzer.d.ts +12 -0
- package/dist/text-analyzer.js +28 -0
- package/dist/thresholds.d.ts +4 -0
- package/dist/thresholds.js +4 -0
- package/dist/types.d.ts +20 -1
- package/dist/types.js +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Supports multiple workspaces simultaneously
|
|
5
5
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
6
6
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
8
8
|
import { z } from 'zod';
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import { readFile, writeFile } from 'fs/promises';
|
|
@@ -14,8 +14,8 @@ import { getLobeConfigs } from './config.js';
|
|
|
14
14
|
import { ConfigManager } from './config-manager.js';
|
|
15
15
|
import { normalizeArgs } from './normalize.js';
|
|
16
16
|
import { buildCrashReport, writeCrashReport, writeCrashReportSync, readLatestCrash, readCrashHistory, clearLatestCrash, formatCrashReport, formatCrashSummary, markServerStarted, } from './crash-journal.js';
|
|
17
|
-
import { formatStaleSection, formatConflictWarning, formatStats, formatBehaviorConfigSection, mergeTagFrequencies, buildQueryFooter, buildBriefingTagPrimerSections } from './formatters.js';
|
|
18
|
-
import { parseFilter } from './text-analyzer.js';
|
|
17
|
+
import { formatStaleSection, formatConflictWarning, formatStats, formatBehaviorConfigSection, mergeTagFrequencies, buildQueryFooter, buildBriefingTagPrimerSections, formatSearchMode, formatLootDrop } from './formatters.js';
|
|
18
|
+
import { parseFilter, extractTitle } from './text-analyzer.js';
|
|
19
19
|
import { VOCABULARY_ECHO_LIMIT, WARN_SEPARATOR } from './thresholds.js';
|
|
20
20
|
import { matchRootsToLobeNames, buildLobeResolution } from './lobe-resolution.js';
|
|
21
21
|
let serverMode = { kind: 'running' };
|
|
@@ -143,7 +143,7 @@ function inferLobeFromPaths(paths) {
|
|
|
143
143
|
// Only return if unambiguous — exactly one lobe matched
|
|
144
144
|
return matchedLobes.size === 1 ? matchedLobes.values().next().value : undefined;
|
|
145
145
|
}
|
|
146
|
-
const server = new Server({ name: 'memory-mcp', version: '0.1.0' }, { capabilities: { tools: {} } });
|
|
146
|
+
const server = new Server({ name: 'memory-mcp', version: '0.1.0' }, { capabilities: { tools: {}, resources: {} } });
|
|
147
147
|
// --- Lobe resolution for read operations ---
|
|
148
148
|
// When the agent doesn't specify a lobe, we determine which lobe(s) to search
|
|
149
149
|
// via a degradation ladder (see lobe-resolution.ts for the pure logic):
|
|
@@ -208,170 +208,180 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
208
208
|
const currentLobeNames = configManager.getLobeNames();
|
|
209
209
|
const lobeProperty = buildLobeProperty(currentLobeNames);
|
|
210
210
|
return { tools: [
|
|
211
|
-
//
|
|
212
|
-
//
|
|
211
|
+
// ─── New v2 tool surface ─────────────────────────────────────────────
|
|
212
|
+
// 4 retrieval tools + 4 storage tools + 1 maintenance tool.
|
|
213
|
+
// Old tools (memory_store, memory_query, memory_correct, memory_context) are hidden
|
|
214
|
+
// from listing but their handlers remain active for backward compatibility.
|
|
215
|
+
// --- Retrieval ---
|
|
213
216
|
{
|
|
214
|
-
name: '
|
|
215
|
-
|
|
216
|
-
// "entries" (not "content") signals a collection; fighting the "content = string" prior
|
|
217
|
-
// is an architectural fix rather than patching the description after the fact.
|
|
218
|
-
description: 'memory_store(topic: "gotchas", entries: [{title: "Build cache", fact: "Must clean build after Tuist changes"}, {title: "Tuist version", fact: "Project requires Tuist 4.x"}], tags: ["build"]). Stores enduring knowledge — (1) Codebase facts (architecture, conventions, gotchas, modules): what IS true now, not past actions. Wrong: "Completed migration to StateFlow." Right: "All ViewModels use StateFlow." (2) User knowledge (user, preferences): who the person is, how they work. "user" and "preferences" are stored in the alwaysInclude lobe (shared across projects). One insight per object; use multiple objects instead of bundling. If content looks likely-ephemeral, the tool returns a review-required response; re-run with durabilityDecision: "store-anyway" only when deliberate.',
|
|
217
|
+
name: 'brief',
|
|
218
|
+
description: 'Session start. Returns user identity, preferences, gotchas overview, stale entries. Call once at the beginning of a conversation. Example: brief(lobe: "android")',
|
|
219
219
|
inputSchema: {
|
|
220
220
|
type: 'object',
|
|
221
221
|
properties: {
|
|
222
222
|
lobe: lobeProperty,
|
|
223
|
-
|
|
223
|
+
},
|
|
224
|
+
required: [],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'recall',
|
|
229
|
+
description: 'Pre-task lookup. Describe what you are about to do and get relevant knowledge (semantic + keyword search). Example: recall(lobe: "android", context: "writing a Kotlin reducer for the messaging feature")',
|
|
230
|
+
inputSchema: {
|
|
231
|
+
type: 'object',
|
|
232
|
+
properties: {
|
|
233
|
+
lobe: lobeProperty,
|
|
234
|
+
context: {
|
|
224
235
|
type: 'string',
|
|
225
|
-
|
|
226
|
-
// doesn't restrict it — agents can pass any "modules/foo" value and it works.
|
|
227
|
-
// The description makes this explicit.
|
|
228
|
-
description: 'Predefined: user | preferences | architecture | conventions | gotchas | recent-work. Custom namespace: modules/<name> (e.g. modules/brainstorm, modules/game-design, modules/api-notes). Use modules/<name> for any domain that doesn\'t fit the built-in topics.',
|
|
229
|
-
enum: ['user', 'preferences', 'architecture', 'conventions', 'gotchas', 'recent-work'],
|
|
230
|
-
},
|
|
231
|
-
entries: {
|
|
232
|
-
type: 'array',
|
|
233
|
-
// Type annotation first — agents trained on code read type signatures before prose.
|
|
234
|
-
description: 'Array<{title: string, fact: string}> — not a string. One object per insight. title: short label (2-5 words). fact: codebase topics → present-tense state ("X uses Y", not "Completed X"); user/preferences → what the person expressed. Wrong: one object bundling two facts. Right: two objects.',
|
|
235
|
-
items: {
|
|
236
|
-
type: 'object',
|
|
237
|
-
properties: {
|
|
238
|
-
title: {
|
|
239
|
-
type: 'string',
|
|
240
|
-
description: 'Short label for this insight (2-5 words)',
|
|
241
|
-
},
|
|
242
|
-
fact: {
|
|
243
|
-
type: 'string',
|
|
244
|
-
description: 'The insight itself — one focused fact or observation',
|
|
245
|
-
},
|
|
246
|
-
},
|
|
247
|
-
required: ['title', 'fact'],
|
|
248
|
-
},
|
|
249
|
-
minItems: 1,
|
|
236
|
+
description: 'What you are about to do, in natural language.',
|
|
250
237
|
},
|
|
251
|
-
|
|
252
|
-
type: '
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
default: [],
|
|
238
|
+
maxResults: {
|
|
239
|
+
type: 'number',
|
|
240
|
+
description: 'Max results (default: 10)',
|
|
241
|
+
default: 10,
|
|
256
242
|
},
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
243
|
+
},
|
|
244
|
+
required: ['context'],
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: 'gotchas',
|
|
249
|
+
description: 'Get stored gotchas for a codebase area. Example: gotchas(lobe: "android", area: "auth")',
|
|
250
|
+
inputSchema: {
|
|
251
|
+
type: 'object',
|
|
252
|
+
properties: {
|
|
253
|
+
lobe: lobeProperty,
|
|
254
|
+
area: {
|
|
255
|
+
type: 'string',
|
|
256
|
+
description: 'Optional keyword filter for a specific area (e.g. "auth", "build", "navigation").',
|
|
262
257
|
},
|
|
263
|
-
|
|
258
|
+
},
|
|
259
|
+
required: [],
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
name: 'conventions',
|
|
264
|
+
description: 'Get stored conventions for a codebase area. Example: conventions(lobe: "android", area: "testing")',
|
|
265
|
+
inputSchema: {
|
|
266
|
+
type: 'object',
|
|
267
|
+
properties: {
|
|
268
|
+
lobe: lobeProperty,
|
|
269
|
+
area: {
|
|
264
270
|
type: 'string',
|
|
265
|
-
|
|
266
|
-
description: 'user (from human) > agent-confirmed > agent-inferred',
|
|
267
|
-
default: 'agent-inferred',
|
|
271
|
+
description: 'Optional keyword filter for a specific area (e.g. "testing", "naming", "architecture").',
|
|
268
272
|
},
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
273
|
+
},
|
|
274
|
+
required: [],
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
// --- Storage ---
|
|
278
|
+
{
|
|
279
|
+
name: 'gotcha',
|
|
280
|
+
description: 'Store a gotcha — a pitfall, surprising behavior, or trap. Write naturally; title is auto-extracted. Example: gotcha(lobe: "android", observation: "Gradle cache must be cleaned after Tuist changes or builds silently use stale artifacts")',
|
|
281
|
+
inputSchema: {
|
|
282
|
+
type: 'object',
|
|
283
|
+
properties: {
|
|
284
|
+
lobe: lobeProperty,
|
|
285
|
+
observation: {
|
|
286
|
+
type: 'string',
|
|
287
|
+
description: 'The gotcha — write naturally. First sentence becomes the title.',
|
|
274
288
|
},
|
|
275
289
|
durabilityDecision: {
|
|
276
290
|
type: 'string',
|
|
277
291
|
enum: ['default', 'store-anyway'],
|
|
278
|
-
description: '
|
|
292
|
+
description: 'Use "store-anyway" only when re-storing after a review-required response.',
|
|
279
293
|
default: 'default',
|
|
280
294
|
},
|
|
281
295
|
},
|
|
282
|
-
required: ['
|
|
296
|
+
required: ['lobe', 'observation'],
|
|
283
297
|
},
|
|
284
298
|
},
|
|
285
299
|
{
|
|
286
|
-
name: '
|
|
287
|
-
description: '
|
|
300
|
+
name: 'convention',
|
|
301
|
+
description: 'Store a convention — a pattern, rule, or standard the codebase follows. Example: convention(lobe: "android", observation: "All ViewModels use StateFlow for UI state. LiveData is banned.")',
|
|
288
302
|
inputSchema: {
|
|
289
303
|
type: 'object',
|
|
290
304
|
properties: {
|
|
291
305
|
lobe: lobeProperty,
|
|
292
|
-
|
|
293
|
-
type: 'string',
|
|
294
|
-
description: 'Optional. Defaults to "*" (all topics). Options: * | user | preferences | architecture | conventions | gotchas | recent-work | modules/<name>',
|
|
295
|
-
},
|
|
296
|
-
detail: {
|
|
306
|
+
observation: {
|
|
297
307
|
type: 'string',
|
|
298
|
-
|
|
299
|
-
description: 'brief = titles only, standard = summaries, full = complete content + metadata',
|
|
300
|
-
default: 'brief',
|
|
308
|
+
description: 'The convention — write naturally. First sentence becomes the title.',
|
|
301
309
|
},
|
|
302
|
-
|
|
303
|
-
type: 'string',
|
|
304
|
-
description: 'Search terms. "A B" = AND, "A|B" = OR, "-A" = NOT, "#tag" = exact tag, "=term" = exact keyword (no stemming). Example: "#auth reducer -deprecated"',
|
|
305
|
-
},
|
|
306
|
-
branch: {
|
|
310
|
+
durabilityDecision: {
|
|
307
311
|
type: 'string',
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
type: 'boolean',
|
|
312
|
-
description: 'Set true on first memory call in a conversation to include identity/preferences from alwaysInclude lobes. Set false on subsequent calls to skip redundant global knowledge.',
|
|
313
|
-
default: true,
|
|
312
|
+
enum: ['default', 'store-anyway'],
|
|
313
|
+
description: 'Use "store-anyway" only when re-storing after a review-required response.',
|
|
314
|
+
default: 'default',
|
|
314
315
|
},
|
|
315
316
|
},
|
|
316
|
-
required: [],
|
|
317
|
+
required: ['lobe', 'observation'],
|
|
317
318
|
},
|
|
318
319
|
},
|
|
319
320
|
{
|
|
320
|
-
name: '
|
|
321
|
-
description: '
|
|
321
|
+
name: 'learn',
|
|
322
|
+
description: 'Store a general observation — architecture decisions, dependency info, or any insight. Example: learn(lobe: "android", observation: "The messaging feature uses MVVM with a FlowCoordinator for navigation state")',
|
|
322
323
|
inputSchema: {
|
|
323
324
|
type: 'object',
|
|
324
325
|
properties: {
|
|
325
326
|
lobe: lobeProperty,
|
|
326
|
-
|
|
327
|
+
observation: {
|
|
327
328
|
type: 'string',
|
|
328
|
-
description: '
|
|
329
|
+
description: 'The observation — write naturally. First sentence becomes the title.',
|
|
329
330
|
},
|
|
330
|
-
|
|
331
|
+
durabilityDecision: {
|
|
331
332
|
type: 'string',
|
|
332
|
-
|
|
333
|
+
enum: ['default', 'store-anyway'],
|
|
334
|
+
description: 'Use "store-anyway" only when re-storing after a review-required response.',
|
|
335
|
+
default: 'default',
|
|
333
336
|
},
|
|
334
|
-
|
|
337
|
+
},
|
|
338
|
+
required: ['lobe', 'observation'],
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
name: 'prefer',
|
|
343
|
+
description: 'Store a user preference or working style rule. Stored with high trust. Example: prefer(rule: "Always suggest the simplest solution first")',
|
|
344
|
+
inputSchema: {
|
|
345
|
+
type: 'object',
|
|
346
|
+
properties: {
|
|
347
|
+
rule: {
|
|
335
348
|
type: 'string',
|
|
336
|
-
|
|
337
|
-
|
|
349
|
+
description: 'The preference or rule — write naturally.',
|
|
350
|
+
},
|
|
351
|
+
lobe: {
|
|
352
|
+
...lobeProperty,
|
|
353
|
+
description: `Optional. Lobe to scope this preference to. Omit for global preferences. Available: ${currentLobeNames.join(', ')}`,
|
|
338
354
|
},
|
|
339
355
|
},
|
|
340
|
-
required: ['
|
|
356
|
+
required: ['rule'],
|
|
341
357
|
},
|
|
342
358
|
},
|
|
359
|
+
// --- Maintenance ---
|
|
343
360
|
{
|
|
344
|
-
name: '
|
|
345
|
-
description: '
|
|
361
|
+
name: 'fix',
|
|
362
|
+
description: 'Fix or delete an entry. With correction: replaces content. Without: deletes. Example: fix(id: "gotcha-3f7a", correction: "updated text") or fix(id: "gotcha-3f7a")',
|
|
346
363
|
inputSchema: {
|
|
347
364
|
type: 'object',
|
|
348
365
|
properties: {
|
|
349
|
-
|
|
350
|
-
context: {
|
|
366
|
+
id: {
|
|
351
367
|
type: 'string',
|
|
352
|
-
description: '
|
|
368
|
+
description: 'Entry ID (e.g. gotcha-3f7a, arch-5c9b).',
|
|
353
369
|
},
|
|
354
|
-
|
|
355
|
-
type: '
|
|
356
|
-
description: '
|
|
357
|
-
default: 10,
|
|
358
|
-
},
|
|
359
|
-
minMatch: {
|
|
360
|
-
type: 'number',
|
|
361
|
-
description: 'Min keyword match ratio 0-1 (default: 0.2). Higher = stricter.',
|
|
362
|
-
default: 0.2,
|
|
370
|
+
correction: {
|
|
371
|
+
type: 'string',
|
|
372
|
+
description: 'New text. Omit to delete the entry.',
|
|
363
373
|
},
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
description:
|
|
367
|
-
default: true,
|
|
374
|
+
lobe: {
|
|
375
|
+
...lobeProperty,
|
|
376
|
+
description: `Optional. Searches all lobes if omitted. Available: ${currentLobeNames.join(', ')}`,
|
|
368
377
|
},
|
|
369
378
|
},
|
|
370
|
-
required: [],
|
|
379
|
+
required: ['id'],
|
|
371
380
|
},
|
|
372
381
|
},
|
|
373
|
-
//
|
|
374
|
-
//
|
|
382
|
+
// --- Legacy tools (still listed for backward compatibility) ---
|
|
383
|
+
// memory_store, memory_query, memory_correct, memory_context handlers remain active
|
|
384
|
+
// but are hidden from tool discovery. Agents use the new v2 tools above.
|
|
375
385
|
{
|
|
376
386
|
name: 'memory_bootstrap',
|
|
377
387
|
description: 'First-time setup: scan repo structure, README, and build system to seed initial knowledge. Run once per new codebase. If the lobe does not exist yet, provide "root" to auto-add it to memory-config.json and proceed without a manual restart.',
|
|
@@ -380,7 +390,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
380
390
|
properties: {
|
|
381
391
|
lobe: {
|
|
382
392
|
type: 'string',
|
|
383
|
-
// No enum restriction: agents pass new lobe names not yet in the config
|
|
384
393
|
description: `Memory lobe name. If the lobe doesn't exist yet, also pass "root" to auto-create it. Available lobes: ${currentLobeNames.join(', ')}`,
|
|
385
394
|
},
|
|
386
395
|
root: {
|
|
@@ -395,11 +404,38 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
395
404
|
required: [],
|
|
396
405
|
},
|
|
397
406
|
},
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
407
|
+
// Hidden tools — handlers still active:
|
|
408
|
+
// memory_stats, memory_diagnose, memory_reembed, memory_list_lobes,
|
|
409
|
+
// memory_store, memory_query, memory_correct, memory_context
|
|
401
410
|
] };
|
|
402
411
|
});
|
|
412
|
+
// --- MCP Resource: memory://lobes ---
|
|
413
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
414
|
+
await configManager.ensureFresh();
|
|
415
|
+
return {
|
|
416
|
+
resources: [{
|
|
417
|
+
uri: 'memory://lobes',
|
|
418
|
+
name: 'Available memory lobes',
|
|
419
|
+
description: 'Lists all configured memory lobes with their health status and entry counts.',
|
|
420
|
+
mimeType: 'application/json',
|
|
421
|
+
}],
|
|
422
|
+
};
|
|
423
|
+
});
|
|
424
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
425
|
+
const uri = request.params.uri;
|
|
426
|
+
if (uri === 'memory://lobes') {
|
|
427
|
+
await configManager.ensureFresh();
|
|
428
|
+
const lobeInfo = await buildLobeInfo();
|
|
429
|
+
return {
|
|
430
|
+
contents: [{
|
|
431
|
+
uri: 'memory://lobes',
|
|
432
|
+
mimeType: 'application/json',
|
|
433
|
+
text: JSON.stringify({ lobes: lobeInfo }, null, 2),
|
|
434
|
+
}],
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
438
|
+
});
|
|
403
439
|
// --- Tool handlers ---
|
|
404
440
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
405
441
|
const { name, arguments: rawArgs } = request.params;
|
|
@@ -447,6 +483,340 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
447
483
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
448
484
|
};
|
|
449
485
|
}
|
|
486
|
+
// ─── New v2 tool handlers ────────────────────────────────────────────
|
|
487
|
+
// Shared helper for gotcha/convention/learn — identical store logic, different topic.
|
|
488
|
+
case 'gotcha':
|
|
489
|
+
case 'convention':
|
|
490
|
+
case 'learn': {
|
|
491
|
+
const topicMap = {
|
|
492
|
+
gotcha: 'gotchas', convention: 'conventions', learn: 'general',
|
|
493
|
+
};
|
|
494
|
+
const topic = topicMap[name];
|
|
495
|
+
const { lobe: rawLobe, observation, durabilityDecision } = z.object({
|
|
496
|
+
lobe: z.string().min(1),
|
|
497
|
+
observation: z.string().min(1),
|
|
498
|
+
durabilityDecision: z.enum(['default', 'store-anyway']).default('default'),
|
|
499
|
+
}).parse(args);
|
|
500
|
+
process.stderr.write(`[memory-mcp] tool=${name} lobe=${rawLobe}\n`);
|
|
501
|
+
const ctx = resolveToolContext(rawLobe);
|
|
502
|
+
if (!ctx.ok)
|
|
503
|
+
return contextError(ctx);
|
|
504
|
+
const { title, content } = extractTitle(observation);
|
|
505
|
+
const result = await ctx.store.store(topic, title, content, [], 'agent-inferred', [], [], durabilityDecision);
|
|
506
|
+
if (result.kind === 'review-required') {
|
|
507
|
+
return {
|
|
508
|
+
content: [{ type: 'text', text: `[${ctx.label}] Review required: ${result.warning}\n\nIf intentional, re-run: ${name}(lobe: "${rawLobe}", observation: "...", durabilityDecision: "store-anyway")` }],
|
|
509
|
+
isError: true,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
if (result.kind === 'rejected') {
|
|
513
|
+
return {
|
|
514
|
+
content: [{ type: 'text', text: `[${ctx.label}] Failed: ${result.warning}` }],
|
|
515
|
+
isError: true,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
const lootDrop = result.relatedEntries ? formatLootDrop(result.relatedEntries) : '';
|
|
519
|
+
const label = name === 'learn' ? '' : `${name} `;
|
|
520
|
+
return {
|
|
521
|
+
content: [{ type: 'text', text: `[${ctx.label}] Stored ${label}${result.id}: "${title}" (confidence: ${result.confidence})${lootDrop}` }],
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
case 'brief': {
|
|
525
|
+
// Session-start briefing — user identity, preferences, gotchas overview, stale entries.
|
|
526
|
+
// Delegates to the same briefing logic as memory_context(context: undefined).
|
|
527
|
+
const { lobe: rawLobe } = z.object({
|
|
528
|
+
lobe: z.string().optional(),
|
|
529
|
+
}).parse(args ?? {});
|
|
530
|
+
process.stderr.write(`[memory-mcp] tool=brief lobe=${rawLobe ?? 'auto'}\n`);
|
|
531
|
+
// Surface previous crash report
|
|
532
|
+
const previousCrash = await readLatestCrash();
|
|
533
|
+
const crashSection = previousCrash
|
|
534
|
+
? `## Previous Crash Detected\n${formatCrashSummary(previousCrash)}\nRun **memory_diagnose** for full details.\n`
|
|
535
|
+
: '';
|
|
536
|
+
if (previousCrash)
|
|
537
|
+
await clearLatestCrash();
|
|
538
|
+
// Surface degraded lobes warning
|
|
539
|
+
const allBriefingLobes = rawLobe
|
|
540
|
+
? [rawLobe, ...configManager.getAlwaysIncludeLobes().filter(n => n !== rawLobe)]
|
|
541
|
+
: configManager.getLobeNames();
|
|
542
|
+
const degradedLobeNames = allBriefingLobes.filter(n => configManager.getLobeHealth(n)?.status === 'degraded');
|
|
543
|
+
const degradedSection = degradedLobeNames.length > 0
|
|
544
|
+
? `## Degraded Lobes: ${degradedLobeNames.join(', ')}\nRun **memory_diagnose** for details.`
|
|
545
|
+
: '';
|
|
546
|
+
const sections = [];
|
|
547
|
+
if (crashSection)
|
|
548
|
+
sections.push(crashSection);
|
|
549
|
+
if (degradedSection)
|
|
550
|
+
sections.push(degradedSection);
|
|
551
|
+
// Collect briefing across all lobes (or specified lobe + alwaysInclude)
|
|
552
|
+
const briefingLobeNames = allBriefingLobes;
|
|
553
|
+
const allStale = [];
|
|
554
|
+
let totalEntries = 0;
|
|
555
|
+
const alwaysIncludeSet = new Set(configManager.getAlwaysIncludeLobes());
|
|
556
|
+
for (const lobeName of briefingLobeNames) {
|
|
557
|
+
const health = configManager.getLobeHealth(lobeName);
|
|
558
|
+
if (health?.status === 'degraded')
|
|
559
|
+
continue;
|
|
560
|
+
const store = configManager.getStore(lobeName);
|
|
561
|
+
if (!store)
|
|
562
|
+
continue;
|
|
563
|
+
const budget = alwaysIncludeSet.has(lobeName) ? 300 : 100;
|
|
564
|
+
const lobeBriefing = await store.briefing(budget);
|
|
565
|
+
if (lobeBriefing.entryCount > 0)
|
|
566
|
+
sections.push(lobeBriefing.briefing);
|
|
567
|
+
if (lobeBriefing.staleDetails)
|
|
568
|
+
allStale.push(...lobeBriefing.staleDetails);
|
|
569
|
+
totalEntries += lobeBriefing.entryCount;
|
|
570
|
+
}
|
|
571
|
+
if (allStale.length > 0)
|
|
572
|
+
sections.push(formatStaleSection(allStale));
|
|
573
|
+
if (sections.length === 0) {
|
|
574
|
+
sections.push('No knowledge stored yet. As you work, use **gotcha**, **convention**, or **learn** to store observations. Try **memory_bootstrap** to seed initial knowledge.');
|
|
575
|
+
}
|
|
576
|
+
const briefLobes = briefingLobeNames.filter(n => configManager.getLobeHealth(n)?.status !== 'degraded');
|
|
577
|
+
sections.push(`---\n*${totalEntries} entries across ${briefLobes.length} ${briefLobes.length === 1 ? 'lobe' : 'lobes'}. Use **recall(context: "what you are doing")** for task-specific knowledge.*`);
|
|
578
|
+
return { content: [{ type: 'text', text: sections.join('\n\n---\n\n') }] };
|
|
579
|
+
}
|
|
580
|
+
case 'recall': {
|
|
581
|
+
// Pre-task lookup — semantic + keyword search on the target lobe.
|
|
582
|
+
// Pure task-relevant search — no global preferences (that's what brief is for).
|
|
583
|
+
const { lobe: rawLobe, context, maxResults } = z.object({
|
|
584
|
+
lobe: z.string().optional(),
|
|
585
|
+
context: z.string().min(1),
|
|
586
|
+
maxResults: z.number().optional(),
|
|
587
|
+
}).parse(args);
|
|
588
|
+
process.stderr.write(`[memory-mcp] tool=recall lobe=${rawLobe ?? 'auto'} context="${context.slice(0, 50)}"\n`);
|
|
589
|
+
const max = maxResults ?? 10;
|
|
590
|
+
const allLobeResults = [];
|
|
591
|
+
const ctxEntryLobeMap = new Map();
|
|
592
|
+
let label;
|
|
593
|
+
let primaryStore;
|
|
594
|
+
if (rawLobe) {
|
|
595
|
+
const ctx = resolveToolContext(rawLobe);
|
|
596
|
+
if (!ctx.ok)
|
|
597
|
+
return contextError(ctx);
|
|
598
|
+
label = ctx.label;
|
|
599
|
+
primaryStore = ctx.store;
|
|
600
|
+
const lobeResults = await ctx.store.contextSearch(context, max);
|
|
601
|
+
allLobeResults.push(...lobeResults);
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
// Search all non-global lobes (recall is task-specific, not identity)
|
|
605
|
+
const resolution = await resolveLobesForRead(false);
|
|
606
|
+
switch (resolution.kind) {
|
|
607
|
+
case 'resolved': {
|
|
608
|
+
label = resolution.label;
|
|
609
|
+
for (const lobeName of resolution.lobes) {
|
|
610
|
+
const store = configManager.getStore(lobeName);
|
|
611
|
+
if (!store)
|
|
612
|
+
continue;
|
|
613
|
+
if (!primaryStore)
|
|
614
|
+
primaryStore = store;
|
|
615
|
+
const lobeResults = await store.contextSearch(context, max);
|
|
616
|
+
if (resolution.lobes.length > 1) {
|
|
617
|
+
for (const r of lobeResults)
|
|
618
|
+
ctxEntryLobeMap.set(r.entry.id, lobeName);
|
|
619
|
+
}
|
|
620
|
+
allLobeResults.push(...lobeResults);
|
|
621
|
+
}
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
624
|
+
case 'global-only': {
|
|
625
|
+
// No project lobes matched — ask agent to specify
|
|
626
|
+
return {
|
|
627
|
+
content: [{ type: 'text', text: `Cannot determine which lobe to search. Specify the lobe: recall(lobe: "...", context: "${context}")\nAvailable: ${configManager.getLobeNames().join(', ')}` }],
|
|
628
|
+
isError: true,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
// Dedupe, sort, slice
|
|
634
|
+
const seenIds = new Set();
|
|
635
|
+
const results = allLobeResults
|
|
636
|
+
.sort((a, b) => b.score - a.score)
|
|
637
|
+
.filter(r => {
|
|
638
|
+
if (seenIds.has(r.entry.id))
|
|
639
|
+
return false;
|
|
640
|
+
seenIds.add(r.entry.id);
|
|
641
|
+
return true;
|
|
642
|
+
})
|
|
643
|
+
.slice(0, max);
|
|
644
|
+
if (results.length === 0) {
|
|
645
|
+
const modeHint = primaryStore
|
|
646
|
+
? `\n${formatSearchMode(primaryStore.hasEmbedder, primaryStore.vectorCount, primaryStore.entryCount)}`
|
|
647
|
+
: '';
|
|
648
|
+
return {
|
|
649
|
+
content: [{
|
|
650
|
+
type: 'text',
|
|
651
|
+
text: `No relevant knowledge found for: "${context}"\n\nProceed without prior context. Use **gotcha**, **convention**, or **learn** to store observations as you work.${modeHint}`,
|
|
652
|
+
}],
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
// Format results grouped by topic
|
|
656
|
+
const sections = [`## Recall: "${context}"\n`];
|
|
657
|
+
const byTopic = new Map();
|
|
658
|
+
for (const r of results) {
|
|
659
|
+
const list = byTopic.get(r.entry.topic) ?? [];
|
|
660
|
+
list.push(r);
|
|
661
|
+
byTopic.set(r.entry.topic, list);
|
|
662
|
+
}
|
|
663
|
+
const topicOrder = ['gotchas', 'conventions', 'architecture', 'general'];
|
|
664
|
+
const orderedTopics = [
|
|
665
|
+
...topicOrder.filter(t => byTopic.has(t)),
|
|
666
|
+
...Array.from(byTopic.keys()).filter(t => !topicOrder.includes(t)).sort(),
|
|
667
|
+
];
|
|
668
|
+
const showLobeLabels = ctxEntryLobeMap.size > 0;
|
|
669
|
+
for (const topic of orderedTopics) {
|
|
670
|
+
const topicResults = byTopic.get(topic);
|
|
671
|
+
const heading = topic === 'gotchas' ? 'Gotchas'
|
|
672
|
+
: topic === 'conventions' ? 'Conventions'
|
|
673
|
+
: topic === 'general' ? 'General'
|
|
674
|
+
: topic.startsWith('modules/') ? `Module: ${topic.split('/')[1]}`
|
|
675
|
+
: topic.charAt(0).toUpperCase() + topic.slice(1);
|
|
676
|
+
sections.push(`### ${heading}`);
|
|
677
|
+
for (const r of topicResults) {
|
|
678
|
+
const marker = topic === 'gotchas' ? '[!] ' : '';
|
|
679
|
+
const lobeLabel = showLobeLabels ? ` [${ctxEntryLobeMap.get(r.entry.id) ?? '?'}]` : '';
|
|
680
|
+
sections.push(`- **${marker}${r.entry.title}**${lobeLabel}: ${r.entry.content}`);
|
|
681
|
+
}
|
|
682
|
+
sections.push('');
|
|
683
|
+
}
|
|
684
|
+
// Mode indicator
|
|
685
|
+
if (primaryStore) {
|
|
686
|
+
sections.push(formatSearchMode(primaryStore.hasEmbedder, primaryStore.vectorCount, primaryStore.entryCount));
|
|
687
|
+
}
|
|
688
|
+
return { content: [{ type: 'text', text: sections.join('\n') }] };
|
|
689
|
+
}
|
|
690
|
+
case 'gotchas': {
|
|
691
|
+
// Topic-filtered query for gotchas.
|
|
692
|
+
const { lobe: rawLobe, area } = z.object({
|
|
693
|
+
lobe: z.string().optional(),
|
|
694
|
+
area: z.string().optional(),
|
|
695
|
+
}).parse(args ?? {});
|
|
696
|
+
process.stderr.write(`[memory-mcp] tool=gotchas lobe=${rawLobe ?? 'auto'} area=${area ?? '*'}\n`);
|
|
697
|
+
const ctx = resolveToolContext(rawLobe);
|
|
698
|
+
if (!ctx.ok)
|
|
699
|
+
return contextError(ctx);
|
|
700
|
+
const result = await ctx.store.query('gotchas', 'standard', area);
|
|
701
|
+
if (result.entries.length === 0) {
|
|
702
|
+
return {
|
|
703
|
+
content: [{ type: 'text', text: `[${ctx.label}] No gotchas found${area ? ` for "${area}"` : ''}. Use **gotcha(lobe, observation)** to store one.` }],
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
const lines = result.entries.map(e => `- **[!] ${e.title}** (${e.id}): ${e.content}`);
|
|
707
|
+
return {
|
|
708
|
+
content: [{ type: 'text', text: `## [${ctx.label}] Gotchas${area ? ` — ${area}` : ''}\n\n${lines.join('\n')}` }],
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
case 'conventions': {
|
|
712
|
+
// Topic-filtered query for conventions.
|
|
713
|
+
const { lobe: rawLobe, area } = z.object({
|
|
714
|
+
lobe: z.string().optional(),
|
|
715
|
+
area: z.string().optional(),
|
|
716
|
+
}).parse(args ?? {});
|
|
717
|
+
process.stderr.write(`[memory-mcp] tool=conventions lobe=${rawLobe ?? 'auto'} area=${area ?? '*'}\n`);
|
|
718
|
+
const ctx = resolveToolContext(rawLobe);
|
|
719
|
+
if (!ctx.ok)
|
|
720
|
+
return contextError(ctx);
|
|
721
|
+
const result = await ctx.store.query('conventions', 'standard', area);
|
|
722
|
+
if (result.entries.length === 0) {
|
|
723
|
+
return {
|
|
724
|
+
content: [{ type: 'text', text: `[${ctx.label}] No conventions found${area ? ` for "${area}"` : ''}. Use **convention(lobe, observation)** to store one.` }],
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
const lines = result.entries.map(e => `- **${e.title}** (${e.id}): ${e.content}`);
|
|
728
|
+
return {
|
|
729
|
+
content: [{ type: 'text', text: `## [${ctx.label}] Conventions${area ? ` — ${area}` : ''}\n\n${lines.join('\n')}` }],
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
case 'prefer': {
|
|
733
|
+
// Store a user preference. Routes to alwaysInclude lobe (or specified lobe).
|
|
734
|
+
const { rule, lobe: rawLobe } = z.object({
|
|
735
|
+
rule: z.string().min(1),
|
|
736
|
+
lobe: z.string().optional(),
|
|
737
|
+
}).parse(args);
|
|
738
|
+
process.stderr.write(`[memory-mcp] tool=prefer lobe=${rawLobe ?? 'global'}\n`);
|
|
739
|
+
// Route to alwaysInclude lobe when no lobe specified
|
|
740
|
+
let effectiveLobe = rawLobe;
|
|
741
|
+
if (!effectiveLobe) {
|
|
742
|
+
const alwaysIncludeLobes = configManager.getAlwaysIncludeLobes();
|
|
743
|
+
effectiveLobe = alwaysIncludeLobes.length > 0 ? alwaysIncludeLobes[0] : undefined;
|
|
744
|
+
}
|
|
745
|
+
const ctx = resolveToolContext(effectiveLobe);
|
|
746
|
+
if (!ctx.ok) {
|
|
747
|
+
return {
|
|
748
|
+
content: [{ type: 'text', text: `No global lobe configured for preferences. Specify a lobe: prefer(rule: "...", lobe: "...").\nAvailable: ${configManager.getLobeNames().join(', ')}` }],
|
|
749
|
+
isError: true,
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
const { title, content } = extractTitle(rule);
|
|
753
|
+
const result = await ctx.store.store('preferences', title, content, [], 'user');
|
|
754
|
+
if (result.kind !== 'stored') {
|
|
755
|
+
return {
|
|
756
|
+
content: [{ type: 'text', text: `[${ctx.label}] Failed to store preference: ${result.kind === 'review-required' ? result.warning : result.kind === 'rejected' ? result.warning : 'unknown error'}` }],
|
|
757
|
+
isError: true,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
return {
|
|
761
|
+
content: [{ type: 'text', text: `[${ctx.label}] Stored preference ${result.id}: "${title}" (trust: user)` }],
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
case 'fix': {
|
|
765
|
+
// Fix or delete an entry. Search all stores if no lobe specified.
|
|
766
|
+
const { id, correction, lobe: rawLobe } = z.object({
|
|
767
|
+
id: z.string().min(1),
|
|
768
|
+
correction: z.string().optional(),
|
|
769
|
+
lobe: z.string().optional(),
|
|
770
|
+
}).parse(args);
|
|
771
|
+
process.stderr.write(`[memory-mcp] tool=fix id=${id} action=${correction ? 'replace' : 'delete'}\n`);
|
|
772
|
+
const action = correction !== undefined && correction.length > 0 ? 'replace' : 'delete';
|
|
773
|
+
// Resolve lobe — search all stores if not specified
|
|
774
|
+
let effectiveFixLobe = rawLobe;
|
|
775
|
+
let foundInLobe;
|
|
776
|
+
if (!effectiveFixLobe) {
|
|
777
|
+
for (const lobeName of configManager.getLobeNames()) {
|
|
778
|
+
const store = configManager.getStore(lobeName);
|
|
779
|
+
if (!store)
|
|
780
|
+
continue;
|
|
781
|
+
try {
|
|
782
|
+
if (await store.hasEntry(id)) {
|
|
783
|
+
effectiveFixLobe = lobeName;
|
|
784
|
+
foundInLobe = lobeName;
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
catch {
|
|
789
|
+
// Skip degraded lobes
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (!effectiveFixLobe) {
|
|
794
|
+
return {
|
|
795
|
+
content: [{ type: 'text', text: `Entry "${id}" not found in any lobe. Available: ${configManager.getLobeNames().join(', ')}` }],
|
|
796
|
+
isError: true,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
const ctx = resolveToolContext(effectiveFixLobe);
|
|
800
|
+
if (!ctx.ok)
|
|
801
|
+
return contextError(ctx);
|
|
802
|
+
const result = await ctx.store.correct(id, correction ?? '', action);
|
|
803
|
+
if (!result.corrected) {
|
|
804
|
+
return {
|
|
805
|
+
content: [{ type: 'text', text: `[${ctx.label}] Failed: ${result.error}` }],
|
|
806
|
+
isError: true,
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
const lobeNote = foundInLobe && !rawLobe ? ` (found in lobe: ${foundInLobe})` : '';
|
|
810
|
+
if (action === 'delete') {
|
|
811
|
+
return {
|
|
812
|
+
content: [{ type: 'text', text: `Deleted entry ${id}${lobeNote}.` }],
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
return {
|
|
816
|
+
content: [{ type: 'text', text: `Fixed entry ${id}${lobeNote} (confidence: ${result.newConfidence}, trust: ${result.trust}).` }],
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
// ─── Legacy tool handlers (hidden from listing) ──────────────────────
|
|
450
820
|
case 'memory_store': {
|
|
451
821
|
const { lobe: rawLobe, topic: rawTopic, entries: rawEntries, sources, references, trust: rawTrust, tags: rawTags, durabilityDecision } = z.object({
|
|
452
822
|
lobe: z.string().optional(),
|
|
@@ -468,7 +838,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
468
838
|
const topic = parseTopicScope(rawTopic);
|
|
469
839
|
if (!topic) {
|
|
470
840
|
return {
|
|
471
|
-
content: [{ type: 'text', text: `Invalid topic: "${rawTopic}". Valid: user | preferences | architecture | conventions | gotchas | recent-work | modules/<name>` }],
|
|
841
|
+
content: [{ type: 'text', text: `Invalid topic: "${rawTopic}". Valid: user | preferences | architecture | conventions | gotchas | general | recent-work | modules/<name>` }],
|
|
472
842
|
isError: true,
|
|
473
843
|
};
|
|
474
844
|
}
|
|
@@ -974,10 +1344,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
974
1344
|
const noResultHint = ctxGlobalOnlyHint
|
|
975
1345
|
? `\n\n> ${ctxGlobalOnlyHint}`
|
|
976
1346
|
: '\n\nThis is fine — proceed without prior context. As you learn things worth remembering, store them with memory_store.';
|
|
1347
|
+
// Mode indicator on no-results path — helps diagnose why nothing was found
|
|
1348
|
+
const modeHint = primaryStore
|
|
1349
|
+
? `\n${formatSearchMode(primaryStore.hasEmbedder, primaryStore.vectorCount, primaryStore.entryCount)}`
|
|
1350
|
+
: '';
|
|
977
1351
|
return {
|
|
978
1352
|
content: [{
|
|
979
1353
|
type: 'text',
|
|
980
|
-
text: `[${label}] No relevant knowledge found for: "${context}"${noResultHint}\n\n---\n${ctxFooter}`,
|
|
1354
|
+
text: `[${label}] No relevant knowledge found for: "${context}"${noResultHint}${modeHint}\n\n---\n${ctxFooter}`,
|
|
981
1355
|
}],
|
|
982
1356
|
};
|
|
983
1357
|
}
|
|
@@ -1020,6 +1394,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1020
1394
|
sections.push(formatConflictWarning(ctxConflicts));
|
|
1021
1395
|
}
|
|
1022
1396
|
}
|
|
1397
|
+
// Search mode indicator — lightweight getters, no extra disk reload
|
|
1398
|
+
if (primaryStore) {
|
|
1399
|
+
sections.push(formatSearchMode(primaryStore.hasEmbedder, primaryStore.vectorCount, primaryStore.entryCount));
|
|
1400
|
+
}
|
|
1023
1401
|
// Collect all matched keywords and topics for the dedup hint
|
|
1024
1402
|
const allMatchedKeywords = new Set();
|
|
1025
1403
|
const matchedTopics = new Set();
|
|
@@ -1071,6 +1449,28 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1071
1449
|
}
|
|
1072
1450
|
return { content: [{ type: 'text', text: sections.join('\n\n---\n\n') }] };
|
|
1073
1451
|
}
|
|
1452
|
+
case 'memory_reembed': {
|
|
1453
|
+
const { lobe: rawLobe } = z.object({
|
|
1454
|
+
lobe: z.string().optional(),
|
|
1455
|
+
}).parse(args ?? {});
|
|
1456
|
+
const lobeName = rawLobe ?? lobeNames[0];
|
|
1457
|
+
const ctx = resolveToolContext(lobeName);
|
|
1458
|
+
if (!ctx.ok)
|
|
1459
|
+
return contextError(ctx);
|
|
1460
|
+
const result = await ctx.store.reEmbed();
|
|
1461
|
+
if (result.error) {
|
|
1462
|
+
return { content: [{ type: 'text', text: `[${ctx.label}] Re-embed failed: ${result.error}` }] };
|
|
1463
|
+
}
|
|
1464
|
+
const parts = [
|
|
1465
|
+
`[${ctx.label}] Re-embedded ${result.embedded} entries`,
|
|
1466
|
+
`(${result.skipped} skipped, ${result.failed} failed).`,
|
|
1467
|
+
];
|
|
1468
|
+
// Hint if many entries were vectorized
|
|
1469
|
+
if (result.embedded > 0) {
|
|
1470
|
+
parts.push('\nSemantic search is now active for these entries.');
|
|
1471
|
+
}
|
|
1472
|
+
return { content: [{ type: 'text', text: parts.join(' ') }] };
|
|
1473
|
+
}
|
|
1074
1474
|
case 'memory_bootstrap': {
|
|
1075
1475
|
const { lobe: rawLobe, root, budgetMB } = z.object({
|
|
1076
1476
|
lobe: z.string().optional(),
|