@levalicious/server-memory 0.0.8 → 0.0.9
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 +46 -33
- package/dist/server.js +41 -9
- package/dist/tests/memory-server.test.js +28 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,33 +8,37 @@ A basic implementation of persistent memory using a local knowledge graph. This
|
|
|
8
8
|
Entities are the primary nodes in the knowledge graph. Each entity has:
|
|
9
9
|
- A unique name (identifier)
|
|
10
10
|
- An entity type (e.g., "person", "organization", "event")
|
|
11
|
-
- A list of observations
|
|
11
|
+
- A list of observations (max 2, each max 140 characters)
|
|
12
|
+
- Modification timestamps (`mtime` for any change, `obsMtime` for observation changes)
|
|
12
13
|
|
|
13
14
|
Example:
|
|
14
15
|
```json
|
|
15
16
|
{
|
|
16
17
|
"name": "John_Smith",
|
|
17
18
|
"entityType": "person",
|
|
18
|
-
"observations": ["Speaks fluent Spanish"]
|
|
19
|
+
"observations": ["Speaks fluent Spanish"],
|
|
20
|
+
"mtime": 1733423456789,
|
|
21
|
+
"obsMtime": 1733423456789
|
|
19
22
|
}
|
|
20
23
|
```
|
|
21
24
|
|
|
22
25
|
### Relations
|
|
23
|
-
Relations define directed connections between entities. They are always stored in active voice and describe how entities interact or relate to each other.
|
|
26
|
+
Relations define directed connections between entities. They are always stored in active voice and describe how entities interact or relate to each other. Each relation has a modification timestamp (`mtime`).
|
|
24
27
|
|
|
25
28
|
Example:
|
|
26
29
|
```json
|
|
27
30
|
{
|
|
28
31
|
"from": "John_Smith",
|
|
29
32
|
"to": "Anthropic",
|
|
30
|
-
"relationType": "works_at"
|
|
33
|
+
"relationType": "works_at",
|
|
34
|
+
"mtime": 1733423456789
|
|
31
35
|
}
|
|
32
36
|
```
|
|
33
37
|
### Observations
|
|
34
38
|
Observations are discrete pieces of information about an entity. They are:
|
|
35
39
|
|
|
36
|
-
- Stored as strings
|
|
37
|
-
- Attached to specific entities
|
|
40
|
+
- Stored as strings (max 140 characters each)
|
|
41
|
+
- Attached to specific entities (max 2 per entity)
|
|
38
42
|
- Can be added or removed independently
|
|
39
43
|
- Should be atomic (one fact per observation)
|
|
40
44
|
|
|
@@ -44,8 +48,7 @@ Example:
|
|
|
44
48
|
"entityName": "John_Smith",
|
|
45
49
|
"observations": [
|
|
46
50
|
"Speaks fluent Spanish",
|
|
47
|
-
"Graduated in 2019"
|
|
48
|
-
"Prefers morning meetings"
|
|
51
|
+
"Graduated in 2019"
|
|
49
52
|
]
|
|
50
53
|
}
|
|
51
54
|
```
|
|
@@ -59,7 +62,7 @@ Example:
|
|
|
59
62
|
- Each object contains:
|
|
60
63
|
- `name` (string): Entity identifier
|
|
61
64
|
- `entityType` (string): Type classification
|
|
62
|
-
- `observations` (string[]): Associated observations
|
|
65
|
+
- `observations` (string[]): Associated observations (max 2, each max 140 chars)
|
|
63
66
|
- Ignores entities with existing names
|
|
64
67
|
|
|
65
68
|
- **create_relations**
|
|
@@ -76,9 +79,9 @@ Example:
|
|
|
76
79
|
- Input: `observations` (array of objects)
|
|
77
80
|
- Each object contains:
|
|
78
81
|
- `entityName` (string): Target entity
|
|
79
|
-
- `contents` (string[]): New observations to add
|
|
82
|
+
- `contents` (string[]): New observations to add (each max 140 chars)
|
|
80
83
|
- Returns added observations per entity
|
|
81
|
-
- Fails if entity doesn't exist
|
|
84
|
+
- Fails if entity doesn't exist or would exceed 2 observations
|
|
82
85
|
|
|
83
86
|
- **delete_entities**
|
|
84
87
|
- Remove entities and their relations
|
|
@@ -103,42 +106,45 @@ Example:
|
|
|
103
106
|
- `relationType` (string): Relationship type
|
|
104
107
|
- Silent operation if relation doesn't exist
|
|
105
108
|
|
|
106
|
-
- **read_graph**
|
|
107
|
-
- Read the entire knowledge graph
|
|
108
|
-
- No input required
|
|
109
|
-
- Returns complete graph structure with all entities and relations
|
|
110
|
-
|
|
111
109
|
- **search_nodes**
|
|
112
|
-
- Search for nodes
|
|
113
|
-
- Input: `query` (string)
|
|
110
|
+
- Search for nodes using a regex pattern
|
|
111
|
+
- Input: `query` (string), `entityCursor` (number, optional), `relationCursor` (number, optional)
|
|
114
112
|
- Searches across:
|
|
115
113
|
- Entity names
|
|
116
114
|
- Entity types
|
|
117
115
|
- Observation content
|
|
118
|
-
- Returns matching entities and their relations
|
|
116
|
+
- Returns matching entities and their relations (paginated)
|
|
117
|
+
|
|
118
|
+
- **open_nodes_filtered**
|
|
119
|
+
- Retrieve specific nodes by name with filtered relations
|
|
120
|
+
- Input: `names` (string[]), `entityCursor` (number, optional), `relationCursor` (number, optional)
|
|
121
|
+
- Returns:
|
|
122
|
+
- Requested entities
|
|
123
|
+
- Only relations where both endpoints are in the requested set
|
|
124
|
+
- Silently skips non-existent nodes (paginated)
|
|
119
125
|
|
|
120
126
|
- **open_nodes**
|
|
121
127
|
- Retrieve specific nodes by name
|
|
122
|
-
- Input: `names` (string[])
|
|
128
|
+
- Input: `names` (string[]), `entityCursor` (number, optional), `relationCursor` (number, optional)
|
|
123
129
|
- Returns:
|
|
124
130
|
- Requested entities
|
|
125
|
-
- Relations
|
|
126
|
-
- Silently skips non-existent nodes
|
|
131
|
+
- Relations originating from requested entities
|
|
132
|
+
- Silently skips non-existent nodes (paginated)
|
|
127
133
|
|
|
128
134
|
- **get_neighbors**
|
|
129
135
|
- Get neighboring entities connected to a specific entity within a given depth
|
|
130
|
-
- Input: `entityName` (string), `depth` (number, default:
|
|
131
|
-
- Returns entities connected within specified depth
|
|
136
|
+
- Input: `entityName` (string), `depth` (number, default: 0), `withEntities` (boolean, default: false), `entityCursor` (number, optional), `relationCursor` (number, optional)
|
|
137
|
+
- Returns relations (and optionally entities) connected within specified depth (paginated)
|
|
132
138
|
|
|
133
139
|
- **find_path**
|
|
134
140
|
- Find a path between two entities in the knowledge graph
|
|
135
|
-
- Input: `fromEntity` (string), `toEntity` (string), `maxDepth` (number, default: 5)
|
|
136
|
-
- Returns path between entities if one exists
|
|
141
|
+
- Input: `fromEntity` (string), `toEntity` (string), `maxDepth` (number, default: 5), `cursor` (number, optional)
|
|
142
|
+
- Returns path between entities if one exists (paginated)
|
|
137
143
|
|
|
138
144
|
- **get_entities_by_type**
|
|
139
145
|
- Get all entities of a specific type
|
|
140
|
-
- Input: `entityType` (string)
|
|
141
|
-
- Returns all entities matching the specified type
|
|
146
|
+
- Input: `entityType` (string), `cursor` (number, optional)
|
|
147
|
+
- Returns all entities matching the specified type (paginated)
|
|
142
148
|
|
|
143
149
|
- **get_entity_types**
|
|
144
150
|
- Get all unique entity types in the knowledge graph
|
|
@@ -156,14 +162,15 @@ Example:
|
|
|
156
162
|
- Returns entity count, relation count, entity types count, relation types count
|
|
157
163
|
|
|
158
164
|
- **get_orphaned_entities**
|
|
159
|
-
- Get entities that have no relations
|
|
160
|
-
-
|
|
161
|
-
-
|
|
165
|
+
- Get entities that have no relations (orphaned entities)
|
|
166
|
+
- Input: `strict` (boolean, default: false), `cursor` (number, optional)
|
|
167
|
+
- In strict mode, returns entities not connected to 'Self' entity (directly or indirectly)
|
|
168
|
+
- Returns entities with no connections (paginated)
|
|
162
169
|
|
|
163
170
|
- **validate_graph**
|
|
164
|
-
- Validate the knowledge graph
|
|
171
|
+
- Validate the knowledge graph
|
|
165
172
|
- No input required
|
|
166
|
-
- Returns
|
|
173
|
+
- Returns missing entities referenced in relations and observation limit violations
|
|
167
174
|
|
|
168
175
|
- **evaluate_bcl**
|
|
169
176
|
- Evaluate a Binary Combinatory Logic (BCL) program
|
|
@@ -182,6 +189,12 @@ Example:
|
|
|
182
189
|
- No input required
|
|
183
190
|
- Resets BCL constructor
|
|
184
191
|
|
|
192
|
+
- **sequentialthinking**
|
|
193
|
+
- Record a thought in the knowledge graph
|
|
194
|
+
- Input: `observations` (string[], max 2, each max 140 chars), `previousCtxId` (string, optional)
|
|
195
|
+
- Creates a Thought entity and links it to the previous thought if provided
|
|
196
|
+
- Returns the new thought's context ID for chaining
|
|
197
|
+
|
|
185
198
|
# Usage with Claude Desktop
|
|
186
199
|
|
|
187
200
|
### Setup
|
package/dist/server.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
3
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
4
|
import { promises as fs } from 'fs';
|
|
5
|
+
import { randomBytes } from 'crypto';
|
|
5
6
|
import path from 'path';
|
|
6
7
|
import { fileURLToPath } from 'url';
|
|
7
8
|
import lockfile from 'proper-lockfile';
|
|
@@ -382,14 +383,44 @@ export class KnowledgeGraphManager {
|
|
|
382
383
|
relationTypes: relationTypes.size
|
|
383
384
|
};
|
|
384
385
|
}
|
|
385
|
-
async getOrphanedEntities() {
|
|
386
|
+
async getOrphanedEntities(strict = false) {
|
|
386
387
|
const graph = await this.loadGraph();
|
|
387
|
-
|
|
388
|
+
if (!strict) {
|
|
389
|
+
// Simple mode: entities with no relations at all
|
|
390
|
+
const connectedEntityNames = new Set();
|
|
391
|
+
graph.relations.forEach(r => {
|
|
392
|
+
connectedEntityNames.add(r.from);
|
|
393
|
+
connectedEntityNames.add(r.to);
|
|
394
|
+
});
|
|
395
|
+
return graph.entities.filter(e => !connectedEntityNames.has(e.name));
|
|
396
|
+
}
|
|
397
|
+
// Strict mode: entities not connected to "Self" (directly or indirectly)
|
|
398
|
+
// Build adjacency list (bidirectional)
|
|
399
|
+
const neighbors = new Map();
|
|
400
|
+
graph.entities.forEach(e => neighbors.set(e.name, new Set()));
|
|
388
401
|
graph.relations.forEach(r => {
|
|
389
|
-
|
|
390
|
-
|
|
402
|
+
neighbors.get(r.from)?.add(r.to);
|
|
403
|
+
neighbors.get(r.to)?.add(r.from);
|
|
391
404
|
});
|
|
392
|
-
|
|
405
|
+
// BFS from Self to find all connected entities
|
|
406
|
+
const connectedToSelf = new Set();
|
|
407
|
+
const queue = ['Self'];
|
|
408
|
+
while (queue.length > 0) {
|
|
409
|
+
const current = queue.shift();
|
|
410
|
+
if (connectedToSelf.has(current))
|
|
411
|
+
continue;
|
|
412
|
+
connectedToSelf.add(current);
|
|
413
|
+
const currentNeighbors = neighbors.get(current);
|
|
414
|
+
if (currentNeighbors) {
|
|
415
|
+
for (const neighbor of currentNeighbors) {
|
|
416
|
+
if (!connectedToSelf.has(neighbor)) {
|
|
417
|
+
queue.push(neighbor);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// Return entities not connected to Self (excluding Self itself if it exists)
|
|
423
|
+
return graph.entities.filter(e => !connectedToSelf.has(e.name));
|
|
393
424
|
}
|
|
394
425
|
async validateGraph() {
|
|
395
426
|
const graph = await this.loadGraph();
|
|
@@ -596,9 +627,9 @@ export class KnowledgeGraphManager {
|
|
|
596
627
|
throw new Error(`Observation exceeds 140 characters (${obs.length} chars): "${obs.substring(0, 50)}..."`);
|
|
597
628
|
}
|
|
598
629
|
}
|
|
599
|
-
// Generate new context ID
|
|
630
|
+
// Generate new context ID (24-char hex)
|
|
600
631
|
const now = Date.now();
|
|
601
|
-
const ctxId =
|
|
632
|
+
const ctxId = randomBytes(12).toString('hex');
|
|
602
633
|
// Create thought entity
|
|
603
634
|
const thoughtEntity = {
|
|
604
635
|
name: ctxId,
|
|
@@ -897,10 +928,11 @@ export function createServer(memoryFilePath) {
|
|
|
897
928
|
},
|
|
898
929
|
{
|
|
899
930
|
name: "get_orphaned_entities",
|
|
900
|
-
description: "Get entities that have no relations (orphaned entities). Results are paginated (max 512 chars).",
|
|
931
|
+
description: "Get entities that have no relations (orphaned entities). In strict mode, returns entities not connected to 'Self' entity. Results are paginated (max 512 chars).",
|
|
901
932
|
inputSchema: {
|
|
902
933
|
type: "object",
|
|
903
934
|
properties: {
|
|
935
|
+
strict: { type: "boolean", description: "If true, returns entities not connected to 'Self' (directly or indirectly). Default: false" },
|
|
904
936
|
cursor: { type: "number", description: "Cursor for pagination" },
|
|
905
937
|
},
|
|
906
938
|
},
|
|
@@ -1024,7 +1056,7 @@ Use this to build chains of reasoning that persist in the graph. Each thought ca
|
|
|
1024
1056
|
case "get_stats":
|
|
1025
1057
|
return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.getStats(), null, 2) }] };
|
|
1026
1058
|
case "get_orphaned_entities": {
|
|
1027
|
-
const entities = await knowledgeGraphManager.getOrphanedEntities();
|
|
1059
|
+
const entities = await knowledgeGraphManager.getOrphanedEntities(args.strict ?? false);
|
|
1028
1060
|
return { content: [{ type: "text", text: JSON.stringify(paginateItems(entities, args.cursor ?? 0)) }] };
|
|
1029
1061
|
}
|
|
1030
1062
|
case "validate_graph":
|
|
@@ -441,6 +441,32 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
441
441
|
expect(result.items).toHaveLength(1);
|
|
442
442
|
expect(result.items[0].name).toBe('Orphan');
|
|
443
443
|
});
|
|
444
|
+
it('should find entities not connected to Self in strict mode', async () => {
|
|
445
|
+
await callTool(client, 'create_entities', {
|
|
446
|
+
entities: [
|
|
447
|
+
{ name: 'Self', entityType: 'Agent', observations: [] },
|
|
448
|
+
{ name: 'ConnectedToSelf', entityType: 'Node', observations: [] },
|
|
449
|
+
{ name: 'IndirectlyConnected', entityType: 'Node', observations: [] },
|
|
450
|
+
{ name: 'Island1', entityType: 'Node', observations: [] },
|
|
451
|
+
{ name: 'Island2', entityType: 'Node', observations: [] }
|
|
452
|
+
]
|
|
453
|
+
});
|
|
454
|
+
await callTool(client, 'create_relations', {
|
|
455
|
+
relations: [
|
|
456
|
+
{ from: 'Self', to: 'ConnectedToSelf', relationType: 'knows' },
|
|
457
|
+
{ from: 'ConnectedToSelf', to: 'IndirectlyConnected', relationType: 'links' },
|
|
458
|
+
{ from: 'Island1', to: 'Island2', relationType: 'links' } // Connected to each other but not to Self
|
|
459
|
+
]
|
|
460
|
+
});
|
|
461
|
+
// Non-strict: Island1 and Island2 are connected, so not orphaned
|
|
462
|
+
const nonStrict = await callTool(client, 'get_orphaned_entities', {});
|
|
463
|
+
expect(nonStrict.items).toHaveLength(0);
|
|
464
|
+
// Strict: Island1 and Island2 are not connected to Self
|
|
465
|
+
const strict = await callTool(client, 'get_orphaned_entities', { strict: true });
|
|
466
|
+
expect(strict.items).toHaveLength(2);
|
|
467
|
+
const names = strict.items.map(e => e.name).sort();
|
|
468
|
+
expect(names).toEqual(['Island1', 'Island2']);
|
|
469
|
+
});
|
|
444
470
|
it('should validate graph and report violations', async () => {
|
|
445
471
|
// Directly write invalid data to test validation
|
|
446
472
|
const invalidData = [
|
|
@@ -486,7 +512,7 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
486
512
|
const result = await callTool(client, 'sequentialthinking', {
|
|
487
513
|
observations: ['First thought observation']
|
|
488
514
|
});
|
|
489
|
-
expect(result.ctxId).toMatch(/^
|
|
515
|
+
expect(result.ctxId).toMatch(/^[0-9a-f]{24}$/);
|
|
490
516
|
});
|
|
491
517
|
it('should chain thoughts with relations', async () => {
|
|
492
518
|
// Create first thought
|
|
@@ -511,7 +537,7 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
511
537
|
previousCtxId: 'nonexistent_thought',
|
|
512
538
|
observations: ['Orphaned thought']
|
|
513
539
|
});
|
|
514
|
-
expect(result.ctxId).toMatch(/^
|
|
540
|
+
expect(result.ctxId).toMatch(/^[0-9a-f]{24}$/);
|
|
515
541
|
// Verify no relations were created
|
|
516
542
|
const neighbors = await callTool(client, 'get_neighbors', {
|
|
517
543
|
entityName: result.ctxId,
|