rag-memory-epf-mcp 1.6.0 → 1.8.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,14 @@ 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
+ // Safe rowid for vec0 virtual tables (require literal integer, not parameterized)
28
+ function safeRowid(value) {
29
+ const n = Number(value);
30
+ if (!Number.isInteger(n) || n < 0) {
31
+ throw new Error(`Invalid rowid: ${value}`);
32
+ }
33
+ return n;
34
+ }
27
35
  // Enhanced RAG-enabled Knowledge Graph Manager
28
36
  class RAGKnowledgeGraphManager {
29
37
  db = null;
@@ -36,6 +44,14 @@ class RAGKnowledgeGraphManager {
36
44
  this.db = new Database(DB_FILE_PATH);
37
45
  // Load sqlite-vec extension
38
46
  sqliteVec.load(this.db);
47
+ // SQLite performance & safety optimizations
48
+ this.db.pragma('journal_mode = WAL');
49
+ this.db.pragma('synchronous = NORMAL');
50
+ this.db.pragma('busy_timeout = 5000');
51
+ this.db.pragma('cache_size = -32000');
52
+ this.db.pragma('temp_store = MEMORY');
53
+ this.db.pragma('mmap_size = 268435456');
54
+ this.db.pragma('foreign_keys = ON');
39
55
  // Initialize tiktoken
40
56
  this.encoding = get_encoding("cl100k_base");
41
57
  // Initialize embedding model
@@ -300,11 +316,50 @@ class RAGKnowledgeGraphManager {
300
316
  const sourceId = `entity_${relation.from.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
301
317
  const targetId = `entity_${relation.to.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
302
318
  this.db.prepare(`
303
- DELETE FROM relationships
319
+ DELETE FROM relationships
304
320
  WHERE source_entity = ? AND target_entity = ? AND relationType = ?
305
321
  `).run(sourceId, targetId, relation.relationType);
306
322
  }
307
323
  }
324
+ async updateRelations(updates) {
325
+ if (!this.db)
326
+ throw new Error('Database not initialized');
327
+ let updated = 0;
328
+ let notFound = 0;
329
+ for (const update of updates) {
330
+ const sourceId = `entity_${update.from.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
331
+ const targetId = `entity_${update.to.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
332
+ const relationId = `rel_${sourceId}_${update.relationType}_${targetId}`.toLowerCase();
333
+ // Check if relation exists
334
+ const existing = this.db.prepare(`
335
+ SELECT id FROM relationships WHERE id = ?
336
+ `).get(relationId);
337
+ if (!existing) {
338
+ notFound++;
339
+ continue;
340
+ }
341
+ // Build dynamic update
342
+ const setClauses = [];
343
+ const values = [];
344
+ if (update.confidence !== undefined) {
345
+ setClauses.push('confidence = ?');
346
+ values.push(update.confidence);
347
+ }
348
+ if (update.metadata !== undefined) {
349
+ setClauses.push('metadata = ?');
350
+ values.push(JSON.stringify(update.metadata));
351
+ }
352
+ if (setClauses.length === 0) {
353
+ continue;
354
+ }
355
+ values.push(relationId);
356
+ this.db.prepare(`
357
+ UPDATE relationships SET ${setClauses.join(', ')} WHERE id = ?
358
+ `).run(...values);
359
+ updated++;
360
+ }
361
+ return { updated, notFound };
362
+ }
308
363
  async readGraph() {
309
364
  if (!this.db)
310
365
  throw new Error('Database not initialized');
@@ -330,7 +385,7 @@ class RAGKnowledgeGraphManager {
330
385
  }));
331
386
  return { entities, relations };
332
387
  }
333
- async searchNodes(query, limit = 10) {
388
+ async searchNodes(query, limit = 10, since, until) {
334
389
  if (!this.db)
335
390
  throw new Error('Database not initialized');
336
391
  console.error(`🔍 Semantic entity search: "${query}"`);
@@ -353,11 +408,25 @@ class RAGKnowledgeGraphManager {
353
408
  AND k = ?
354
409
  ORDER BY ee.distance
355
410
  `).all(Buffer.from(queryEmbedding.buffer), limit);
356
- if (entityResults.length === 0) {
411
+ // Filter by temporal range if specified
412
+ let filteredResults = entityResults;
413
+ if (since || until) {
414
+ filteredResults = entityResults.filter(r => {
415
+ const entity = this.db.prepare('SELECT created_at FROM entities WHERE id = ?').get(r.entity_id);
416
+ if (!entity)
417
+ return false;
418
+ if (since && entity.created_at < since)
419
+ return false;
420
+ if (until && entity.created_at > until)
421
+ return false;
422
+ return true;
423
+ });
424
+ }
425
+ if (filteredResults.length === 0) {
357
426
  console.error(`ℹ️ No semantic matches found for "${query}"`);
358
427
  return { entities: [], relations: [] };
359
428
  }
360
- const entities = entityResults.map(result => ({
429
+ const entities = filteredResults.map(result => ({
361
430
  name: result.name,
362
431
  entityType: result.entityType,
363
432
  observations: JSON.parse(result.observations),
@@ -591,11 +660,11 @@ class RAGKnowledgeGraphManager {
591
660
  SELECT id FROM entities
592
661
  `).all();
593
662
  let embeddedCount = 0;
594
- for (const entity of entities) {
595
- const success = await this.embedEntity(entity.id);
596
- if (success) {
597
- embeddedCount++;
598
- }
663
+ const batchSize = 32;
664
+ for (let i = 0; i < entities.length; i += batchSize) {
665
+ const batch = entities.slice(i, i + batchSize);
666
+ const results = await Promise.all(batch.map(e => this.embedEntity(e.id)));
667
+ embeddedCount += results.filter(Boolean).length;
599
668
  }
600
669
  console.error(`✅ Entity embeddings completed: ${embeddedCount}/${entities.length} entities embedded`);
601
670
  return {
@@ -678,7 +747,7 @@ class RAGKnowledgeGraphManager {
678
747
  for (const chunk of chunks) {
679
748
  // Generate embedding
680
749
  const embedding = await this.generateEmbedding(chunk.text);
681
- const rowid = Number(chunk.rowid);
750
+ const rowid = safeRowid(chunk.rowid);
682
751
  try {
683
752
  // Delete existing embedding if any
684
753
  this.db.exec(`DELETE FROM chunks WHERE rowid = ${rowid}`);
@@ -722,7 +791,7 @@ class RAGKnowledgeGraphManager {
722
791
  // Delete vectors and associations
723
792
  for (const chunk of existingChunks) {
724
793
  // Delete vector embeddings (vec0 needs literal integer, not parameterized)
725
- this.db.exec(`DELETE FROM chunks WHERE rowid = ${Number(chunk.rowid)}`);
794
+ this.db.exec(`DELETE FROM chunks WHERE rowid = ${safeRowid(chunk.rowid)}`);
726
795
  deletedVectors++;
727
796
  // Delete chunk-entity associations
728
797
  const associations = this.db.prepare(`
@@ -1099,7 +1168,7 @@ class RAGKnowledgeGraphManager {
1099
1168
  // Store in vector table
1100
1169
  try {
1101
1170
  // First, delete any existing embedding for this rowid
1102
- this.db.exec(`DELETE FROM chunks WHERE rowid = ${rowid}`);
1171
+ this.db.exec(`DELETE FROM chunks WHERE rowid = ${safeRowid(rowid)}`);
1103
1172
  // Insert new embedding with explicit rowid to match chunk_metadata
1104
1173
  // Use parameterized only for embedding blob, rowid as literal integer
1105
1174
  this.db.prepare(`
@@ -1275,7 +1344,7 @@ class RAGKnowledgeGraphManager {
1275
1344
  `).run(chunk.rowid);
1276
1345
  deletedAssociations += associations.changes;
1277
1346
  // Delete vector embeddings (vec0 needs literal integer, not parameterized)
1278
- this.db.exec(`DELETE FROM chunks WHERE rowid = ${Number(chunk.rowid)}`);
1347
+ this.db.exec(`DELETE FROM chunks WHERE rowid = ${safeRowid(chunk.rowid)}`);
1279
1348
  deletedVectors++;
1280
1349
  }
1281
1350
  // Delete chunk metadata
@@ -1405,6 +1474,117 @@ class RAGKnowledgeGraphManager {
1405
1474
  console.error(`✅ Found ${documents.length} documents`);
1406
1475
  return { documents };
1407
1476
  }
1477
+ async exportGraph() {
1478
+ if (!this.db)
1479
+ throw new Error('Database not initialized');
1480
+ console.error('📦 Exporting knowledge graph...');
1481
+ const entities = this.db.prepare(`
1482
+ SELECT id, name, entityType, observations, metadata, created_at FROM entities
1483
+ `).all().map((row) => ({
1484
+ id: row.id,
1485
+ name: row.name,
1486
+ entityType: row.entityType,
1487
+ observations: JSON.parse(row.observations),
1488
+ metadata: JSON.parse(row.metadata || '{}'),
1489
+ created_at: row.created_at
1490
+ }));
1491
+ const relations = this.db.prepare(`
1492
+ SELECT id, source_entity, target_entity, relationType, confidence, metadata, created_at FROM relationships
1493
+ `).all().map((row) => ({
1494
+ id: row.id,
1495
+ source_entity: row.source_entity,
1496
+ target_entity: row.target_entity,
1497
+ relationType: row.relationType,
1498
+ confidence: row.confidence,
1499
+ metadata: JSON.parse(row.metadata || '{}'),
1500
+ created_at: row.created_at
1501
+ }));
1502
+ const documents = this.db.prepare(`
1503
+ SELECT id, content, metadata, created_at FROM documents
1504
+ `).all().map((row) => ({
1505
+ id: row.id,
1506
+ content: row.content,
1507
+ metadata: JSON.parse(row.metadata || '{}'),
1508
+ created_at: row.created_at
1509
+ }));
1510
+ console.error(`✅ Export completed: ${entities.length} entities, ${relations.length} relations, ${documents.length} documents`);
1511
+ return {
1512
+ entities,
1513
+ relations,
1514
+ documents,
1515
+ metadata: {
1516
+ exportedAt: new Date().toISOString(),
1517
+ version: '1.0.0',
1518
+ entityCount: entities.length,
1519
+ relationCount: relations.length,
1520
+ documentCount: documents.length
1521
+ }
1522
+ };
1523
+ }
1524
+ async importGraph(data, options = { merge: true }) {
1525
+ if (!this.db)
1526
+ throw new Error('Database not initialized');
1527
+ console.error(`📥 Importing knowledge graph (merge: ${options.merge !== false})...`);
1528
+ const imported = { entities: 0, relations: 0, documents: 0 };
1529
+ const skipped = { entities: 0, relations: 0, documents: 0 };
1530
+ // If merge=false, clear existing data first
1531
+ if (options.merge === false) {
1532
+ this.db.exec(`DELETE FROM relationships`);
1533
+ this.db.exec(`DELETE FROM entities`);
1534
+ this.db.exec(`DELETE FROM documents`);
1535
+ console.error('🗑️ Cleared existing data for full import');
1536
+ }
1537
+ // Import entities using INSERT OR IGNORE
1538
+ if (data.entities && Array.isArray(data.entities)) {
1539
+ const stmt = this.db.prepare(`
1540
+ INSERT OR IGNORE INTO entities (id, name, entityType, observations, metadata, created_at)
1541
+ VALUES (?, ?, ?, ?, ?, ?)
1542
+ `);
1543
+ for (const entity of data.entities) {
1544
+ const result = stmt.run(entity.id, entity.name, entity.entityType || 'CONCEPT', JSON.stringify(entity.observations || []), JSON.stringify(entity.metadata || {}), entity.created_at || new Date().toISOString());
1545
+ if (result.changes > 0) {
1546
+ imported.entities++;
1547
+ }
1548
+ else {
1549
+ skipped.entities++;
1550
+ }
1551
+ }
1552
+ }
1553
+ // Import relations using INSERT OR IGNORE
1554
+ if (data.relations && Array.isArray(data.relations)) {
1555
+ const stmt = this.db.prepare(`
1556
+ INSERT OR IGNORE INTO relationships (id, source_entity, target_entity, relationType, confidence, metadata, created_at)
1557
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1558
+ `);
1559
+ for (const relation of data.relations) {
1560
+ const result = stmt.run(relation.id, relation.source_entity, relation.target_entity, relation.relationType, relation.confidence ?? 1.0, JSON.stringify(relation.metadata || {}), relation.created_at || new Date().toISOString());
1561
+ if (result.changes > 0) {
1562
+ imported.relations++;
1563
+ }
1564
+ else {
1565
+ skipped.relations++;
1566
+ }
1567
+ }
1568
+ }
1569
+ // Import documents using INSERT OR REPLACE
1570
+ if (data.documents && Array.isArray(data.documents)) {
1571
+ const stmt = this.db.prepare(`
1572
+ INSERT OR REPLACE INTO documents (id, content, metadata, created_at)
1573
+ VALUES (?, ?, ?, ?)
1574
+ `);
1575
+ for (const doc of data.documents) {
1576
+ const result = stmt.run(doc.id, doc.content, JSON.stringify(doc.metadata || {}), doc.created_at || new Date().toISOString());
1577
+ if (result.changes > 0) {
1578
+ imported.documents++;
1579
+ }
1580
+ else {
1581
+ skipped.documents++;
1582
+ }
1583
+ }
1584
+ }
1585
+ console.error(`✅ Import completed: ${imported.entities} entities, ${imported.relations} relations, ${imported.documents} documents imported`);
1586
+ return { imported, skipped };
1587
+ }
1408
1588
  async hybridSearch(query, limit = 5, useGraph = true) {
1409
1589
  if (!this.db)
1410
1590
  throw new Error('Database not initialized');
@@ -1463,8 +1643,89 @@ class RAGKnowledgeGraphManager {
1463
1643
  }
1464
1644
  }
1465
1645
  const vectorResults = Array.from(resultMap.values()).sort((a, b) => a.distance - b.distance);
1646
+ // FTS5 full-text search as additional signal (Reciprocal Rank Fusion)
1647
+ const ftsBoostMap = new Map();
1648
+ try {
1649
+ const ftsSearchQuery = (q) => {
1650
+ // Escape FTS5 special characters and build a query with OR between terms
1651
+ const sanitized = q.replace(/["\*\(\)\-]/g, ' ').trim();
1652
+ if (!sanitized)
1653
+ return [];
1654
+ const terms = sanitized.split(/\s+/).filter(t => t.length > 0);
1655
+ if (terms.length === 0)
1656
+ return [];
1657
+ const ftsExpr = terms.map(t => `"${t}"`).join(' OR ');
1658
+ return this.db.prepare(`
1659
+ SELECT cm.rowid, cm.chunk_id, bm25(chunks_fts) as fts_score
1660
+ FROM chunks_fts
1661
+ JOIN chunk_metadata cm ON chunks_fts.rowid = cm.rowid
1662
+ WHERE chunks_fts MATCH ?
1663
+ ORDER BY bm25(chunks_fts)
1664
+ LIMIT ?
1665
+ `).all(ftsExpr, limit * 3);
1666
+ };
1667
+ const ftsOriginal = ftsSearchQuery(query);
1668
+ const ftsTranslated = translatedQuery ? ftsSearchQuery(translatedQuery) : [];
1669
+ // Merge FTS5 results, keeping best score per chunk_id
1670
+ const ftsResultMap = new Map();
1671
+ let rank = 1;
1672
+ for (const r of ftsOriginal) {
1673
+ ftsResultMap.set(r.chunk_id, { chunk_id: r.chunk_id, fts_score: r.fts_score, rank });
1674
+ rank++;
1675
+ }
1676
+ for (const r of ftsTranslated) {
1677
+ if (!ftsResultMap.has(r.chunk_id)) {
1678
+ ftsResultMap.set(r.chunk_id, { chunk_id: r.chunk_id, fts_score: r.fts_score, rank });
1679
+ rank++;
1680
+ }
1681
+ }
1682
+ // Build vector rank map for RRF
1683
+ const vectorRankMap = new Map();
1684
+ vectorResults.forEach((r, idx) => vectorRankMap.set(r.chunk_id, idx + 1));
1685
+ // Calculate RRF-based FTS5 boost (k=60)
1686
+ const k = 60;
1687
+ for (const [chunkId, ftsResult] of ftsResultMap) {
1688
+ const ftsComponent = 1 / (k + ftsResult.rank);
1689
+ ftsBoostMap.set(chunkId, ftsComponent);
1690
+ }
1691
+ // Add FTS5-only results to the vector result pool
1692
+ for (const [chunkId] of ftsResultMap) {
1693
+ if (!resultMap.has(chunkId)) {
1694
+ const chunkRow = this.db.prepare(`
1695
+ SELECT
1696
+ cm.rowid,
1697
+ cm.chunk_id,
1698
+ cm.chunk_type,
1699
+ cm.document_id,
1700
+ cm.entity_id,
1701
+ cm.relationship_id,
1702
+ cm.chunk_index,
1703
+ cm.text,
1704
+ cm.start_pos,
1705
+ cm.end_pos,
1706
+ COALESCE(cm.metadata, '{}') as chunk_metadata,
1707
+ COALESCE(d.metadata, '{}') as doc_metadata
1708
+ FROM chunk_metadata cm
1709
+ LEFT JOIN documents d ON cm.document_id = d.id
1710
+ WHERE cm.chunk_id = ?
1711
+ `).get(chunkId);
1712
+ if (chunkRow) {
1713
+ vectorResults.push({
1714
+ ...chunkRow,
1715
+ distance: 2.0
1716
+ });
1717
+ }
1718
+ }
1719
+ }
1720
+ const ftsCount = ftsResultMap.size;
1721
+ const ftsOnlyCount = [...ftsResultMap.keys()].filter(id => !vectorRankMap.has(id)).length;
1722
+ console.error(`📝 FTS5 search: ${ftsCount} matches (${ftsOnlyCount} FTS5-only), ${ftsBoostMap.size} boosted`);
1723
+ }
1724
+ catch (ftsError) {
1725
+ console.error(`⚠️ FTS5 search unavailable (graceful degradation):`, ftsError instanceof Error ? ftsError.message : ftsError);
1726
+ }
1466
1727
  if (vectorResults.length === 0) {
1467
- console.error(`ℹ️ No vector matches found for "${query}"`);
1728
+ console.error(`ℹ️ No vector or FTS5 matches found for "${query}"`);
1468
1729
  return [];
1469
1730
  }
1470
1731
  // Get entity information for graph enhancement via vector similarity
@@ -1617,7 +1878,8 @@ class RAGKnowledgeGraphManager {
1617
1878
  const { summary, keyHighlight, relevanceScore } = await this.generateContentSummary(result.text, queryEmbedding, chunkEntities, result.chunk_type === 'relationship' ? 1 : 2 // Shorter summary for relationships
1618
1879
  );
1619
1880
  const vectorSimilarity = Math.max(0, 1 - result.distance / 2);
1620
- const finalScore = Math.max(vectorSimilarity, relevanceScore) + graphBoost;
1881
+ const ftsBoost = ftsBoostMap.get(result.chunk_id) || 0;
1882
+ const finalScore = Math.max(vectorSimilarity, relevanceScore) + graphBoost + ftsBoost;
1621
1883
  // Determine document title and source ID
1622
1884
  let documentTitle;
1623
1885
  let sourceId;
@@ -1647,6 +1909,7 @@ class RAGKnowledgeGraphManager {
1647
1909
  entities: chunkEntities,
1648
1910
  vector_similarity: vectorSimilarity,
1649
1911
  graph_boost: useGraph ? graphBoost : undefined,
1912
+ fts_boost: ftsBoost > 0 ? ftsBoost : undefined,
1650
1913
  full_context_available: true,
1651
1914
  chunk_type: result.chunk_type,
1652
1915
  source_id: sourceId
@@ -1856,10 +2119,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1856
2119
  case "deleteRelations":
1857
2120
  await ragKgManager.deleteRelations(validatedArgs.relations);
1858
2121
  return { content: [{ type: "text", text: "Relations deleted successfully" }] };
2122
+ case "updateRelations":
2123
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.updateRelations(validatedArgs.updates), null, 2) }] };
1859
2124
  case "readGraph":
1860
2125
  return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.readGraph(), null, 2) }] };
1861
2126
  case "searchNodes":
1862
- return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.searchNodes(validatedArgs.query, validatedArgs.limit || 10), null, 2) }] };
2127
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.searchNodes(validatedArgs.query, validatedArgs.limit || 10, validatedArgs.since, validatedArgs.until), null, 2) }] };
1863
2128
  case "openNodes":
1864
2129
  return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.openNodes(validatedArgs.names), null, 2) }] };
1865
2130
  // New RAG tools
@@ -1888,6 +2153,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1888
2153
  // NEW: Entity embedding tools
1889
2154
  case "embedAllEntities":
1890
2155
  return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.embedAllEntities(), null, 2) }] };
2156
+ // NEW: Export/Import tools
2157
+ case "exportGraph":
2158
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.exportGraph(), null, 2) }] };
2159
+ case "importGraph":
2160
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.importGraph(validatedArgs.data, { merge: validatedArgs.merge !== false }), null, 2) }] };
1891
2161
  // NEW: Migration tools
1892
2162
  case "getMigrationStatus":
1893
2163
  return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.getMigrationStatus(), null, 2) }] };
@@ -1913,12 +2183,17 @@ async function main() {
1913
2183
  const transport = new StdioServerTransport();
1914
2184
  await server.connect(transport);
1915
2185
  console.error("🚀 Enhanced RAG Knowledge Graph MCP Server running on stdio");
1916
- // Cleanup on exit
1917
- process.on('SIGINT', () => {
2186
+ // Cleanup on exit — avoid process.exit() to prevent ONNX runtime mutex crash
2187
+ const shutdown = () => {
1918
2188
  console.error('\n🧹 Cleaning up...');
1919
- ragKgManager.cleanup();
1920
- process.exit(0);
1921
- });
2189
+ try {
2190
+ ragKgManager.cleanup();
2191
+ }
2192
+ catch { }
2193
+ };
2194
+ process.on('SIGINT', shutdown);
2195
+ process.on('SIGTERM', shutdown);
2196
+ process.on('exit', shutdown);
1922
2197
  }
1923
2198
  catch (error) {
1924
2199
  console.error("Failed to initialize server:", error);