server-memory-enhanced 2.1.0 → 2.2.1

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 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
- // Initialize knowledge graph manager with the memory directory path
497
- knowledgeGraphManager = new KnowledgeGraphManager(MEMORY_DIR_PATH);
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");
@@ -10,8 +10,9 @@ export const MAX_OBSERVATION_LENGTH = 150;
10
10
  /**
11
11
  * Maximum number of sentences allowed per observation
12
12
  * Per spec Section 2: Hard Limits on Observation Length
13
+ * Increased to 3 to accommodate technical facts with version numbers and metrics
13
14
  */
14
- export const MAX_SENTENCES = 2;
15
+ export const MAX_SENTENCES = 3;
15
16
  /**
16
17
  * Minimum observation length in characters
17
18
  */
@@ -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 (Skeleton Implementation)
2
+ * Neo4j Storage Adapter
3
3
  *
4
- * This is a skeleton implementation showing how a Neo4j storage adapter could be built.
5
- * To use this in production, you would need to:
6
- * 1. Install neo4j-driver: npm install neo4j-driver
7
- * 2. Implement the actual Cypher queries
8
- * 3. Add error handling and connection management
9
- * 4. Add transaction support for atomic operations
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
- * This is a skeleton implementation - requires neo4j-driver package
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
- // private driver: any; // Would be neo4j.Driver from neo4j-driver package
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
- // TODO: Initialize Neo4j driver
41
- // this.driver = neo4j.driver(
42
- // this.config.uri,
43
- // neo4j.auth.basic(this.config.username, this.config.password)
44
- // );
45
- // TODO: Verify connectivity
46
- // await this.driver.verifyConnectivity();
47
- // TODO: Create constraints and indexes
48
- // const session = this.driver.session({ database: this.config.database });
49
- // try {
50
- // await session.run('CREATE CONSTRAINT IF NOT EXISTS FOR (e:Entity) REQUIRE e.name IS UNIQUE');
51
- // await session.run('CREATE INDEX IF NOT EXISTS FOR (e:Entity) ON (e.entityType)');
52
- // await session.run('CREATE INDEX IF NOT EXISTS FOR (e:Entity) ON (e.agentThreadId)');
53
- // } finally {
54
- // await session.close();
55
- // }
56
- throw new Error('Neo4jStorageAdapter requires neo4j-driver package to be installed and methods to be implemented. See STORAGE.md documentation for setup instructions.');
57
- }
58
- /**
59
- * Load the complete knowledge graph from Neo4j
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
- // TODO: Implement Cypher query to load all entities and relations
63
- // Example Cypher for entities:
64
- // MATCH (e:Entity)
65
- // RETURN e.name as name,
66
- // e.entityType as entityType,
67
- // e.observations as observations,
68
- // e.agentThreadId as agentThreadId,
69
- // e.timestamp as timestamp,
70
- // e.confidence as confidence,
71
- // e.importance as importance
72
- // Example Cypher for relations:
73
- // MATCH (from:Entity)-[r:RELATES_TO]->(to:Entity)
74
- // RETURN from.name as from,
75
- // to.name as to,
76
- // r.relationType as relationType,
77
- // r.agentThreadId as agentThreadId,
78
- // r.timestamp as timestamp,
79
- // r.confidence as confidence,
80
- // r.importance as importance
81
- throw new Error('Neo4jStorageAdapter.loadGraph() is not implemented. This is a skeleton - install neo4j-driver and implement the Cypher queries shown in comments above.');
82
- }
83
- /**
84
- * Save the complete knowledge graph to Neo4j
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
- // TODO: Implement transactional save
88
- // This should:
89
- // 1. Start a transaction
90
- // 2. Delete all existing nodes and relationships (or use MERGE for upsert)
91
- // 3. Create all entities as nodes
92
- // 4. Create all relations as relationships
93
- // 5. Commit the transaction
94
- // Example Cypher for creating entity:
95
- // MERGE (e:Entity {name: $name})
96
- // SET e.entityType = $entityType,
97
- // e.observations = $observations,
98
- // e.agentThreadId = $agentThreadId,
99
- // e.timestamp = $timestamp,
100
- // e.confidence = $confidence,
101
- // e.importance = $importance
102
- // Example Cypher for creating relation:
103
- // MATCH (from:Entity {name: $from})
104
- // MATCH (to:Entity {name: $to})
105
- // MERGE (from)-[r:RELATES_TO {relationType: $relationType}]->(to)
106
- // SET r.agentThreadId = $agentThreadId,
107
- // r.timestamp = $timestamp,
108
- // r.confidence = $confidence,
109
- // r.importance = $importance
110
- throw new Error('Neo4jStorageAdapter.saveGraph() is not implemented. This is a skeleton - install neo4j-driver and implement the transactional save logic shown in comments above.');
111
- }
112
- /**
113
- * Close Neo4j connection
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
- // TODO: Close the driver
117
- // if (this.driver) {
118
- // await this.driver.close();
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
+ };
@@ -2,11 +2,26 @@
2
2
  * Validation logic for save_memory tool (Section 1 of spec)
3
3
  */
4
4
  import { MAX_OBSERVATION_LENGTH, MIN_OBSERVATION_LENGTH, MAX_SENTENCES, SENTENCE_TERMINATORS, TARGET_AVG_RELATIONS, RELATION_SCORE_WEIGHT, OBSERVATION_SCORE_WEIGHT } from './constants.js';
5
+ /**
6
+ * Counts actual sentences in text, ignoring periods in version numbers and decimals
7
+ * @param text The text to analyze
8
+ * @returns Number of actual sentences
9
+ */
10
+ function countSentences(text) {
11
+ // Remove version numbers (e.g., 1.2.0, v5.4.3, V2.1.0) and decimal numbers before counting
12
+ // This prevents false positives where technical data is incorrectly counted as sentences
13
+ // Using explicit case handling [vV] for version prefix
14
+ const cleaned = text
15
+ .replace(/\b[vV]?\d+\.\d+(\.\d+)*\b/g, 'VERSION'); // handles version numbers and decimals
16
+ // Split on actual sentence terminators
17
+ const sentences = cleaned.split(SENTENCE_TERMINATORS).filter(s => s.trim().length > 0);
18
+ return sentences.length;
19
+ }
5
20
  /**
6
21
  * Validates a single observation according to spec requirements:
7
22
  * - Min 5 characters
8
23
  * - Max 150 characters
9
- * - Max 2 sentences (simple count by periods)
24
+ * - Max 3 sentences (ignoring periods in version numbers and decimals)
10
25
  */
11
26
  export function validateObservation(obs) {
12
27
  if (obs.length < MIN_OBSERVATION_LENGTH) {
@@ -23,12 +38,12 @@ export function validateObservation(obs) {
23
38
  suggestion: `Split into multiple observations.`
24
39
  };
25
40
  }
26
- const sentences = obs.split(SENTENCE_TERMINATORS).filter(s => s.trim().length > 0);
27
- if (sentences.length > MAX_SENTENCES) {
41
+ const sentenceCount = countSentences(obs);
42
+ if (sentenceCount > MAX_SENTENCES) {
28
43
  return {
29
44
  valid: false,
30
- error: `Too many sentences (${sentences.length}). Max ${MAX_SENTENCES}.`,
31
- suggestion: `One fact per observation. Split this into ${sentences.length} separate observations.`
45
+ error: `Too many sentences (${sentenceCount}). Max ${MAX_SENTENCES}.`,
46
+ suggestion: `One fact per observation. Split this into ${sentenceCount} separate observations.`
32
47
  };
33
48
  }
34
49
  return { valid: true };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "server-memory-enhanced",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
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": {