@levalicious/server-memory 0.0.6 → 0.0.8
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/server.js +302 -81
- package/dist/tests/memory-server.test.js +144 -43
- package/dist/tests/test-utils.js +16 -0
- package/package.json +4 -2
package/dist/server.js
CHANGED
|
@@ -4,6 +4,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextpro
|
|
|
4
4
|
import { promises as fs } from 'fs';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
|
+
import lockfile from 'proper-lockfile';
|
|
7
8
|
// Define memory file path using environment variable with fallback
|
|
8
9
|
const defaultMemoryPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'memory.json');
|
|
9
10
|
// If MEMORY_FILE_PATH is just a filename, put it in the same directory as the script
|
|
@@ -12,6 +13,83 @@ const DEFAULT_MEMORY_FILE_PATH = process.env.MEMORY_FILE_PATH
|
|
|
12
13
|
? process.env.MEMORY_FILE_PATH
|
|
13
14
|
: path.join(path.dirname(fileURLToPath(import.meta.url)), process.env.MEMORY_FILE_PATH)
|
|
14
15
|
: defaultMemoryPath;
|
|
16
|
+
export const MAX_CHARS = 2048;
|
|
17
|
+
function paginateItems(items, cursor = 0, maxChars = MAX_CHARS) {
|
|
18
|
+
const result = [];
|
|
19
|
+
let i = cursor;
|
|
20
|
+
// Calculate overhead for wrapper: {"items":[],"nextCursor":null,"totalCount":123}
|
|
21
|
+
const wrapperTemplate = { items: [], nextCursor: null, totalCount: items.length };
|
|
22
|
+
let overhead = JSON.stringify(wrapperTemplate).length;
|
|
23
|
+
let charCount = overhead;
|
|
24
|
+
while (i < items.length) {
|
|
25
|
+
const itemJson = JSON.stringify(items[i]);
|
|
26
|
+
const addedChars = itemJson.length + (result.length > 0 ? 1 : 0); // +1 for comma
|
|
27
|
+
if (charCount + addedChars > maxChars) {
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
result.push(items[i]);
|
|
31
|
+
charCount += addedChars;
|
|
32
|
+
i++;
|
|
33
|
+
}
|
|
34
|
+
// Update nextCursor - recalculate if we stopped early (cursor digits may differ from null)
|
|
35
|
+
const nextCursor = i < items.length ? i : null;
|
|
36
|
+
return {
|
|
37
|
+
items: result,
|
|
38
|
+
nextCursor,
|
|
39
|
+
totalCount: items.length
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function paginateGraph(graph, entityCursor = 0, relationCursor = 0) {
|
|
43
|
+
// Build incrementally, measuring actual serialized size
|
|
44
|
+
const entityCount = graph.entities.length;
|
|
45
|
+
const relationCount = graph.relations.length;
|
|
46
|
+
// Start with empty result to measure base overhead
|
|
47
|
+
const emptyResult = {
|
|
48
|
+
entities: { items: [], nextCursor: null, totalCount: entityCount },
|
|
49
|
+
relations: { items: [], nextCursor: null, totalCount: relationCount }
|
|
50
|
+
};
|
|
51
|
+
let currentSize = JSON.stringify(emptyResult).length;
|
|
52
|
+
const resultEntities = [];
|
|
53
|
+
const resultRelations = [];
|
|
54
|
+
let entityIdx = entityCursor;
|
|
55
|
+
let relationIdx = relationCursor;
|
|
56
|
+
// Add entities until we hit the limit
|
|
57
|
+
while (entityIdx < graph.entities.length) {
|
|
58
|
+
const entity = graph.entities[entityIdx];
|
|
59
|
+
const entityJson = JSON.stringify(entity);
|
|
60
|
+
const addedChars = entityJson.length + (resultEntities.length > 0 ? 1 : 0);
|
|
61
|
+
if (currentSize + addedChars > MAX_CHARS) {
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
resultEntities.push(entity);
|
|
65
|
+
currentSize += addedChars;
|
|
66
|
+
entityIdx++;
|
|
67
|
+
}
|
|
68
|
+
// Add relations with remaining space
|
|
69
|
+
while (relationIdx < graph.relations.length) {
|
|
70
|
+
const relation = graph.relations[relationIdx];
|
|
71
|
+
const relationJson = JSON.stringify(relation);
|
|
72
|
+
const addedChars = relationJson.length + (resultRelations.length > 0 ? 1 : 0);
|
|
73
|
+
if (currentSize + addedChars > MAX_CHARS) {
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
resultRelations.push(relation);
|
|
77
|
+
currentSize += addedChars;
|
|
78
|
+
relationIdx++;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
entities: {
|
|
82
|
+
items: resultEntities,
|
|
83
|
+
nextCursor: entityIdx < graph.entities.length ? entityIdx : null,
|
|
84
|
+
totalCount: entityCount
|
|
85
|
+
},
|
|
86
|
+
relations: {
|
|
87
|
+
items: resultRelations,
|
|
88
|
+
nextCursor: relationIdx < graph.relations.length ? relationIdx : null,
|
|
89
|
+
totalCount: relationCount
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
15
93
|
// The KnowledgeGraphManager class contains all operations to interact with the knowledge graph
|
|
16
94
|
export class KnowledgeGraphManager {
|
|
17
95
|
bclCtr = 0;
|
|
@@ -20,6 +98,22 @@ export class KnowledgeGraphManager {
|
|
|
20
98
|
constructor(memoryFilePath = DEFAULT_MEMORY_FILE_PATH) {
|
|
21
99
|
this.memoryFilePath = memoryFilePath;
|
|
22
100
|
}
|
|
101
|
+
async withLock(fn) {
|
|
102
|
+
// Ensure file exists for locking
|
|
103
|
+
try {
|
|
104
|
+
await fs.access(this.memoryFilePath);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
await fs.writeFile(this.memoryFilePath, "");
|
|
108
|
+
}
|
|
109
|
+
const release = await lockfile.lock(this.memoryFilePath, { retries: { retries: 5, minTimeout: 100 } });
|
|
110
|
+
try {
|
|
111
|
+
return await fn();
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
await release();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
23
117
|
async loadGraph() {
|
|
24
118
|
try {
|
|
25
119
|
const data = await fs.readFile(this.memoryFilePath, "utf-8");
|
|
@@ -45,81 +139,118 @@ export class KnowledgeGraphManager {
|
|
|
45
139
|
...graph.entities.map(e => JSON.stringify({ type: "entity", ...e })),
|
|
46
140
|
...graph.relations.map(r => JSON.stringify({ type: "relation", ...r })),
|
|
47
141
|
];
|
|
48
|
-
|
|
142
|
+
const content = lines.join("\n") + (lines.length > 0 ? "\n" : "");
|
|
143
|
+
await fs.writeFile(this.memoryFilePath, content);
|
|
49
144
|
}
|
|
50
145
|
async createEntities(entities) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
146
|
+
return this.withLock(async () => {
|
|
147
|
+
const graph = await this.loadGraph();
|
|
148
|
+
// Validate observation limits
|
|
149
|
+
for (const entity of entities) {
|
|
150
|
+
if (entity.observations.length > 2) {
|
|
151
|
+
throw new Error(`Entity "${entity.name}" has ${entity.observations.length} observations. Maximum allowed is 2.`);
|
|
152
|
+
}
|
|
153
|
+
for (const obs of entity.observations) {
|
|
154
|
+
if (obs.length > 140) {
|
|
155
|
+
throw new Error(`Observation in entity "${entity.name}" exceeds 140 characters (${obs.length} chars): "${obs.substring(0, 50)}..."`);
|
|
156
|
+
}
|
|
60
157
|
}
|
|
61
158
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
159
|
+
const now = Date.now();
|
|
160
|
+
const newEntities = entities
|
|
161
|
+
.filter(e => !graph.entities.some(existingEntity => existingEntity.name === e.name))
|
|
162
|
+
.map(e => ({ ...e, mtime: now, obsMtime: e.observations.length > 0 ? now : undefined }));
|
|
163
|
+
graph.entities.push(...newEntities);
|
|
164
|
+
await this.saveGraph(graph);
|
|
165
|
+
return newEntities;
|
|
166
|
+
});
|
|
67
167
|
}
|
|
68
168
|
async createRelations(relations) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
169
|
+
return this.withLock(async () => {
|
|
170
|
+
const graph = await this.loadGraph();
|
|
171
|
+
const now = Date.now();
|
|
172
|
+
// Update mtime on 'from' entities when relations are added
|
|
173
|
+
const fromEntityNames = new Set(relations.map(r => r.from));
|
|
174
|
+
graph.entities.forEach(e => {
|
|
175
|
+
if (fromEntityNames.has(e.name)) {
|
|
176
|
+
e.mtime = now;
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
const newRelations = relations
|
|
180
|
+
.filter(r => !graph.relations.some(existingRelation => existingRelation.from === r.from &&
|
|
181
|
+
existingRelation.to === r.to &&
|
|
182
|
+
existingRelation.relationType === r.relationType))
|
|
183
|
+
.map(r => ({ ...r, mtime: now }));
|
|
184
|
+
graph.relations.push(...newRelations);
|
|
185
|
+
await this.saveGraph(graph);
|
|
186
|
+
return newRelations;
|
|
187
|
+
});
|
|
76
188
|
}
|
|
77
189
|
async addObservations(observations) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
// Validate observation character limits
|
|
85
|
-
for (const obs of o.contents) {
|
|
86
|
-
if (obs.length > 140) {
|
|
87
|
-
throw new Error(`Observation for "${o.entityName}" exceeds 140 characters (${obs.length} chars): "${obs.substring(0, 50)}..."`);
|
|
190
|
+
return this.withLock(async () => {
|
|
191
|
+
const graph = await this.loadGraph();
|
|
192
|
+
const results = observations.map(o => {
|
|
193
|
+
const entity = graph.entities.find(e => e.name === o.entityName);
|
|
194
|
+
if (!entity) {
|
|
195
|
+
throw new Error(`Entity with name ${o.entityName} not found`);
|
|
88
196
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
197
|
+
// Validate observation character limits
|
|
198
|
+
for (const obs of o.contents) {
|
|
199
|
+
if (obs.length > 140) {
|
|
200
|
+
throw new Error(`Observation for "${o.entityName}" exceeds 140 characters (${obs.length} chars): "${obs.substring(0, 50)}..."`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const newObservations = o.contents.filter(content => !entity.observations.includes(content));
|
|
204
|
+
// Validate total observation count
|
|
205
|
+
if (entity.observations.length + newObservations.length > 2) {
|
|
206
|
+
throw new Error(`Adding ${newObservations.length} observations to "${o.entityName}" would exceed limit of 2 (currently has ${entity.observations.length}).`);
|
|
207
|
+
}
|
|
208
|
+
entity.observations.push(...newObservations);
|
|
209
|
+
if (newObservations.length > 0) {
|
|
210
|
+
const now = Date.now();
|
|
211
|
+
entity.mtime = now;
|
|
212
|
+
entity.obsMtime = now;
|
|
213
|
+
}
|
|
214
|
+
return { entityName: o.entityName, addedObservations: newObservations };
|
|
215
|
+
});
|
|
216
|
+
await this.saveGraph(graph);
|
|
217
|
+
return results;
|
|
97
218
|
});
|
|
98
|
-
await this.saveGraph(graph);
|
|
99
|
-
return results;
|
|
100
219
|
}
|
|
101
220
|
async deleteEntities(entityNames) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
221
|
+
return this.withLock(async () => {
|
|
222
|
+
const graph = await this.loadGraph();
|
|
223
|
+
graph.entities = graph.entities.filter(e => !entityNames.includes(e.name));
|
|
224
|
+
graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to));
|
|
225
|
+
await this.saveGraph(graph);
|
|
226
|
+
});
|
|
106
227
|
}
|
|
107
228
|
async deleteObservations(deletions) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
entity
|
|
113
|
-
|
|
229
|
+
return this.withLock(async () => {
|
|
230
|
+
const graph = await this.loadGraph();
|
|
231
|
+
const now = Date.now();
|
|
232
|
+
deletions.forEach(d => {
|
|
233
|
+
const entity = graph.entities.find(e => e.name === d.entityName);
|
|
234
|
+
if (entity) {
|
|
235
|
+
const originalLen = entity.observations.length;
|
|
236
|
+
entity.observations = entity.observations.filter(o => !d.observations.includes(o));
|
|
237
|
+
if (entity.observations.length !== originalLen) {
|
|
238
|
+
entity.mtime = now;
|
|
239
|
+
entity.obsMtime = now;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
await this.saveGraph(graph);
|
|
114
244
|
});
|
|
115
|
-
await this.saveGraph(graph);
|
|
116
245
|
}
|
|
117
246
|
async deleteRelations(relations) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
r.
|
|
121
|
-
|
|
122
|
-
|
|
247
|
+
return this.withLock(async () => {
|
|
248
|
+
const graph = await this.loadGraph();
|
|
249
|
+
graph.relations = graph.relations.filter(r => !relations.some(delRelation => r.from === delRelation.from &&
|
|
250
|
+
r.to === delRelation.to &&
|
|
251
|
+
r.relationType === delRelation.relationType));
|
|
252
|
+
await this.saveGraph(graph);
|
|
253
|
+
});
|
|
123
254
|
}
|
|
124
255
|
// Regex-based search function
|
|
125
256
|
async searchNodes(query) {
|
|
@@ -453,6 +584,44 @@ export class KnowledgeGraphManager {
|
|
|
453
584
|
this.bclCtr = 0;
|
|
454
585
|
this.bclTerm = "";
|
|
455
586
|
}
|
|
587
|
+
async addThought(observations, previousCtxId) {
|
|
588
|
+
return this.withLock(async () => {
|
|
589
|
+
const graph = await this.loadGraph();
|
|
590
|
+
// Validate observations
|
|
591
|
+
if (observations.length > 2) {
|
|
592
|
+
throw new Error(`Thought has ${observations.length} observations. Maximum allowed is 2.`);
|
|
593
|
+
}
|
|
594
|
+
for (const obs of observations) {
|
|
595
|
+
if (obs.length > 140) {
|
|
596
|
+
throw new Error(`Observation exceeds 140 characters (${obs.length} chars): "${obs.substring(0, 50)}..."`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
// Generate new context ID
|
|
600
|
+
const now = Date.now();
|
|
601
|
+
const ctxId = `thought_${now}_${Math.random().toString(36).substring(2, 8)}`;
|
|
602
|
+
// Create thought entity
|
|
603
|
+
const thoughtEntity = {
|
|
604
|
+
name: ctxId,
|
|
605
|
+
entityType: "Thought",
|
|
606
|
+
observations,
|
|
607
|
+
mtime: now,
|
|
608
|
+
obsMtime: observations.length > 0 ? now : undefined,
|
|
609
|
+
};
|
|
610
|
+
graph.entities.push(thoughtEntity);
|
|
611
|
+
// Link to previous thought if it exists
|
|
612
|
+
if (previousCtxId) {
|
|
613
|
+
const prevEntity = graph.entities.find(e => e.name === previousCtxId);
|
|
614
|
+
if (prevEntity) {
|
|
615
|
+
// Update mtime on previous entity since we're adding a relation from it
|
|
616
|
+
prevEntity.mtime = now;
|
|
617
|
+
// Bidirectional chain: previous -> new (follows) and new -> previous (preceded_by)
|
|
618
|
+
graph.relations.push({ from: previousCtxId, to: ctxId, relationType: "follows", mtime: now }, { from: ctxId, to: previousCtxId, relationType: "preceded_by", mtime: now });
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
await this.saveGraph(graph);
|
|
622
|
+
return { ctxId };
|
|
623
|
+
});
|
|
624
|
+
}
|
|
456
625
|
}
|
|
457
626
|
/**
|
|
458
627
|
* Creates a configured MCP server instance with all tools registered.
|
|
@@ -616,18 +785,20 @@ export function createServer(memoryFilePath) {
|
|
|
616
785
|
},
|
|
617
786
|
{
|
|
618
787
|
name: "search_nodes",
|
|
619
|
-
description: "Search for nodes in the knowledge graph using a regex pattern",
|
|
788
|
+
description: "Search for nodes in the knowledge graph using a regex pattern. Results are paginated (max 512 chars).",
|
|
620
789
|
inputSchema: {
|
|
621
790
|
type: "object",
|
|
622
791
|
properties: {
|
|
623
|
-
query: { type: "string", description: "Regex pattern to match against entity names, types, and observations.
|
|
792
|
+
query: { type: "string", description: "Regex pattern to match against entity names, types, and observations." },
|
|
793
|
+
entityCursor: { type: "number", description: "Cursor for entity pagination (from previous response's nextCursor)" },
|
|
794
|
+
relationCursor: { type: "number", description: "Cursor for relation pagination" },
|
|
624
795
|
},
|
|
625
796
|
required: ["query"],
|
|
626
797
|
},
|
|
627
798
|
},
|
|
628
799
|
{
|
|
629
800
|
name: "open_nodes_filtered",
|
|
630
|
-
description: "Open specific nodes in the knowledge graph by their names, filtering relations to only those between the opened nodes",
|
|
801
|
+
description: "Open specific nodes in the knowledge graph by their names, filtering relations to only those between the opened nodes. Results are paginated (max 512 chars).",
|
|
631
802
|
inputSchema: {
|
|
632
803
|
type: "object",
|
|
633
804
|
properties: {
|
|
@@ -636,13 +807,15 @@ export function createServer(memoryFilePath) {
|
|
|
636
807
|
items: { type: "string" },
|
|
637
808
|
description: "An array of entity names to retrieve",
|
|
638
809
|
},
|
|
810
|
+
entityCursor: { type: "number", description: "Cursor for entity pagination" },
|
|
811
|
+
relationCursor: { type: "number", description: "Cursor for relation pagination" },
|
|
639
812
|
},
|
|
640
813
|
required: ["names"],
|
|
641
814
|
},
|
|
642
815
|
},
|
|
643
816
|
{
|
|
644
817
|
name: "open_nodes",
|
|
645
|
-
description: "Open specific nodes in the knowledge graph by their names",
|
|
818
|
+
description: "Open specific nodes in the knowledge graph by their names. Results are paginated (max 512 chars).",
|
|
646
819
|
inputSchema: {
|
|
647
820
|
type: "object",
|
|
648
821
|
properties: {
|
|
@@ -651,43 +824,49 @@ export function createServer(memoryFilePath) {
|
|
|
651
824
|
items: { type: "string" },
|
|
652
825
|
description: "An array of entity names to retrieve",
|
|
653
826
|
},
|
|
827
|
+
entityCursor: { type: "number", description: "Cursor for entity pagination" },
|
|
828
|
+
relationCursor: { type: "number", description: "Cursor for relation pagination" },
|
|
654
829
|
},
|
|
655
830
|
required: ["names"],
|
|
656
831
|
},
|
|
657
832
|
},
|
|
658
833
|
{
|
|
659
834
|
name: "get_neighbors",
|
|
660
|
-
description: "Get neighboring entities connected to a specific entity within a given depth",
|
|
835
|
+
description: "Get neighboring entities connected to a specific entity within a given depth. Results are paginated (max 512 chars).",
|
|
661
836
|
inputSchema: {
|
|
662
837
|
type: "object",
|
|
663
838
|
properties: {
|
|
664
839
|
entityName: { type: "string", description: "The name of the entity to find neighbors for" },
|
|
665
840
|
depth: { type: "number", description: "Maximum depth to traverse (default: 0)", default: 0 },
|
|
666
841
|
withEntities: { type: "boolean", description: "If true, include full entity data. Default returns only relations for lightweight structure exploration.", default: false },
|
|
842
|
+
entityCursor: { type: "number", description: "Cursor for entity pagination" },
|
|
843
|
+
relationCursor: { type: "number", description: "Cursor for relation pagination" },
|
|
667
844
|
},
|
|
668
845
|
required: ["entityName"],
|
|
669
846
|
},
|
|
670
847
|
},
|
|
671
848
|
{
|
|
672
849
|
name: "find_path",
|
|
673
|
-
description: "Find a path between two entities in the knowledge graph",
|
|
850
|
+
description: "Find a path between two entities in the knowledge graph. Results are paginated (max 512 chars).",
|
|
674
851
|
inputSchema: {
|
|
675
852
|
type: "object",
|
|
676
853
|
properties: {
|
|
677
854
|
fromEntity: { type: "string", description: "The name of the starting entity" },
|
|
678
855
|
toEntity: { type: "string", description: "The name of the target entity" },
|
|
679
856
|
maxDepth: { type: "number", description: "Maximum depth to search (default: 5)", default: 5 },
|
|
857
|
+
cursor: { type: "number", description: "Cursor for pagination" },
|
|
680
858
|
},
|
|
681
859
|
required: ["fromEntity", "toEntity"],
|
|
682
860
|
},
|
|
683
861
|
},
|
|
684
862
|
{
|
|
685
863
|
name: "get_entities_by_type",
|
|
686
|
-
description: "Get all entities of a specific type",
|
|
864
|
+
description: "Get all entities of a specific type. Results are paginated (max 512 chars).",
|
|
687
865
|
inputSchema: {
|
|
688
866
|
type: "object",
|
|
689
867
|
properties: {
|
|
690
868
|
entityType: { type: "string", description: "The type of entities to retrieve" },
|
|
869
|
+
cursor: { type: "number", description: "Cursor for pagination" },
|
|
691
870
|
},
|
|
692
871
|
required: ["entityType"],
|
|
693
872
|
},
|
|
@@ -718,10 +897,12 @@ export function createServer(memoryFilePath) {
|
|
|
718
897
|
},
|
|
719
898
|
{
|
|
720
899
|
name: "get_orphaned_entities",
|
|
721
|
-
description: "Get entities that have no relations (orphaned entities)",
|
|
900
|
+
description: "Get entities that have no relations (orphaned entities). Results are paginated (max 512 chars).",
|
|
722
901
|
inputSchema: {
|
|
723
902
|
type: "object",
|
|
724
|
-
properties: {
|
|
903
|
+
properties: {
|
|
904
|
+
cursor: { type: "number", description: "Cursor for pagination" },
|
|
905
|
+
},
|
|
725
906
|
},
|
|
726
907
|
},
|
|
727
908
|
{
|
|
@@ -766,6 +947,28 @@ export function createServer(memoryFilePath) {
|
|
|
766
947
|
properties: {},
|
|
767
948
|
},
|
|
768
949
|
},
|
|
950
|
+
{
|
|
951
|
+
name: "sequentialthinking",
|
|
952
|
+
description: `Record a thought in the knowledge graph. Creates a Thought entity with observations and links it to the previous thought if provided. Returns the new thought's context ID for chaining.
|
|
953
|
+
|
|
954
|
+
Use this to build chains of reasoning that persist in the graph. Each thought can have up to 2 observations (max 140 chars each).`,
|
|
955
|
+
inputSchema: {
|
|
956
|
+
type: "object",
|
|
957
|
+
properties: {
|
|
958
|
+
previousCtxId: {
|
|
959
|
+
type: "string",
|
|
960
|
+
description: "Context ID of the previous thought to chain from. Omit for first thought in a chain."
|
|
961
|
+
},
|
|
962
|
+
observations: {
|
|
963
|
+
type: "array",
|
|
964
|
+
items: { type: "string", maxLength: 140 },
|
|
965
|
+
maxItems: 2,
|
|
966
|
+
description: "Observations for this thought (max 2, each max 140 chars)"
|
|
967
|
+
},
|
|
968
|
+
},
|
|
969
|
+
required: ["observations"],
|
|
970
|
+
},
|
|
971
|
+
},
|
|
769
972
|
],
|
|
770
973
|
};
|
|
771
974
|
});
|
|
@@ -790,26 +993,40 @@ export function createServer(memoryFilePath) {
|
|
|
790
993
|
case "delete_relations":
|
|
791
994
|
await knowledgeGraphManager.deleteRelations(args.relations);
|
|
792
995
|
return { content: [{ type: "text", text: "Relations deleted successfully" }] };
|
|
793
|
-
case "search_nodes":
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
case "
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
case "
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
996
|
+
case "search_nodes": {
|
|
997
|
+
const graph = await knowledgeGraphManager.searchNodes(args.query);
|
|
998
|
+
return { content: [{ type: "text", text: JSON.stringify(paginateGraph(graph, args.entityCursor ?? 0, args.relationCursor ?? 0)) }] };
|
|
999
|
+
}
|
|
1000
|
+
case "open_nodes_filtered": {
|
|
1001
|
+
const graph = await knowledgeGraphManager.openNodesFiltered(args.names);
|
|
1002
|
+
return { content: [{ type: "text", text: JSON.stringify(paginateGraph(graph, args.entityCursor ?? 0, args.relationCursor ?? 0)) }] };
|
|
1003
|
+
}
|
|
1004
|
+
case "open_nodes": {
|
|
1005
|
+
const graph = await knowledgeGraphManager.openNodes(args.names);
|
|
1006
|
+
return { content: [{ type: "text", text: JSON.stringify(paginateGraph(graph, args.entityCursor ?? 0, args.relationCursor ?? 0)) }] };
|
|
1007
|
+
}
|
|
1008
|
+
case "get_neighbors": {
|
|
1009
|
+
const graph = await knowledgeGraphManager.getNeighbors(args.entityName, args.depth, args.withEntities);
|
|
1010
|
+
return { content: [{ type: "text", text: JSON.stringify(paginateGraph(graph, args.entityCursor ?? 0, args.relationCursor ?? 0)) }] };
|
|
1011
|
+
}
|
|
1012
|
+
case "find_path": {
|
|
1013
|
+
const path = await knowledgeGraphManager.findPath(args.fromEntity, args.toEntity, args.maxDepth);
|
|
1014
|
+
return { content: [{ type: "text", text: JSON.stringify(paginateItems(path, args.cursor ?? 0)) }] };
|
|
1015
|
+
}
|
|
1016
|
+
case "get_entities_by_type": {
|
|
1017
|
+
const entities = await knowledgeGraphManager.getEntitiesByType(args.entityType);
|
|
1018
|
+
return { content: [{ type: "text", text: JSON.stringify(paginateItems(entities, args.cursor ?? 0)) }] };
|
|
1019
|
+
}
|
|
805
1020
|
case "get_entity_types":
|
|
806
1021
|
return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.getEntityTypes(), null, 2) }] };
|
|
807
1022
|
case "get_relation_types":
|
|
808
1023
|
return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.getRelationTypes(), null, 2) }] };
|
|
809
1024
|
case "get_stats":
|
|
810
1025
|
return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.getStats(), null, 2) }] };
|
|
811
|
-
case "get_orphaned_entities":
|
|
812
|
-
|
|
1026
|
+
case "get_orphaned_entities": {
|
|
1027
|
+
const entities = await knowledgeGraphManager.getOrphanedEntities();
|
|
1028
|
+
return { content: [{ type: "text", text: JSON.stringify(paginateItems(entities, args.cursor ?? 0)) }] };
|
|
1029
|
+
}
|
|
813
1030
|
case "validate_graph":
|
|
814
1031
|
return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.validateGraph(), null, 2) }] };
|
|
815
1032
|
case "evaluate_bcl":
|
|
@@ -819,6 +1036,10 @@ export function createServer(memoryFilePath) {
|
|
|
819
1036
|
case "clear_bcl_term":
|
|
820
1037
|
await knowledgeGraphManager.clearBCLTerm();
|
|
821
1038
|
return { content: [{ type: "text", text: "BCL term constructor cleared successfully" }] };
|
|
1039
|
+
case "sequentialthinking": {
|
|
1040
|
+
const result = await knowledgeGraphManager.addThought(args.observations, args.previousCtxId);
|
|
1041
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1042
|
+
}
|
|
822
1043
|
default:
|
|
823
1044
|
throw new Error(`Unknown tool: ${name}`);
|
|
824
1045
|
}
|
|
@@ -129,7 +129,7 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
129
129
|
deletions: [{ entityName: 'TestEntity', observations: ['Delete'] }]
|
|
130
130
|
});
|
|
131
131
|
const result = await callTool(client, 'open_nodes', { names: ['TestEntity'] });
|
|
132
|
-
expect(result.entities[0].observations).toEqual(['Keep']);
|
|
132
|
+
expect(result.entities.items[0].observations).toEqual(['Keep']);
|
|
133
133
|
});
|
|
134
134
|
});
|
|
135
135
|
describe('Relation Operations', () => {
|
|
@@ -192,25 +192,41 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
192
192
|
});
|
|
193
193
|
});
|
|
194
194
|
it('should search by regex pattern', async () => {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
195
|
+
// Accumulate all entities across pagination
|
|
196
|
+
const allEntities = [];
|
|
197
|
+
let entityCursor = 0;
|
|
198
|
+
while (entityCursor !== null) {
|
|
199
|
+
const result = await callTool(client, 'search_nodes', {
|
|
200
|
+
query: 'Script',
|
|
201
|
+
entityCursor
|
|
202
|
+
});
|
|
203
|
+
allEntities.push(...result.entities.items);
|
|
204
|
+
entityCursor = result.entities.nextCursor;
|
|
205
|
+
}
|
|
206
|
+
expect(allEntities).toHaveLength(2);
|
|
207
|
+
expect(allEntities.map(e => e.name)).toContain('JavaScript');
|
|
208
|
+
expect(allEntities.map(e => e.name)).toContain('TypeScript');
|
|
201
209
|
});
|
|
202
210
|
it('should search with alternation', async () => {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
211
|
+
// Accumulate all entities across pagination
|
|
212
|
+
const allEntities = [];
|
|
213
|
+
let entityCursor = 0;
|
|
214
|
+
while (entityCursor !== null) {
|
|
215
|
+
const result = await callTool(client, 'search_nodes', {
|
|
216
|
+
query: 'JavaScript|Python',
|
|
217
|
+
entityCursor
|
|
218
|
+
});
|
|
219
|
+
allEntities.push(...result.entities.items);
|
|
220
|
+
entityCursor = result.entities.nextCursor;
|
|
221
|
+
}
|
|
222
|
+
expect(allEntities).toHaveLength(2);
|
|
207
223
|
});
|
|
208
224
|
it('should search in observations', async () => {
|
|
209
225
|
const result = await callTool(client, 'search_nodes', {
|
|
210
226
|
query: 'Static'
|
|
211
227
|
});
|
|
212
|
-
expect(result.entities).toHaveLength(1);
|
|
213
|
-
expect(result.entities[0].name).toBe('TypeScript');
|
|
228
|
+
expect(result.entities.items).toHaveLength(1);
|
|
229
|
+
expect(result.entities.items[0].name).toBe('TypeScript');
|
|
214
230
|
});
|
|
215
231
|
it('should reject invalid regex', async () => {
|
|
216
232
|
await expect(callTool(client, 'search_nodes', { query: '[invalid' })).rejects.toThrow(/Invalid regex pattern/);
|
|
@@ -236,17 +252,17 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
236
252
|
const result = await callTool(client, 'open_nodes', {
|
|
237
253
|
names: ['A', 'B']
|
|
238
254
|
});
|
|
239
|
-
expect(result.entities).toHaveLength(2);
|
|
255
|
+
expect(result.entities.items).toHaveLength(2);
|
|
240
256
|
// open_nodes returns all relations where 'from' is in the requested set
|
|
241
257
|
// A->B and A->C both have from='A' which is in the set
|
|
242
|
-
expect(result.relations).toHaveLength(2);
|
|
258
|
+
expect(result.relations.items).toHaveLength(2);
|
|
243
259
|
});
|
|
244
260
|
it('should open nodes filtered (only internal relations)', async () => {
|
|
245
261
|
const result = await callTool(client, 'open_nodes_filtered', {
|
|
246
262
|
names: ['B', 'C']
|
|
247
263
|
});
|
|
248
|
-
expect(result.entities).toHaveLength(2);
|
|
249
|
-
expect(result.relations).toHaveLength(0); // No relations between B and C
|
|
264
|
+
expect(result.entities.items).toHaveLength(2);
|
|
265
|
+
expect(result.relations.items).toHaveLength(0); // No relations between B and C
|
|
250
266
|
});
|
|
251
267
|
});
|
|
252
268
|
describe('Graph Traversal', () => {
|
|
@@ -272,27 +288,57 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
272
288
|
entityName: 'Root',
|
|
273
289
|
depth: 0
|
|
274
290
|
});
|
|
275
|
-
expect(result.entities).toHaveLength(0); // withEntities defaults to false
|
|
276
|
-
expect(result.relations).toHaveLength(2); // Root's direct relations
|
|
291
|
+
expect(result.entities.items).toHaveLength(0); // withEntities defaults to false
|
|
292
|
+
expect(result.relations.items).toHaveLength(2); // Root's direct relations
|
|
277
293
|
});
|
|
278
294
|
it('should get neighbors with entities when requested', async () => {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
295
|
+
// Accumulate all entities across pagination
|
|
296
|
+
const allEntities = [];
|
|
297
|
+
let entityCursor = 0;
|
|
298
|
+
while (entityCursor !== null) {
|
|
299
|
+
const result = await callTool(client, 'get_neighbors', {
|
|
300
|
+
entityName: 'Root',
|
|
301
|
+
depth: 1,
|
|
302
|
+
withEntities: true,
|
|
303
|
+
entityCursor
|
|
304
|
+
});
|
|
305
|
+
allEntities.push(...result.entities.items);
|
|
306
|
+
entityCursor = result.entities.nextCursor;
|
|
307
|
+
}
|
|
308
|
+
expect(allEntities).toHaveLength(3);
|
|
309
|
+
expect(allEntities.map(e => e.name)).toContain('Root');
|
|
310
|
+
expect(allEntities.map(e => e.name)).toContain('Child1');
|
|
311
|
+
expect(allEntities.map(e => e.name)).toContain('Child2');
|
|
287
312
|
});
|
|
288
313
|
it('should traverse to specified depth', async () => {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
314
|
+
// Collect all entities and relations using pagination
|
|
315
|
+
const allEntities = [];
|
|
316
|
+
const allRelations = [];
|
|
317
|
+
let entityCursor = 0;
|
|
318
|
+
let relationCursor = 0;
|
|
319
|
+
// Paginate through all results
|
|
320
|
+
while (entityCursor !== null || relationCursor !== null) {
|
|
321
|
+
const result = await callTool(client, 'get_neighbors', {
|
|
322
|
+
entityName: 'Root',
|
|
323
|
+
depth: 2,
|
|
324
|
+
withEntities: true,
|
|
325
|
+
// Pass large cursor to skip when done, 0 to fetch
|
|
326
|
+
entityCursor: entityCursor !== null ? entityCursor : 999999,
|
|
327
|
+
relationCursor: relationCursor !== null ? relationCursor : 999999
|
|
328
|
+
});
|
|
329
|
+
// Collect entities if we still need them
|
|
330
|
+
if (entityCursor !== null) {
|
|
331
|
+
allEntities.push(...result.entities.items);
|
|
332
|
+
entityCursor = result.entities.nextCursor;
|
|
333
|
+
}
|
|
334
|
+
// Collect relations if we still need them
|
|
335
|
+
if (relationCursor !== null) {
|
|
336
|
+
allRelations.push(...result.relations.items);
|
|
337
|
+
relationCursor = result.relations.nextCursor;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
expect(allEntities).toHaveLength(4); // All nodes
|
|
341
|
+
expect(allRelations).toHaveLength(3); // All relations
|
|
296
342
|
});
|
|
297
343
|
it('should deduplicate relations in traversal', async () => {
|
|
298
344
|
// Add a bidirectional relation
|
|
@@ -304,7 +350,7 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
304
350
|
depth: 1
|
|
305
351
|
});
|
|
306
352
|
// Each unique relation should appear only once
|
|
307
|
-
const relationKeys = result.relations.map(r => `${r.from}|${r.relationType}|${r.to}`);
|
|
353
|
+
const relationKeys = result.relations.items.map(r => `${r.from}|${r.relationType}|${r.to}`);
|
|
308
354
|
const uniqueKeys = [...new Set(relationKeys)];
|
|
309
355
|
expect(relationKeys.length).toBe(uniqueKeys.length);
|
|
310
356
|
});
|
|
@@ -313,9 +359,9 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
313
359
|
fromEntity: 'Root',
|
|
314
360
|
toEntity: 'Grandchild'
|
|
315
361
|
});
|
|
316
|
-
expect(result).toHaveLength(2);
|
|
317
|
-
expect(result[0].from).toBe('Root');
|
|
318
|
-
expect(result[1].to).toBe('Grandchild');
|
|
362
|
+
expect(result.items).toHaveLength(2);
|
|
363
|
+
expect(result.items[0].from).toBe('Root');
|
|
364
|
+
expect(result.items[1].to).toBe('Grandchild');
|
|
319
365
|
});
|
|
320
366
|
it('should return empty path when no path exists', async () => {
|
|
321
367
|
await callTool(client, 'create_entities', {
|
|
@@ -325,7 +371,7 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
325
371
|
fromEntity: 'Root',
|
|
326
372
|
toEntity: 'Isolated'
|
|
327
373
|
});
|
|
328
|
-
expect(result).toHaveLength(0);
|
|
374
|
+
expect(result.items).toHaveLength(0);
|
|
329
375
|
});
|
|
330
376
|
});
|
|
331
377
|
describe('Type Queries', () => {
|
|
@@ -348,8 +394,8 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
348
394
|
const result = await callTool(client, 'get_entities_by_type', {
|
|
349
395
|
entityType: 'Person'
|
|
350
396
|
});
|
|
351
|
-
expect(result).toHaveLength(2);
|
|
352
|
-
expect(result.every(e => e.entityType === 'Person')).toBe(true);
|
|
397
|
+
expect(result.items).toHaveLength(2);
|
|
398
|
+
expect(result.items.every(e => e.entityType === 'Person')).toBe(true);
|
|
353
399
|
});
|
|
354
400
|
it('should get all entity types', async () => {
|
|
355
401
|
const result = await callTool(client, 'get_entity_types', {});
|
|
@@ -392,8 +438,8 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
392
438
|
relations: [{ from: 'Connected1', to: 'Connected2', relationType: 'links' }]
|
|
393
439
|
});
|
|
394
440
|
const result = await callTool(client, 'get_orphaned_entities', {});
|
|
395
|
-
expect(result).toHaveLength(1);
|
|
396
|
-
expect(result[0].name).toBe('Orphan');
|
|
441
|
+
expect(result.items).toHaveLength(1);
|
|
442
|
+
expect(result.items[0].name).toBe('Orphan');
|
|
397
443
|
});
|
|
398
444
|
it('should validate graph and report violations', async () => {
|
|
399
445
|
// Directly write invalid data to test validation
|
|
@@ -435,4 +481,59 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
435
481
|
await expect(callTool(client, 'add_bcl_term', { term: 'invalid' })).rejects.toThrow(/Invalid BCL term/);
|
|
436
482
|
});
|
|
437
483
|
});
|
|
484
|
+
describe('Sequential Thinking', () => {
|
|
485
|
+
it('should create a thought and return ctxId', async () => {
|
|
486
|
+
const result = await callTool(client, 'sequentialthinking', {
|
|
487
|
+
observations: ['First thought observation']
|
|
488
|
+
});
|
|
489
|
+
expect(result.ctxId).toMatch(/^thought_\d+_[a-z0-9]+$/);
|
|
490
|
+
});
|
|
491
|
+
it('should chain thoughts with relations', async () => {
|
|
492
|
+
// Create first thought
|
|
493
|
+
const first = await callTool(client, 'sequentialthinking', {
|
|
494
|
+
observations: ['Starting point']
|
|
495
|
+
});
|
|
496
|
+
// Create second thought chained to first
|
|
497
|
+
const second = await callTool(client, 'sequentialthinking', {
|
|
498
|
+
previousCtxId: first.ctxId,
|
|
499
|
+
observations: ['Following up']
|
|
500
|
+
});
|
|
501
|
+
// Verify the chain via relations
|
|
502
|
+
const neighbors = await callTool(client, 'get_neighbors', {
|
|
503
|
+
entityName: first.ctxId,
|
|
504
|
+
depth: 1
|
|
505
|
+
});
|
|
506
|
+
// Should have 'follows' relation from first to second
|
|
507
|
+
expect(neighbors.relations.items.some(r => r.from === first.ctxId && r.to === second.ctxId && r.relationType === 'follows')).toBe(true);
|
|
508
|
+
});
|
|
509
|
+
it('should ignore invalid previousCtxId gracefully', async () => {
|
|
510
|
+
const result = await callTool(client, 'sequentialthinking', {
|
|
511
|
+
previousCtxId: 'nonexistent_thought',
|
|
512
|
+
observations: ['Orphaned thought']
|
|
513
|
+
});
|
|
514
|
+
expect(result.ctxId).toMatch(/^thought_\d+_[a-z0-9]+$/);
|
|
515
|
+
// Verify no relations were created
|
|
516
|
+
const neighbors = await callTool(client, 'get_neighbors', {
|
|
517
|
+
entityName: result.ctxId,
|
|
518
|
+
depth: 1
|
|
519
|
+
});
|
|
520
|
+
expect(neighbors.relations.totalCount).toBe(0);
|
|
521
|
+
});
|
|
522
|
+
it('should enforce observation limits on thoughts', async () => {
|
|
523
|
+
await expect(callTool(client, 'sequentialthinking', {
|
|
524
|
+
observations: ['One', 'Two', 'Three']
|
|
525
|
+
})).rejects.toThrow(/Maximum allowed is 2/);
|
|
526
|
+
});
|
|
527
|
+
it('should set mtime and obsMtime on thought entities', async () => {
|
|
528
|
+
const result = await callTool(client, 'sequentialthinking', {
|
|
529
|
+
observations: ['Timed thought']
|
|
530
|
+
});
|
|
531
|
+
const graph = await callTool(client, 'open_nodes', {
|
|
532
|
+
names: [result.ctxId]
|
|
533
|
+
});
|
|
534
|
+
const thought = graph.entities.items[0];
|
|
535
|
+
expect(thought.mtime).toBeDefined();
|
|
536
|
+
expect(thought.obsMtime).toBeDefined();
|
|
537
|
+
});
|
|
538
|
+
});
|
|
438
539
|
});
|
package/dist/tests/test-utils.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
2
|
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
3
|
+
import { MAX_CHARS } from "../server.js";
|
|
4
|
+
export { MAX_CHARS };
|
|
3
5
|
export async function createTestClient(server) {
|
|
4
6
|
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
5
7
|
const client = new Client({
|
|
@@ -18,6 +20,16 @@ export async function createTestClient(server) {
|
|
|
18
20
|
}
|
|
19
21
|
};
|
|
20
22
|
}
|
|
23
|
+
// Read operations that should be paginated
|
|
24
|
+
const PAGINATED_TOOLS = new Set([
|
|
25
|
+
'search_nodes',
|
|
26
|
+
'open_nodes',
|
|
27
|
+
'open_nodes_filtered',
|
|
28
|
+
'get_neighbors',
|
|
29
|
+
'find_path',
|
|
30
|
+
'get_entities_by_type',
|
|
31
|
+
'get_orphaned_entities',
|
|
32
|
+
]);
|
|
21
33
|
export async function callTool(client, name, args) {
|
|
22
34
|
const result = await client.callTool({ name, arguments: args });
|
|
23
35
|
const content = result.content;
|
|
@@ -26,6 +38,10 @@ export async function callTool(client, name, args) {
|
|
|
26
38
|
}
|
|
27
39
|
const first = content[0];
|
|
28
40
|
if (first.type === 'text' && first.text) {
|
|
41
|
+
// Only enforce char limit on paginated read operations
|
|
42
|
+
if (PAGINATED_TOOLS.has(name) && first.text.length > MAX_CHARS) {
|
|
43
|
+
throw new Error(`Response exceeds ${MAX_CHARS} char limit: got ${first.text.length} chars`);
|
|
44
|
+
}
|
|
29
45
|
try {
|
|
30
46
|
return JSON.parse(first.text);
|
|
31
47
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@levalicious/server-memory",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "MCP server for enabling memory for Claude through a knowledge graph",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Levalicious",
|
|
@@ -20,11 +20,13 @@
|
|
|
20
20
|
"test": "NODE_OPTIONS='--experimental-vm-modules' jest"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@modelcontextprotocol/sdk": "1.
|
|
23
|
+
"@modelcontextprotocol/sdk": "1.24.0",
|
|
24
|
+
"proper-lockfile": "^4.1.2"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
26
27
|
"@types/jest": "^30.0.0",
|
|
27
28
|
"@types/node": "^24",
|
|
29
|
+
"@types/proper-lockfile": "^4.1.4",
|
|
28
30
|
"jest": "^30.2.0",
|
|
29
31
|
"shx": "^0.4.0",
|
|
30
32
|
"ts-jest": "^29.4.5",
|