@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/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
- // memory_list_lobes is hidden lobe info is surfaced in memory_context() hints
212
- // and memory_stats. The handler still works if called directly.
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: 'memory_store',
215
- // Example comes first agents form their call shape from the first concrete pattern they see.
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
- topic: {
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
- // modules/<name> is intentionally excluded from the enum so the MCP schema
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
- sources: {
252
- type: 'array',
253
- items: { type: 'string' },
254
- description: 'File paths that informed this (provenance, for freshness tracking)',
255
- default: [],
238
+ maxResults: {
239
+ type: 'number',
240
+ description: 'Max results (default: 10)',
241
+ default: 10,
256
242
  },
257
- references: {
258
- type: 'array',
259
- items: { type: 'string' },
260
- description: 'Files, classes, or symbols this knowledge is about (semantic pointers). Example: ["features/messaging/impl/MessagingReducer.kt"]',
261
- default: [],
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
- trust: {
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
- enum: ['user', 'agent-confirmed', 'agent-inferred'],
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
- tags: {
270
- type: 'array',
271
- items: { type: 'string' },
272
- description: 'Category labels for exact-match retrieval (lowercase slugs). Query with filter: "#tag". Example: ["auth", "critical-path", "mite-combat"]',
273
- default: [],
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: 'Write intent for content that may require review. Use "default" normally. Use "store-anyway" only when intentionally persisting content after a review-required response.',
292
+ description: 'Use "store-anyway" only when re-storing after a review-required response.',
279
293
  default: 'default',
280
294
  },
281
295
  },
282
- required: ['topic', 'entries'],
296
+ required: ['lobe', 'observation'],
283
297
  },
284
298
  },
285
299
  {
286
- name: 'memory_query',
287
- description: 'Search stored knowledge. Searches all lobes when lobe is omitted. Filter supports: keywords (stemmed), #tag (exact tag match), =term (exact keyword, no stemming), -term (NOT). Example: memory_query(scope: "*", filter: "#auth reducer", detail: "full")',
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
- scope: {
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
- enum: ['brief', 'standard', 'full'],
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
- filter: {
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
- description: 'Branch for recent-work. Omit = current branch, "*" = all branches.',
309
- },
310
- isFirstMemoryToolCall: {
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: 'memory_correct',
321
- description: 'Fix or delete an entry. Example: memory_correct(id: "arch-3f7a", action: "replace", correction: "updated content")',
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
- id: {
327
+ observation: {
327
328
  type: 'string',
328
- description: 'Entry ID (e.g. arch-3f7a, pref-5c9b)',
329
+ description: 'The observation — write naturally. First sentence becomes the title.',
329
330
  },
330
- correction: {
331
+ durabilityDecision: {
331
332
  type: 'string',
332
- description: 'New text (for append/replace). Not needed for delete.',
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
- action: {
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
- enum: ['append', 'replace', 'delete'],
337
- description: 'append | replace | delete',
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: ['id', 'action'],
356
+ required: ['rule'],
341
357
  },
342
358
  },
359
+ // --- Maintenance ---
343
360
  {
344
- name: 'memory_context',
345
- description: 'Session start AND pre-task lookup. Call with no args at session start to get user identity, preferences, and stale entries. Call with context to get task-specific knowledge. When lobe is omitted, uses client workspace roots to select the matching lobe; falls back to global-only if roots are unavailable. Example: memory_context() or memory_context(context: "writing a Kotlin reducer")',
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
- lobe: lobeProperty,
350
- context: {
366
+ id: {
351
367
  type: 'string',
352
- description: 'Optional. What you are about to do, in natural language. Omit for session-start briefing (user + preferences + stale entries).',
368
+ description: 'Entry ID (e.g. gotcha-3f7a, arch-5c9b).',
353
369
  },
354
- maxResults: {
355
- type: 'number',
356
- description: 'Max results (default: 10)',
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
- isFirstMemoryToolCall: {
365
- type: 'boolean',
366
- 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.',
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
- // memory_stats is hidden agents rarely need it proactively. Mentioned in
374
- // hints when storage is running low. The handler still works if called directly.
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
- // memory_diagnose is intentionally hidden from the tool list — it clutters
399
- // agent tool discovery and should only be called when directed by error messages
400
- // or crash reports. The handler still works if called directly.
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(),