server-memory-enhanced 2.1.0 → 2.2.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/dist/index.js +100 -2
- package/dist/lib/neo4j-queries.js +82 -0
- package/dist/lib/neo4j-storage-adapter.js +235 -106
- package/dist/lib/storage-config.js +40 -0
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -8,6 +8,9 @@ import { fileURLToPath } from 'url';
|
|
|
8
8
|
import { KnowledgeGraphManager } from './lib/knowledge-graph-manager.js';
|
|
9
9
|
import { EntitySchema, RelationSchema, SaveMemoryInputSchema, SaveMemoryOutputSchema, GetAnalyticsInputSchema, GetAnalyticsOutputSchema, GetObservationHistoryInputSchema, GetObservationHistoryOutputSchema } from './lib/schemas.js';
|
|
10
10
|
import { handleSaveMemory } from './lib/save-memory-handler.js';
|
|
11
|
+
import { JsonlStorageAdapter } from './lib/jsonl-storage-adapter.js';
|
|
12
|
+
import { Neo4jStorageAdapter } from './lib/neo4j-storage-adapter.js';
|
|
13
|
+
import { NEO4J_ENV_VARS, STORAGE_LOG_MESSAGES, NEO4J_ERROR_MESSAGES } from './lib/storage-config.js';
|
|
11
14
|
// Define memory directory path using environment variable with fallback
|
|
12
15
|
export const defaultMemoryDir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'memory-data');
|
|
13
16
|
export async function ensureMemoryDirectory() {
|
|
@@ -25,6 +28,71 @@ export async function ensureMemoryDirectory() {
|
|
|
25
28
|
}
|
|
26
29
|
return memoryDir;
|
|
27
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Get Neo4j configuration from environment variables.
|
|
33
|
+
* Extracted for testability and Single Responsibility Principle.
|
|
34
|
+
*/
|
|
35
|
+
function getNeo4jConfig() {
|
|
36
|
+
const uri = process.env[NEO4J_ENV_VARS.URI];
|
|
37
|
+
const username = process.env[NEO4J_ENV_VARS.USERNAME];
|
|
38
|
+
const password = process.env[NEO4J_ENV_VARS.PASSWORD];
|
|
39
|
+
const database = process.env[NEO4J_ENV_VARS.DATABASE];
|
|
40
|
+
if (!uri || !username || !password) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return { uri, username, password, database };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Create Neo4j storage adapter if configured.
|
|
47
|
+
* Extracted for Single Responsibility Principle and testability.
|
|
48
|
+
*/
|
|
49
|
+
async function createNeo4jAdapter(config) {
|
|
50
|
+
try {
|
|
51
|
+
console.error(STORAGE_LOG_MESSAGES.ATTEMPTING_NEO4J, config.uri);
|
|
52
|
+
const neo4jAdapter = new Neo4jStorageAdapter(config);
|
|
53
|
+
await neo4jAdapter.initialize();
|
|
54
|
+
console.error(STORAGE_LOG_MESSAGES.NEO4J_SUCCESS);
|
|
55
|
+
return neo4jAdapter;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
// Sanitize error message to avoid exposing credentials
|
|
59
|
+
const safeErrorMessage = error instanceof Error ? error.message.replace(/password[=:][\S]+/gi, 'password:***') : 'Connection failed';
|
|
60
|
+
console.error(STORAGE_LOG_MESSAGES.NEO4J_FALLBACK, safeErrorMessage);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Create JSONL storage adapter.
|
|
66
|
+
* Extracted for DRY and testability.
|
|
67
|
+
*/
|
|
68
|
+
async function createJsonlAdapter(memoryDirPath) {
|
|
69
|
+
const jsonlAdapter = new JsonlStorageAdapter(memoryDirPath);
|
|
70
|
+
await jsonlAdapter.initialize();
|
|
71
|
+
console.error(STORAGE_LOG_MESSAGES.USING_JSONL, memoryDirPath);
|
|
72
|
+
return jsonlAdapter;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Create storage adapter based on environment variables.
|
|
76
|
+
* Falls back to JSONL storage if Neo4j is not configured or connection fails.
|
|
77
|
+
*
|
|
78
|
+
* Follows Open/Closed Principle: Open for extension (add new storage types)
|
|
79
|
+
* without modifying existing code.
|
|
80
|
+
*/
|
|
81
|
+
async function createStorageAdapter(memoryDirPath) {
|
|
82
|
+
// Try Neo4j if configured
|
|
83
|
+
const neo4jConfig = getNeo4jConfig();
|
|
84
|
+
if (neo4jConfig) {
|
|
85
|
+
const neo4jAdapter = await createNeo4jAdapter(neo4jConfig);
|
|
86
|
+
if (neo4jAdapter) {
|
|
87
|
+
return neo4jAdapter;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
console.error(NEO4J_ERROR_MESSAGES.NOT_CONFIGURED);
|
|
92
|
+
}
|
|
93
|
+
// Fall back to JSONL storage
|
|
94
|
+
return createJsonlAdapter(memoryDirPath);
|
|
95
|
+
}
|
|
28
96
|
// Initialize memory directory path (will be set during startup)
|
|
29
97
|
let MEMORY_DIR_PATH;
|
|
30
98
|
export { KnowledgeGraphManager } from './lib/knowledge-graph-manager.js';
|
|
@@ -493,8 +561,38 @@ server.registerTool("get_observation_history", {
|
|
|
493
561
|
async function main() {
|
|
494
562
|
// Initialize memory directory path
|
|
495
563
|
MEMORY_DIR_PATH = await ensureMemoryDirectory();
|
|
496
|
-
//
|
|
497
|
-
|
|
564
|
+
// Create storage adapter based on environment variables
|
|
565
|
+
// Falls back to JSONL if Neo4j is not configured or connection fails
|
|
566
|
+
const storageAdapter = await createStorageAdapter(MEMORY_DIR_PATH);
|
|
567
|
+
// Initialize knowledge graph manager with the storage adapter
|
|
568
|
+
knowledgeGraphManager = new KnowledgeGraphManager(MEMORY_DIR_PATH, storageAdapter);
|
|
569
|
+
// Register graceful shutdown handlers to ensure storage adapter is closed
|
|
570
|
+
let isShuttingDown = false;
|
|
571
|
+
const shutdown = async (signal) => {
|
|
572
|
+
if (isShuttingDown) {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
isShuttingDown = true;
|
|
576
|
+
console.error(`Received ${signal}, shutting down gracefully...`);
|
|
577
|
+
try {
|
|
578
|
+
// Close storage adapter (including Neo4j connections) before exiting
|
|
579
|
+
if (storageAdapter && 'close' in storageAdapter && typeof storageAdapter.close === 'function') {
|
|
580
|
+
await storageAdapter.close();
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
catch (err) {
|
|
584
|
+
console.error("Error during storage adapter shutdown:", err);
|
|
585
|
+
}
|
|
586
|
+
finally {
|
|
587
|
+
process.exit(0);
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
process.on("SIGINT", () => {
|
|
591
|
+
void shutdown("SIGINT");
|
|
592
|
+
});
|
|
593
|
+
process.on("SIGTERM", () => {
|
|
594
|
+
void shutdown("SIGTERM");
|
|
595
|
+
});
|
|
498
596
|
const transport = new StdioServerTransport();
|
|
499
597
|
await server.connect(transport);
|
|
500
598
|
console.error("Enhanced Knowledge Graph MCP Server running on stdio");
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Neo4j Cypher Queries
|
|
3
|
+
*
|
|
4
|
+
* Centralizes all Cypher queries for the Neo4j storage adapter.
|
|
5
|
+
* Following Single Responsibility Principle - this module is responsible only for query definitions.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Constraint and index queries for schema initialization
|
|
9
|
+
*/
|
|
10
|
+
export const SCHEMA_QUERIES = {
|
|
11
|
+
createUniqueConstraint: 'CREATE CONSTRAINT entity_name_unique IF NOT EXISTS FOR (e:Entity) REQUIRE e.name IS UNIQUE',
|
|
12
|
+
createEntityTypeIndex: 'CREATE INDEX entity_type_idx IF NOT EXISTS FOR (e:Entity) ON (e.entityType)',
|
|
13
|
+
createThreadIndex: 'CREATE INDEX entity_thread_idx IF NOT EXISTS FOR (e:Entity) ON (e.agentThreadId)',
|
|
14
|
+
createTimestampIndex: 'CREATE INDEX entity_timestamp_idx IF NOT EXISTS FOR (e:Entity) ON (e.timestamp)',
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Entity queries
|
|
18
|
+
*/
|
|
19
|
+
export const ENTITY_QUERIES = {
|
|
20
|
+
loadAll: `
|
|
21
|
+
MATCH (e:Entity)
|
|
22
|
+
RETURN e.name as name,
|
|
23
|
+
e.entityType as entityType,
|
|
24
|
+
e.observations as observations,
|
|
25
|
+
e.agentThreadId as agentThreadId,
|
|
26
|
+
e.timestamp as timestamp,
|
|
27
|
+
e.confidence as confidence,
|
|
28
|
+
e.importance as importance
|
|
29
|
+
`,
|
|
30
|
+
// Note: Using CREATE instead of MERGE is intentional here.
|
|
31
|
+
// The saveGraph() method first deletes all existing data, then creates fresh entities.
|
|
32
|
+
// This is a complete graph replacement operation, not an upsert.
|
|
33
|
+
// The unique constraint on entity names prevents duplicates within a single transaction.
|
|
34
|
+
create: `
|
|
35
|
+
CREATE (e:Entity {
|
|
36
|
+
name: $name,
|
|
37
|
+
entityType: $entityType,
|
|
38
|
+
observations: $observations,
|
|
39
|
+
agentThreadId: $agentThreadId,
|
|
40
|
+
timestamp: $timestamp,
|
|
41
|
+
confidence: $confidence,
|
|
42
|
+
importance: $importance
|
|
43
|
+
})
|
|
44
|
+
`,
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Relation queries
|
|
48
|
+
*/
|
|
49
|
+
export const RELATION_QUERIES = {
|
|
50
|
+
loadAll: `
|
|
51
|
+
MATCH (from:Entity)-[r:RELATES_TO]->(to:Entity)
|
|
52
|
+
RETURN from.name as from,
|
|
53
|
+
to.name as to,
|
|
54
|
+
r.relationType as relationType,
|
|
55
|
+
r.agentThreadId as agentThreadId,
|
|
56
|
+
r.timestamp as timestamp,
|
|
57
|
+
r.confidence as confidence,
|
|
58
|
+
r.importance as importance
|
|
59
|
+
`,
|
|
60
|
+
// Note: Using MATCH for both entities is intentional.
|
|
61
|
+
// The saveGraph() method ensures entities are created first, then relations.
|
|
62
|
+
// Since this runs in a transaction after entity creation, both entities must exist.
|
|
63
|
+
// If either entity doesn't exist, the relation creation will fail the transaction,
|
|
64
|
+
// maintaining data integrity (no orphaned relations).
|
|
65
|
+
create: `
|
|
66
|
+
MATCH (from:Entity {name: $from})
|
|
67
|
+
MATCH (to:Entity {name: $to})
|
|
68
|
+
CREATE (from)-[r:RELATES_TO {
|
|
69
|
+
relationType: $relationType,
|
|
70
|
+
agentThreadId: $agentThreadId,
|
|
71
|
+
timestamp: $timestamp,
|
|
72
|
+
confidence: $confidence,
|
|
73
|
+
importance: $importance
|
|
74
|
+
}]->(to)
|
|
75
|
+
`,
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Maintenance queries
|
|
79
|
+
*/
|
|
80
|
+
export const MAINTENANCE_QUERIES = {
|
|
81
|
+
deleteAll: 'MATCH (n:Entity) DETACH DELETE n',
|
|
82
|
+
};
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Neo4j Storage Adapter
|
|
2
|
+
* Neo4j Storage Adapter
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Production-ready implementation of the Neo4j storage adapter.
|
|
5
|
+
* Provides full CRUD operations for the knowledge graph using Neo4j.
|
|
6
|
+
*
|
|
7
|
+
* SOLID Principles Applied:
|
|
8
|
+
* - Single Responsibility: Adapter only handles Neo4j storage operations
|
|
9
|
+
* - Open/Closed: Can be extended without modification through IStorageAdapter
|
|
10
|
+
* - Liskov Substitution: Can replace any IStorageAdapter implementation
|
|
11
|
+
* - Interface Segregation: Implements minimal IStorageAdapter interface
|
|
12
|
+
* - Dependency Inversion: Depends on IStorageAdapter abstraction
|
|
10
13
|
*
|
|
11
14
|
* Example usage:
|
|
12
15
|
* ```typescript
|
|
@@ -23,120 +26,246 @@
|
|
|
23
26
|
* const manager = new KnowledgeGraphManager('', neo4jAdapter);
|
|
24
27
|
* ```
|
|
25
28
|
*/
|
|
29
|
+
import neo4j from 'neo4j-driver';
|
|
30
|
+
import { SCHEMA_QUERIES, ENTITY_QUERIES, RELATION_QUERIES, MAINTENANCE_QUERIES } from './neo4j-queries.js';
|
|
31
|
+
import { NEO4J_ERROR_MESSAGES } from './storage-config.js';
|
|
26
32
|
/**
|
|
27
|
-
* Neo4j-based storage adapter for the knowledge graph
|
|
28
|
-
*
|
|
33
|
+
* Neo4j-based storage adapter for the knowledge graph.
|
|
34
|
+
* Follows Single Responsibility Principle - only handles Neo4j storage operations.
|
|
29
35
|
*/
|
|
30
36
|
export class Neo4jStorageAdapter {
|
|
31
37
|
config;
|
|
32
|
-
|
|
38
|
+
driver = null;
|
|
33
39
|
constructor(config) {
|
|
34
40
|
this.config = config;
|
|
35
41
|
}
|
|
36
42
|
/**
|
|
37
|
-
* Initialize Neo4j connection
|
|
43
|
+
* Initialize Neo4j connection and schema.
|
|
44
|
+
* Creates constraints and indexes for optimal performance.
|
|
38
45
|
*/
|
|
39
46
|
async initialize() {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
47
|
+
try {
|
|
48
|
+
await this.initializeDriver();
|
|
49
|
+
await this.verifyConnection();
|
|
50
|
+
await this.initializeSchema();
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
throw new Error(`${NEO4J_ERROR_MESSAGES.CONNECTION_FAILED}: ${error instanceof Error ? error.message : String(error)}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Initialize the Neo4j driver.
|
|
58
|
+
* Extracted for better testability and separation of concerns.
|
|
59
|
+
*/
|
|
60
|
+
async initializeDriver() {
|
|
61
|
+
this.driver = neo4j.driver(this.config.uri, neo4j.auth.basic(this.config.username, this.config.password));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Verify connection to Neo4j.
|
|
65
|
+
* Extracted for better error handling and testability.
|
|
66
|
+
*/
|
|
67
|
+
async verifyConnection() {
|
|
68
|
+
if (!this.driver) {
|
|
69
|
+
throw new Error(NEO4J_ERROR_MESSAGES.NOT_INITIALIZED);
|
|
70
|
+
}
|
|
71
|
+
await this.driver.verifyConnectivity();
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Initialize database schema (constraints and indexes).
|
|
75
|
+
* Extracted for Single Responsibility Principle.
|
|
76
|
+
*/
|
|
77
|
+
async initializeSchema() {
|
|
78
|
+
const session = await this.createSession();
|
|
79
|
+
try {
|
|
80
|
+
await session.run(SCHEMA_QUERIES.createUniqueConstraint);
|
|
81
|
+
await session.run(SCHEMA_QUERIES.createEntityTypeIndex);
|
|
82
|
+
await session.run(SCHEMA_QUERIES.createThreadIndex);
|
|
83
|
+
await session.run(SCHEMA_QUERIES.createTimestampIndex);
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
await session.close();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Create a Neo4j session.
|
|
91
|
+
* Centralized session creation for DRY principle.
|
|
92
|
+
*/
|
|
93
|
+
async createSession() {
|
|
94
|
+
this.ensureDriverInitialized();
|
|
95
|
+
return this.driver.session({ database: this.config.database });
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Ensure driver is initialized.
|
|
99
|
+
* Guard clause for better error handling.
|
|
100
|
+
*/
|
|
101
|
+
ensureDriverInitialized() {
|
|
102
|
+
if (!this.driver) {
|
|
103
|
+
throw new Error(NEO4J_ERROR_MESSAGES.NOT_INITIALIZED);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Serialize observations for Neo4j storage.
|
|
108
|
+
* Extracted for testability and reusability (DRY).
|
|
109
|
+
*/
|
|
110
|
+
serializeObservations(observations) {
|
|
111
|
+
return JSON.stringify(observations);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Deserialize observations from Neo4j storage.
|
|
115
|
+
* Extracted for testability and reusability (DRY).
|
|
116
|
+
* Returns empty array on parse error for robustness.
|
|
117
|
+
*/
|
|
118
|
+
deserializeObservations(observationsJson) {
|
|
119
|
+
try {
|
|
120
|
+
return JSON.parse(observationsJson);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Map Neo4j record to Entity object.
|
|
128
|
+
* Extracted for Single Responsibility Principle and DRY.
|
|
129
|
+
*/
|
|
130
|
+
mapRecordToEntity(record) {
|
|
131
|
+
return {
|
|
132
|
+
name: record.get('name'),
|
|
133
|
+
entityType: record.get('entityType'),
|
|
134
|
+
observations: this.deserializeObservations(record.get('observations')),
|
|
135
|
+
agentThreadId: record.get('agentThreadId'),
|
|
136
|
+
timestamp: record.get('timestamp'),
|
|
137
|
+
confidence: record.get('confidence'),
|
|
138
|
+
importance: record.get('importance')
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Map Neo4j record to Relation object.
|
|
143
|
+
* Extracted for Single Responsibility Principle and DRY.
|
|
144
|
+
*/
|
|
145
|
+
mapRecordToRelation(record) {
|
|
146
|
+
return {
|
|
147
|
+
from: record.get('from'),
|
|
148
|
+
to: record.get('to'),
|
|
149
|
+
relationType: record.get('relationType'),
|
|
150
|
+
agentThreadId: record.get('agentThreadId'),
|
|
151
|
+
timestamp: record.get('timestamp'),
|
|
152
|
+
confidence: record.get('confidence'),
|
|
153
|
+
importance: record.get('importance')
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Load the complete knowledge graph from Neo4j.
|
|
158
|
+
* Delegates to specialized methods for clarity.
|
|
60
159
|
*/
|
|
61
160
|
async loadGraph() {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
161
|
+
this.ensureDriverInitialized();
|
|
162
|
+
const session = await this.createSession();
|
|
163
|
+
try {
|
|
164
|
+
const entities = await this.loadEntities(session);
|
|
165
|
+
const relations = await this.loadRelations(session);
|
|
166
|
+
return { entities, relations };
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
await session.close();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Load all entities from Neo4j.
|
|
174
|
+
* Extracted for Single Responsibility Principle.
|
|
175
|
+
*/
|
|
176
|
+
async loadEntities(session) {
|
|
177
|
+
const result = await session.run(ENTITY_QUERIES.loadAll);
|
|
178
|
+
return result.records.map(record => this.mapRecordToEntity(record));
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Load all relations from Neo4j.
|
|
182
|
+
* Extracted for Single Responsibility Principle.
|
|
183
|
+
*/
|
|
184
|
+
async loadRelations(session) {
|
|
185
|
+
const result = await session.run(RELATION_QUERIES.loadAll);
|
|
186
|
+
return result.records.map(record => this.mapRecordToRelation(record));
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Save the complete knowledge graph to Neo4j.
|
|
190
|
+
* Uses transactions for atomicity.
|
|
85
191
|
*/
|
|
86
192
|
async saveGraph(graph) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
193
|
+
this.ensureDriverInitialized();
|
|
194
|
+
const session = await this.createSession();
|
|
195
|
+
try {
|
|
196
|
+
await session.executeWrite(async (tx) => {
|
|
197
|
+
await this.clearDatabase(tx);
|
|
198
|
+
await this.saveEntities(tx, graph.entities);
|
|
199
|
+
await this.saveRelations(tx, graph.relations);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
await session.close();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Clear all data from the database.
|
|
208
|
+
* Extracted for Single Responsibility Principle.
|
|
209
|
+
*/
|
|
210
|
+
async clearDatabase(tx) {
|
|
211
|
+
await tx.run(MAINTENANCE_QUERIES.deleteAll);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Save all entities to Neo4j.
|
|
215
|
+
* Extracted for Single Responsibility Principle and testability.
|
|
216
|
+
*/
|
|
217
|
+
async saveEntities(tx, entities) {
|
|
218
|
+
for (const entity of entities) {
|
|
219
|
+
await this.saveEntity(tx, entity);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Save a single entity to Neo4j.
|
|
224
|
+
* Extracted for DRY and testability.
|
|
225
|
+
*/
|
|
226
|
+
async saveEntity(tx, entity) {
|
|
227
|
+
await tx.run(ENTITY_QUERIES.create, {
|
|
228
|
+
name: entity.name,
|
|
229
|
+
entityType: entity.entityType,
|
|
230
|
+
observations: this.serializeObservations(entity.observations),
|
|
231
|
+
agentThreadId: entity.agentThreadId,
|
|
232
|
+
timestamp: entity.timestamp,
|
|
233
|
+
confidence: entity.confidence,
|
|
234
|
+
importance: entity.importance
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Save all relations to Neo4j.
|
|
239
|
+
* Extracted for Single Responsibility Principle and testability.
|
|
240
|
+
*/
|
|
241
|
+
async saveRelations(tx, relations) {
|
|
242
|
+
for (const relation of relations) {
|
|
243
|
+
await this.saveRelation(tx, relation);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Save a single relation to Neo4j.
|
|
248
|
+
* Extracted for DRY and testability.
|
|
249
|
+
*/
|
|
250
|
+
async saveRelation(tx, relation) {
|
|
251
|
+
await tx.run(RELATION_QUERIES.create, {
|
|
252
|
+
from: relation.from,
|
|
253
|
+
to: relation.to,
|
|
254
|
+
relationType: relation.relationType,
|
|
255
|
+
agentThreadId: relation.agentThreadId,
|
|
256
|
+
timestamp: relation.timestamp,
|
|
257
|
+
confidence: relation.confidence,
|
|
258
|
+
importance: relation.importance
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Close Neo4j connection.
|
|
263
|
+
* Properly cleans up resources.
|
|
114
264
|
*/
|
|
115
265
|
async close() {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
266
|
+
if (this.driver) {
|
|
267
|
+
await this.driver.close();
|
|
268
|
+
this.driver = null;
|
|
269
|
+
}
|
|
120
270
|
}
|
|
121
271
|
}
|
|
122
|
-
/**
|
|
123
|
-
* Example of how this adapter could be used:
|
|
124
|
-
*
|
|
125
|
-
* const neo4jAdapter = new Neo4jStorageAdapter({
|
|
126
|
-
* uri: 'neo4j://localhost:7687',
|
|
127
|
-
* username: 'neo4j',
|
|
128
|
-
* password: 'password',
|
|
129
|
-
* database: 'knowledge-graph'
|
|
130
|
-
* });
|
|
131
|
-
*
|
|
132
|
-
* await neo4jAdapter.initialize();
|
|
133
|
-
*
|
|
134
|
-
* const manager = new KnowledgeGraphManager('', neo4jAdapter);
|
|
135
|
-
*
|
|
136
|
-
* // Use the manager as normal - all operations will now use Neo4j
|
|
137
|
-
* await manager.createEntities([...]);
|
|
138
|
-
* const graph = await manager.readGraph();
|
|
139
|
-
*
|
|
140
|
-
* // Clean up when done
|
|
141
|
-
* await neo4jAdapter.close();
|
|
142
|
-
*/
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage Configuration Constants
|
|
3
|
+
*
|
|
4
|
+
* Centralizes all configuration constants for storage adapters.
|
|
5
|
+
* Following DRY principle - define once, use everywhere.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Environment variable names for Neo4j configuration
|
|
9
|
+
*/
|
|
10
|
+
export const NEO4J_ENV_VARS = {
|
|
11
|
+
URI: 'NEO4J_URI',
|
|
12
|
+
USERNAME: 'NEO4J_USERNAME',
|
|
13
|
+
PASSWORD: 'NEO4J_PASSWORD',
|
|
14
|
+
DATABASE: 'NEO4J_DATABASE',
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Default Neo4j configuration values
|
|
18
|
+
*/
|
|
19
|
+
export const NEO4J_DEFAULTS = {
|
|
20
|
+
URI: 'neo4j://localhost:7687',
|
|
21
|
+
USERNAME: 'neo4j',
|
|
22
|
+
PASSWORD: 'testpassword',
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Error messages for Neo4j storage
|
|
26
|
+
*/
|
|
27
|
+
export const NEO4J_ERROR_MESSAGES = {
|
|
28
|
+
NOT_INITIALIZED: 'Neo4j driver not initialized. Call initialize() first.',
|
|
29
|
+
CONNECTION_FAILED: 'Failed to initialize Neo4j connection',
|
|
30
|
+
NOT_CONFIGURED: 'Neo4j not configured (NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD), using JSONL storage',
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Log messages for storage selection
|
|
34
|
+
*/
|
|
35
|
+
export const STORAGE_LOG_MESSAGES = {
|
|
36
|
+
ATTEMPTING_NEO4J: 'Attempting to connect to Neo4j at',
|
|
37
|
+
NEO4J_SUCCESS: 'Successfully connected to Neo4j storage',
|
|
38
|
+
NEO4J_FALLBACK: 'Failed to connect to Neo4j, falling back to JSONL storage:',
|
|
39
|
+
USING_JSONL: 'Using JSONL storage at',
|
|
40
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "server-memory-enhanced",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Enhanced MCP server for memory with agent threading, timestamps, and confidence scoring",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"mcpName": "io.github.modelcontextprotocol/server-memory-enhanced",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
27
|
+
"neo4j-driver": "^6.0.1",
|
|
27
28
|
"zod": "^3.25.0"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|