@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 +39 -18
- package/package.json +3 -2
- package/src/server.js +138 -93
- package/src/tools/case-timeline-range.js +77 -0
- package/src/tools/index.js +7 -0
- package/src/tools/search-decisions-and-contracts.js +109 -0
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
|
|
79
|
-
|
|
80
|
-
The MCP server exposes
|
|
81
|
-
|
|
82
|
-
| Tool
|
|
83
|
-
|
|
|
84
|
-
| `save_decision`
|
|
85
|
-
| `recall_decision`
|
|
86
|
-
| `suggest_decision`
|
|
87
|
-
| `list_decisions`
|
|
88
|
-
| `update_outcome`
|
|
89
|
-
| `search_narrative`
|
|
90
|
-
| `ingest_conversation`
|
|
91
|
-
| `save_checkpoint`
|
|
92
|
-
| `load_checkpoint`
|
|
93
|
-
| `generate_quality_report`
|
|
94
|
-
| `get_restart_metrics`
|
|
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.
|
|
285
|
+
**Version:** 1.13.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jungjaehoon/mama-server",
|
|
3
|
-
"version": "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.
|
|
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:
|
|
375
|
-
description:
|
|
376
|
-
inputSchema:
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
+
};
|
package/src/tools/index.js
CHANGED
|
@@ -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 };
|