rag-memory-epf-mcp 1.7.0 → 1.9.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.
package/README.md CHANGED
@@ -5,7 +5,9 @@
5
5
  [![GitHub license](https://img.shields.io/github/license/heesongkoh/rag-memory-epf-mcp)](https://github.com/heesongkoh/rag-memory-epf-mcp/blob/main/LICENSE)
6
6
  [![Platforms](https://img.shields.io/badge/Platform-Windows%20%7C%20macOS%20%7C%20Linux-blue)](https://github.com/heesongkoh/rag-memory-epf-mcp)
7
7
 
8
- An advanced MCP server for **RAG-enabled memory** through a knowledge graph with **multilingual vector search** capabilities.
8
+ An advanced MCP server for **project-local RAG memory** through a knowledge graph with **multilingual vector search** capabilities.
9
+
10
+ **Each project folder gets its own isolated memory database** — set `DB_FILE_PATH` to a project-local `.memory/rag-memory.db` so every project maintains its own entities, relations, and documents independently. Multiple projects can run simultaneously without interference since they read/write to separate SQLite databases while sharing the same server binary.
9
11
 
10
12
  **Fork of:** [rag-memory-mcp](https://github.com/ttommyth/rag-memory-mcp) — upgraded with **Qwen3-Embedding-0.6B** (1024-dim, 100+ languages) for significantly better multilingual semantic search.
11
13
 
@@ -34,7 +36,7 @@ An advanced MCP server for **RAG-enabled memory** through a knowledge graph with
34
36
  }
35
37
  ```
36
38
 
37
- **With custom database path:**
39
+ **Project-local memory (recommended):**
38
40
  ```json
39
41
  {
40
42
  "mcpServers": {
@@ -42,13 +44,15 @@ An advanced MCP server for **RAG-enabled memory** through a knowledge graph with
42
44
  "command": "npx",
43
45
  "args": ["-y", "rag-memory-epf-mcp@latest"],
44
46
  "env": {
45
- "DB_FILE_PATH": "/path/to/your/rag-memory.db"
47
+ "DB_FILE_PATH": "/path/to/your-project/.memory/rag-memory.db"
46
48
  }
47
49
  }
48
50
  }
49
51
  }
50
52
  ```
51
53
 
54
+ Place this `.mcp.json` in each project folder with its own `DB_FILE_PATH`. Each project maintains completely isolated memory — entities, relations, and documents are never mixed between projects.
55
+
52
56
  ## Document Processing Pipeline
53
57
 
54
58
  `embedChunks` automatically links entities to the specific chunks where they appear:
@@ -110,19 +114,22 @@ Your entities, relationships, documents, and chunk text are preserved. Only vect
110
114
  - `createEntities`: Create entities with observations and types
111
115
  - `createRelations`: Establish relationships between entities
112
116
  - `addObservations`: Add contextual information to entities
117
+ - `updateRelations`: Update relationship confidence and metadata
113
118
  - `deleteEntities`: Remove entities and relationships
114
119
  - `deleteRelations`: Remove specific relationships
115
120
  - `deleteObservations`: Remove specific observations
116
- - `embedAllEntities`: Generate embeddings for all entities
121
+ - `embedAllEntities`: Generate embeddings for all entities (batch 32 parallel)
117
122
 
118
123
  ### Search & Retrieval
119
- - `hybridSearch`: Vector similarity + graph traversal
120
- - `searchNodes`: Semantic entity search (multilingual)
124
+ - `hybridSearch`: Vector + FTS5 BM25 + graph traversal (3-signal hybrid)
125
+ - `searchNodes`: Semantic entity search (multilingual, with since/until temporal filtering)
121
126
  - `openNodes`: Retrieve specific entities
122
127
  - `readGraph`: Get complete knowledge graph
123
128
  - `getDetailedContext`: Get full context for a chunk
124
129
 
125
- ### Analytics & Migration
130
+ ### Backup & Migration
131
+ - `exportGraph`: Export full knowledge graph as JSON (entities, relations, documents)
132
+ - `importGraph`: Import knowledge graph from JSON (merge or replace mode)
126
133
  - `getKnowledgeGraphStats`: Knowledge base statistics
127
134
  - `getMigrationStatus`: Check database schema version
128
135
  - `runMigrations`: Apply pending migrations
@@ -130,6 +137,19 @@ Your entities, relationships, documents, and chunk text are preserved. Only vect
130
137
 
131
138
  ## Changelog
132
139
 
140
+ ### v1.7.0
141
+
142
+ - **SQLite optimization** — WAL mode, 32MB cache, 256MB mmap, busy_timeout for concurrent access
143
+ - **FTS5 full-text search** — keyword-exact matching via BM25, combined with vector search using Reciprocal Rank Fusion (RRF, k=60)
144
+ - **updateRelations** — update relationship confidence scores and metadata without delete+recreate
145
+ - **exportGraph / importGraph** — JSON backup and restore with merge or replace mode
146
+ - **Batch embedding** — `embedAllEntities` processes 32 entities in parallel instead of sequential
147
+ - **Temporal filtering** — `searchNodes` supports `since` and `until` (ISO 8601) date filters
148
+ - **better-sqlite3 12.x** — SQLite 3.51.3 with query planner improvements
149
+ - **sqlite-vec 0.1.7** — DELETE space reclaim, KNN distance constraints
150
+ - **Missing indexes** — entityType, relationType, chunk lookups for faster queries
151
+ - **SQL safety** — `safeRowid()` validation for vec0 virtual table operations
152
+
133
153
  ### v1.6.0
134
154
 
135
155
  - **Entity upsert** — `createEntities` now merges new observations into existing entities instead of silently ignoring duplicates. Entity type is also updated if a more specific type is provided.
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ const DB_FILE_PATH = process.env.DB_FILE_PATH
24
24
  ? process.env.DB_FILE_PATH
25
25
  : path.join(path.dirname(fileURLToPath(import.meta.url)), process.env.DB_FILE_PATH)
26
26
  : defaultDbPath;
27
+ const EMBEDDING_MODEL = process.env.EMBEDDING_MODEL || 'onnx-community/Qwen3-Embedding-0.6B-ONNX';
27
28
  // Safe rowid for vec0 virtual tables (require literal integer, not parameterized)
28
29
  function safeRowid(value) {
29
30
  const n = Number(value);
@@ -38,6 +39,8 @@ class RAGKnowledgeGraphManager {
38
39
  encoding = null;
39
40
  embeddingModel = null;
40
41
  modelInitialized = false;
42
+ embeddingCache = new Map();
43
+ EMBEDDING_CACHE_MAX = 500;
41
44
  async initialize() {
42
45
  console.error('🚀 Initializing RAG Knowledge Graph MCP Server...');
43
46
  // Initialize database
@@ -65,16 +68,16 @@ class RAGKnowledgeGraphManager {
65
68
  }
66
69
  async initializeEmbeddingModel() {
67
70
  try {
68
- console.error('🤖 Loading embedding model: Qwen3-Embedding-0.6B (1024-dim, 100+ languages)...');
71
+ console.error(`🤖 Loading embedding model: ${EMBEDDING_MODEL} (1024-dim, 100+ languages)...`);
69
72
  // Configure environment to allow remote model downloads
70
73
  env.allowRemoteModels = true;
71
74
  env.allowLocalModels = true;
72
- this.embeddingModel = await pipeline('feature-extraction', 'onnx-community/Qwen3-Embedding-0.6B-ONNX', {
75
+ this.embeddingModel = await pipeline('feature-extraction', EMBEDDING_MODEL, {
73
76
  revision: 'main',
74
77
  dtype: 'fp16',
75
78
  });
76
79
  this.modelInitialized = true;
77
- console.error('✅ Qwen3-Embedding-0.6B model loaded successfully');
80
+ console.error(`✅ ${EMBEDDING_MODEL} model loaded successfully`);
78
81
  }
79
82
  catch (error) {
80
83
  console.error('❌ Failed to load embedding model:', error);
@@ -116,6 +119,7 @@ class RAGKnowledgeGraphManager {
116
119
  this.embeddingModel = null;
117
120
  this.modelInitialized = false;
118
121
  }
122
+ this.embeddingCache.clear();
119
123
  if (this.db) {
120
124
  this.db.close();
121
125
  this.db = null;
@@ -385,6 +389,115 @@ class RAGKnowledgeGraphManager {
385
389
  }));
386
390
  return { entities, relations };
387
391
  }
392
+ async getNeighbors(entityNames, depth = 1, relationType) {
393
+ if (!this.db)
394
+ throw new Error('Database not initialized');
395
+ // Cap depth at 5 to prevent runaway queries
396
+ const effectiveDepth = Math.min(Math.max(depth, 1), 5);
397
+ // Convert entity names to IDs
398
+ const seedIds = entityNames.map(name => `entity_${name.toLowerCase().replace(/[^a-z0-9]/g, '_')}`);
399
+ // Build dynamic placeholders for the seed IDs
400
+ const seedPlaceholders = seedIds.map(() => '?').join(',');
401
+ // Build the recursive CTE query
402
+ const relationFilter = relationType
403
+ ? `AND r.relationType = ?`
404
+ : '';
405
+ const cteQuery = `
406
+ WITH RECURSIVE traversal(entity_id, depth, path) AS (
407
+ -- Base case: seed entities
408
+ SELECT id, 0, id FROM entities WHERE id IN (${seedPlaceholders})
409
+ UNION ALL
410
+ -- Recursive: follow relationships up to max depth
411
+ SELECT
412
+ CASE WHEN r.source_entity = t.entity_id THEN r.target_entity ELSE r.source_entity END,
413
+ t.depth + 1,
414
+ t.path || ',' || CASE WHEN r.source_entity = t.entity_id THEN r.target_entity ELSE r.source_entity END
415
+ FROM traversal t
416
+ JOIN relationships r ON (r.source_entity = t.entity_id OR r.target_entity = t.entity_id)
417
+ WHERE t.depth < ?
418
+ ${relationFilter}
419
+ -- Cycle detection: don't revisit entities already in path
420
+ AND instr(t.path, CASE WHEN r.source_entity = t.entity_id THEN r.target_entity ELSE r.source_entity END) = 0
421
+ )
422
+ SELECT DISTINCT entity_id, MIN(depth) as min_depth, path
423
+ FROM traversal
424
+ GROUP BY entity_id
425
+ `;
426
+ // Build parameters
427
+ const params = [...seedIds, effectiveDepth];
428
+ if (relationType) {
429
+ params.push(relationType);
430
+ }
431
+ const traversalResults = this.db.prepare(cteQuery).all(...params);
432
+ if (traversalResults.length === 0) {
433
+ return { entities: [], relations: [], paths: [] };
434
+ }
435
+ // Collect all discovered entity IDs
436
+ const discoveredIds = traversalResults.map(r => r.entity_id);
437
+ const idPlaceholders = discoveredIds.map(() => '?').join(',');
438
+ // Fetch entity details
439
+ const entityRows = this.db.prepare(`
440
+ SELECT id, name, entityType, observations FROM entities WHERE id IN (${idPlaceholders})
441
+ `).all(...discoveredIds);
442
+ // Build id-to-depth and id-to-name maps
443
+ const idToDepth = new Map();
444
+ for (const r of traversalResults) {
445
+ idToDepth.set(r.entity_id, r.min_depth);
446
+ }
447
+ const idToName = new Map();
448
+ for (const row of entityRows) {
449
+ idToName.set(row.id, row.name);
450
+ }
451
+ const entities = entityRows.map(row => ({
452
+ name: row.name,
453
+ entityType: row.entityType,
454
+ observations: JSON.parse(row.observations),
455
+ depth: idToDepth.get(row.id) ?? 0,
456
+ }));
457
+ // Fetch relations between all discovered entities
458
+ let relQuery = `
459
+ SELECT
460
+ r.source_entity,
461
+ r.target_entity,
462
+ e1.name as from_name,
463
+ e2.name as to_name,
464
+ r.relationType
465
+ FROM relationships r
466
+ JOIN entities e1 ON r.source_entity = e1.id
467
+ JOIN entities e2 ON r.target_entity = e2.id
468
+ WHERE r.source_entity IN (${idPlaceholders})
469
+ AND r.target_entity IN (${idPlaceholders})
470
+ `;
471
+ const relParams = [...discoveredIds, ...discoveredIds];
472
+ if (relationType) {
473
+ relQuery += ` AND r.relationType = ?`;
474
+ relParams.push(relationType);
475
+ }
476
+ const relationRows = this.db.prepare(relQuery).all(...relParams);
477
+ const relations = relationRows.map(row => ({
478
+ from: row.from_name,
479
+ to: row.to_name,
480
+ relationType: row.relationType,
481
+ depth: Math.max(idToDepth.get(row.source_entity) ?? 0, idToDepth.get(row.target_entity) ?? 0),
482
+ }));
483
+ // Build shortest paths from seed entities to all discovered entities
484
+ const paths = [];
485
+ for (const result of traversalResults) {
486
+ if (result.min_depth === 0)
487
+ continue; // Skip seed entities themselves
488
+ const pathIds = result.path.split(',');
489
+ const pathNames = pathIds.map(id => idToName.get(id) || id).filter(Boolean);
490
+ if (pathNames.length >= 2) {
491
+ paths.push({
492
+ from: pathNames[0],
493
+ to: pathNames[pathNames.length - 1],
494
+ path: pathNames,
495
+ });
496
+ }
497
+ }
498
+ console.error(`✅ getNeighbors: Found ${entities.length} entities, ${relations.length} relations, ${paths.length} paths (depth=${effectiveDepth})`);
499
+ return { entities, relations, paths };
500
+ }
388
501
  async searchNodes(query, limit = 10, since, until) {
389
502
  if (!this.db)
390
503
  throw new Error('Database not initialized');
@@ -920,6 +1033,11 @@ class RAGKnowledgeGraphManager {
920
1033
  // Generate embeddings using sentence transformers
921
1034
  // isQuery: true for search queries (adds instruction prefix), false for documents/entities
922
1035
  async generateEmbedding(text, dimensions = 1024, isQuery = false) {
1036
+ // Check cache first
1037
+ const cacheKey = `${text.length > 100 ? text.substring(0, 100) : text}_${dimensions}_${isQuery}`;
1038
+ const cached = this.embeddingCache.get(cacheKey);
1039
+ if (cached)
1040
+ return cached;
923
1041
  if (this.modelInitialized && this.embeddingModel) {
924
1042
  try {
925
1043
  // Qwen3: add instruction prefix for queries (task-specific instruction improves accuracy)
@@ -930,7 +1048,15 @@ class RAGKnowledgeGraphManager {
930
1048
  const result = await this.embeddingModel(inputText, { pooling: 'last_token', normalize: true });
931
1049
  // Extract the embedding array and convert to Float32Array
932
1050
  const embedding = result.data;
933
- return new Float32Array(embedding.slice(0, dimensions));
1051
+ const modelResult = new Float32Array(embedding.slice(0, dimensions));
1052
+ // Cache the result (LRU: evict oldest if full)
1053
+ if (this.embeddingCache.size >= this.EMBEDDING_CACHE_MAX) {
1054
+ const firstKey = this.embeddingCache.keys().next().value;
1055
+ if (firstKey)
1056
+ this.embeddingCache.delete(firstKey);
1057
+ }
1058
+ this.embeddingCache.set(cacheKey, modelResult);
1059
+ return modelResult;
934
1060
  }
935
1061
  catch (error) {
936
1062
  console.error(`⚠️ Embedding model failed for text "${text.slice(0, 50)}...":`, error instanceof Error ? error.message : error);
@@ -943,7 +1069,15 @@ class RAGKnowledgeGraphManager {
943
1069
  const normalizedText = text.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, ' ').replace(/\s+/g, ' ').trim();
944
1070
  const words = normalizedText.split(' ').filter(word => word.length > 1);
945
1071
  if (words.length === 0) {
946
- return new Float32Array(embedding);
1072
+ const emptyResult = new Float32Array(embedding);
1073
+ // Cache the result (LRU: evict oldest if full)
1074
+ if (this.embeddingCache.size >= this.EMBEDDING_CACHE_MAX) {
1075
+ const firstKey = this.embeddingCache.keys().next().value;
1076
+ if (firstKey)
1077
+ this.embeddingCache.delete(firstKey);
1078
+ }
1079
+ this.embeddingCache.set(cacheKey, emptyResult);
1080
+ return emptyResult;
947
1081
  }
948
1082
  // Enhanced word importance calculation
949
1083
  const wordFreq = new Map();
@@ -1076,7 +1210,15 @@ class RAGKnowledgeGraphManager {
1076
1210
  // L2 normalization for cosine similarity
1077
1211
  const magnitude = Math.sqrt(embedding.reduce((sum, val) => sum + val * val, 0));
1078
1212
  const normalizedEmbedding = magnitude > 0 ? embedding.map(val => val / magnitude) : embedding;
1079
- return new Float32Array(normalizedEmbedding);
1213
+ const fallbackResult = new Float32Array(normalizedEmbedding);
1214
+ // Cache the result (LRU: evict oldest if full)
1215
+ if (this.embeddingCache.size >= this.EMBEDDING_CACHE_MAX) {
1216
+ const firstKey = this.embeddingCache.keys().next().value;
1217
+ if (firstKey)
1218
+ this.embeddingCache.delete(firstKey);
1219
+ }
1220
+ this.embeddingCache.set(cacheKey, fallbackResult);
1221
+ return fallbackResult;
1080
1222
  }
1081
1223
  // Calculate position-based importance weight
1082
1224
  calculatePositionWeight(position, totalWords) {
@@ -2127,6 +2269,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2127
2269
  return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.searchNodes(validatedArgs.query, validatedArgs.limit || 10, validatedArgs.since, validatedArgs.until), null, 2) }] };
2128
2270
  case "openNodes":
2129
2271
  return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.openNodes(validatedArgs.names), null, 2) }] };
2272
+ case "getNeighbors":
2273
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.getNeighbors(validatedArgs.entityNames, validatedArgs.depth || 1, validatedArgs.relationType), null, 2) }] };
2130
2274
  // New RAG tools
2131
2275
  case "storeDocument":
2132
2276
  return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.storeDocument(validatedArgs.id, validatedArgs.content, validatedArgs.metadata || {}), null, 2) }] };
@@ -2183,12 +2327,17 @@ async function main() {
2183
2327
  const transport = new StdioServerTransport();
2184
2328
  await server.connect(transport);
2185
2329
  console.error("🚀 Enhanced RAG Knowledge Graph MCP Server running on stdio");
2186
- // Cleanup on exit
2187
- process.on('SIGINT', () => {
2330
+ // Cleanup on exit — avoid process.exit() to prevent ONNX runtime mutex crash
2331
+ const shutdown = () => {
2188
2332
  console.error('\n🧹 Cleaning up...');
2189
- ragKgManager.cleanup();
2190
- process.exit(0);
2191
- });
2333
+ try {
2334
+ ragKgManager.cleanup();
2335
+ }
2336
+ catch { }
2337
+ };
2338
+ process.on('SIGINT', shutdown);
2339
+ process.on('SIGTERM', shutdown);
2340
+ process.on('exit', shutdown);
2192
2341
  }
2193
2342
  catch (error) {
2194
2343
  console.error("Failed to initialize server:", error);