@jungjaehoon/mama-server 1.12.1 → 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.
package/README.md CHANGED
@@ -75,23 +75,25 @@ Any MCP-compatible client can use MAMA with:
75
75
  npx -y @jungjaehoon/mama-server
76
76
  ```
77
77
 
78
- ## Available Tools (v1.9)
79
-
80
- The MCP server exposes 11 tools:
81
-
82
- | Tool | Description |
83
- | ------------------------- | ----------------------------------------------------------------------- |
84
- | `save_decision` | Save decision with optional scopes and event_date for temporal tracking |
85
- | `recall_decision` | Recall decision history by topic, scope-filtered via recallMemory v2 |
86
- | `suggest_decision` | Semantic search for relevant past decisions, scope-aware |
87
- | `list_decisions` | List recent decisions, scope-filterable |
88
- | `update_outcome` | Update decision outcome (case-insensitive: success/failed/partial) |
89
- | `search_narrative` | Narrative search with link expansion (depth 0-2) |
90
- | `ingest_conversation` | Ingest conversation messages into memory with optional LLM extraction |
91
- | `save_checkpoint` | Save session checkpoint for later resumption |
92
- | `load_checkpoint` | Resume previous session |
93
- | `generate_quality_report` | Quality metrics and observability report |
94
- | `get_restart_metrics` | Restart success rate and latency monitoring |
78
+ ## Available Tools
79
+
80
+ The MCP server exposes 13 tools:
81
+
82
+ | Tool | Description |
83
+ | -------------------------------- | ----------------------------------------------------------------------- |
84
+ | `save_decision` | Save decision with optional scopes and event_date for temporal tracking |
85
+ | `recall_decision` | Recall decision history by topic, scope-filtered via recallMemory v2 |
86
+ | `suggest_decision` | Semantic search with scopes, strictness controls, and diagnostics |
87
+ | `list_decisions` | List recent decisions, scope-filterable |
88
+ | `update_outcome` | Update decision outcome (case-insensitive: success/failed/partial) |
89
+ | `search_narrative` | Narrative search with link expansion (depth 0-2) |
90
+ | `ingest_conversation` | Ingest conversation messages into memory with optional LLM extraction |
91
+ | `save_checkpoint` | Save session checkpoint for later resumption |
92
+ | `load_checkpoint` | Resume previous session |
93
+ | `generate_quality_report` | Quality metrics and observability report |
94
+ | `get_restart_metrics` | Restart success rate and latency monitoring |
95
+ | `search_decisions_and_contracts` | Decision + contract lookup for tooling and hook pipelines |
96
+ | `case_timeline_range` | Read bounded case timeline windows for case-first workflows |
95
97
 
96
98
  ### Edge Types
97
99
 
@@ -104,6 +106,25 @@ Decisions connect through relationships. Include patterns in your reasoning:
104
106
  | `debates` | `debates: decision_xxx` | Alternative view |
105
107
  | `synthesizes` | `synthesizes: [id1, id2]` | Merges multiple approaches |
106
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
+
107
128
  ## Usage Example
108
129
 
109
130
  Once configured, use MAMA through your MCP client:
@@ -261,4 +282,4 @@ MAMA was inspired by [mem0](https://github.com/mem0ai/mem0) (Apache 2.0). While
261
282
  ---
262
283
 
263
284
  **Author:** SpineLift Team
264
- **Version:** 1.9.0
285
+ **Version:** 1.13.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jungjaehoon/mama-server",
3
- "version": "1.12.1",
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,10 +39,11 @@
39
39
  "node": ">=22.13.0"
40
40
  },
41
41
  "dependencies": {
42
- "@jungjaehoon/mama-core": "^1.4.0",
42
+ "@jungjaehoon/mama-core": "^1.6.0",
43
43
  "@modelcontextprotocol/sdk": "^1.0.1"
44
44
  },
45
45
  "devDependencies": {
46
+ "better-sqlite3": "^12.8.0",
46
47
  "vitest": "^1.0.0"
47
48
  },
48
49
  "files": [
package/src/server.js CHANGED
@@ -26,7 +26,6 @@ const {
26
26
  CallToolRequestSchema,
27
27
  ListToolsRequestSchema,
28
28
  } = require('@modelcontextprotocol/sdk/types.js');
29
- const path = require('path');
30
29
 
31
30
  // Import all MAMA tools from src/tools/ — single source of truth for tool definitions
32
31
  const { createMemoryTools } = require('./tools/index.js');
@@ -35,8 +34,6 @@ const mama = require('@jungjaehoon/mama-core/mama-api');
35
34
 
36
35
  // Import core modules from mama-core
37
36
  const { initDB } = require('@jungjaehoon/mama-core/db-manager');
38
- const { generateEmbedding } = require('@jungjaehoon/mama-core/embeddings');
39
- const { vectorSearch } = require('@jungjaehoon/mama-core/memory-store');
40
37
  const embeddingServer = require('@jungjaehoon/mama-core/embedding-server');
41
38
  const http = require('http');
42
39
 
@@ -330,6 +327,41 @@ class MAMAServer {
330
327
  description: "Filter by type. Default: 'all'",
331
328
  },
332
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
+ },
333
365
  scopes: {
334
366
  type: 'array',
335
367
  items: {
@@ -369,24 +401,17 @@ After failure → save a NEW decision with same topic to create evolution histor
369
401
  required: ['id', 'outcome'],
370
402
  },
371
403
  },
372
- // 4. SEARCH_DECISIONS_AND_CONTRACTS — PreToolUse hook RPC
404
+ // 4. SEARCH_DECISIONS_AND_CONTRACTS — PreToolUse hook RPC (defined in src/tools/)
373
405
  {
374
- name: 'search_decisions_and_contracts',
375
- description: 'Search decisions and contracts for PreToolUse hook injection.',
376
- inputSchema: {
377
- type: 'object',
378
- properties: {
379
- query: { type: 'string', description: 'Search query for decisions.' },
380
- filePath: { type: 'string', description: 'File path context.' },
381
- toolName: { type: 'string', description: 'Tool name (Edit/Write/apply_patch).' },
382
- decisionLimit: { type: 'number', description: 'Max decisions (default: 5).' },
383
- contractLimit: { type: 'number', description: 'Max contracts (default: 3).' },
384
- similarityThreshold: {
385
- type: 'number',
386
- description: 'Similarity threshold (default: 0.7).',
387
- },
388
- },
389
- },
406
+ name: memoryTools.search_decisions_and_contracts.name,
407
+ description: memoryTools.search_decisions_and_contracts.description,
408
+ inputSchema: memoryTools.search_decisions_and_contracts.inputSchema,
409
+ },
410
+ // 5. CASE_TIMELINE_RANGE — Phase 3 case timeline RPC (defined in src/tools/)
411
+ {
412
+ name: memoryTools.case_timeline_range.name,
413
+ description: memoryTools.case_timeline_range.description,
414
+ inputSchema: memoryTools.case_timeline_range.inputSchema,
390
415
  },
391
416
  ];
392
417
 
@@ -516,14 +541,39 @@ After failure → save a NEW decision with same topic to create evolution histor
516
541
  * Handle unified search (decisions + checkpoints)
517
542
  */
518
543
  async handleSearch(args) {
519
- const { query, type = 'all', limit = 10, scopes } = args;
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;
520
558
 
521
- // type='checkpoint' without query → load latest checkpoint (resume session)
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.
522
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
+ }
523
572
  return await memoryTools.load_checkpoint.handler(args);
524
573
  }
525
574
 
526
575
  const results = [];
576
+ let searchDiagnostics;
527
577
 
528
578
  // Search decisions
529
579
  if (type === 'all' || type === 'decision') {
@@ -532,8 +582,52 @@ After failure → save a NEW decision with same topic to create evolution histor
532
582
  const suggestResult = await mama.suggest(query, {
533
583
  limit,
534
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 }),
535
593
  });
536
- 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 : [];
537
631
  } else {
538
632
  decisions = await mama.list({ limit, ...(scopes && { scopes }) });
539
633
  }
@@ -547,8 +641,25 @@ After failure → save a NEW decision with same topic to create evolution histor
547
641
  }
548
642
  }
549
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
+
550
661
  // Search checkpoints (with query = search, without = handled above as load)
551
- if ((type === 'all' || type === 'checkpoint') && query) {
662
+ if ((type === 'all' || type === 'checkpoint') && query && !checkpointReadsBlockedByScope) {
552
663
  const checkpoints = await mama.listCheckpoints(limit);
553
664
  results.push(
554
665
  ...checkpoints
@@ -564,7 +675,7 @@ After failure → save a NEW decision with same topic to create evolution histor
564
675
  }
565
676
 
566
677
  // type='all' without query — include recent checkpoints
567
- if (type === 'all' && !query) {
678
+ if (type === 'all' && !query && !checkpointReadsBlockedByScope) {
568
679
  const checkpoints = await mama.listCheckpoints(limit);
569
680
  results.push(
570
681
  ...checkpoints.map((c) => ({
@@ -587,76 +698,10 @@ After failure → save a NEW decision with same topic to create evolution histor
587
698
 
588
699
  return {
589
700
  success: true,
701
+ ...(query ? { query } : {}),
590
702
  count: limited.length,
591
703
  results: limited,
592
- };
593
- }
594
-
595
- /**
596
- * Handle PreToolUse search for decisions + contracts
597
- */
598
- async handleSearchDecisionsAndContracts(args = {}) {
599
- const {
600
- query = '',
601
- filePath = '',
602
- toolName = '',
603
- decisionLimit = 5,
604
- contractLimit = 3,
605
- similarityThreshold = 0.7,
606
- } = args;
607
-
608
- await initDB();
609
-
610
- let decisionResults = [];
611
- let contractResults = [];
612
-
613
- // Decision search
614
- if (decisionLimit > 0 && query) {
615
- try {
616
- const queryEmbedding = await generateEmbedding(query);
617
- const results = await vectorSearch(queryEmbedding, decisionLimit, similarityThreshold);
618
- if (Array.isArray(results)) {
619
- decisionResults = results.slice(0, decisionLimit);
620
- }
621
- } catch (err) {
622
- console.error('[MAMA MCP] Decision search failed:', err.message);
623
- }
624
- }
625
-
626
- // Contract search (file-specific)
627
- const contractTools = ['Edit', 'Write', 'apply_patch'];
628
- const codeExtensions = ['.js', '.ts', '.jsx', '.tsx', '.py', '.go', '.rs', '.java'];
629
- const ext = filePath ? path.extname(filePath) : '';
630
-
631
- if (
632
- contractLimit > 0 &&
633
- filePath &&
634
- contractTools.includes(toolName) &&
635
- codeExtensions.includes(ext)
636
- ) {
637
- const basename = path.basename(filePath, ext);
638
- const keywords = basename.split(/[-_]/).filter(Boolean);
639
- const contractQuery = `contract api ${keywords.join(' ')}`.trim();
640
-
641
- if (contractQuery) {
642
- try {
643
- const contractEmbedding = await generateEmbedding(contractQuery);
644
- const contractMatches = await vectorSearch(contractEmbedding, 10, similarityThreshold);
645
- if (Array.isArray(contractMatches)) {
646
- contractResults = contractMatches
647
- .filter((r) => r.topic && r.topic.startsWith('contract_'))
648
- .slice(0, contractLimit);
649
- }
650
- } catch (err) {
651
- console.error('[MAMA MCP] Contract search failed:', err.message);
652
- }
653
- }
654
- }
655
-
656
- return {
657
- success: true,
658
- decisionResults,
659
- contractResults,
704
+ ...(searchDiagnostics !== undefined ? { diagnostics: searchDiagnostics } : {}),
660
705
  };
661
706
  }
662
707
 
@@ -0,0 +1,77 @@
1
+ /**
2
+ * MCP Tool: case_timeline_range
3
+ *
4
+ * Thin MCP wrapper around mama-core caseTimelineRange().
5
+ */
6
+
7
+ const { caseTimelineRange } = require('@jungjaehoon/mama-core');
8
+ const { initDB, getAdapter } = require('@jungjaehoon/mama-core/db-manager');
9
+
10
+ let adapterOverrideForTest = null;
11
+
12
+ const caseTimelineRangeTool = {
13
+ name: 'case_timeline_range',
14
+ description:
15
+ 'Return a bounded, chronological timeline for a case. Includes decision, event, observation, and artifact memberships resolved through canonical case chains.',
16
+ inputSchema: {
17
+ type: 'object',
18
+ properties: {
19
+ case_id: {
20
+ type: 'string',
21
+ description: 'Case UUID to read. Merged cases resolve through their canonical case chain.',
22
+ },
23
+ from: {
24
+ oneOf: [{ type: 'string' }, { type: 'number' }],
25
+ description: 'Optional inclusive lower date bound. ISO 8601 string or epoch milliseconds.',
26
+ },
27
+ to: {
28
+ oneOf: [{ type: 'string' }, { type: 'number' }],
29
+ description: 'Optional inclusive upper date bound. ISO 8601 string or epoch milliseconds.',
30
+ },
31
+ order: {
32
+ type: 'string',
33
+ enum: ['asc', 'desc'],
34
+ description: "Timeline order. Default: 'asc'.",
35
+ },
36
+ limit: {
37
+ type: 'number',
38
+ minimum: 0,
39
+ maximum: 500,
40
+ description: 'Maximum items to return. Default: 100. Maximum: 500.',
41
+ },
42
+ include_connector_enrichments: {
43
+ type: 'boolean',
44
+ description: 'Include connector event snapshots for observations/artifacts when available.',
45
+ },
46
+ },
47
+ required: ['case_id'],
48
+ },
49
+
50
+ async handler(args) {
51
+ const adapter = await getCaseTimelineRangeAdapter();
52
+ return caseTimelineRange(adapter, args || {});
53
+ },
54
+ };
55
+
56
+ async function getCaseTimelineRangeAdapter() {
57
+ if (adapterOverrideForTest) {
58
+ return adapterOverrideForTest;
59
+ }
60
+
61
+ await initDB();
62
+ return getAdapter();
63
+ }
64
+
65
+ function setCaseTimelineRangeAdapterForTest(adapter) {
66
+ adapterOverrideForTest = adapter;
67
+ }
68
+
69
+ function resetCaseTimelineRangeAdapterForTest() {
70
+ adapterOverrideForTest = null;
71
+ }
72
+
73
+ module.exports = {
74
+ caseTimelineRangeTool,
75
+ setCaseTimelineRangeAdapterForTest,
76
+ resetCaseTimelineRangeAdapterForTest,
77
+ };
@@ -16,6 +16,7 @@
16
16
  * - generate_quality_report: Generate coverage and quality metrics report ✅
17
17
  * - get_restart_metrics: Get restart success rate and latency metrics ✅
18
18
  * - ingest_conversation: Ingest conversation messages into memory ✅
19
+ * - case_timeline_range: Read bounded case timeline ranges ✅
19
20
  *
20
21
  * @module tools
21
22
  */
@@ -29,6 +30,8 @@ const { saveCheckpointTool, loadCheckpointTool } = require('./checkpoint-tools.j
29
30
  const { searchNarrativeTool } = require('./search-narrative.js');
30
31
  const { generateQualityReportTool, getRestartMetricsTool } = require('./quality-metrics-tools.js');
31
32
  const { ingestConversationTool } = require('./ingest-conversation.js');
33
+ const { searchDecisionsAndContractsTool } = require('./search-decisions-and-contracts.js');
34
+ const { caseTimelineRangeTool } = require('./case-timeline-range.js');
32
35
 
33
36
  /**
34
37
  * Create all MAMA memory tools
@@ -50,6 +53,8 @@ function createMemoryTools() {
50
53
  generate_quality_report: generateQualityReportTool,
51
54
  get_restart_metrics: getRestartMetricsTool,
52
55
  ingest_conversation: ingestConversationTool,
56
+ search_decisions_and_contracts: searchDecisionsAndContractsTool,
57
+ case_timeline_range: caseTimelineRangeTool,
53
58
  };
54
59
  }
55
60
 
@@ -67,4 +72,6 @@ module.exports = {
67
72
  generateQualityReportTool,
68
73
  getRestartMetricsTool,
69
74
  ingestConversationTool,
75
+ searchDecisionsAndContractsTool,
76
+ caseTimelineRangeTool,
70
77
  };
@@ -0,0 +1,109 @@
1
+ /**
2
+ * MCP Tool: search_decisions_and_contracts
3
+ *
4
+ * PreToolUse hook RPC — searches decisions and contract-specific memories
5
+ * for file-aware context injection before Edit/Write/apply_patch tool calls.
6
+ *
7
+ * @module search-decisions-and-contracts
8
+ */
9
+
10
+ const path = require('path');
11
+ const { initDB } = require('@jungjaehoon/mama-core/db-manager');
12
+ const { generateEmbedding } = require('@jungjaehoon/mama-core/embeddings');
13
+ const { vectorSearch } = require('@jungjaehoon/mama-core/memory-store');
14
+
15
+ /**
16
+ * search_decisions_and_contracts tool definition
17
+ */
18
+ const searchDecisionsAndContractsTool = {
19
+ name: 'search_decisions_and_contracts',
20
+ description: 'Search decisions and contracts for PreToolUse hook injection.',
21
+ inputSchema: {
22
+ type: 'object',
23
+ properties: {
24
+ query: { type: 'string', description: 'Search query for decisions.' },
25
+ filePath: { type: 'string', description: 'File path context.' },
26
+ toolName: { type: 'string', description: 'Tool name (Edit/Write/apply_patch).' },
27
+ decisionLimit: { type: 'number', description: 'Max decisions (default: 5).' },
28
+ contractLimit: { type: 'number', description: 'Max contracts (default: 3).' },
29
+ similarityThreshold: {
30
+ type: 'number',
31
+ description: 'Similarity threshold (default: 0.7).',
32
+ },
33
+ },
34
+ },
35
+
36
+ async handler(args = {}) {
37
+ try {
38
+ const {
39
+ query = '',
40
+ filePath = '',
41
+ toolName = '',
42
+ decisionLimit = 5,
43
+ contractLimit = 3,
44
+ similarityThreshold = 0.7,
45
+ } = args;
46
+
47
+ await initDB();
48
+
49
+ let decisionResults = [];
50
+ let contractResults = [];
51
+
52
+ // Decision search
53
+ if (decisionLimit > 0 && query) {
54
+ try {
55
+ const queryEmbedding = await generateEmbedding(query);
56
+ const results = await vectorSearch(queryEmbedding, decisionLimit, similarityThreshold);
57
+ if (Array.isArray(results)) {
58
+ decisionResults = results.slice(0, decisionLimit);
59
+ }
60
+ } catch (err) {
61
+ console.error('[MAMA MCP] Decision search failed:', err.message);
62
+ }
63
+ }
64
+
65
+ // Contract search (file-specific)
66
+ const contractTools = ['Edit', 'Write', 'apply_patch'];
67
+ const codeExtensions = ['.js', '.ts', '.jsx', '.tsx', '.py', '.go', '.rs', '.java'];
68
+ const ext = filePath ? path.extname(filePath) : '';
69
+
70
+ if (
71
+ contractLimit > 0 &&
72
+ filePath &&
73
+ contractTools.includes(toolName) &&
74
+ codeExtensions.includes(ext)
75
+ ) {
76
+ const basename = path.basename(filePath, ext);
77
+ const keywords = basename.split(/[-_]/).filter(Boolean);
78
+ const contractQuery = `contract api ${keywords.join(' ')}`.trim();
79
+
80
+ if (contractQuery) {
81
+ try {
82
+ const contractEmbedding = await generateEmbedding(contractQuery);
83
+ const contractMatches = await vectorSearch(contractEmbedding, 10, similarityThreshold);
84
+ if (Array.isArray(contractMatches)) {
85
+ contractResults = contractMatches
86
+ .filter((r) => r.topic && r.topic.startsWith('contract_'))
87
+ .slice(0, contractLimit);
88
+ }
89
+ } catch (err) {
90
+ console.error('[MAMA MCP] Contract search failed:', err.message);
91
+ }
92
+ }
93
+ }
94
+
95
+ return {
96
+ success: true,
97
+ decisionResults,
98
+ contractResults,
99
+ };
100
+ } catch (err) {
101
+ return {
102
+ success: false,
103
+ error: err instanceof Error ? err.message : String(err),
104
+ };
105
+ }
106
+ },
107
+ };
108
+
109
+ module.exports = { searchDecisionsAndContractsTool };