@danielsimonjr/memory-mcp 0.7.2 → 0.41.0

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.
Files changed (61) hide show
  1. package/dist/__tests__/edge-cases/edge-cases.test.js +406 -0
  2. package/dist/__tests__/file-path.test.js +5 -5
  3. package/dist/__tests__/integration/workflows.test.js +449 -0
  4. package/dist/__tests__/knowledge-graph.test.js +8 -3
  5. package/dist/__tests__/performance/benchmarks.test.js +411 -0
  6. package/dist/__tests__/unit/core/EntityManager.test.js +334 -0
  7. package/dist/__tests__/unit/core/GraphStorage.test.js +205 -0
  8. package/dist/__tests__/unit/core/RelationManager.test.js +274 -0
  9. package/dist/__tests__/unit/features/CompressionManager.test.js +350 -0
  10. package/dist/__tests__/unit/search/BasicSearch.test.js +311 -0
  11. package/dist/__tests__/unit/search/BooleanSearch.test.js +432 -0
  12. package/dist/__tests__/unit/search/FuzzySearch.test.js +448 -0
  13. package/dist/__tests__/unit/search/RankedSearch.test.js +379 -0
  14. package/dist/__tests__/unit/utils/levenshtein.test.js +77 -0
  15. package/dist/core/EntityManager.js +554 -0
  16. package/dist/core/GraphStorage.js +172 -0
  17. package/dist/core/KnowledgeGraphManager.js +400 -0
  18. package/dist/core/ObservationManager.js +129 -0
  19. package/dist/core/RelationManager.js +186 -0
  20. package/dist/core/TransactionManager.js +389 -0
  21. package/dist/core/index.js +9 -0
  22. package/dist/features/AnalyticsManager.js +222 -0
  23. package/dist/features/ArchiveManager.js +74 -0
  24. package/dist/features/BackupManager.js +311 -0
  25. package/dist/features/CompressionManager.js +310 -0
  26. package/dist/features/ExportManager.js +305 -0
  27. package/dist/features/HierarchyManager.js +219 -0
  28. package/dist/features/ImportExportManager.js +50 -0
  29. package/dist/features/ImportManager.js +328 -0
  30. package/dist/features/TagManager.js +210 -0
  31. package/dist/features/index.js +12 -0
  32. package/dist/index.js +13 -996
  33. package/dist/memory.jsonl +225 -0
  34. package/dist/search/BasicSearch.js +161 -0
  35. package/dist/search/BooleanSearch.js +304 -0
  36. package/dist/search/FuzzySearch.js +115 -0
  37. package/dist/search/RankedSearch.js +206 -0
  38. package/dist/search/SavedSearchManager.js +145 -0
  39. package/dist/search/SearchManager.js +305 -0
  40. package/dist/search/SearchSuggestions.js +57 -0
  41. package/dist/search/TFIDFIndexManager.js +217 -0
  42. package/dist/search/index.js +10 -0
  43. package/dist/server/MCPServer.js +889 -0
  44. package/dist/types/analytics.types.js +6 -0
  45. package/dist/types/entity.types.js +7 -0
  46. package/dist/types/import-export.types.js +7 -0
  47. package/dist/types/index.js +12 -0
  48. package/dist/types/search.types.js +7 -0
  49. package/dist/types/tag.types.js +6 -0
  50. package/dist/utils/constants.js +127 -0
  51. package/dist/utils/dateUtils.js +89 -0
  52. package/dist/utils/errors.js +121 -0
  53. package/dist/utils/index.js +13 -0
  54. package/dist/utils/levenshtein.js +62 -0
  55. package/dist/utils/logger.js +33 -0
  56. package/dist/utils/pathUtils.js +115 -0
  57. package/dist/utils/schemas.js +184 -0
  58. package/dist/utils/searchCache.js +209 -0
  59. package/dist/utils/tfidf.js +90 -0
  60. package/dist/utils/validationUtils.js +109 -0
  61. package/package.json +50 -48
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
2
  import { promises as fs } from 'fs';
6
3
  import path from 'path';
7
4
  import { fileURLToPath } from 'url';
5
+ import { logger } from './utils/logger.js';
6
+ import { KnowledgeGraphManager } from './core/KnowledgeGraphManager.js';
7
+ import { MCPServer } from './server/MCPServer.js';
8
8
  // Define memory file path using environment variable with fallback
9
9
  export const defaultMemoryPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'memory.jsonl');
10
10
  // Handle backward compatibility: migrate memory.json to memory.jsonl if needed
@@ -28,9 +28,9 @@ export async function ensureMemoryFilePath() {
28
28
  }
29
29
  catch {
30
30
  // Old file exists, new file doesn't - migrate
31
- console.error('DETECTED: Found legacy memory.json file, migrating to memory.jsonl for JSONL format compatibility');
31
+ logger.info('Found legacy memory.json file, migrating to memory.jsonl for JSONL format compatibility');
32
32
  await fs.rename(oldMemoryPath, newMemoryPath);
33
- console.error('COMPLETED: Successfully migrated memory.json to memory.jsonl');
33
+ logger.info('Successfully migrated memory.json to memory.jsonl');
34
34
  return newMemoryPath;
35
35
  }
36
36
  }
@@ -39,1002 +39,19 @@ export async function ensureMemoryFilePath() {
39
39
  return newMemoryPath;
40
40
  }
41
41
  }
42
- // Initialize memory file path (will be set during startup)
43
- let MEMORY_FILE_PATH;
44
- // The KnowledgeGraphManager class contains all operations to interact with the knowledge graph
45
- export class KnowledgeGraphManager {
46
- memoryFilePath;
47
- constructor(memoryFilePath) {
48
- this.memoryFilePath = memoryFilePath;
49
- }
50
- async loadGraph() {
51
- try {
52
- const data = await fs.readFile(this.memoryFilePath, "utf-8");
53
- const lines = data.split("\n").filter(line => line.trim() !== "");
54
- return lines.reduce((graph, line) => {
55
- const item = JSON.parse(line);
56
- if (item.type === "entity") {
57
- // Add createdAt if missing for backward compatibility
58
- if (!item.createdAt)
59
- item.createdAt = new Date().toISOString();
60
- // Add lastModified if missing for backward compatibility
61
- if (!item.lastModified)
62
- item.lastModified = item.createdAt;
63
- // Phase 3: Backward compatibility for tags and importance
64
- // These fields are optional and will be undefined if not present
65
- graph.entities.push(item);
66
- }
67
- if (item.type === "relation") {
68
- // Add createdAt if missing for backward compatibility
69
- if (!item.createdAt)
70
- item.createdAt = new Date().toISOString();
71
- // Add lastModified if missing for backward compatibility
72
- if (!item.lastModified)
73
- item.lastModified = item.createdAt;
74
- graph.relations.push(item);
75
- }
76
- return graph;
77
- }, { entities: [], relations: [] });
78
- }
79
- catch (error) {
80
- if (error instanceof Error && 'code' in error && error.code === "ENOENT") {
81
- return { entities: [], relations: [] };
82
- }
83
- throw error;
84
- }
85
- }
86
- async saveGraph(graph) {
87
- const lines = [
88
- ...graph.entities.map(e => {
89
- const entityData = {
90
- type: "entity",
91
- name: e.name,
92
- entityType: e.entityType,
93
- observations: e.observations,
94
- createdAt: e.createdAt,
95
- lastModified: e.lastModified
96
- };
97
- // Phase 3: Only include tags and importance if they exist
98
- if (e.tags !== undefined)
99
- entityData.tags = e.tags;
100
- if (e.importance !== undefined)
101
- entityData.importance = e.importance;
102
- return JSON.stringify(entityData);
103
- }),
104
- ...graph.relations.map(r => JSON.stringify({
105
- type: "relation",
106
- from: r.from,
107
- to: r.to,
108
- relationType: r.relationType,
109
- createdAt: r.createdAt,
110
- lastModified: r.lastModified
111
- })),
112
- ];
113
- await fs.writeFile(this.memoryFilePath, lines.join("\n"));
114
- }
115
- /**
116
- * Phase 4: Create multiple entities in a single batch operation.
117
- * Batch optimization: All entities are processed and saved in a single saveGraph() call,
118
- * minimizing disk I/O. This is significantly more efficient than creating entities one at a time.
119
- */
120
- async createEntities(entities) {
121
- const graph = await this.loadGraph();
122
- const timestamp = new Date().toISOString();
123
- const newEntities = entities
124
- .filter(e => !graph.entities.some(existingEntity => existingEntity.name === e.name))
125
- .map(e => {
126
- const entity = {
127
- ...e,
128
- createdAt: e.createdAt || timestamp,
129
- lastModified: e.lastModified || timestamp
130
- };
131
- // Phase 3: Normalize tags to lowercase if provided
132
- if (e.tags) {
133
- entity.tags = e.tags.map(tag => tag.toLowerCase());
134
- }
135
- // Phase 3: Validate importance if provided
136
- if (e.importance !== undefined) {
137
- if (e.importance < 0 || e.importance > 10) {
138
- throw new Error(`Importance must be between 0 and 10, got ${e.importance}`);
139
- }
140
- entity.importance = e.importance;
141
- }
142
- return entity;
143
- });
144
- graph.entities.push(...newEntities);
145
- // Phase 4: Single save operation for all entities ensures batch efficiency
146
- await this.saveGraph(graph);
147
- return newEntities;
148
- }
149
- /**
150
- * Phase 4: Create multiple relations in a single batch operation.
151
- * Batch optimization: All relations are processed and saved in a single saveGraph() call,
152
- * minimizing disk I/O. This is significantly more efficient than creating relations one at a time.
153
- */
154
- async createRelations(relations) {
155
- const graph = await this.loadGraph();
156
- const timestamp = new Date().toISOString();
157
- const newRelations = relations
158
- .filter(r => !graph.relations.some(existingRelation => existingRelation.from === r.from &&
159
- existingRelation.to === r.to &&
160
- existingRelation.relationType === r.relationType))
161
- .map(r => ({ ...r, createdAt: r.createdAt || timestamp, lastModified: r.lastModified || timestamp }));
162
- graph.relations.push(...newRelations);
163
- // Phase 4: Single save operation for all relations ensures batch efficiency
164
- await this.saveGraph(graph);
165
- return newRelations;
166
- }
167
- async addObservations(observations) {
168
- const graph = await this.loadGraph();
169
- const timestamp = new Date().toISOString();
170
- const results = observations.map(o => {
171
- const entity = graph.entities.find(e => e.name === o.entityName);
172
- if (!entity) {
173
- throw new Error(`Entity with name ${o.entityName} not found`);
174
- }
175
- const newObservations = o.contents.filter(content => !entity.observations.includes(content));
176
- entity.observations.push(...newObservations);
177
- // Update lastModified timestamp if observations were added
178
- if (newObservations.length > 0) {
179
- entity.lastModified = timestamp;
180
- }
181
- return { entityName: o.entityName, addedObservations: newObservations };
182
- });
183
- await this.saveGraph(graph);
184
- return results;
185
- }
186
- async deleteEntities(entityNames) {
187
- const graph = await this.loadGraph();
188
- graph.entities = graph.entities.filter(e => !entityNames.includes(e.name));
189
- graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to));
190
- await this.saveGraph(graph);
191
- }
192
- async deleteObservations(deletions) {
193
- const graph = await this.loadGraph();
194
- const timestamp = new Date().toISOString();
195
- deletions.forEach(d => {
196
- const entity = graph.entities.find(e => e.name === d.entityName);
197
- if (entity) {
198
- const originalLength = entity.observations.length;
199
- entity.observations = entity.observations.filter(o => !d.observations.includes(o));
200
- // Update lastModified timestamp if observations were deleted
201
- if (entity.observations.length < originalLength) {
202
- entity.lastModified = timestamp;
203
- }
204
- }
205
- });
206
- await this.saveGraph(graph);
207
- }
208
- async deleteRelations(relations) {
209
- const graph = await this.loadGraph();
210
- const timestamp = new Date().toISOString();
211
- // Track which entities are affected by relation deletions
212
- const affectedEntityNames = new Set();
213
- relations.forEach(rel => {
214
- affectedEntityNames.add(rel.from);
215
- affectedEntityNames.add(rel.to);
216
- });
217
- graph.relations = graph.relations.filter(r => !relations.some(delRelation => r.from === delRelation.from &&
218
- r.to === delRelation.to &&
219
- r.relationType === delRelation.relationType));
220
- // Update lastModified for affected entities
221
- graph.entities.forEach(entity => {
222
- if (affectedEntityNames.has(entity.name)) {
223
- entity.lastModified = timestamp;
224
- }
225
- });
226
- await this.saveGraph(graph);
227
- }
228
- async readGraph() {
229
- return this.loadGraph();
230
- }
231
- // Phase 3: Enhanced search function with tags and importance filters
232
- async searchNodes(query, tags, minImportance, maxImportance) {
233
- const graph = await this.loadGraph();
234
- // Normalize tags to lowercase for case-insensitive matching
235
- const normalizedTags = tags?.map(tag => tag.toLowerCase());
236
- // Filter entities
237
- const filteredEntities = graph.entities.filter(e => {
238
- // Text search
239
- const matchesQuery = e.name.toLowerCase().includes(query.toLowerCase()) ||
240
- e.entityType.toLowerCase().includes(query.toLowerCase()) ||
241
- e.observations.some(o => o.toLowerCase().includes(query.toLowerCase()));
242
- if (!matchesQuery)
243
- return false;
244
- // Phase 3: Tag filter
245
- if (normalizedTags && normalizedTags.length > 0) {
246
- if (!e.tags || e.tags.length === 0)
247
- return false;
248
- const entityTags = e.tags.map(tag => tag.toLowerCase());
249
- const hasMatchingTag = normalizedTags.some(tag => entityTags.includes(tag));
250
- if (!hasMatchingTag)
251
- return false;
252
- }
253
- // Phase 3: Importance filter
254
- if (minImportance !== undefined && (e.importance === undefined || e.importance < minImportance)) {
255
- return false;
256
- }
257
- if (maxImportance !== undefined && (e.importance === undefined || e.importance > maxImportance)) {
258
- return false;
259
- }
260
- return true;
261
- });
262
- // Create a Set of filtered entity names for quick lookup
263
- const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
264
- // Filter relations to only include those between filtered entities
265
- const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
266
- const filteredGraph = {
267
- entities: filteredEntities,
268
- relations: filteredRelations,
269
- };
270
- return filteredGraph;
271
- }
272
- async openNodes(names) {
273
- const graph = await this.loadGraph();
274
- // Filter entities
275
- const filteredEntities = graph.entities.filter(e => names.includes(e.name));
276
- // Create a Set of filtered entity names for quick lookup
277
- const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
278
- // Filter relations to only include those between filtered entities
279
- const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
280
- const filteredGraph = {
281
- entities: filteredEntities,
282
- relations: filteredRelations,
283
- };
284
- return filteredGraph;
285
- }
286
- // Phase 3: Enhanced searchByDateRange with tags filter
287
- async searchByDateRange(startDate, endDate, entityType, tags) {
288
- const graph = await this.loadGraph();
289
- const start = startDate ? new Date(startDate) : null;
290
- const end = endDate ? new Date(endDate) : null;
291
- // Normalize tags to lowercase for case-insensitive matching
292
- const normalizedTags = tags?.map(tag => tag.toLowerCase());
293
- // Filter entities by date range and optionally by entity type and tags
294
- const filteredEntities = graph.entities.filter(e => {
295
- // Check entity type filter
296
- if (entityType && e.entityType !== entityType) {
297
- return false;
298
- }
299
- // Phase 3: Tag filter
300
- if (normalizedTags && normalizedTags.length > 0) {
301
- if (!e.tags || e.tags.length === 0)
302
- return false;
303
- const entityTags = e.tags.map(tag => tag.toLowerCase());
304
- const hasMatchingTag = normalizedTags.some(tag => entityTags.includes(tag));
305
- if (!hasMatchingTag)
306
- return false;
307
- }
308
- // Check date range using createdAt or lastModified
309
- const entityDate = new Date(e.lastModified || e.createdAt || '');
310
- if (start && entityDate < start) {
311
- return false;
312
- }
313
- if (end && entityDate > end) {
314
- return false;
315
- }
316
- return true;
317
- });
318
- // Create a Set of filtered entity names for quick lookup
319
- const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
320
- // Filter relations by date range and only include those between filtered entities
321
- const filteredRelations = graph.relations.filter(r => {
322
- // Must be between filtered entities
323
- if (!filteredEntityNames.has(r.from) || !filteredEntityNames.has(r.to)) {
324
- return false;
325
- }
326
- // Check date range using createdAt or lastModified
327
- const relationDate = new Date(r.lastModified || r.createdAt || '');
328
- if (start && relationDate < start) {
329
- return false;
330
- }
331
- if (end && relationDate > end) {
332
- return false;
333
- }
334
- return true;
335
- });
336
- return {
337
- entities: filteredEntities,
338
- relations: filteredRelations,
339
- };
340
- }
341
- async getGraphStats() {
342
- const graph = await this.loadGraph();
343
- // Calculate entity type counts
344
- const entityTypesCounts = {};
345
- graph.entities.forEach(e => {
346
- entityTypesCounts[e.entityType] = (entityTypesCounts[e.entityType] || 0) + 1;
347
- });
348
- // Calculate relation type counts
349
- const relationTypesCounts = {};
350
- graph.relations.forEach(r => {
351
- relationTypesCounts[r.relationType] = (relationTypesCounts[r.relationType] || 0) + 1;
352
- });
353
- // Find oldest and newest entities
354
- let oldestEntity;
355
- let newestEntity;
356
- let earliestEntityDate = null;
357
- let latestEntityDate = null;
358
- graph.entities.forEach(e => {
359
- const date = new Date(e.createdAt || '');
360
- if (!earliestEntityDate || date < earliestEntityDate) {
361
- earliestEntityDate = date;
362
- oldestEntity = { name: e.name, date: e.createdAt || '' };
363
- }
364
- if (!latestEntityDate || date > latestEntityDate) {
365
- latestEntityDate = date;
366
- newestEntity = { name: e.name, date: e.createdAt || '' };
367
- }
368
- });
369
- // Find oldest and newest relations
370
- let oldestRelation;
371
- let newestRelation;
372
- let earliestRelationDate = null;
373
- let latestRelationDate = null;
374
- graph.relations.forEach(r => {
375
- const date = new Date(r.createdAt || '');
376
- if (!earliestRelationDate || date < earliestRelationDate) {
377
- earliestRelationDate = date;
378
- oldestRelation = { from: r.from, to: r.to, relationType: r.relationType, date: r.createdAt || '' };
379
- }
380
- if (!latestRelationDate || date > latestRelationDate) {
381
- latestRelationDate = date;
382
- newestRelation = { from: r.from, to: r.to, relationType: r.relationType, date: r.createdAt || '' };
383
- }
384
- });
385
- return {
386
- totalEntities: graph.entities.length,
387
- totalRelations: graph.relations.length,
388
- entityTypesCounts,
389
- relationTypesCounts,
390
- oldestEntity,
391
- newestEntity,
392
- oldestRelation,
393
- newestRelation,
394
- entityDateRange: earliestEntityDate && latestEntityDate ? {
395
- earliest: earliestEntityDate.toISOString(),
396
- latest: latestEntityDate.toISOString()
397
- } : undefined,
398
- relationDateRange: earliestRelationDate && latestRelationDate ? {
399
- earliest: earliestRelationDate.toISOString(),
400
- latest: latestRelationDate.toISOString()
401
- } : undefined,
402
- };
403
- }
404
- // Phase 3: Add tags to an entity
405
- async addTags(entityName, tags) {
406
- const graph = await this.loadGraph();
407
- const timestamp = new Date().toISOString();
408
- const entity = graph.entities.find(e => e.name === entityName);
409
- if (!entity) {
410
- throw new Error(`Entity with name ${entityName} not found`);
411
- }
412
- // Initialize tags array if it doesn't exist
413
- if (!entity.tags) {
414
- entity.tags = [];
415
- }
416
- // Normalize tags to lowercase and filter out duplicates
417
- const normalizedTags = tags.map(tag => tag.toLowerCase());
418
- const newTags = normalizedTags.filter(tag => !entity.tags.includes(tag));
419
- entity.tags.push(...newTags);
420
- // Update lastModified timestamp if tags were added
421
- if (newTags.length > 0) {
422
- entity.lastModified = timestamp;
423
- }
424
- await this.saveGraph(graph);
425
- return { entityName, addedTags: newTags };
426
- }
427
- // Phase 3: Remove tags from an entity
428
- async removeTags(entityName, tags) {
429
- const graph = await this.loadGraph();
430
- const timestamp = new Date().toISOString();
431
- const entity = graph.entities.find(e => e.name === entityName);
432
- if (!entity) {
433
- throw new Error(`Entity with name ${entityName} not found`);
434
- }
435
- if (!entity.tags) {
436
- return { entityName, removedTags: [] };
437
- }
438
- // Normalize tags to lowercase
439
- const normalizedTags = tags.map(tag => tag.toLowerCase());
440
- const originalLength = entity.tags.length;
441
- // Filter out the tags to remove
442
- entity.tags = entity.tags.filter(tag => !normalizedTags.includes(tag.toLowerCase()));
443
- const removedTags = normalizedTags.filter(tag => originalLength > entity.tags.length ||
444
- !entity.tags.map(t => t.toLowerCase()).includes(tag));
445
- // Update lastModified timestamp if tags were removed
446
- if (entity.tags.length < originalLength) {
447
- entity.lastModified = timestamp;
448
- }
449
- await this.saveGraph(graph);
450
- return { entityName, removedTags };
451
- }
452
- // Phase 3: Set importance level for an entity
453
- async setImportance(entityName, importance) {
454
- const graph = await this.loadGraph();
455
- const timestamp = new Date().toISOString();
456
- // Validate importance range
457
- if (importance < 0 || importance > 10) {
458
- throw new Error(`Importance must be between 0 and 10, got ${importance}`);
459
- }
460
- const entity = graph.entities.find(e => e.name === entityName);
461
- if (!entity) {
462
- throw new Error(`Entity with name ${entityName} not found`);
463
- }
464
- entity.importance = importance;
465
- entity.lastModified = timestamp;
466
- await this.saveGraph(graph);
467
- return { entityName, importance };
468
- }
469
- // Phase 4: Export graph in various formats
470
- /**
471
- * Export the knowledge graph in the specified format with optional filtering.
472
- * Supports JSON, CSV, and GraphML formats for different use cases.
473
- *
474
- * @param format - Export format: 'json', 'csv', or 'graphml'
475
- * @param filter - Optional filter object with same structure as searchByDateRange
476
- * @returns Exported graph data as a formatted string
477
- */
478
- async exportGraph(format, filter) {
479
- // Get filtered or full graph based on filter parameter
480
- let graph;
481
- if (filter) {
482
- graph = await this.searchByDateRange(filter.startDate, filter.endDate, filter.entityType, filter.tags);
483
- }
484
- else {
485
- graph = await this.loadGraph();
486
- }
487
- switch (format) {
488
- case 'json':
489
- return this.exportAsJson(graph);
490
- case 'csv':
491
- return this.exportAsCsv(graph);
492
- case 'graphml':
493
- return this.exportAsGraphML(graph);
494
- default:
495
- throw new Error(`Unsupported export format: ${format}`);
496
- }
497
- }
498
- /**
499
- * Export graph as pretty-printed JSON with all entity and relation data
500
- */
501
- exportAsJson(graph) {
502
- return JSON.stringify(graph, null, 2);
503
- }
504
- /**
505
- * Export graph as CSV with two sections: entities and relations
506
- * Uses proper escaping for fields containing commas, quotes, and newlines
507
- */
508
- exportAsCsv(graph) {
509
- const lines = [];
510
- // Helper function to escape CSV fields
511
- const escapeCsvField = (field) => {
512
- if (field === undefined || field === null)
513
- return '';
514
- const str = String(field);
515
- // Escape quotes by doubling them and wrap in quotes if contains comma, quote, or newline
516
- if (str.includes(',') || str.includes('"') || str.includes('\n')) {
517
- return `"${str.replace(/"/g, '""')}"`;
518
- }
519
- return str;
520
- };
521
- // Entities section
522
- lines.push('# ENTITIES');
523
- lines.push('name,entityType,observations,createdAt,lastModified,tags,importance');
524
- for (const entity of graph.entities) {
525
- const observationsStr = entity.observations.join('; ');
526
- const tagsStr = entity.tags ? entity.tags.join('; ') : '';
527
- const importanceStr = entity.importance !== undefined ? String(entity.importance) : '';
528
- lines.push([
529
- escapeCsvField(entity.name),
530
- escapeCsvField(entity.entityType),
531
- escapeCsvField(observationsStr),
532
- escapeCsvField(entity.createdAt),
533
- escapeCsvField(entity.lastModified),
534
- escapeCsvField(tagsStr),
535
- escapeCsvField(importanceStr)
536
- ].join(','));
537
- }
538
- // Relations section
539
- lines.push('');
540
- lines.push('# RELATIONS');
541
- lines.push('from,to,relationType,createdAt,lastModified');
542
- for (const relation of graph.relations) {
543
- lines.push([
544
- escapeCsvField(relation.from),
545
- escapeCsvField(relation.to),
546
- escapeCsvField(relation.relationType),
547
- escapeCsvField(relation.createdAt),
548
- escapeCsvField(relation.lastModified)
549
- ].join(','));
550
- }
551
- return lines.join('\n');
552
- }
553
- /**
554
- * Export graph as GraphML XML format for graph visualization tools
555
- * Compatible with Gephi, Cytoscape, yEd, and other graph analysis tools
556
- */
557
- exportAsGraphML(graph) {
558
- const lines = [];
559
- // Helper function to escape XML special characters
560
- const escapeXml = (str) => {
561
- if (str === undefined || str === null)
562
- return '';
563
- return String(str)
564
- .replace(/&/g, '&amp;')
565
- .replace(/</g, '&lt;')
566
- .replace(/>/g, '&gt;')
567
- .replace(/"/g, '&quot;')
568
- .replace(/'/g, '&apos;');
569
- };
570
- // GraphML header
571
- lines.push('<?xml version="1.0" encoding="UTF-8"?>');
572
- lines.push('<graphml xmlns="http://graphml.graphdrawing.org/xmlns"');
573
- lines.push(' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"');
574
- lines.push(' xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns');
575
- lines.push(' http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">');
576
- // Define node attributes (keys)
577
- lines.push(' <!-- Node attributes -->');
578
- lines.push(' <key id="d0" for="node" attr.name="entityType" attr.type="string"/>');
579
- lines.push(' <key id="d1" for="node" attr.name="observations" attr.type="string"/>');
580
- lines.push(' <key id="d2" for="node" attr.name="createdAt" attr.type="string"/>');
581
- lines.push(' <key id="d3" for="node" attr.name="lastModified" attr.type="string"/>');
582
- lines.push(' <key id="d4" for="node" attr.name="tags" attr.type="string"/>');
583
- lines.push(' <key id="d5" for="node" attr.name="importance" attr.type="double"/>');
584
- // Define edge attributes (keys)
585
- lines.push(' <!-- Edge attributes -->');
586
- lines.push(' <key id="e0" for="edge" attr.name="relationType" attr.type="string"/>');
587
- lines.push(' <key id="e1" for="edge" attr.name="createdAt" attr.type="string"/>');
588
- lines.push(' <key id="e2" for="edge" attr.name="lastModified" attr.type="string"/>');
589
- // Start graph (directed graph)
590
- lines.push(' <graph id="G" edgedefault="directed">');
591
- // Add nodes (entities)
592
- for (const entity of graph.entities) {
593
- // Use entity name as node ID (escape for XML attribute)
594
- const nodeId = escapeXml(entity.name);
595
- lines.push(` <node id="${nodeId}">`);
596
- lines.push(` <data key="d0">${escapeXml(entity.entityType)}</data>`);
597
- lines.push(` <data key="d1">${escapeXml(entity.observations.join('; '))}</data>`);
598
- if (entity.createdAt) {
599
- lines.push(` <data key="d2">${escapeXml(entity.createdAt)}</data>`);
600
- }
601
- if (entity.lastModified) {
602
- lines.push(` <data key="d3">${escapeXml(entity.lastModified)}</data>`);
603
- }
604
- if (entity.tags && entity.tags.length > 0) {
605
- lines.push(` <data key="d4">${escapeXml(entity.tags.join('; '))}</data>`);
606
- }
607
- if (entity.importance !== undefined) {
608
- lines.push(` <data key="d5">${entity.importance}</data>`);
609
- }
610
- lines.push(' </node>');
611
- }
612
- // Add edges (relations)
613
- let edgeId = 0;
614
- for (const relation of graph.relations) {
615
- const sourceId = escapeXml(relation.from);
616
- const targetId = escapeXml(relation.to);
617
- lines.push(` <edge id="e${edgeId}" source="${sourceId}" target="${targetId}">`);
618
- lines.push(` <data key="e0">${escapeXml(relation.relationType)}</data>`);
619
- if (relation.createdAt) {
620
- lines.push(` <data key="e1">${escapeXml(relation.createdAt)}</data>`);
621
- }
622
- if (relation.lastModified) {
623
- lines.push(` <data key="e2">${escapeXml(relation.lastModified)}</data>`);
624
- }
625
- lines.push(' </edge>');
626
- edgeId++;
627
- }
628
- // Close graph and graphml
629
- lines.push(' </graph>');
630
- lines.push('</graphml>');
631
- return lines.join('\n');
632
- }
633
- }
42
+ // Re-export KnowledgeGraphManager for backward compatibility
43
+ export { KnowledgeGraphManager };
634
44
  let knowledgeGraphManager;
635
- // The server instance and tools exposed to Claude
636
- const server = new Server({
637
- name: "memory-server",
638
- version: "0.7.0",
639
- }, {
640
- capabilities: {
641
- tools: {},
642
- },
643
- });
644
- server.setRequestHandler(ListToolsRequestSchema, async () => {
645
- return {
646
- tools: [
647
- {
648
- name: "create_entities",
649
- description: "Create multiple new entities in the knowledge graph",
650
- inputSchema: {
651
- type: "object",
652
- properties: {
653
- entities: {
654
- type: "array",
655
- items: {
656
- type: "object",
657
- properties: {
658
- name: { type: "string", description: "The name of the entity" },
659
- entityType: { type: "string", description: "The type of the entity" },
660
- observations: {
661
- type: "array",
662
- items: { type: "string" },
663
- description: "An array of observation contents associated with the entity"
664
- },
665
- },
666
- required: ["name", "entityType", "observations"],
667
- additionalProperties: false,
668
- },
669
- },
670
- },
671
- required: ["entities"],
672
- additionalProperties: false,
673
- },
674
- },
675
- {
676
- name: "create_relations",
677
- description: "Create multiple new relations between entities in the knowledge graph. Relations should be in active voice",
678
- inputSchema: {
679
- type: "object",
680
- properties: {
681
- relations: {
682
- type: "array",
683
- items: {
684
- type: "object",
685
- properties: {
686
- from: { type: "string", description: "The name of the entity where the relation starts" },
687
- to: { type: "string", description: "The name of the entity where the relation ends" },
688
- relationType: { type: "string", description: "The type of the relation" },
689
- },
690
- required: ["from", "to", "relationType"],
691
- additionalProperties: false,
692
- },
693
- },
694
- },
695
- required: ["relations"],
696
- additionalProperties: false,
697
- },
698
- },
699
- {
700
- name: "add_observations",
701
- description: "Add new observations to existing entities in the knowledge graph",
702
- inputSchema: {
703
- type: "object",
704
- properties: {
705
- observations: {
706
- type: "array",
707
- items: {
708
- type: "object",
709
- properties: {
710
- entityName: { type: "string", description: "The name of the entity to add the observations to" },
711
- contents: {
712
- type: "array",
713
- items: { type: "string" },
714
- description: "An array of observation contents to add"
715
- },
716
- },
717
- required: ["entityName", "contents"],
718
- additionalProperties: false,
719
- },
720
- },
721
- },
722
- required: ["observations"],
723
- additionalProperties: false,
724
- },
725
- },
726
- {
727
- name: "delete_entities",
728
- description: "Delete multiple entities and their associated relations from the knowledge graph",
729
- inputSchema: {
730
- type: "object",
731
- properties: {
732
- entityNames: {
733
- type: "array",
734
- items: { type: "string" },
735
- description: "An array of entity names to delete"
736
- },
737
- },
738
- required: ["entityNames"],
739
- additionalProperties: false,
740
- },
741
- },
742
- {
743
- name: "delete_observations",
744
- description: "Delete specific observations from entities in the knowledge graph",
745
- inputSchema: {
746
- type: "object",
747
- properties: {
748
- deletions: {
749
- type: "array",
750
- items: {
751
- type: "object",
752
- properties: {
753
- entityName: { type: "string", description: "The name of the entity containing the observations" },
754
- observations: {
755
- type: "array",
756
- items: { type: "string" },
757
- description: "An array of observations to delete"
758
- },
759
- },
760
- required: ["entityName", "observations"],
761
- additionalProperties: false,
762
- },
763
- },
764
- },
765
- required: ["deletions"],
766
- additionalProperties: false,
767
- },
768
- },
769
- {
770
- name: "delete_relations",
771
- description: "Delete multiple relations from the knowledge graph",
772
- inputSchema: {
773
- type: "object",
774
- properties: {
775
- relations: {
776
- type: "array",
777
- items: {
778
- type: "object",
779
- properties: {
780
- from: { type: "string", description: "The name of the entity where the relation starts" },
781
- to: { type: "string", description: "The name of the entity where the relation ends" },
782
- relationType: { type: "string", description: "The type of the relation" },
783
- },
784
- required: ["from", "to", "relationType"],
785
- additionalProperties: false,
786
- },
787
- description: "An array of relations to delete"
788
- },
789
- },
790
- required: ["relations"],
791
- additionalProperties: false,
792
- },
793
- },
794
- {
795
- name: "read_graph",
796
- description: "Read the entire knowledge graph",
797
- inputSchema: {
798
- type: "object",
799
- properties: {},
800
- additionalProperties: false,
801
- },
802
- },
803
- {
804
- name: "search_nodes",
805
- description: "Search for nodes in the knowledge graph based on a query, with optional filters for tags and importance",
806
- inputSchema: {
807
- type: "object",
808
- properties: {
809
- query: { type: "string", description: "The search query to match against entity names, types, and observation content" },
810
- tags: {
811
- type: "array",
812
- items: { type: "string" },
813
- description: "Optional array of tags to filter by (case-insensitive)"
814
- },
815
- minImportance: {
816
- type: "number",
817
- description: "Optional minimum importance level (0-10)"
818
- },
819
- maxImportance: {
820
- type: "number",
821
- description: "Optional maximum importance level (0-10)"
822
- },
823
- },
824
- required: ["query"],
825
- additionalProperties: false,
826
- },
827
- },
828
- {
829
- name: "open_nodes",
830
- description: "Open specific nodes in the knowledge graph by their names",
831
- inputSchema: {
832
- type: "object",
833
- properties: {
834
- names: {
835
- type: "array",
836
- items: { type: "string" },
837
- description: "An array of entity names to retrieve",
838
- },
839
- },
840
- required: ["names"],
841
- additionalProperties: false,
842
- },
843
- },
844
- {
845
- name: "search_by_date_range",
846
- description: "Search for entities and relations within a specific date range, optionally filtered by entity type and tags. Uses createdAt or lastModified timestamps.",
847
- inputSchema: {
848
- type: "object",
849
- properties: {
850
- startDate: {
851
- type: "string",
852
- description: "ISO 8601 start date (optional). If not provided, no lower bound is applied."
853
- },
854
- endDate: {
855
- type: "string",
856
- description: "ISO 8601 end date (optional). If not provided, no upper bound is applied."
857
- },
858
- entityType: {
859
- type: "string",
860
- description: "Filter by specific entity type (optional)"
861
- },
862
- tags: {
863
- type: "array",
864
- items: { type: "string" },
865
- description: "Optional array of tags to filter by (case-insensitive)"
866
- },
867
- },
868
- additionalProperties: false,
869
- },
870
- },
871
- {
872
- name: "get_graph_stats",
873
- description: "Get comprehensive statistics about the knowledge graph including counts, types, and date ranges",
874
- inputSchema: {
875
- type: "object",
876
- properties: {},
877
- additionalProperties: false,
878
- },
879
- },
880
- {
881
- name: "add_tags",
882
- description: "Add tags to an existing entity in the knowledge graph. Tags are stored as lowercase for case-insensitive matching.",
883
- inputSchema: {
884
- type: "object",
885
- properties: {
886
- entityName: {
887
- type: "string",
888
- description: "The name of the entity to add tags to"
889
- },
890
- tags: {
891
- type: "array",
892
- items: { type: "string" },
893
- description: "An array of tags to add to the entity"
894
- },
895
- },
896
- required: ["entityName", "tags"],
897
- additionalProperties: false,
898
- },
899
- },
900
- {
901
- name: "remove_tags",
902
- description: "Remove tags from an existing entity in the knowledge graph",
903
- inputSchema: {
904
- type: "object",
905
- properties: {
906
- entityName: {
907
- type: "string",
908
- description: "The name of the entity to remove tags from"
909
- },
910
- tags: {
911
- type: "array",
912
- items: { type: "string" },
913
- description: "An array of tags to remove from the entity"
914
- },
915
- },
916
- required: ["entityName", "tags"],
917
- additionalProperties: false,
918
- },
919
- },
920
- {
921
- name: "set_importance",
922
- description: "Set the importance level for an entity. Importance must be a number between 0 and 10.",
923
- inputSchema: {
924
- type: "object",
925
- properties: {
926
- entityName: {
927
- type: "string",
928
- description: "The name of the entity to set importance for"
929
- },
930
- importance: {
931
- type: "number",
932
- description: "The importance level (0-10, where 0 is least important and 10 is most important)",
933
- minimum: 0,
934
- maximum: 10
935
- },
936
- },
937
- required: ["entityName", "importance"],
938
- additionalProperties: false,
939
- },
940
- },
941
- {
942
- name: "export_graph",
943
- description: "Export the knowledge graph in various formats (JSON, CSV, or GraphML) with optional filtering. GraphML format is compatible with graph visualization tools like Gephi and Cytoscape.",
944
- inputSchema: {
945
- type: "object",
946
- properties: {
947
- format: {
948
- type: "string",
949
- enum: ["json", "csv", "graphml"],
950
- description: "Export format: 'json' for pretty-printed JSON, 'csv' for comma-separated values with entities and relations sections, 'graphml' for GraphML XML format"
951
- },
952
- filter: {
953
- type: "object",
954
- properties: {
955
- startDate: {
956
- type: "string",
957
- description: "ISO 8601 start date for filtering (optional)"
958
- },
959
- endDate: {
960
- type: "string",
961
- description: "ISO 8601 end date for filtering (optional)"
962
- },
963
- entityType: {
964
- type: "string",
965
- description: "Filter by specific entity type (optional)"
966
- },
967
- tags: {
968
- type: "array",
969
- items: { type: "string" },
970
- description: "Filter by tags (optional, case-insensitive)"
971
- }
972
- },
973
- description: "Optional filter to export a subset of the graph"
974
- }
975
- },
976
- required: ["format"],
977
- additionalProperties: false,
978
- },
979
- }
980
- ],
981
- };
982
- });
983
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
984
- const { name, arguments: args } = request.params;
985
- if (name === "read_graph") {
986
- return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.readGraph(), null, 2) }] };
987
- }
988
- if (name === "get_graph_stats") {
989
- return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.getGraphStats(), null, 2) }] };
990
- }
991
- if (!args) {
992
- throw new Error(`No arguments provided for tool: ${name}`);
993
- }
994
- switch (name) {
995
- case "create_entities":
996
- return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createEntities(args.entities), null, 2) }] };
997
- case "create_relations":
998
- return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createRelations(args.relations), null, 2) }] };
999
- case "add_observations":
1000
- return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.addObservations(args.observations), null, 2) }] };
1001
- case "delete_entities":
1002
- await knowledgeGraphManager.deleteEntities(args.entityNames);
1003
- return { content: [{ type: "text", text: "Entities deleted successfully" }] };
1004
- case "delete_observations":
1005
- await knowledgeGraphManager.deleteObservations(args.deletions);
1006
- return { content: [{ type: "text", text: "Observations deleted successfully" }] };
1007
- case "delete_relations":
1008
- await knowledgeGraphManager.deleteRelations(args.relations);
1009
- return { content: [{ type: "text", text: "Relations deleted successfully" }] };
1010
- case "search_nodes":
1011
- return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.searchNodes(args.query, args.tags, args.minImportance, args.maxImportance), null, 2) }] };
1012
- case "open_nodes":
1013
- return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.openNodes(args.names), null, 2) }] };
1014
- case "search_by_date_range":
1015
- return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.searchByDateRange(args.startDate, args.endDate, args.entityType, args.tags), null, 2) }] };
1016
- case "add_tags":
1017
- return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.addTags(args.entityName, args.tags), null, 2) }] };
1018
- case "remove_tags":
1019
- return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.removeTags(args.entityName, args.tags), null, 2) }] };
1020
- case "set_importance":
1021
- return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.setImportance(args.entityName, args.importance), null, 2) }] };
1022
- case "export_graph":
1023
- return { content: [{ type: "text", text: await knowledgeGraphManager.exportGraph(args.format, args.filter) }] };
1024
- default:
1025
- throw new Error(`Unknown tool: ${name}`);
1026
- }
1027
- });
1028
45
  async function main() {
1029
46
  // Initialize memory file path with backward compatibility
1030
- MEMORY_FILE_PATH = await ensureMemoryFilePath();
47
+ const memoryFilePath = await ensureMemoryFilePath();
1031
48
  // Initialize knowledge graph manager with the memory file path
1032
- knowledgeGraphManager = new KnowledgeGraphManager(MEMORY_FILE_PATH);
1033
- const transport = new StdioServerTransport();
1034
- await server.connect(transport);
1035
- console.error("Knowledge Graph MCP Server running on stdio");
49
+ knowledgeGraphManager = new KnowledgeGraphManager(memoryFilePath);
50
+ // Initialize and start MCP server
51
+ const server = new MCPServer(knowledgeGraphManager);
52
+ await server.start();
1036
53
  }
1037
54
  main().catch((error) => {
1038
- console.error("Fatal error in main():", error);
55
+ logger.error("Fatal error in main():", error);
1039
56
  process.exit(1);
1040
57
  });