@levalicious/server-memory 0.0.3 → 0.0.5
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 +59 -4
- package/dist/index.js +399 -338
- package/dist/tests/memory-server.test.js +438 -0
- package/dist/tests/test-utils.js +37 -0
- package/package.json +7 -3
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { createServer } from '../index.js';
|
|
6
|
+
import { createTestClient, callTool } from './test-utils.js';
|
|
7
|
+
describe('MCP Memory Server E2E Tests', () => {
|
|
8
|
+
let testDir;
|
|
9
|
+
let memoryFile;
|
|
10
|
+
let client;
|
|
11
|
+
let cleanup;
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
// Create a unique temp directory for each test
|
|
14
|
+
testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-memory-test-'));
|
|
15
|
+
memoryFile = path.join(testDir, 'test-memory.json');
|
|
16
|
+
const server = createServer(memoryFile);
|
|
17
|
+
const result = await createTestClient(server);
|
|
18
|
+
client = result.client;
|
|
19
|
+
cleanup = result.cleanup;
|
|
20
|
+
});
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
await cleanup();
|
|
23
|
+
// Clean up test directory
|
|
24
|
+
try {
|
|
25
|
+
await fs.rm(testDir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Ignore cleanup errors
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
describe('Entity Operations', () => {
|
|
32
|
+
it('should create entities successfully', async () => {
|
|
33
|
+
const result = await callTool(client, 'create_entities', {
|
|
34
|
+
entities: [
|
|
35
|
+
{ name: 'Alice', entityType: 'Person', observations: ['Likes coding'] },
|
|
36
|
+
{ name: 'Bob', entityType: 'Person', observations: ['Likes music'] }
|
|
37
|
+
]
|
|
38
|
+
});
|
|
39
|
+
expect(result).toHaveLength(2);
|
|
40
|
+
expect(result[0].name).toBe('Alice');
|
|
41
|
+
expect(result[1].name).toBe('Bob');
|
|
42
|
+
});
|
|
43
|
+
it('should not duplicate existing entities', async () => {
|
|
44
|
+
await callTool(client, 'create_entities', {
|
|
45
|
+
entities: [{ name: 'Alice', entityType: 'Person', observations: ['First'] }]
|
|
46
|
+
});
|
|
47
|
+
const result = await callTool(client, 'create_entities', {
|
|
48
|
+
entities: [
|
|
49
|
+
{ name: 'Alice', entityType: 'Person', observations: ['Second'] },
|
|
50
|
+
{ name: 'Bob', entityType: 'Person', observations: ['New'] }
|
|
51
|
+
]
|
|
52
|
+
});
|
|
53
|
+
// Only Bob should be returned as new
|
|
54
|
+
expect(result).toHaveLength(1);
|
|
55
|
+
expect(result[0].name).toBe('Bob');
|
|
56
|
+
});
|
|
57
|
+
it('should reject entities with more than 2 observations', async () => {
|
|
58
|
+
await expect(callTool(client, 'create_entities', {
|
|
59
|
+
entities: [{
|
|
60
|
+
name: 'TooMany',
|
|
61
|
+
entityType: 'Test',
|
|
62
|
+
observations: ['One', 'Two', 'Three']
|
|
63
|
+
}]
|
|
64
|
+
})).rejects.toThrow(/Maximum allowed is 2/);
|
|
65
|
+
});
|
|
66
|
+
it('should reject observations longer than 140 characters', async () => {
|
|
67
|
+
const longObservation = 'x'.repeat(141);
|
|
68
|
+
await expect(callTool(client, 'create_entities', {
|
|
69
|
+
entities: [{
|
|
70
|
+
name: 'LongObs',
|
|
71
|
+
entityType: 'Test',
|
|
72
|
+
observations: [longObservation]
|
|
73
|
+
}]
|
|
74
|
+
})).rejects.toThrow(/exceeds 140 characters/);
|
|
75
|
+
});
|
|
76
|
+
it('should delete entities and their relations', async () => {
|
|
77
|
+
await callTool(client, 'create_entities', {
|
|
78
|
+
entities: [
|
|
79
|
+
{ name: 'A', entityType: 'Node', observations: [] },
|
|
80
|
+
{ name: 'B', entityType: 'Node', observations: [] }
|
|
81
|
+
]
|
|
82
|
+
});
|
|
83
|
+
await callTool(client, 'create_relations', {
|
|
84
|
+
relations: [{ from: 'A', to: 'B', relationType: 'connects' }]
|
|
85
|
+
});
|
|
86
|
+
await callTool(client, 'delete_entities', { entityNames: ['A'] });
|
|
87
|
+
const stats = await callTool(client, 'get_stats', {});
|
|
88
|
+
expect(stats.entityCount).toBe(1);
|
|
89
|
+
expect(stats.relationCount).toBe(0); // Relation should be deleted too
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe('Observation Operations', () => {
|
|
93
|
+
beforeEach(async () => {
|
|
94
|
+
await callTool(client, 'create_entities', {
|
|
95
|
+
entities: [{ name: 'TestEntity', entityType: 'Test', observations: [] }]
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
it('should add observations to entities', async () => {
|
|
99
|
+
const result = await callTool(client, 'add_observations', {
|
|
100
|
+
observations: [{
|
|
101
|
+
entityName: 'TestEntity',
|
|
102
|
+
contents: ['New observation']
|
|
103
|
+
}]
|
|
104
|
+
});
|
|
105
|
+
expect(result[0].addedObservations).toContain('New observation');
|
|
106
|
+
});
|
|
107
|
+
it('should not duplicate existing observations', async () => {
|
|
108
|
+
await callTool(client, 'add_observations', {
|
|
109
|
+
observations: [{ entityName: 'TestEntity', contents: ['Existing'] }]
|
|
110
|
+
});
|
|
111
|
+
const result = await callTool(client, 'add_observations', {
|
|
112
|
+
observations: [{ entityName: 'TestEntity', contents: ['Existing', 'New'] }]
|
|
113
|
+
});
|
|
114
|
+
expect(result[0].addedObservations).toEqual(['New']);
|
|
115
|
+
});
|
|
116
|
+
it('should reject adding observations that would exceed limit', async () => {
|
|
117
|
+
await callTool(client, 'add_observations', {
|
|
118
|
+
observations: [{ entityName: 'TestEntity', contents: ['One', 'Two'] }]
|
|
119
|
+
});
|
|
120
|
+
await expect(callTool(client, 'add_observations', {
|
|
121
|
+
observations: [{ entityName: 'TestEntity', contents: ['Three'] }]
|
|
122
|
+
})).rejects.toThrow(/would exceed limit of 2/);
|
|
123
|
+
});
|
|
124
|
+
it('should delete specific observations', async () => {
|
|
125
|
+
await callTool(client, 'add_observations', {
|
|
126
|
+
observations: [{ entityName: 'TestEntity', contents: ['Keep', 'Delete'] }]
|
|
127
|
+
});
|
|
128
|
+
await callTool(client, 'delete_observations', {
|
|
129
|
+
deletions: [{ entityName: 'TestEntity', observations: ['Delete'] }]
|
|
130
|
+
});
|
|
131
|
+
const result = await callTool(client, 'open_nodes', { names: ['TestEntity'] });
|
|
132
|
+
expect(result.entities[0].observations).toEqual(['Keep']);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
describe('Relation Operations', () => {
|
|
136
|
+
beforeEach(async () => {
|
|
137
|
+
await callTool(client, 'create_entities', {
|
|
138
|
+
entities: [
|
|
139
|
+
{ name: 'A', entityType: 'Node', observations: [] },
|
|
140
|
+
{ name: 'B', entityType: 'Node', observations: [] },
|
|
141
|
+
{ name: 'C', entityType: 'Node', observations: [] }
|
|
142
|
+
]
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
it('should create relations', async () => {
|
|
146
|
+
const result = await callTool(client, 'create_relations', {
|
|
147
|
+
relations: [
|
|
148
|
+
{ from: 'A', to: 'B', relationType: 'connects' },
|
|
149
|
+
{ from: 'B', to: 'C', relationType: 'connects' }
|
|
150
|
+
]
|
|
151
|
+
});
|
|
152
|
+
expect(result).toHaveLength(2);
|
|
153
|
+
});
|
|
154
|
+
it('should not duplicate relations', async () => {
|
|
155
|
+
await callTool(client, 'create_relations', {
|
|
156
|
+
relations: [{ from: 'A', to: 'B', relationType: 'connects' }]
|
|
157
|
+
});
|
|
158
|
+
const result = await callTool(client, 'create_relations', {
|
|
159
|
+
relations: [
|
|
160
|
+
{ from: 'A', to: 'B', relationType: 'connects' },
|
|
161
|
+
{ from: 'A', to: 'C', relationType: 'connects' }
|
|
162
|
+
]
|
|
163
|
+
});
|
|
164
|
+
expect(result).toHaveLength(1);
|
|
165
|
+
expect(result[0].to).toBe('C');
|
|
166
|
+
});
|
|
167
|
+
it('should delete relations', async () => {
|
|
168
|
+
await callTool(client, 'create_relations', {
|
|
169
|
+
relations: [
|
|
170
|
+
{ from: 'A', to: 'B', relationType: 'connects' },
|
|
171
|
+
{ from: 'B', to: 'C', relationType: 'connects' }
|
|
172
|
+
]
|
|
173
|
+
});
|
|
174
|
+
await callTool(client, 'delete_relations', {
|
|
175
|
+
relations: [{ from: 'A', to: 'B', relationType: 'connects' }]
|
|
176
|
+
});
|
|
177
|
+
const stats = await callTool(client, 'get_stats', {});
|
|
178
|
+
expect(stats.relationCount).toBe(1);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
describe('Search Operations', () => {
|
|
182
|
+
beforeEach(async () => {
|
|
183
|
+
await callTool(client, 'create_entities', {
|
|
184
|
+
entities: [
|
|
185
|
+
{ name: 'JavaScript', entityType: 'Language', observations: ['Dynamic typing'] },
|
|
186
|
+
{ name: 'TypeScript', entityType: 'Language', observations: ['Static typing'] },
|
|
187
|
+
{ name: 'Python', entityType: 'Language', observations: ['Dynamic typing'] }
|
|
188
|
+
]
|
|
189
|
+
});
|
|
190
|
+
await callTool(client, 'create_relations', {
|
|
191
|
+
relations: [{ from: 'TypeScript', to: 'JavaScript', relationType: 'extends' }]
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
it('should search by regex pattern', async () => {
|
|
195
|
+
const result = await callTool(client, 'search_nodes', {
|
|
196
|
+
query: 'Script'
|
|
197
|
+
});
|
|
198
|
+
expect(result.entities).toHaveLength(2);
|
|
199
|
+
expect(result.entities.map(e => e.name)).toContain('JavaScript');
|
|
200
|
+
expect(result.entities.map(e => e.name)).toContain('TypeScript');
|
|
201
|
+
});
|
|
202
|
+
it('should search with alternation', async () => {
|
|
203
|
+
const result = await callTool(client, 'search_nodes', {
|
|
204
|
+
query: 'JavaScript|Python'
|
|
205
|
+
});
|
|
206
|
+
expect(result.entities).toHaveLength(2);
|
|
207
|
+
});
|
|
208
|
+
it('should search in observations', async () => {
|
|
209
|
+
const result = await callTool(client, 'search_nodes', {
|
|
210
|
+
query: 'Static'
|
|
211
|
+
});
|
|
212
|
+
expect(result.entities).toHaveLength(1);
|
|
213
|
+
expect(result.entities[0].name).toBe('TypeScript');
|
|
214
|
+
});
|
|
215
|
+
it('should reject invalid regex', async () => {
|
|
216
|
+
await expect(callTool(client, 'search_nodes', { query: '[invalid' })).rejects.toThrow(/Invalid regex pattern/);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
describe('Node Retrieval', () => {
|
|
220
|
+
beforeEach(async () => {
|
|
221
|
+
await callTool(client, 'create_entities', {
|
|
222
|
+
entities: [
|
|
223
|
+
{ name: 'A', entityType: 'Node', observations: ['Root'] },
|
|
224
|
+
{ name: 'B', entityType: 'Node', observations: [] },
|
|
225
|
+
{ name: 'C', entityType: 'Node', observations: [] }
|
|
226
|
+
]
|
|
227
|
+
});
|
|
228
|
+
await callTool(client, 'create_relations', {
|
|
229
|
+
relations: [
|
|
230
|
+
{ from: 'A', to: 'B', relationType: 'parent_of' },
|
|
231
|
+
{ from: 'A', to: 'C', relationType: 'parent_of' }
|
|
232
|
+
]
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
it('should open nodes by name', async () => {
|
|
236
|
+
const result = await callTool(client, 'open_nodes', {
|
|
237
|
+
names: ['A', 'B']
|
|
238
|
+
});
|
|
239
|
+
expect(result.entities).toHaveLength(2);
|
|
240
|
+
// open_nodes returns all relations where 'from' is in the requested set
|
|
241
|
+
// A->B and A->C both have from='A' which is in the set
|
|
242
|
+
expect(result.relations).toHaveLength(2);
|
|
243
|
+
});
|
|
244
|
+
it('should open nodes filtered (only internal relations)', async () => {
|
|
245
|
+
const result = await callTool(client, 'open_nodes_filtered', {
|
|
246
|
+
names: ['B', 'C']
|
|
247
|
+
});
|
|
248
|
+
expect(result.entities).toHaveLength(2);
|
|
249
|
+
expect(result.relations).toHaveLength(0); // No relations between B and C
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
describe('Graph Traversal', () => {
|
|
253
|
+
beforeEach(async () => {
|
|
254
|
+
await callTool(client, 'create_entities', {
|
|
255
|
+
entities: [
|
|
256
|
+
{ name: 'Root', entityType: 'Node', observations: [] },
|
|
257
|
+
{ name: 'Child1', entityType: 'Node', observations: [] },
|
|
258
|
+
{ name: 'Child2', entityType: 'Node', observations: [] },
|
|
259
|
+
{ name: 'Grandchild', entityType: 'Node', observations: [] }
|
|
260
|
+
]
|
|
261
|
+
});
|
|
262
|
+
await callTool(client, 'create_relations', {
|
|
263
|
+
relations: [
|
|
264
|
+
{ from: 'Root', to: 'Child1', relationType: 'parent_of' },
|
|
265
|
+
{ from: 'Root', to: 'Child2', relationType: 'parent_of' },
|
|
266
|
+
{ from: 'Child1', to: 'Grandchild', relationType: 'parent_of' }
|
|
267
|
+
]
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
it('should get neighbors at depth 0 (relations only by default)', async () => {
|
|
271
|
+
const result = await callTool(client, 'get_neighbors', {
|
|
272
|
+
entityName: 'Root',
|
|
273
|
+
depth: 0
|
|
274
|
+
});
|
|
275
|
+
expect(result.entities).toHaveLength(0); // withEntities defaults to false
|
|
276
|
+
expect(result.relations).toHaveLength(2); // Root's direct relations
|
|
277
|
+
});
|
|
278
|
+
it('should get neighbors with entities when requested', async () => {
|
|
279
|
+
const result = await callTool(client, 'get_neighbors', {
|
|
280
|
+
entityName: 'Root',
|
|
281
|
+
depth: 1,
|
|
282
|
+
withEntities: true
|
|
283
|
+
});
|
|
284
|
+
expect(result.entities.map(e => e.name)).toContain('Root');
|
|
285
|
+
expect(result.entities.map(e => e.name)).toContain('Child1');
|
|
286
|
+
expect(result.entities.map(e => e.name)).toContain('Child2');
|
|
287
|
+
});
|
|
288
|
+
it('should traverse to specified depth', async () => {
|
|
289
|
+
const result = await callTool(client, 'get_neighbors', {
|
|
290
|
+
entityName: 'Root',
|
|
291
|
+
depth: 2,
|
|
292
|
+
withEntities: true
|
|
293
|
+
});
|
|
294
|
+
expect(result.entities).toHaveLength(4); // All nodes
|
|
295
|
+
expect(result.relations).toHaveLength(3); // All relations
|
|
296
|
+
});
|
|
297
|
+
it('should deduplicate relations in traversal', async () => {
|
|
298
|
+
// Add a bidirectional relation
|
|
299
|
+
await callTool(client, 'create_relations', {
|
|
300
|
+
relations: [{ from: 'Child2', to: 'Root', relationType: 'child_of' }]
|
|
301
|
+
});
|
|
302
|
+
const result = await callTool(client, 'get_neighbors', {
|
|
303
|
+
entityName: 'Root',
|
|
304
|
+
depth: 1
|
|
305
|
+
});
|
|
306
|
+
// Each unique relation should appear only once
|
|
307
|
+
const relationKeys = result.relations.map(r => `${r.from}|${r.relationType}|${r.to}`);
|
|
308
|
+
const uniqueKeys = [...new Set(relationKeys)];
|
|
309
|
+
expect(relationKeys.length).toBe(uniqueKeys.length);
|
|
310
|
+
});
|
|
311
|
+
it('should find path between entities', async () => {
|
|
312
|
+
const result = await callTool(client, 'find_path', {
|
|
313
|
+
fromEntity: 'Root',
|
|
314
|
+
toEntity: 'Grandchild'
|
|
315
|
+
});
|
|
316
|
+
expect(result).toHaveLength(2);
|
|
317
|
+
expect(result[0].from).toBe('Root');
|
|
318
|
+
expect(result[1].to).toBe('Grandchild');
|
|
319
|
+
});
|
|
320
|
+
it('should return empty path when no path exists', async () => {
|
|
321
|
+
await callTool(client, 'create_entities', {
|
|
322
|
+
entities: [{ name: 'Isolated', entityType: 'Node', observations: [] }]
|
|
323
|
+
});
|
|
324
|
+
const result = await callTool(client, 'find_path', {
|
|
325
|
+
fromEntity: 'Root',
|
|
326
|
+
toEntity: 'Isolated'
|
|
327
|
+
});
|
|
328
|
+
expect(result).toHaveLength(0);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
describe('Type Queries', () => {
|
|
332
|
+
beforeEach(async () => {
|
|
333
|
+
await callTool(client, 'create_entities', {
|
|
334
|
+
entities: [
|
|
335
|
+
{ name: 'Alice', entityType: 'Person', observations: [] },
|
|
336
|
+
{ name: 'Bob', entityType: 'Person', observations: [] },
|
|
337
|
+
{ name: 'Acme', entityType: 'Company', observations: [] }
|
|
338
|
+
]
|
|
339
|
+
});
|
|
340
|
+
await callTool(client, 'create_relations', {
|
|
341
|
+
relations: [
|
|
342
|
+
{ from: 'Alice', to: 'Acme', relationType: 'works_at' },
|
|
343
|
+
{ from: 'Bob', to: 'Acme', relationType: 'works_at' }
|
|
344
|
+
]
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
it('should get entities by type', async () => {
|
|
348
|
+
const result = await callTool(client, 'get_entities_by_type', {
|
|
349
|
+
entityType: 'Person'
|
|
350
|
+
});
|
|
351
|
+
expect(result).toHaveLength(2);
|
|
352
|
+
expect(result.every(e => e.entityType === 'Person')).toBe(true);
|
|
353
|
+
});
|
|
354
|
+
it('should get all entity types', async () => {
|
|
355
|
+
const result = await callTool(client, 'get_entity_types', {});
|
|
356
|
+
expect(result).toContain('Person');
|
|
357
|
+
expect(result).toContain('Company');
|
|
358
|
+
expect(result).toHaveLength(2);
|
|
359
|
+
});
|
|
360
|
+
it('should get all relation types', async () => {
|
|
361
|
+
const result = await callTool(client, 'get_relation_types', {});
|
|
362
|
+
expect(result).toContain('works_at');
|
|
363
|
+
expect(result).toHaveLength(1);
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
describe('Statistics and Validation', () => {
|
|
367
|
+
it('should return correct stats', async () => {
|
|
368
|
+
await callTool(client, 'create_entities', {
|
|
369
|
+
entities: [
|
|
370
|
+
{ name: 'A', entityType: 'Type1', observations: [] },
|
|
371
|
+
{ name: 'B', entityType: 'Type2', observations: [] }
|
|
372
|
+
]
|
|
373
|
+
});
|
|
374
|
+
await callTool(client, 'create_relations', {
|
|
375
|
+
relations: [{ from: 'A', to: 'B', relationType: 'rel1' }]
|
|
376
|
+
});
|
|
377
|
+
const stats = await callTool(client, 'get_stats', {});
|
|
378
|
+
expect(stats.entityCount).toBe(2);
|
|
379
|
+
expect(stats.relationCount).toBe(1);
|
|
380
|
+
expect(stats.entityTypes).toBe(2);
|
|
381
|
+
expect(stats.relationTypes).toBe(1);
|
|
382
|
+
});
|
|
383
|
+
it('should find orphaned entities', async () => {
|
|
384
|
+
await callTool(client, 'create_entities', {
|
|
385
|
+
entities: [
|
|
386
|
+
{ name: 'Connected1', entityType: 'Node', observations: [] },
|
|
387
|
+
{ name: 'Connected2', entityType: 'Node', observations: [] },
|
|
388
|
+
{ name: 'Orphan', entityType: 'Node', observations: [] }
|
|
389
|
+
]
|
|
390
|
+
});
|
|
391
|
+
await callTool(client, 'create_relations', {
|
|
392
|
+
relations: [{ from: 'Connected1', to: 'Connected2', relationType: 'links' }]
|
|
393
|
+
});
|
|
394
|
+
const result = await callTool(client, 'get_orphaned_entities', {});
|
|
395
|
+
expect(result).toHaveLength(1);
|
|
396
|
+
expect(result[0].name).toBe('Orphan');
|
|
397
|
+
});
|
|
398
|
+
it('should validate graph and report violations', async () => {
|
|
399
|
+
// Directly write invalid data to test validation
|
|
400
|
+
const invalidData = [
|
|
401
|
+
JSON.stringify({ type: 'entity', name: 'Valid', entityType: 'Test', observations: [] }),
|
|
402
|
+
JSON.stringify({ type: 'relation', from: 'Valid', to: 'Missing', relationType: 'refs' })
|
|
403
|
+
].join('\n');
|
|
404
|
+
await fs.writeFile(memoryFile, invalidData);
|
|
405
|
+
const result = await callTool(client, 'validate_graph', {});
|
|
406
|
+
expect(result.missingEntities).toContain('Missing');
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
describe('BCL Evaluator', () => {
|
|
410
|
+
it('should evaluate K combinator (identity for first arg)', async () => {
|
|
411
|
+
// K = 00, evaluating K applied to two args should return first
|
|
412
|
+
// This is a simplified test - BCL semantics are complex
|
|
413
|
+
const result = await callTool(client, 'evaluate_bcl', {
|
|
414
|
+
program: '00',
|
|
415
|
+
maxSteps: 100
|
|
416
|
+
});
|
|
417
|
+
expect(result.halted).toBe(true);
|
|
418
|
+
});
|
|
419
|
+
it('should construct BCL terms incrementally', async () => {
|
|
420
|
+
let result = await callTool(client, 'add_bcl_term', { term: 'App' });
|
|
421
|
+
expect(result).toContain('more term');
|
|
422
|
+
result = await callTool(client, 'add_bcl_term', { term: 'K' });
|
|
423
|
+
expect(result).toContain('more term');
|
|
424
|
+
result = await callTool(client, 'add_bcl_term', { term: 'S' });
|
|
425
|
+
expect(result).toContain('Constructed Program');
|
|
426
|
+
});
|
|
427
|
+
it('should clear BCL constructor state', async () => {
|
|
428
|
+
await callTool(client, 'add_bcl_term', { term: 'App' });
|
|
429
|
+
await callTool(client, 'clear_bcl_term', {});
|
|
430
|
+
// After clearing, we should need to start fresh
|
|
431
|
+
const result = await callTool(client, 'add_bcl_term', { term: 'K' });
|
|
432
|
+
expect(result).toContain('Constructed Program');
|
|
433
|
+
});
|
|
434
|
+
it('should reject invalid BCL terms', async () => {
|
|
435
|
+
await expect(callTool(client, 'add_bcl_term', { term: 'invalid' })).rejects.toThrow(/Invalid BCL term/);
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
3
|
+
export async function createTestClient(server) {
|
|
4
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
5
|
+
const client = new Client({
|
|
6
|
+
name: "test-client",
|
|
7
|
+
version: "1.0.0",
|
|
8
|
+
}, {
|
|
9
|
+
capabilities: {}
|
|
10
|
+
});
|
|
11
|
+
await server.connect(serverTransport);
|
|
12
|
+
await client.connect(clientTransport);
|
|
13
|
+
return {
|
|
14
|
+
client,
|
|
15
|
+
cleanup: async () => {
|
|
16
|
+
await client.close();
|
|
17
|
+
await server.close();
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export async function callTool(client, name, args) {
|
|
22
|
+
const result = await client.callTool({ name, arguments: args });
|
|
23
|
+
const content = result.content;
|
|
24
|
+
if (!content || content.length === 0) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const first = content[0];
|
|
28
|
+
if (first.type === 'text' && first.text) {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(first.text);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return first.text;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return first;
|
|
37
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@levalicious/server-memory",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "MCP server for enabling memory for Claude through a knowledge graph",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Levalicious",
|
|
@@ -16,14 +16,18 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "tsc && shx chmod +x dist/*.js",
|
|
18
18
|
"prepare": "npm run build",
|
|
19
|
-
"watch": "tsc --watch"
|
|
19
|
+
"watch": "tsc --watch",
|
|
20
|
+
"test": "NODE_OPTIONS='--experimental-vm-modules' jest"
|
|
20
21
|
},
|
|
21
22
|
"dependencies": {
|
|
22
|
-
"@modelcontextprotocol/sdk": "1.
|
|
23
|
+
"@modelcontextprotocol/sdk": "1.22.0"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|
|
26
|
+
"@types/jest": "^30.0.0",
|
|
25
27
|
"@types/node": "^24",
|
|
28
|
+
"jest": "^30.2.0",
|
|
26
29
|
"shx": "^0.4.0",
|
|
30
|
+
"ts-jest": "^29.4.5",
|
|
27
31
|
"typescript": "^5.6.2"
|
|
28
32
|
}
|
|
29
33
|
}
|