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 +245 -44
- package/dist/index.js +27 -1
- package/dist/lib/jsonl-storage-adapter.js +8 -4
- package/dist/lib/knowledge-graph-manager.js +174 -13
- package/dist/lib/schemas.js +15 -0
- package/package.json +1 -1
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
|
|
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
|
-
|
|
57
|
-
|
|
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. **
|
|
60
|
-
5. **
|
|
61
|
-
6. **
|
|
62
|
-
7. **
|
|
63
|
-
8. **
|
|
64
|
-
9. **
|
|
65
|
-
10. **
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
108
|
-
await
|
|
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: "
|
|
113
|
-
observations: ["
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
//
|
|
123
|
-
await
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
//
|
|
65
|
-
|
|
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
|
-
|
|
96
|
-
graph.
|
|
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 =
|
|
343
|
-
const incoming =
|
|
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 =
|
|
402
|
-
const obs2HasNegation =
|
|
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);
|
package/dist/lib/schemas.js
CHANGED
|
@@ -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.
|
|
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",
|