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 +27 -7
- package/dist/index.js +160 -11
- package/dist/index.js.map +1 -1
- package/dist/src/tools/graph-query-tools.d.ts +2 -0
- package/dist/src/tools/graph-query-tools.d.ts.map +1 -1
- package/dist/src/tools/graph-query-tools.js +96 -0
- package/dist/src/tools/graph-query-tools.js.map +1 -1
- package/dist/src/tools/knowledge-graph-tools.d.ts.map +1 -1
- package/dist/src/tools/knowledge-graph-tools.js +7 -0
- package/dist/src/tools/knowledge-graph-tools.js.map +1 -1
- package/dist/src/tools/migration-tools.d.ts.map +1 -1
- package/dist/src/tools/migration-tools.js +3 -0
- package/dist/src/tools/migration-tools.js.map +1 -1
- package/dist/src/tools/rag-tools.d.ts.map +1 -1
- package/dist/src/tools/rag-tools.js +6 -0
- package/dist/src/tools/rag-tools.js.map +1 -1
- package/dist/src/tools/tool-registry.d.ts +1 -0
- package/dist/src/tools/tool-registry.d.ts.map +1 -1
- package/dist/src/tools/tool-registry.js +1 -0
- package/dist/src/tools/tool-registry.js.map +1 -1
- package/dist/src/tools/types.d.ts +8 -0
- package/dist/src/tools/types.d.ts.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
[](https://github.com/heesongkoh/rag-memory-epf-mcp/blob/main/LICENSE)
|
|
6
6
|
[](https://github.com/heesongkoh/rag-memory-epf-mcp)
|
|
7
7
|
|
|
8
|
-
An advanced MCP server for **
|
|
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
|
-
**
|
|
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
|
|
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
|
-
###
|
|
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(
|
|
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',
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2330
|
+
// Cleanup on exit — avoid process.exit() to prevent ONNX runtime mutex crash
|
|
2331
|
+
const shutdown = () => {
|
|
2188
2332
|
console.error('\n🧹 Cleaning up...');
|
|
2189
|
-
|
|
2190
|
-
|
|
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);
|