@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 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 based on query
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 between requested entities
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: 1)
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
- - No input required
161
- - Returns entities with no connections
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 and return missing entities referenced in relations
171
+ - Validate the knowledge graph
165
172
  - No input required
166
- - Returns list of missing entities
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
- const connectedEntityNames = new Set();
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
- connectedEntityNames.add(r.from);
390
- connectedEntityNames.add(r.to);
402
+ neighbors.get(r.from)?.add(r.to);
403
+ neighbors.get(r.to)?.add(r.from);
391
404
  });
392
- return graph.entities.filter(e => !connectedEntityNames.has(e.name));
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 = `thought_${now}_${Math.random().toString(36).substring(2, 8)}`;
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(/^thought_\d+_[a-z0-9]+$/);
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(/^thought_\d+_[a-z0-9]+$/);
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@levalicious/server-memory",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "MCP server for enabling memory for Claude through a knowledge graph",
5
5
  "license": "MIT",
6
6
  "author": "Levalicious",