@levalicious/server-memory 0.0.11 → 0.0.12

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.
@@ -44,9 +44,10 @@ describe('MCP Memory Server E2E Tests', () => {
44
44
  await callTool(client, 'create_entities', {
45
45
  entities: [{ name: 'Alice', entityType: 'Person', observations: ['First'] }]
46
46
  });
47
+ // Exact same entity should be silently skipped
47
48
  const result = await callTool(client, 'create_entities', {
48
49
  entities: [
49
- { name: 'Alice', entityType: 'Person', observations: ['Second'] },
50
+ { name: 'Alice', entityType: 'Person', observations: ['First'] },
50
51
  { name: 'Bob', entityType: 'Person', observations: ['New'] }
51
52
  ]
52
53
  });
@@ -54,6 +55,19 @@ describe('MCP Memory Server E2E Tests', () => {
54
55
  expect(result).toHaveLength(1);
55
56
  expect(result[0].name).toBe('Bob');
56
57
  });
58
+ it('should error on duplicate name with different data', async () => {
59
+ await callTool(client, 'create_entities', {
60
+ entities: [{ name: 'Alice', entityType: 'Person', observations: ['First'] }]
61
+ });
62
+ // Same name, different type — should error
63
+ await expect(callTool(client, 'create_entities', {
64
+ entities: [{ name: 'Alice', entityType: 'Organization', observations: ['First'] }]
65
+ })).rejects.toThrow(/already exists/);
66
+ // Same name, different observations — should error
67
+ await expect(callTool(client, 'create_entities', {
68
+ entities: [{ name: 'Alice', entityType: 'Person', observations: ['Different'] }]
69
+ })).rejects.toThrow(/already exists/);
70
+ });
57
71
  it('should reject entities with more than 2 observations', async () => {
58
72
  await expect(callTool(client, 'create_entities', {
59
73
  entities: [{
@@ -257,13 +271,6 @@ describe('MCP Memory Server E2E Tests', () => {
257
271
  // A->B and A->C both have from='A' which is in the set
258
272
  expect(result.relations.items).toHaveLength(2);
259
273
  });
260
- it('should open nodes filtered (only internal relations)', async () => {
261
- const result = await callTool(client, 'open_nodes_filtered', {
262
- names: ['B', 'C']
263
- });
264
- expect(result.entities.items).toHaveLength(2);
265
- expect(result.relations.items).toHaveLength(0); // No relations between B and C
266
- });
267
274
  });
268
275
  describe('Graph Traversal', () => {
269
276
  beforeEach(async () => {
@@ -446,15 +453,21 @@ describe('MCP Memory Server E2E Tests', () => {
446
453
  const names = strict.items.map(e => e.name).sort();
447
454
  expect(names).toEqual(['Island1', 'Island2']);
448
455
  });
449
- it('should validate graph and report violations', async () => {
450
- // Directly write invalid data to test validation
451
- const invalidData = [
452
- JSON.stringify({ type: 'entity', name: 'Valid', entityType: 'Test', observations: [] }),
453
- JSON.stringify({ type: 'relation', from: 'Valid', to: 'Missing', relationType: 'refs' })
454
- ].join('\n');
455
- await fs.writeFile(memoryFile, invalidData);
456
+ it('should validate graph and report no violations on clean graph', async () => {
457
+ // Create a valid graph through the API
458
+ await callTool(client, 'create_entities', {
459
+ entities: [
460
+ { name: 'Valid', entityType: 'Test', observations: [] },
461
+ { name: 'Also_Valid', entityType: 'Test', observations: ['Short obs'] }
462
+ ]
463
+ });
464
+ await callTool(client, 'create_relations', {
465
+ relations: [{ from: 'Valid', to: 'Also_Valid', relationType: 'refs' }]
466
+ });
456
467
  const result = await callTool(client, 'validate_graph', {});
457
- expect(result.missingEntities).toContain('Missing');
468
+ // Binary store enforces referential integrity — no missing entities possible
469
+ expect(result.missingEntities).toHaveLength(0);
470
+ expect(result.observationViolations).toHaveLength(0);
458
471
  });
459
472
  });
460
473
  describe('Sequential Thinking', () => {
@@ -620,12 +633,16 @@ describe('MCP Memory Server E2E Tests', () => {
620
633
  { name: 'Gamma', entityType: 'Letter', observations: ['Third letter'] }
621
634
  ]);
622
635
  });
623
- it('should preserve insertion order when sortBy is omitted', async () => {
636
+ it('should use llmrank as default sort when sortBy is omitted', async () => {
624
637
  const result = await callTool(client, 'search_nodes', {
625
638
  query: 'Letter'
626
639
  });
640
+ // With llmrank default, all entities returned (order varies due to random tiebreak)
627
641
  const names = result.entities.items.map(e => e.name);
628
- expect(names).toEqual(['Alpha', 'Beta', 'Gamma']);
642
+ expect(names).toHaveLength(3);
643
+ expect(names).toContain('Alpha');
644
+ expect(names).toContain('Beta');
645
+ expect(names).toContain('Gamma');
629
646
  });
630
647
  it('should sort by name ascending', async () => {
631
648
  const result = await callTool(client, 'search_nodes', {
@@ -689,12 +706,16 @@ describe('MCP Memory Server E2E Tests', () => {
689
706
  { name: 'Monkey', entityType: 'Animal', observations: ['Clever'] }
690
707
  ]);
691
708
  });
692
- it('should preserve insertion order when sortBy is omitted', async () => {
709
+ it('should use llmrank as default sort when sortBy is omitted', async () => {
693
710
  const result = await callTool(client, 'get_entities_by_type', {
694
711
  entityType: 'Animal'
695
712
  });
713
+ // With llmrank default, all entities returned (order varies due to random tiebreak)
696
714
  const names = result.items.map(e => e.name);
697
- expect(names).toEqual(['Zebra', 'Aardvark', 'Monkey']);
715
+ expect(names).toHaveLength(3);
716
+ expect(names).toContain('Zebra');
717
+ expect(names).toContain('Aardvark');
718
+ expect(names).toContain('Monkey');
698
719
  });
699
720
  it('should sort by name ascending (default for name)', async () => {
700
721
  const result = await callTool(client, 'get_entities_by_type', {
@@ -742,10 +763,14 @@ describe('MCP Memory Server E2E Tests', () => {
742
763
  { name: 'Orphan_M', entityType: 'Orphan', observations: ['Isolated'] }
743
764
  ]);
744
765
  });
745
- it('should preserve insertion order when sortBy is omitted', async () => {
766
+ it('should use llmrank as default sort when sortBy is omitted', async () => {
746
767
  const result = await callTool(client, 'get_orphaned_entities', {});
768
+ // With llmrank default, all entities returned (order varies due to random tiebreak)
747
769
  const names = result.items.map(e => e.name);
748
- expect(names).toEqual(['Orphan_Z', 'Orphan_A', 'Orphan_M']);
770
+ expect(names).toHaveLength(3);
771
+ expect(names).toContain('Orphan_Z');
772
+ expect(names).toContain('Orphan_A');
773
+ expect(names).toContain('Orphan_M');
749
774
  });
750
775
  it('should sort by name ascending', async () => {
751
776
  const result = await callTool(client, 'get_orphaned_entities', {
@@ -807,12 +832,12 @@ describe('MCP Memory Server E2E Tests', () => {
807
832
  ]
808
833
  });
809
834
  });
810
- it('should return unsorted neighbors when sortBy is omitted', async () => {
835
+ it('should use llmrank as default sort when sortBy is omitted', async () => {
811
836
  const result = await callTool(client, 'get_neighbors', {
812
837
  entityName: 'Hub'
813
838
  });
839
+ // With llmrank default, all neighbors returned (order varies due to random tiebreak)
814
840
  expect(result.items).toHaveLength(3);
815
- // Just verify all neighbors are present
816
841
  const names = result.items.map(n => n.name);
817
842
  expect(names).toContain('Neighbor_Z');
818
843
  expect(names).toContain('Neighbor_A');
@@ -915,5 +940,91 @@ describe('MCP Memory Server E2E Tests', () => {
915
940
  expect(names).toEqual(sortedNames);
916
941
  });
917
942
  });
943
+ describe('pagerank sorting', () => {
944
+ it('should sort by pagerank (structural rank)', async () => {
945
+ // Build a star graph: Hub -> A, Hub -> B, Hub -> C
946
+ // A, B, C are dangling nodes
947
+ await callTool(client, 'create_entities', {
948
+ entities: [
949
+ { name: 'Hub', entityType: 'Node', observations: ['Central node'] },
950
+ { name: 'LeafA', entityType: 'Node', observations: ['Leaf A'] },
951
+ { name: 'LeafB', entityType: 'Node', observations: ['Leaf B'] },
952
+ { name: 'LeafC', entityType: 'Node', observations: ['Leaf C'] },
953
+ ]
954
+ });
955
+ await callTool(client, 'create_relations', {
956
+ relations: [
957
+ { from: 'Hub', to: 'LeafA', relationType: 'LINKS' },
958
+ { from: 'Hub', to: 'LeafB', relationType: 'LINKS' },
959
+ { from: 'Hub', to: 'LeafC', relationType: 'LINKS' },
960
+ ]
961
+ });
962
+ // Sort by pagerank descending (default for ranks)
963
+ const result = await callTool(client, 'search_nodes', {
964
+ query: 'Node',
965
+ sortBy: 'pagerank'
966
+ });
967
+ // All entities should be returned
968
+ expect(result.entities.items).toHaveLength(4);
969
+ // With structural rank, the leaves should rank higher than the hub
970
+ // because they receive visits from Hub's walks.
971
+ // We just verify the sort works and returns all entities.
972
+ const names = result.entities.items.map(e => e.name);
973
+ expect(names).toContain('Hub');
974
+ expect(names).toContain('LeafA');
975
+ expect(names).toContain('LeafB');
976
+ expect(names).toContain('LeafC');
977
+ });
978
+ });
979
+ describe('llmrank sorting', () => {
980
+ it('should sort by llmrank (walker visits)', async () => {
981
+ await callTool(client, 'create_entities', {
982
+ entities: [
983
+ { name: 'Hot', entityType: 'Test', observations: ['Frequently accessed'] },
984
+ { name: 'Cold', entityType: 'Test', observations: ['Rarely accessed'] },
985
+ ]
986
+ });
987
+ // Access 'Hot' multiple times via open_nodes (which increments walker visits)
988
+ await callTool(client, 'open_nodes', { names: ['Hot'] });
989
+ await callTool(client, 'open_nodes', { names: ['Hot'] });
990
+ await callTool(client, 'open_nodes', { names: ['Hot'] });
991
+ await callTool(client, 'open_nodes', { names: ['Hot'] });
992
+ await callTool(client, 'open_nodes', { names: ['Hot'] });
993
+ // Access 'Cold' just once
994
+ await callTool(client, 'open_nodes', { names: ['Cold'] });
995
+ // Sort by llmrank descending
996
+ const result = await callTool(client, 'search_nodes', {
997
+ query: 'Test',
998
+ sortBy: 'llmrank'
999
+ });
1000
+ // 'Hot' should rank higher than 'Cold' due to more walker visits
1001
+ expect(result.entities.items).toHaveLength(2);
1002
+ expect(result.entities.items[0].name).toBe('Hot');
1003
+ expect(result.entities.items[1].name).toBe('Cold');
1004
+ });
1005
+ it('should fall back to pagerank on llmrank tie', async () => {
1006
+ // Create entities with no prior walker visits
1007
+ await callTool(client, 'create_entities', {
1008
+ entities: [
1009
+ { name: 'Center', entityType: 'Fallback', observations: ['Hub'] },
1010
+ { name: 'Spoke', entityType: 'Fallback', observations: ['Leaf'] },
1011
+ ]
1012
+ });
1013
+ // Create a relation so structural rank differs
1014
+ await callTool(client, 'create_relations', {
1015
+ relations: [{ from: 'Center', to: 'Spoke', relationType: 'POINTS_TO' }]
1016
+ });
1017
+ // Both have 0 walker visits (tie), so llmrank should fall back to pagerank
1018
+ const result = await callTool(client, 'search_nodes', {
1019
+ query: 'Fallback',
1020
+ sortBy: 'llmrank'
1021
+ });
1022
+ // Just verify we get both entities (ordering depends on structural rank + random tiebreak)
1023
+ expect(result.entities.items).toHaveLength(2);
1024
+ const names = result.entities.items.map(e => e.name);
1025
+ expect(names).toContain('Center');
1026
+ expect(names).toContain('Spoke');
1027
+ });
1028
+ });
918
1029
  });
919
1030
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@levalicious/server-memory",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
4
4
  "description": "MCP server for enabling memory for Claude through a knowledge graph",
5
5
  "license": "MIT",
6
6
  "author": "Levalicious",
@@ -14,13 +14,14 @@
14
14
  "dist"
15
15
  ],
16
16
  "scripts": {
17
- "build": "tsc && shx chmod +x dist/*.js",
17
+ "build": "node-gyp rebuild && tsc && shx chmod +x dist/*.js",
18
+ "build:native": "node-gyp rebuild",
18
19
  "prepare": "husky && npm run build",
19
20
  "watch": "tsc --watch",
20
21
  "test": "NODE_OPTIONS='--experimental-vm-modules' jest"
21
22
  },
22
23
  "dependencies": {
23
- "@modelcontextprotocol/sdk": "1.25.1",
24
+ "@modelcontextprotocol/sdk": "1.26.0",
24
25
  "proper-lockfile": "^4.1.2"
25
26
  },
26
27
  "devDependencies": {