@jungjaehoon/mama-server 1.13.0 → 1.14.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 (3) hide show
  1. package/README.md +21 -2
  2. package/package.json +2 -2
  3. package/src/server.js +129 -6
package/README.md CHANGED
@@ -75,7 +75,7 @@ Any MCP-compatible client can use MAMA with:
75
75
  npx -y @jungjaehoon/mama-server
76
76
  ```
77
77
 
78
- ## Available Tools (v1.13)
78
+ ## Available Tools
79
79
 
80
80
  The MCP server exposes 13 tools:
81
81
 
@@ -83,7 +83,7 @@ The MCP server exposes 13 tools:
83
83
  | -------------------------------- | ----------------------------------------------------------------------- |
84
84
  | `save_decision` | Save decision with optional scopes and event_date for temporal tracking |
85
85
  | `recall_decision` | Recall decision history by topic, scope-filtered via recallMemory v2 |
86
- | `suggest_decision` | Semantic search for relevant past decisions, scope-aware |
86
+ | `suggest_decision` | Semantic search with scopes, strictness controls, and diagnostics |
87
87
  | `list_decisions` | List recent decisions, scope-filterable |
88
88
  | `update_outcome` | Update decision outcome (case-insensitive: success/failed/partial) |
89
89
  | `search_narrative` | Narrative search with link expansion (depth 0-2) |
@@ -106,6 +106,25 @@ Decisions connect through relationships. Include patterns in your reasoning:
106
106
  | `debates` | `debates: decision_xxx` | Alternative view |
107
107
  | `synthesizes` | `synthesizes: [id1, id2]` | Merges multiple approaches |
108
108
 
109
+ ### Search Quality Controls
110
+
111
+ `suggest_decision` accepts optional search-quality parameters for agents and operators:
112
+
113
+ | Parameter | Use |
114
+ | ------------------- | ------------------------------------------------------------- |
115
+ | `strictness` | `'recall'`, `'balanced'`, or `'strict'` retrieval mode |
116
+ | `strict` | Shortcut for strict mode |
117
+ | `threshold` | Override the mode's minimum candidate threshold |
118
+ | `disableRecency` | Remove recency boosting when relevance matters more than time |
119
+ | `includeRelated` | Include or suppress graph-expanded related hits |
120
+ | `topicPrefix` | Limit search to a topic namespace |
121
+ | `minLexicalSupport` | Require independent relevance confirmation |
122
+ | `diagnostics` | Return why each result was included or rejected |
123
+ | `scopes` | Limit search to project/channel/user/global memory scopes |
124
+
125
+ Use `strictness: "balanced"` for normal agent work and `strictness: "strict"` when a result will
126
+ drive a code change, user-facing answer, or provenance claim.
127
+
109
128
  ## Usage Example
110
129
 
111
130
  Once configured, use MAMA through your MCP client:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jungjaehoon/mama-server",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "description": "MAMA MCP Server - Memory-Augmented MCP Assistant for Claude Code & Desktop",
5
5
  "main": "src/server.js",
6
6
  "bin": {
@@ -39,7 +39,7 @@
39
39
  "node": ">=22.13.0"
40
40
  },
41
41
  "dependencies": {
42
- "@jungjaehoon/mama-core": "^1.5.0",
42
+ "@jungjaehoon/mama-core": "^1.6.0",
43
43
  "@modelcontextprotocol/sdk": "^1.0.1"
44
44
  },
45
45
  "devDependencies": {
package/src/server.js CHANGED
@@ -327,6 +327,41 @@ class MAMAServer {
327
327
  description: "Filter by type. Default: 'all'",
328
328
  },
329
329
  limit: { type: 'number', description: 'Max results. Default: 10' },
330
+ threshold: {
331
+ type: 'number',
332
+ minimum: 0,
333
+ maximum: 1,
334
+ description: 'Minimum retrieval threshold. Omit for mode default.',
335
+ },
336
+ strict: {
337
+ type: 'boolean',
338
+ description: 'Shortcut for strict search mode.',
339
+ },
340
+ strictness: {
341
+ type: 'string',
342
+ enum: ['recall', 'balanced', 'strict'],
343
+ description: "Search quality mode. Default: 'recall'.",
344
+ },
345
+ disableRecency: {
346
+ type: 'boolean',
347
+ description: 'Disable recency weighting in search.',
348
+ },
349
+ includeRelated: {
350
+ type: 'boolean',
351
+ description: 'Include related graph-expanded results.',
352
+ },
353
+ topicPrefix: {
354
+ type: 'string',
355
+ description: 'Restrict search to topics with this prefix.',
356
+ },
357
+ minLexicalSupport: {
358
+ type: 'boolean',
359
+ description: 'Require lexical/entity/exact-topic confirmation.',
360
+ },
361
+ diagnostics: {
362
+ type: 'boolean',
363
+ description: 'Return retrieval diagnostics for search quality inspection.',
364
+ },
330
365
  scopes: {
331
366
  type: 'array',
332
367
  items: {
@@ -506,14 +541,39 @@ After failure → save a NEW decision with same topic to create evolution histor
506
541
  * Handle unified search (decisions + checkpoints)
507
542
  */
508
543
  async handleSearch(args) {
509
- const { query, type = 'all', limit = 10, scopes } = args;
510
-
511
- // type='checkpoint' without query → load latest checkpoint (resume session)
544
+ const {
545
+ query,
546
+ type = 'all',
547
+ limit = 10,
548
+ scopes,
549
+ threshold,
550
+ strict,
551
+ strictness,
552
+ disableRecency,
553
+ includeRelated,
554
+ topicPrefix,
555
+ minLexicalSupport,
556
+ diagnostics,
557
+ } = args;
558
+
559
+ // type='checkpoint' without query → load latest checkpoint (resume session).
560
+ // load_checkpoint does not yet honor scopes, so reject scoped checkpoint reads
561
+ // explicitly rather than silently bypass scope isolation.
512
562
  if (type === 'checkpoint' && !query) {
563
+ if (Array.isArray(scopes) && scopes.length > 0) {
564
+ return {
565
+ success: false,
566
+ code: 'scoped_checkpoint_unsupported',
567
+ count: 0,
568
+ results: [],
569
+ message: 'Scoped checkpoint reads are not supported yet',
570
+ };
571
+ }
513
572
  return await memoryTools.load_checkpoint.handler(args);
514
573
  }
515
574
 
516
575
  const results = [];
576
+ let searchDiagnostics;
517
577
 
518
578
  // Search decisions
519
579
  if (type === 'all' || type === 'decision') {
@@ -522,8 +582,52 @@ After failure → save a NEW decision with same topic to create evolution histor
522
582
  const suggestResult = await mama.suggest(query, {
523
583
  limit,
524
584
  ...(scopes && { scopes }),
585
+ ...(threshold !== undefined && { threshold }),
586
+ ...(strict !== undefined && { strict }),
587
+ ...(strictness !== undefined && { strictness }),
588
+ ...(disableRecency !== undefined && { disableRecency }),
589
+ ...(includeRelated !== undefined && { includeRelated }),
590
+ ...(topicPrefix !== undefined && { topicPrefix }),
591
+ ...(minLexicalSupport !== undefined && { minLexicalSupport }),
592
+ ...(diagnostics !== undefined && { diagnostics }),
525
593
  });
526
- decisions = suggestResult?.results || [];
594
+ // Preserve the failure signal — collapsing a null/invalid suggest
595
+ // response to [] would make callers unable to distinguish "no matches"
596
+ // from "search pipeline failed". Mirror the standalone handler's
597
+ // suggest_returned_null code so behavior stays consistent across
598
+ // transports.
599
+ if (!suggestResult || typeof suggestResult !== 'object') {
600
+ return {
601
+ success: false,
602
+ code: 'suggest_returned_null',
603
+ count: 0,
604
+ results: [],
605
+ message: 'Search failed: suggest() returned no result for query',
606
+ };
607
+ }
608
+ // Forward explicit { success: false, code, error } failures from
609
+ // mama.suggest() unchanged so callers see the real cause instead of
610
+ // a synthetic empty success.
611
+ if (suggestResult.success === false) {
612
+ const hasOwn = Object.prototype.hasOwnProperty;
613
+ const forwarded = {
614
+ ...suggestResult,
615
+ success: false,
616
+ code: suggestResult.code || 'suggest_failed',
617
+ };
618
+ if (!hasOwn.call(forwarded, 'count')) {
619
+ forwarded.count = 0;
620
+ }
621
+ if (!hasOwn.call(forwarded, 'results')) {
622
+ forwarded.results = [];
623
+ }
624
+ if (!hasOwn.call(forwarded, 'message')) {
625
+ forwarded.message = suggestResult.error || 'Search pipeline failed';
626
+ }
627
+ return forwarded;
628
+ }
629
+ searchDiagnostics = suggestResult.diagnostics;
630
+ decisions = Array.isArray(suggestResult.results) ? suggestResult.results : [];
527
631
  } else {
528
632
  decisions = await mama.list({ limit, ...(scopes && { scopes }) });
529
633
  }
@@ -537,8 +641,25 @@ After failure → save a NEW decision with same topic to create evolution histor
537
641
  }
538
642
  }
539
643
 
644
+ // mama.listCheckpoints() does not yet honor the scopes filter, so any
645
+ // checkpoint read with scopes provided would silently bypass scope
646
+ // isolation. Reject explicitly when the caller requested scopes — for
647
+ // type='checkpoint' this fails the whole search; for type='all' we let
648
+ // decisions (which DO honor scopes via mama.suggest/list) return alone
649
+ // and skip the checkpoint blocks below.
650
+ const checkpointReadsBlockedByScope = Array.isArray(scopes) && scopes.length > 0;
651
+ if (checkpointReadsBlockedByScope && type === 'checkpoint') {
652
+ return {
653
+ success: false,
654
+ code: 'scoped_checkpoint_unsupported',
655
+ count: 0,
656
+ results: [],
657
+ message: 'Scoped checkpoint reads are not supported yet',
658
+ };
659
+ }
660
+
540
661
  // Search checkpoints (with query = search, without = handled above as load)
541
- if ((type === 'all' || type === 'checkpoint') && query) {
662
+ if ((type === 'all' || type === 'checkpoint') && query && !checkpointReadsBlockedByScope) {
542
663
  const checkpoints = await mama.listCheckpoints(limit);
543
664
  results.push(
544
665
  ...checkpoints
@@ -554,7 +675,7 @@ After failure → save a NEW decision with same topic to create evolution histor
554
675
  }
555
676
 
556
677
  // type='all' without query — include recent checkpoints
557
- if (type === 'all' && !query) {
678
+ if (type === 'all' && !query && !checkpointReadsBlockedByScope) {
558
679
  const checkpoints = await mama.listCheckpoints(limit);
559
680
  results.push(
560
681
  ...checkpoints.map((c) => ({
@@ -577,8 +698,10 @@ After failure → save a NEW decision with same topic to create evolution histor
577
698
 
578
699
  return {
579
700
  success: true,
701
+ ...(query ? { query } : {}),
580
702
  count: limited.length,
581
703
  results: limited,
704
+ ...(searchDiagnostics !== undefined ? { diagnostics: searchDiagnostics } : {}),
582
705
  };
583
706
  }
584
707