server-memory-enhanced 2.3.0 → 2.3.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/README.md CHANGED
@@ -15,8 +15,10 @@ An enhanced version of the Memory MCP server that provides persistent knowledge
15
15
  ### Entities
16
16
  Each entity now includes:
17
17
  - `name`: Entity identifier
18
- - `entityType`: Type of entity
19
- - `observations`: Array of observation strings
18
+ - `entityType`: Type of entity (free-form, any domain-specific type allowed)
19
+ - `observations`: Array of **versioned Observation objects** (not strings) - **BREAKING CHANGE**
20
+ - Each observation has: `id`, `content`, `timestamp`, `version`, `supersedes`, `superseded_by`
21
+ - Supports full version history tracking
20
22
  - `agentThreadId`: Unique identifier for the agent thread
21
23
  - `timestamp`: ISO 8601 timestamp of creation
22
24
  - `confidence`: Confidence score (0.0 to 1.0)
@@ -52,39 +54,64 @@ The server stores data in separate JSONL files per agent thread:
52
54
 
53
55
  ## Available Tools
54
56
 
57
+ ### ⭐ Recommended Tool (New)
58
+ 1. **save_memory**: **[RECOMMENDED]** Unified tool for creating entities and relations atomically with server-side validation
59
+ - Enforces observation limits (max 300 chars, 3 sentences per observation, ignoring periods in version numbers)
60
+ - Requires at least 1 relation per entity (prevents orphaned nodes)
61
+ - Free-form entity types with soft normalization
62
+ - Atomic transactions (all-or-nothing)
63
+ - Bidirectional relation tracking
64
+ - Quality score calculation
65
+ - Clear, actionable error messages
66
+
55
67
  ### Core Operations
56
- 1. **create_entities**: Create new entities with metadata (including importance)
57
- 2. **create_relations**: Create relationships between entities with metadata (including importance)
68
+ > ⚠️ **Note**: `create_entities` and `create_relations` are **deprecated**. New code should use `save_memory` for better reliability and validation.
69
+
70
+ 1. **create_entities**: Create new entities with metadata (including importance) - **[DEPRECATED - Use save_memory]**
71
+ 2. **create_relations**: Create relationships between entities with metadata (including importance) - **[DEPRECATED - Use save_memory]**
58
72
  3. **add_observations**: Add observations to existing entities with metadata (including importance)
59
- 4. **delete_entities**: Remove entities and cascading relations
60
- 5. **delete_observations**: Remove specific observations
61
- 6. **delete_relations**: Delete relationships
62
- 7. **read_graph**: Read the entire knowledge graph
63
- 8. **search_nodes**: Search entities by name, type, or observation content
64
- 9. **open_nodes**: Retrieve specific entities by name
65
- 10. **query_nodes**: Advanced querying with range-based filtering by timestamp, confidence, and importance
73
+ 4. **update_observation**: **[NEW]** Update an existing observation by creating a new version with updated content, maintaining version history
74
+ 5. **delete_entities**: Remove entities and cascading relations
75
+ 6. **delete_observations**: Remove specific observations
76
+ 7. **delete_relations**: Delete relationships
77
+ 8. **read_graph**: Read the entire knowledge graph
78
+ 9. **search_nodes**: Search entities by name, type, or observation content
79
+ 10. **open_nodes**: Retrieve specific entities by name
80
+ 11. **query_nodes**: Advanced querying with range-based filtering by timestamp, confidence, and importance
81
+ 12. **list_entities**: List entities with optional filtering by type and name pattern for quick discovery
82
+ 13. **validate_memory**: Validate entities without saving (dry-run) - check for errors before attempting save_memory
66
83
 
67
84
  ### Memory Management & Insights
68
- 11. **get_memory_stats**: Get comprehensive statistics (entity counts, thread activity, avg confidence/importance, recent activity)
69
- 12. **get_recent_changes**: Retrieve entities and relations created/modified since a specific timestamp
70
- 13. **prune_memory**: Remove old or low-importance entities to manage memory size
71
- 14. **bulk_update**: Efficiently update multiple entities at once (confidence, importance, observations)
85
+ 14. **get_analytics**: **[NEW]** Get simple, LLM-friendly analytics about your knowledge graph
86
+ - Recent changes (last 10 entities)
87
+ - Top important entities (by importance score)
88
+ - Most connected entities (by relation count)
89
+ - Orphaned entities (quality check)
90
+ 15. **get_observation_history**: **[NEW]** Retrieve version history for observations
91
+ - Track how observations evolve over time
92
+ - View complete version chains
93
+ - Supports rollback by viewing previous versions
94
+ 16. **get_memory_stats**: Get comprehensive statistics (entity counts, thread activity, avg confidence/importance, recent activity)
95
+ 17. **get_recent_changes**: Retrieve entities and relations created/modified since a specific timestamp
96
+ 18. **prune_memory**: Remove old or low-importance entities to manage memory size
97
+ 19. **bulk_update**: Efficiently update multiple entities at once (confidence, importance, observations)
98
+ 20. **list_conversations**: List all available agent threads (conversations) with metadata including entity counts, relation counts, and activity timestamps
72
99
 
73
100
  ### Relationship Intelligence
74
- 15. **find_relation_path**: Find the shortest path of relationships between two entities (useful for "how are they connected?")
75
- 16. **get_context**: Retrieve entities and relations related to specified entities up to a certain depth
101
+ 21. **find_relation_path**: Find the shortest path of relationships between two entities (useful for "how are they connected?")
102
+ 22. **get_context**: Retrieve entities and relations related to specified entities up to a certain depth
76
103
 
77
104
  ### Quality & Review
78
- 17. **detect_conflicts**: Detect potentially conflicting observations using pattern matching and negation detection
79
- 18. **flag_for_review**: Mark entities for human review with a specific reason (Human-in-the-Loop)
80
- 19. **get_flagged_entities**: Retrieve all entities flagged for review
105
+ 23. **detect_conflicts**: Detect potentially conflicting observations using pattern matching and negation detection
106
+ 24. **flag_for_review**: Mark entities for human review with a specific reason (Human-in-the-Loop)
107
+ 25. **get_flagged_entities**: Retrieve all entities flagged for review
81
108
 
82
109
  ## Usage
83
110
 
84
111
  ### Installation
85
112
 
86
113
  ```bash
87
- npm install @modelcontextprotocol/server-memory-enhanced
114
+ npm install server-memory-enhanced
88
115
  ```
89
116
 
90
117
  ### Running the Server
@@ -95,44 +122,150 @@ npx mcp-server-memory-enhanced
95
122
 
96
123
  ### Configuration
97
124
 
125
+ #### File Storage (Default)
126
+
98
127
  Set the `MEMORY_DIR_PATH` environment variable to customize the storage location:
99
128
 
100
129
  ```bash
101
130
  MEMORY_DIR_PATH=/path/to/memory/directory npx mcp-server-memory-enhanced
102
131
  ```
103
132
 
104
- ## Example
133
+ #### Neo4j Storage (Optional)
134
+
135
+ The server supports Neo4j as an alternative storage backend. If Neo4j environment variables are set, the server will attempt to connect to Neo4j. If the connection fails or variables are not set, it will automatically fall back to file-based JSONL storage.
136
+
137
+ **Environment Variables:**
138
+
139
+ ```bash
140
+ # Neo4j connection settings
141
+ export NEO4J_URI=neo4j://localhost:7687
142
+ export NEO4J_USERNAME=neo4j
143
+ export NEO4J_PASSWORD=your_password
144
+ export NEO4J_DATABASE=neo4j # Optional, defaults to 'neo4j'
145
+
146
+ # Run the server
147
+ npx mcp-server-memory-enhanced
148
+ ```
149
+
150
+ **Using Docker Compose:**
151
+
152
+ A `docker-compose.yml` file is provided for local development with Neo4j:
153
+
154
+ ```bash
155
+ # Start Neo4j and the MCP server
156
+ docker-compose up
157
+
158
+ # The Neo4j browser will be available at http://localhost:7474
159
+ # Username: neo4j, Password: testpassword
160
+ ```
161
+
162
+ **Using with Claude Desktop:**
163
+
164
+ Configure the server in your Claude Desktop configuration with Neo4j:
165
+
166
+ ```json
167
+ {
168
+ "mcpServers": {
169
+ "memory-enhanced": {
170
+ "command": "npx",
171
+ "args": ["-y", "mcp-server-memory-enhanced"],
172
+ "env": {
173
+ "NEO4J_URI": "neo4j://localhost:7687",
174
+ "NEO4J_USERNAME": "neo4j",
175
+ "NEO4J_PASSWORD": "your_password"
176
+ }
177
+ }
178
+ }
179
+ }
180
+ ```
181
+
182
+ **Benefits of Neo4j Storage:**
183
+
184
+ - **Graph-native queries**: Faster relationship traversals and path finding
185
+ - **Scalability**: Better performance with large knowledge graphs
186
+ - **Advanced queries**: Native support for graph algorithms
187
+ - **Visualization**: Use Neo4j Browser to visualize your knowledge graph
188
+ - **Automatic fallback**: If Neo4j is not available, automatically uses file storage
189
+
190
+ ## User Guide
191
+
192
+ ### ✨ Using save_memory (Recommended)
193
+
194
+ The `save_memory` tool is the recommended way to create entities and relations. It provides atomic transactions and server-side validation to ensure high-quality knowledge graphs.
195
+
196
+ #### Key Principles
197
+
198
+ 1. **Atomic Observations**: Each observation should be a single, atomic fact
199
+ - ✅ Good: `"Works at Google"`, `"Lives in San Francisco"`
200
+ - ❌ Bad: `"Works at Google and lives in San Francisco and has a PhD in Computer Science"`
201
+ - **Max length**: 300 characters per observation
202
+ - **Max sentences**: 3 sentences per observation (technical content with version numbers supported)
203
+
204
+ 2. **Mandatory Relations**: Every entity must connect to at least one other entity
205
+ - ✅ Good: `{ targetEntity: "Google", relationType: "works at" }`
206
+ - ❌ Bad: Empty relations array `[]`
207
+ - This prevents orphaned nodes and ensures a well-connected knowledge graph
208
+
209
+ 3. **Free Entity Types**: Use any entity type that makes sense for your domain
210
+ - ✅ Good: `"Person"`, `"Company"`, `"Document"`, `"Recipe"`, `"Patient"`, `"API"`
211
+ - Soft normalization: `"person"` → `"Person"` (warning, not error)
212
+ - Space warning: `"API Key"` → suggests `"APIKey"`
213
+
214
+ 4. **Error Messages**: The tool provides clear, actionable error messages
215
+ - Too long: `"Observation too long (350 chars). Max 300. Suggestion: Split into multiple observations."`
216
+ - No relations: `"Entity 'X' must have at least 1 relation. Suggestion: Add relations to show connections."`
217
+ - Too many sentences: `"Too many sentences (4). Max 3. Suggestion: Split this into 4 separate observations."`
218
+
219
+ ### Example Usage
105
220
 
106
221
  ```typescript
107
- // Create entities with metadata including importance
108
- await createEntities({
222
+ // RECOMMENDED: Use save_memory for atomic entity and relation creation
223
+ await save_memory({
109
224
  entities: [
110
225
  {
111
226
  name: "Alice",
112
- entityType: "person",
113
- observations: ["works at Acme Corp"],
114
- agentThreadId: "thread-001",
115
- timestamp: "2024-01-20T10:00:00Z",
116
- confidence: 0.95,
117
- importance: 0.9 // Critical entity
227
+ entityType: "Person",
228
+ observations: ["Works at Google", "Lives in SF"], // Atomic facts, under 300 chars
229
+ relations: [{ targetEntity: "Bob", relationType: "knows" }] // At least 1 relation required
230
+ },
231
+ {
232
+ name: "Bob",
233
+ entityType: "Person",
234
+ observations: ["Works at Microsoft"],
235
+ relations: [{ targetEntity: "Alice", relationType: "knows" }]
118
236
  }
119
- ]
237
+ ],
238
+ threadId: "conversation-001"
120
239
  });
121
240
 
122
- // Create relations with metadata including importance
123
- await createRelations({
124
- relations: [
125
- {
126
- from: "Alice",
127
- to: "Bob",
128
- relationType: "knows",
129
- agentThreadId: "thread-001",
130
- timestamp: "2024-01-20T10:01:00Z",
131
- confidence: 0.9,
132
- importance: 0.75 // Important relationship
133
- }
134
- ]
241
+ // Get analytics about your knowledge graph
242
+ await get_analytics({
243
+ threadId: "conversation-001"
244
+ });
245
+ // Returns: {
246
+ // recent_changes: [...], // Last 10 entities
247
+ // top_important: [...], // Top 10 by importance
248
+ // most_connected: [...], // Top 10 by relation count
249
+ // orphaned_entities: [...] // Quality check
250
+ // }
251
+
252
+ // Get observation version history
253
+ await get_observation_history({
254
+ entityName: "Python Scripts",
255
+ observationId: "obs_abc123"
135
256
  });
257
+ // Returns: { history: [{ id, content, version, timestamp, supersedes, superseded_by }, ...] }
258
+
259
+ // Update an existing observation (creates a new version)
260
+ await update_observation({
261
+ entityName: "Alice",
262
+ observationId: "obs_abc123",
263
+ newContent: "Works at Google (Senior Engineer)",
264
+ agentThreadId: "conversation-001",
265
+ timestamp: "2024-01-20T12:00:00Z",
266
+ confidence: 0.95 // Optional: update confidence
267
+ });
268
+ // Returns: { success: true, updatedObservation: { id, content, version: 2, supersedes, ... }, message: "..." }
136
269
 
137
270
  // Query nodes with range-based filtering
138
271
  await queryNodes({
@@ -146,6 +279,10 @@ await queryNodes({
146
279
  await getMemoryStats();
147
280
  // Returns: { entityCount, relationCount, threadCount, entityTypes, avgConfidence, avgImportance, recentActivity }
148
281
 
282
+ // List all conversations (agent threads)
283
+ await listConversations();
284
+ // Returns: { conversations: [{ agentThreadId, entityCount, relationCount, firstCreated, lastUpdated }, ...] }
285
+
149
286
  // Get recent changes since last interaction
150
287
  await getRecentChanges({ since: "2024-01-20T10:00:00Z" });
151
288
 
@@ -173,6 +310,49 @@ await bulkUpdate({
173
310
  await pruneMemory({ olderThan: "2024-01-01T00:00:00Z", importanceLessThan: 0.3, keepMinEntities: 100 });
174
311
  ```
175
312
 
313
+ ### 🔄 Migration Guide
314
+
315
+ For users of the old `create_entities` and `create_relations` tools:
316
+
317
+ #### What Changed
318
+ - **Old approach**: Two separate tools that could be used independently
319
+ - `create_entities` → creates entities
320
+ - `create_relations` → creates relations (optional, often skipped by LLMs)
321
+ - **New approach**: Single `save_memory` tool with atomic transactions
322
+ - Creates entities and relations together
323
+ - Enforces mandatory relations (at least 1 per entity)
324
+ - Validates observation length and atomicity
325
+
326
+ #### Migrating Your Code
327
+ ```typescript
328
+ // ❌ OLD WAY (deprecated but still works)
329
+ await create_entities({
330
+ entities: [{ name: "Alice", entityType: "person", observations: ["works at Google and lives in SF"] }]
331
+ });
332
+ await create_relations({ // Often forgotten!
333
+ relations: [{ from: "Alice", to: "Bob", relationType: "knows" }]
334
+ });
335
+
336
+ // ✅ NEW WAY (recommended)
337
+ await save_memory({
338
+ entities: [
339
+ {
340
+ name: "Alice",
341
+ entityType: "Person",
342
+ observations: ["Works at Google", "Lives in SF"], // Split into atomic facts
343
+ relations: [{ targetEntity: "Bob", relationType: "knows" }] // Required!
344
+ }
345
+ ],
346
+ threadId: "conversation-001"
347
+ });
348
+ ```
349
+
350
+ #### Migration Strategy
351
+ 1. **Old tools remain available**: `create_entities` and `create_relations` are deprecated but not removed
352
+ 2. **No forced migration**: Update your code gradually at your own pace
353
+ 3. **New code should use `save_memory`**: Benefits from validation and atomic transactions
354
+ 4. **Observation versioning**: New installations use versioned observations (breaking change for data model)
355
+
176
356
  ## Development
177
357
 
178
358
  ### Build
@@ -196,3 +376,24 @@ npm run watch
196
376
  ## License
197
377
 
198
378
  MIT
379
+
380
+ ## 🤝 Contributing
381
+
382
+ Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
383
+
384
+ ## 🔒 Security
385
+
386
+ See [SECURITY.MD](SECURITY.md) for reporting security vulnerabilities.
387
+
388
+ ## 📜 License
389
+
390
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
391
+
392
+ ## 💬 Community
393
+
394
+ - [GitHub Discussions](https://github.com/modelcontextprotocol/servers/discussions)
395
+ - [Model Context Protocol Documentation](https://modelcontextprotocol.io)
396
+
397
+ ---
398
+
399
+ Part of the [Model Context Protocol](https://modelcontextprotocol.io) project.
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { promises as fs } from 'fs';
6
6
  import path from 'path';
7
7
  import { fileURLToPath } from 'url';
8
8
  import { KnowledgeGraphManager } from './lib/knowledge-graph-manager.js';
9
- import { EntitySchema, RelationSchema, SaveMemoryInputSchema, SaveMemoryOutputSchema, GetAnalyticsInputSchema, GetAnalyticsOutputSchema, GetObservationHistoryInputSchema, GetObservationHistoryOutputSchema, ListEntitiesInputSchema, ListEntitiesOutputSchema, ValidateMemoryInputSchema, ValidateMemoryOutputSchema } from './lib/schemas.js';
9
+ import { EntitySchema, RelationSchema, SaveMemoryInputSchema, SaveMemoryOutputSchema, GetAnalyticsInputSchema, GetAnalyticsOutputSchema, GetObservationHistoryInputSchema, GetObservationHistoryOutputSchema, ListEntitiesInputSchema, ListEntitiesOutputSchema, ValidateMemoryInputSchema, ValidateMemoryOutputSchema, UpdateObservationInputSchema, UpdateObservationOutputSchema } from './lib/schemas.js';
10
10
  import { handleSaveMemory } from './lib/save-memory-handler.js';
11
11
  import { validateSaveMemoryRequest } from './lib/validation.js';
12
12
  import { JsonlStorageAdapter } from './lib/jsonl-storage-adapter.js';
@@ -267,6 +267,32 @@ server.registerTool("delete_observations", {
267
267
  structuredContent: { success: true, message: "Observations deleted successfully" }
268
268
  };
269
269
  });
270
+ // Register update_observation tool
271
+ server.registerTool("update_observation", {
272
+ title: "Update Observation",
273
+ description: "Update an existing observation by creating a new version with updated content. This maintains version history through the supersedes/superseded_by chain.",
274
+ inputSchema: UpdateObservationInputSchema.shape,
275
+ outputSchema: UpdateObservationOutputSchema.shape
276
+ }, async ({ entityName, observationId, newContent, agentThreadId, timestamp, confidence, importance }) => {
277
+ const updatedObservation = await knowledgeGraphManager.updateObservation({
278
+ entityName,
279
+ observationId,
280
+ newContent,
281
+ agentThreadId,
282
+ timestamp,
283
+ confidence,
284
+ importance
285
+ });
286
+ const message = `Observation updated successfully. New version: ${updatedObservation.id} (v${updatedObservation.version})`;
287
+ return {
288
+ content: [{ type: "text", text: message }],
289
+ structuredContent: {
290
+ success: true,
291
+ updatedObservation,
292
+ message
293
+ }
294
+ };
295
+ });
270
296
  // Register delete_relations tool
271
297
  server.registerTool("delete_relations", {
272
298
  title: "Delete Relations",
@@ -240,10 +240,14 @@ export class JsonlStorageAdapter {
240
240
  * Serialize thread data to JSONL lines
241
241
  */
242
242
  serializeThreadData(threadData) {
243
- return [
244
- ...threadData.entities.map(e => this.serializeEntity(e)),
245
- ...threadData.relations.map(r => this.serializeRelation(r))
246
- ];
243
+ const lines = [];
244
+ for (const e of threadData.entities) {
245
+ lines.push(this.serializeEntity(e));
246
+ }
247
+ for (const r of threadData.relations) {
248
+ lines.push(this.serializeRelation(r));
249
+ }
250
+ return lines;
247
251
  }
248
252
  /**
249
253
  * Save data for a specific thread
@@ -11,6 +11,38 @@ export class KnowledgeGraphManager {
11
11
  this.storage = storageAdapter || new JsonlStorageAdapter(memoryDirPath);
12
12
  // Lazy initialization - will be called on first operation
13
13
  }
14
+ /**
15
+ * Check if content contains any negation words (using word boundary matching)
16
+ * Handles punctuation and contractions, avoids creating intermediate Set for performance
17
+ */
18
+ hasNegation(content) {
19
+ // Extract words using word boundary regex, preserving contractions (include apostrophes)
20
+ const lowerContent = content.toLowerCase();
21
+ const wordMatches = lowerContent.match(/\b[\w']+\b/g);
22
+ if (!wordMatches) {
23
+ return false;
24
+ }
25
+ // Check each word against negation words without creating intermediate Set
26
+ for (const word of wordMatches) {
27
+ if (KnowledgeGraphManager.NEGATION_WORDS.has(word)) {
28
+ return true;
29
+ }
30
+ }
31
+ return false;
32
+ }
33
+ /**
34
+ * Create a composite key for relation deduplication.
35
+ *
36
+ * We explicitly normalize the components to primitive strings to ensure
37
+ * stable serialization and to document the assumption that `from`, `to`,
38
+ * and `relationType` are simple string identifiers.
39
+ */
40
+ createRelationKey(relation) {
41
+ const from = String(relation.from);
42
+ const to = String(relation.to);
43
+ const relationType = String(relation.relationType);
44
+ return JSON.stringify([from, to, relationType]);
45
+ }
14
46
  /**
15
47
  * Ensure storage is initialized before any operation
16
48
  * This is called automatically by all public methods
@@ -21,12 +53,80 @@ export class KnowledgeGraphManager {
21
53
  }
22
54
  await this.initializePromise;
23
55
  }
56
+ /**
57
+ * Find an entity by name in the knowledge graph.
58
+ * @param graph - The knowledge graph to search
59
+ * @param entityName - Name of the entity to find
60
+ * @returns The found entity
61
+ * @throws Error if entity not found
62
+ */
63
+ findEntity(graph, entityName) {
64
+ const entity = graph.entities.find(e => e.name === entityName);
65
+ if (!entity) {
66
+ throw new Error(`Entity '${entityName}' not found`);
67
+ }
68
+ return entity;
69
+ }
70
+ /**
71
+ * Find an observation by ID within an entity.
72
+ * @param entity - The entity containing the observation
73
+ * @param observationId - ID of the observation to find
74
+ * @returns The found observation
75
+ * @throws Error if observation not found
76
+ */
77
+ findObservation(entity, observationId) {
78
+ const observation = entity.observations.find(o => o.id === observationId);
79
+ if (!observation) {
80
+ throw new Error(`Observation '${observationId}' not found in entity '${entity.name}'`);
81
+ }
82
+ return observation;
83
+ }
84
+ /**
85
+ * Validate that an observation can be updated (not already superseded).
86
+ * @param observation - The observation to validate
87
+ * @throws Error if observation has already been superseded
88
+ */
89
+ validateObservationNotSuperseded(observation) {
90
+ if (observation.superseded_by) {
91
+ throw new Error(`Observation '${observation.id}' has already been superseded by '${observation.superseded_by}'. Update the latest version instead.`);
92
+ }
93
+ }
94
+ /**
95
+ * Resolve confidence value using inheritance chain: params > observation > entity.
96
+ * @param providedValue - Value provided in parameters (optional)
97
+ * @param observationValue - Value from observation (optional)
98
+ * @param entityValue - Value from entity (fallback)
99
+ * @returns Resolved confidence value
100
+ */
101
+ resolveInheritedValue(providedValue, observationValue, entityValue) {
102
+ return providedValue ?? observationValue ?? entityValue;
103
+ }
104
+ /**
105
+ * Create a new observation version from an existing observation.
106
+ * @param oldObs - The observation being updated
107
+ * @param entity - The entity containing the observation
108
+ * @param params - Update parameters
109
+ * @returns New observation with incremented version
110
+ */
111
+ createObservationVersion(oldObs, entity, params) {
112
+ return {
113
+ id: `obs_${randomUUID()}`,
114
+ content: params.newContent,
115
+ timestamp: params.timestamp,
116
+ version: oldObs.version + 1,
117
+ supersedes: oldObs.id,
118
+ agentThreadId: params.agentThreadId,
119
+ confidence: this.resolveInheritedValue(params.confidence, oldObs.confidence, entity.confidence),
120
+ importance: this.resolveInheritedValue(params.importance, oldObs.importance, entity.importance)
121
+ };
122
+ }
24
123
  async createEntities(entities) {
25
124
  await this.ensureInitialized();
26
125
  const graph = await this.storage.loadGraph();
27
126
  // Entity names are globally unique across all threads in the collaborative knowledge graph
28
127
  // This prevents duplicate entities while allowing multiple threads to contribute to the same entity
29
- const newEntities = entities.filter(e => !graph.entities.some(existingEntity => existingEntity.name === e.name));
128
+ const existingNames = new Set(graph.entities.map(e => e.name));
129
+ const newEntities = entities.filter(e => !existingNames.has(e.name));
30
130
  graph.entities.push(...newEntities);
31
131
  await this.storage.saveGraph(graph);
32
132
  return newEntities;
@@ -44,9 +144,15 @@ export class KnowledgeGraphManager {
44
144
  });
45
145
  // Relations are globally unique by (from, to, relationType) across all threads
46
146
  // This enables multiple threads to collaboratively build the knowledge graph
47
- const newRelations = validRelations.filter(r => !graph.relations.some(existingRelation => existingRelation.from === r.from &&
48
- existingRelation.to === r.to &&
49
- existingRelation.relationType === r.relationType));
147
+ const existingRelationKeys = new Set(graph.relations.map(r => this.createRelationKey(r)));
148
+ // Create composite keys once per valid relation to avoid duplicate serialization
149
+ const validRelationsWithKeys = validRelations.map(r => ({
150
+ relation: r,
151
+ key: this.createRelationKey(r)
152
+ }));
153
+ const newRelations = validRelationsWithKeys
154
+ .filter(item => !existingRelationKeys.has(item.key))
155
+ .map(item => item.relation);
50
156
  graph.relations.push(...newRelations);
51
157
  await this.storage.saveGraph(graph);
52
158
  return newRelations;
@@ -60,10 +166,16 @@ export class KnowledgeGraphManager {
60
166
  }
61
167
  // Check for existing observations with same content to create version chain
62
168
  const newObservations = [];
169
+ // Build a Set of existing observation contents for efficient lookup (single-pass)
170
+ const existingContents = entity.observations.reduce((set, obs) => {
171
+ if (!obs.superseded_by) {
172
+ set.add(obs.content);
173
+ }
174
+ return set;
175
+ }, new Set());
63
176
  for (const content of o.contents) {
64
- // Find if there's an existing observation with this content (latest version)
65
- const existingObs = entity.observations.find(obs => obs.content === content && !obs.superseded_by);
66
- if (existingObs) {
177
+ // Check if observation with this content already exists (latest version)
178
+ if (existingContents.has(content)) {
67
179
  // Don't add duplicate - observation with this content already exists
68
180
  // Versioning is for UPDATES to content, not for re-asserting the same content
69
181
  continue;
@@ -92,8 +204,9 @@ export class KnowledgeGraphManager {
92
204
  }
93
205
  async deleteEntities(entityNames) {
94
206
  const graph = await this.storage.loadGraph();
95
- graph.entities = graph.entities.filter(e => !entityNames.includes(e.name));
96
- graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to));
207
+ const namesToDelete = new Set(entityNames);
208
+ graph.entities = graph.entities.filter(e => !namesToDelete.has(e.name));
209
+ graph.relations = graph.relations.filter(r => !namesToDelete.has(r.from) && !namesToDelete.has(r.to));
97
210
  await this.storage.saveGraph(graph);
98
211
  }
99
212
  async deleteObservations(deletions) {
@@ -107,6 +220,41 @@ export class KnowledgeGraphManager {
107
220
  });
108
221
  await this.storage.saveGraph(graph);
109
222
  }
223
+ /**
224
+ * Update an existing observation by creating a new version with updated content.
225
+ * This maintains the version history through the supersedes/superseded_by chain.
226
+ *
227
+ * @param params - Update parameters
228
+ * @param params.entityName - Name of the entity containing the observation
229
+ * @param params.observationId - ID of the observation to update
230
+ * @param params.newContent - New content for the observation
231
+ * @param params.agentThreadId - Agent thread ID making this update
232
+ * @param params.timestamp - ISO 8601 timestamp of the update
233
+ * @param params.confidence - Optional confidence score (0-1), inherits from old observation if not provided
234
+ * @param params.importance - Optional importance score (0-1), inherits from old observation if not provided
235
+ * @returns The newly created observation with incremented version number
236
+ * @throws Error if entity not found
237
+ * @throws Error if observation not found
238
+ * @throws Error if observation has already been superseded (must update latest version)
239
+ */
240
+ async updateObservation(params) {
241
+ await this.ensureInitialized();
242
+ const graph = await this.storage.loadGraph();
243
+ // Find and validate the entity and observation
244
+ const entity = this.findEntity(graph, params.entityName);
245
+ const oldObs = this.findObservation(entity, params.observationId);
246
+ this.validateObservationNotSuperseded(oldObs);
247
+ // Create new version with inheritance chain
248
+ const newObs = this.createObservationVersion(oldObs, entity, params);
249
+ // Link old observation to new one
250
+ oldObs.superseded_by = newObs.id;
251
+ // Add new observation to entity
252
+ entity.observations.push(newObs);
253
+ // Update entity timestamp
254
+ entity.timestamp = params.timestamp;
255
+ await this.storage.saveGraph(graph);
256
+ return newObs;
257
+ }
110
258
  async deleteRelations(relations) {
111
259
  const graph = await this.storage.loadGraph();
112
260
  // Delete relations globally across all threads by matching (from, to, relationType)
@@ -328,6 +476,19 @@ export class KnowledgeGraphManager {
328
476
  if (from === to) {
329
477
  return { found: true, path: [from], relations: [] };
330
478
  }
479
+ // Build indexes for efficient relation lookup
480
+ const relationsFrom = new Map();
481
+ const relationsTo = new Map();
482
+ for (const rel of graph.relations) {
483
+ if (!relationsFrom.has(rel.from)) {
484
+ relationsFrom.set(rel.from, []);
485
+ }
486
+ relationsFrom.get(rel.from).push(rel);
487
+ if (!relationsTo.has(rel.to)) {
488
+ relationsTo.set(rel.to, []);
489
+ }
490
+ relationsTo.get(rel.to).push(rel);
491
+ }
331
492
  // BFS to find shortest path
332
493
  const queue = [
333
494
  { entity: from, path: [from], relations: [] }
@@ -339,8 +500,8 @@ export class KnowledgeGraphManager {
339
500
  continue;
340
501
  }
341
502
  // Find all relations connected to current entity (both outgoing and incoming for bidirectional search)
342
- const outgoing = graph.relations.filter(r => r.from === current.entity);
343
- const incoming = graph.relations.filter(r => r.to === current.entity);
503
+ const outgoing = relationsFrom.get(current.entity) || [];
504
+ const incoming = relationsTo.get(current.entity) || [];
344
505
  // Check outgoing relations
345
506
  for (const rel of outgoing) {
346
507
  if (rel.to === to) {
@@ -398,8 +559,8 @@ export class KnowledgeGraphManager {
398
559
  continue;
399
560
  }
400
561
  // Check for negation patterns
401
- const obs1HasNegation = Array.from(KnowledgeGraphManager.NEGATION_WORDS).some(word => obs1Content.includes(word));
402
- const obs2HasNegation = Array.from(KnowledgeGraphManager.NEGATION_WORDS).some(word => obs2Content.includes(word));
562
+ const obs1HasNegation = this.hasNegation(obs1Content);
563
+ const obs2HasNegation = this.hasNegation(obs2Content);
403
564
  // If one has negation and they share key words, might be a conflict
404
565
  if (obs1HasNegation !== obs2HasNegation) {
405
566
  const words1 = obs1Content.split(/\s+/).filter(w => w.length > 3);
@@ -129,3 +129,18 @@ export const ValidateMemoryOutputSchema = z.object({
129
129
  warnings: z.array(z.string()).describe("List of validation warnings")
130
130
  })).describe("Validation results for each entity")
131
131
  });
132
+ // Schema for update_observation tool
133
+ export const UpdateObservationInputSchema = z.object({
134
+ entityName: z.string().min(1).describe("Name of the entity containing the observation"),
135
+ observationId: z.string().min(1).describe("ID of the observation to update"),
136
+ newContent: z.string().min(1).max(300).describe("New content for the observation (max 300 chars). Minimum 1 character to allow short but valid observations like abbreviations or single words."),
137
+ agentThreadId: z.string().min(1).describe("Agent thread ID making this update"),
138
+ timestamp: z.string().describe("ISO 8601 timestamp of the update"),
139
+ confidence: z.number().min(0).max(1).optional().describe("Optional confidence score (0-1), inherits from old observation if not provided"),
140
+ importance: z.number().min(0).max(1).optional().describe("Optional importance score (0-1), inherits from old observation if not provided")
141
+ });
142
+ export const UpdateObservationOutputSchema = z.object({
143
+ success: z.boolean(),
144
+ updatedObservation: ObservationSchema.describe("The new version of the observation"),
145
+ message: z.string()
146
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "server-memory-enhanced",
3
- "version": "2.3.0",
3
+ "version": "2.3.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",