@jungjaehoon/mama-server 1.11.0 β†’ 1.12.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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/server.js +203 -262
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jungjaehoon/mama-server",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "description": "MAMA MCP Server - Memory-Augmented MCP Assistant for Claude Code & Desktop",
5
5
  "main": "src/server.js",
6
6
  "bin": {
package/src/server.js CHANGED
@@ -192,272 +192,194 @@ class MAMAServer {
192
192
  }
193
193
 
194
194
  setupHandlers() {
195
- // List available tools - Simplified to 4 core tools (2025-11-25)
196
- // Design principle: LLM infers relationships from search results
197
- // Fewer tools = more flexibility, less constraint
198
- this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
199
- tools: [
200
- // 1. SAVE - Unified save for decisions and checkpoints
201
- {
202
- name: 'save',
203
- description: `${this.legacyHttpEmbeddingMode ? `${this.getLegacyMigrationNotice()}\n\n` : ''}🀝 Save a decision or checkpoint to your reasoning graph.
204
-
205
- ⚑ TRIGGERS - Call this when:
206
- β€’ User says: "κΈ°μ–΅ν•΄μ€˜", "remember", "decided", "κ²°μ •ν–ˆμ–΄"
207
- β€’ Lesson learned: "κΉ¨λ‹¬μ•˜μ–΄", "μ•Œκ²Œλμ–΄", "this worked/failed"
208
- β€’ Architectural choice made
209
- β€’ Session ending β†’ use type='checkpoint'
210
-
211
- πŸ”— REQUIRED WORKFLOW (Don't create orphans!):
212
- 1. Call 'search' FIRST to find related decisions
213
- 2. Check if same topic exists (yours will supersede it)
214
- 3. MUST include link in reasoning/summary field
215
-
216
- πŸ“Ž LINKING FORMAT:
217
- β€’ [Decision] reasoning: End with 'builds_on: <id>' or 'debates: <id>' or 'synthesizes: [id1, id2]'
218
- β€’ [Checkpoint] summary: Include 'Related decisions: decision_xxx, decision_yyy'
219
-
220
- type='decision': choices & lessons (same topic = evolution chain)
221
- type='checkpoint': session state for resumption (ALSO requires search first!)`,
222
- inputSchema: {
223
- type: 'object',
224
- properties: {
225
- type: {
226
- type: 'string',
227
- enum: ['decision', 'checkpoint'],
228
- description: "What to save: 'decision' or 'checkpoint'",
229
- },
230
- // Decision fields
231
- topic: {
232
- type: 'string',
233
- description:
234
- "[Decision] Topic identifier (e.g., 'auth_strategy'). ⚑ REUSE same topic = supersedes previous, creating evolution chain.",
235
- },
236
- decision: {
237
- type: 'string',
238
- description: "[Decision] The decision made (e.g., 'Use JWT with refresh tokens').",
239
- },
240
- reasoning: {
241
- type: 'string',
242
- description:
243
- "[Decision] Why this decision was made. Include 5-layer narrative: (1) Context - what problem/situation; (2) Evidence - what proves this works (tests, benchmarks, prior experience); (3) Alternatives - what other options were considered and why rejected; (4) Risks - known limitations or failure modes; (5) Rationale - final reasoning for this choice. ⚠️ REQUIRED: End with 'builds_on: <id>' or 'debates: <id>' or 'synthesizes: [id1, id2]' to link related decisions.",
244
- },
245
- confidence: {
246
- type: 'number',
247
- description: '[Decision] Confidence 0.0-1.0. Default: 0.5',
248
- minimum: 0,
249
- maximum: 1,
250
- },
251
- // Scope & temporal fields
252
- scopes: {
253
- type: 'array',
254
- items: {
255
- type: 'object',
256
- properties: {
257
- kind: {
258
- type: 'string',
259
- enum: ['global', 'user', 'channel', 'project'],
260
- },
261
- id: { type: 'string' },
262
- },
263
- required: ['kind', 'id'],
195
+ // Tool definitions come from src/tools/ (single source of truth).
196
+ // Legacy unified tools (save, search, update) kept as wrappers for backward compat.
197
+ const legacyNotice = this.legacyHttpEmbeddingMode
198
+ ? `${this.getLegacyMigrationNotice()}\n\n`
199
+ : '';
200
+
201
+ const tools = [
202
+ // 1. SAVE β€” decisions, checkpoints, conversation ingestion
203
+ {
204
+ name: 'save',
205
+ description: `${legacyNotice}Save to MAMA memory. Use type parameter to choose what to save.
206
+
207
+ **type='decision'** β€” Save architectural decisions, lessons learned, insights.
208
+ Required: topic, decision, reasoning. Optional: confidence, scopes, event_date.
209
+ Triggers: user says "κΈ°μ–΅ν•΄", "remember", "decided". Reuse same topic to create evolution chain.
210
+
211
+ **type='checkpoint'** β€” Save session state for resumption.
212
+ Required: summary (4-section: Goal, Evidence, Unfinished, Next Briefing).
213
+ Optional: next_steps, open_files. Triggers: session ending, "체크포인트", "save progress".
214
+
215
+ **type='ingest'** β€” Import conversation messages into memory with optional extraction.
216
+ Required: messages (array of {role, content}). Optional: scopes, session_date, extract.
217
+
218
+ **Scopes**: Isolate memories per project/channel. Example: [{"kind":"project","id":"/my/app"}]
219
+ **event_date**: ISO 8601 date when event occurred (e.g. "2024-01-15"), not when saved.`,
220
+ inputSchema: {
221
+ type: 'object',
222
+ properties: {
223
+ type: {
224
+ type: 'string',
225
+ enum: ['decision', 'checkpoint', 'ingest'],
226
+ description: "What to save: 'decision', 'checkpoint', or 'ingest'",
227
+ },
228
+ // Decision fields
229
+ topic: {
230
+ type: 'string',
231
+ description: '[Decision] Topic identifier. Reuse same topic = supersedes previous.',
232
+ },
233
+ decision: {
234
+ type: 'string',
235
+ description: '[Decision] The decision made.',
236
+ },
237
+ reasoning: {
238
+ type: 'string',
239
+ description: "[Decision] Why. End with 'builds_on: <id>' or 'debates: <id>' to link.",
240
+ },
241
+ confidence: {
242
+ type: 'number',
243
+ description: '[Decision] 0.0-1.0. Default: 0.5',
244
+ minimum: 0,
245
+ maximum: 1,
246
+ },
247
+ scopes: {
248
+ type: 'array',
249
+ items: {
250
+ type: 'object',
251
+ properties: {
252
+ kind: { type: 'string', enum: ['global', 'user', 'channel', 'project'] },
253
+ id: { type: 'string' },
264
254
  },
265
- description:
266
- '[Decision] Memory scopes for isolation. Example: [{"kind": "project", "id": "/path/to/project"}]',
267
- },
268
- event_date: {
269
- type: 'string',
270
- description:
271
- '[Decision] ISO 8601 date when the event occurred (e.g., "2024-01-15"). Defaults to now.',
272
- },
273
- // Checkpoint fields
274
- summary: {
275
- type: 'string',
276
- description:
277
- "[Checkpoint] Session state summary. Use 4-section format: (1) 🎯 Goal & Progress - what was the goal, where did you stop; (2) βœ… Evidence - mark each item as Verified/Not run/Assumed with proof; (3) ⏳ Unfinished & Risks - incomplete work, blockers, unknowns; (4) 🚦 Next Agent Briefing - Definition of Done, quick health checks to run first. ⚠️ Include 'Related decisions: decision_xxx, decision_yyy' to link context.",
278
- },
279
- next_steps: {
280
- type: 'string',
281
- description:
282
- '[Checkpoint] Instructions for next session: DoD (Definition of Done), quick verification commands (npm test, curl health), constraints/cautions.',
283
- },
284
- open_files: {
285
- type: 'array',
286
- items: { type: 'string' },
287
- description: '[Checkpoint] Currently relevant files.',
255
+ required: ['kind', 'id'],
288
256
  },
257
+ description: 'Memory scopes for isolation.',
289
258
  },
290
- required: ['type'],
291
- },
292
- },
293
- // 2. SEARCH - Unified search across decisions and checkpoints
294
- {
295
- name: 'search',
296
- description: `πŸ” Search the reasoning graph before acting.
297
-
298
- ⚑ TRIGGERS - Call this BEFORE:
299
- β€’ ⚠️ REQUIRED before 'save' (find links first!)
300
- β€’ Making architectural choices (check prior art)
301
- β€’ Debugging (find past failures on similar issues)
302
- β€’ Starting work on a topic (load context)
303
- β€’ User asks: "λ­μ˜€λ”λΌ", "what did we decide", "이전에"
304
-
305
- πŸ”— USE FOR REASONING GRAPH:
306
- β€’ Find decisions to supersede (same topic)
307
- β€’ Find decisions to link (builds_on, debates, synthesizes)
308
- β€’ Understand decision evolution (time-ordered results)
309
-
310
- Cross-lingual: Works in Korean and English.
311
- ⚠️ High similarity (>0.8) = MUST link with builds_on/debates/synthesizes.
312
-
313
- 🧠 OUTPUT EXPECTATION:
314
- When presenting search results to the user or agent, include a brief **Reasoning Summary** grounded in the actual results:
315
- - Why these results match (tokens/endpoint/field overlap)
316
- - What is known vs unknown (explicitly mark unknowns)
317
- - What to do next (use contract fields, avoid guessing)`,
318
- inputSchema: {
319
- type: 'object',
320
- properties: {
321
- query: {
322
- type: 'string',
323
- description:
324
- 'Search query (optional). Semantic search finds related decisions even with different wording. If empty, returns recent items sorted by time.',
325
- },
326
- type: {
327
- type: 'string',
328
- enum: ['all', 'decision', 'checkpoint'],
329
- description:
330
- "Filter by type: 'decision' for architectural choices, 'checkpoint' for session states, 'all' for both. Default: 'all'",
331
- },
332
- limit: {
333
- type: 'number',
334
- description: 'Maximum results. Default: 10',
335
- },
336
- scopes: {
337
- type: 'array',
338
- items: {
339
- type: 'object',
340
- properties: {
341
- kind: {
342
- type: 'string',
343
- enum: ['global', 'user', 'channel', 'project'],
344
- },
345
- id: { type: 'string' },
346
- },
347
- required: ['kind', 'id'],
259
+ event_date: {
260
+ type: 'string',
261
+ description: 'ISO 8601 date when event occurred (e.g. "2024-01-15").',
262
+ },
263
+ // Checkpoint fields
264
+ summary: {
265
+ type: 'string',
266
+ description: '[Checkpoint] Session state summary.',
267
+ },
268
+ next_steps: {
269
+ type: 'string',
270
+ description: '[Checkpoint] Instructions for next session.',
271
+ },
272
+ open_files: {
273
+ type: 'array',
274
+ items: { type: 'string' },
275
+ description: '[Checkpoint] Currently relevant files.',
276
+ },
277
+ // Ingest fields
278
+ messages: {
279
+ type: 'array',
280
+ items: {
281
+ type: 'object',
282
+ properties: {
283
+ role: { type: 'string', enum: ['user', 'assistant', 'system'] },
284
+ content: { type: 'string' },
348
285
  },
349
- description: 'Filter search results by scope.',
286
+ required: ['role', 'content'],
350
287
  },
288
+ description: '[Ingest] Conversation messages to import.',
351
289
  },
352
- },
353
- },
354
- // 3. UPDATE - Update decision outcome
355
- {
356
- name: 'update',
357
- description: `πŸ“ Update decision outcome after real-world validation.
358
-
359
- ⚑ TRIGGERS - Call this when:
360
- β€’ Days/weeks later: issues discovered β†’ mark 'failed' + reason
361
- β€’ Production success confirmed β†’ mark 'success'
362
- β€’ Partial results with caveats β†’ mark 'partial'
363
- β€’ User says: "이거 μ•ˆλμ–΄", "this didn't work", "μ„±κ³΅ν–ˆμ–΄"
364
-
365
- πŸ”— REASONING GRAPH IMPACT:
366
- β€’ 'failed' outcomes teach future LLMs what to avoid
367
- β€’ After failure β†’ save NEW decision with same topic to supersede
368
-
369
- πŸ’‘ TIP: Don't just update - if approach changed, save a NEW decision with same topic. This creates evolution history.`,
370
- inputSchema: {
371
- type: 'object',
372
- properties: {
373
- id: {
374
- type: 'string',
375
- description: 'Decision ID to update.',
376
- },
377
- outcome: {
378
- type: 'string',
379
- description:
380
- "New outcome status (case-insensitive): 'success' or 'SUCCESS', 'failed' or 'FAILED', 'partial' or 'PARTIAL'.",
381
- },
382
- reason: {
383
- type: 'string',
384
- description:
385
- 'Why it succeeded/failed/was partial. Include specific evidence: error logs, metrics, user feedback, or what broke.',
386
- },
290
+ session_date: {
291
+ type: 'string',
292
+ description: '[Ingest] ISO 8601 date when conversation occurred.',
293
+ },
294
+ extract: {
295
+ type: 'boolean',
296
+ description: '[Ingest] Extract structured memories via LLM. Default: false',
387
297
  },
388
- required: ['id', 'outcome'],
389
298
  },
299
+ required: ['type'],
390
300
  },
391
- // 4. SEARCH_DECISIONS_AND_CONTRACTS - PreToolUse RPC for hooks
392
- {
393
- name: 'search_decisions_and_contracts',
394
- description:
395
- 'Search decisions and related contracts for PreToolUse injection (MAMA v2 hooks).',
396
- inputSchema: {
397
- type: 'object',
398
- properties: {
399
- query: {
400
- type: 'string',
401
- description: 'Search query for decisions.',
402
- },
403
- filePath: {
404
- type: 'string',
405
- description: 'File path context for contract search.',
406
- },
407
- toolName: {
408
- type: 'string',
409
- description: 'Tool name context (Edit/Write/apply_patch).',
410
- },
411
- decisionLimit: {
412
- type: 'number',
413
- description: 'Max decision results (default: 5).',
414
- },
415
- contractLimit: {
416
- type: 'number',
417
- description: 'Max contract results (default: 3).',
418
- },
419
- similarityThreshold: {
420
- type: 'number',
421
- description: 'Similarity threshold for vector search (default: 0.7).',
301
+ },
302
+ // 2. SEARCH β€” unified search across decisions, checkpoints, load latest checkpoint
303
+ {
304
+ name: 'search',
305
+ description: `Search MAMA memory. Returns results ranked by semantic similarity.
306
+
307
+ **With query** β€” Semantic search across decisions and checkpoints. Cross-lingual (Korean + English).
308
+ Triggers: "λ­μ˜€λ”λΌ", "what did we decide", making architectural choices, debugging.
309
+
310
+ **Without query** β€” List recent items sorted by time.
311
+
312
+ **Resume session**: type='checkpoint' without query β†’ loads latest checkpoint with full context (narrative, links, next steps).
313
+ Triggers: "μ΄μ–΄μ„œ", "continue", "where were we", session start.
314
+
315
+ **type parameter**: 'decision' (choices/lessons only), 'checkpoint' (session states / resume), 'all' (both, default).
316
+ **scopes**: Filter by project/channel. Omit for global search.
317
+ **limit**: Max results (default: 10).
318
+
319
+ ⚠️ REQUIRED: Call search BEFORE save to find related decisions and avoid orphans.`,
320
+ inputSchema: {
321
+ type: 'object',
322
+ properties: {
323
+ query: {
324
+ type: 'string',
325
+ description: 'Search query. Omit to list recent items.',
326
+ },
327
+ type: {
328
+ type: 'string',
329
+ enum: ['all', 'decision', 'checkpoint'],
330
+ description: "Filter by type. Default: 'all'",
331
+ },
332
+ limit: { type: 'number', description: 'Max results. Default: 10' },
333
+ scopes: {
334
+ type: 'array',
335
+ items: {
336
+ type: 'object',
337
+ properties: {
338
+ kind: { type: 'string', enum: ['global', 'user', 'channel', 'project'] },
339
+ id: { type: 'string' },
340
+ },
341
+ required: ['kind', 'id'],
422
342
  },
343
+ description: 'Filter by scope.',
423
344
  },
424
345
  },
425
346
  },
426
- // 5. LOAD_CHECKPOINT - Resume previous session
427
- {
428
- name: 'load_checkpoint',
429
- description: `πŸ”„ Resume a previous session with full context.
430
-
431
- ⚑ TRIGGERS - Call this:
432
- β€’ At session start
433
- β€’ User says: "μ΄μ–΄μ„œ", "continue", "where were we", "μ§€λ‚œλ²ˆ"
434
- β€’ After long break from project
435
-
436
- πŸ”— AFTER LOADING:
437
- 1. Verify Evidence items (code may have changed!)
438
- 2. Run health checks from next_steps first
439
- 3. Call 'search' to refresh related decisions
440
-
441
- Returns: summary (4-section), next_steps (DoD + commands), open_files
442
-
443
- ⚠️ WARNING: Checkpoint may be stale. Always verify before continuing.`,
444
- inputSchema: {
445
- type: 'object',
446
- properties: {},
347
+ },
348
+ // 3. UPDATE β€” decision outcome tracking
349
+ {
350
+ name: 'update',
351
+ description: `Update decision outcome after real-world validation.
352
+
353
+ Triggers: "이거 μ•ˆλμ–΄", "this worked", days later when issues discovered.
354
+ outcome: 'success', 'failed', 'partial' (case-insensitive).
355
+ After failure β†’ save a NEW decision with same topic to create evolution history.`,
356
+ inputSchema: {
357
+ type: 'object',
358
+ properties: {
359
+ id: { type: 'string', description: 'Decision ID to update.' },
360
+ outcome: {
361
+ type: 'string',
362
+ description: "'success', 'failed', or 'partial' (case-insensitive).",
363
+ },
364
+ reason: {
365
+ type: 'string',
366
+ description: 'Why it succeeded/failed/was partial. Include evidence.',
367
+ },
447
368
  },
369
+ required: ['id', 'outcome'],
448
370
  },
449
- // === v2 tools from src/tools/ ===
450
- ...Object.values(memoryTools)
451
- .filter((t) => t.name && t.inputSchema)
452
- .map((t) => ({
453
- name: t.name,
454
- description: t.description,
455
- inputSchema: t.inputSchema,
456
- })),
457
- ],
458
- }));
459
-
460
- // Handle tool execution - 4 core tools only
371
+ },
372
+ // 4. SEARCH_DECISIONS_AND_CONTRACTS β€” PreToolUse hook RPC
373
+ {
374
+ name: 'search_decisions_and_contracts',
375
+ description: 'Search decisions and contracts for PreToolUse hook injection.',
376
+ inputSchema: memoryTools.search_decisions_and_contracts.inputSchema,
377
+ },
378
+ ];
379
+
380
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
381
+
382
+ // Handle tool execution β€” legacy wrappers + v2 tools from src/tools/
461
383
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
462
384
  const { name, arguments: args } = request.params;
463
385
  const toolStart = Date.now();
@@ -467,6 +389,7 @@ Returns: summary (4-section), next_steps (DoD + commands), open_files
467
389
  let result;
468
390
 
469
391
  switch (name) {
392
+ // Legacy unified wrappers (backward compat)
470
393
  case 'save':
471
394
  result = await this.handleSave(args);
472
395
  break;
@@ -476,14 +399,8 @@ Returns: summary (4-section), next_steps (DoD + commands), open_files
476
399
  case 'update':
477
400
  result = await this.handleUpdate(args);
478
401
  break;
479
- case 'search_decisions_and_contracts':
480
- result = await this.handleSearchDecisionsAndContracts(args);
481
- break;
482
- case 'load_checkpoint':
483
- result = await memoryTools.load_checkpoint.handler(args);
484
- break;
485
402
  default:
486
- // Route to src/tools/ handlers
403
+ // All other tools β†’ src/tools/ handlers (single source of truth)
487
404
  if (memoryTools[name] && typeof memoryTools[name].handler === 'function') {
488
405
  result = await memoryTools[name].handler(args);
489
406
  } else {
@@ -575,7 +492,11 @@ Returns: summary (4-section), next_steps (DoD + commands), open_files
575
492
  };
576
493
  }
577
494
 
578
- return { success: false, message: "❌ type must be 'decision' or 'checkpoint'" };
495
+ if (type === 'ingest') {
496
+ return await memoryTools.ingest_conversation.handler(args);
497
+ }
498
+
499
+ return { success: false, message: "❌ type must be 'decision', 'checkpoint', or 'ingest'" };
579
500
  }
580
501
 
581
502
  /**
@@ -584,6 +505,11 @@ Returns: summary (4-section), next_steps (DoD + commands), open_files
584
505
  async handleSearch(args) {
585
506
  const { query, type = 'all', limit = 10, scopes } = args;
586
507
 
508
+ // type='checkpoint' without query β†’ load latest checkpoint (resume session)
509
+ if (type === 'checkpoint' && !query) {
510
+ return await memoryTools.load_checkpoint.handler(args);
511
+ }
512
+
587
513
  const results = [];
588
514
 
589
515
  // Search decisions
@@ -598,7 +524,6 @@ Returns: summary (4-section), next_steps (DoD + commands), open_files
598
524
  } else {
599
525
  decisions = await mama.list({ limit, ...(scopes && { scopes }) });
600
526
  }
601
- // Ensure decisions is an array
602
527
  if (Array.isArray(decisions)) {
603
528
  results.push(
604
529
  ...decisions.map((d) => ({
@@ -609,8 +534,24 @@ Returns: summary (4-section), next_steps (DoD + commands), open_files
609
534
  }
610
535
  }
611
536
 
612
- // Search checkpoints
613
- if (type === 'all' || type === 'checkpoint') {
537
+ // Search checkpoints (with query = search, without = handled above as load)
538
+ if ((type === 'all' || type === 'checkpoint') && query) {
539
+ const checkpoints = await mama.listCheckpoints(limit);
540
+ results.push(
541
+ ...checkpoints
542
+ .filter((c) => c.summary && c.summary.toLowerCase().includes(query.toLowerCase()))
543
+ .map((c) => ({
544
+ id: `checkpoint_${c.id}`,
545
+ summary: c.summary,
546
+ next_steps: c.next_steps,
547
+ created_at: c.timestamp,
548
+ _type: 'checkpoint',
549
+ }))
550
+ );
551
+ }
552
+
553
+ // type='all' without query β€” include recent checkpoints
554
+ if (type === 'all' && !query) {
614
555
  const checkpoints = await mama.listCheckpoints(limit);
615
556
  results.push(
616
557
  ...checkpoints.map((c) => ({