server-memory-enhanced 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +198 -0
  2. package/dist/index.js +1022 -0
  3. package/package.json +36 -0
package/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # Memory-Enhanced MCP Server
2
+
3
+ An enhanced version of the Memory MCP server that provides persistent knowledge graph storage with agent threading, timestamps, and confidence scoring.
4
+
5
+ ## Features
6
+
7
+ - **Agent Thread Isolation**: Each agent thread writes to a separate file for better organization and parallel processing
8
+ - **Timestamp Tracking**: Every entity and relation has an ISO 8601 timestamp indicating when it was created
9
+ - **Confidence Scoring**: Each piece of knowledge has a confidence coefficient (0.0 to 1.0) representing certainty
10
+ - **Persistent Storage**: Knowledge graphs are stored in JSONL format, one file per agent thread
11
+ - **Graph Operations**: Full CRUD support for entities, relations, and observations
12
+
13
+ ## Enhanced Data Model
14
+
15
+ ### Entities
16
+ Each entity now includes:
17
+ - `name`: Entity identifier
18
+ - `entityType`: Type of entity
19
+ - `observations`: Array of observation strings
20
+ - `agentThreadId`: Unique identifier for the agent thread
21
+ - `timestamp`: ISO 8601 timestamp of creation
22
+ - `confidence`: Confidence score (0.0 to 1.0)
23
+ - `importance`: Importance for memory integrity if lost (0.0 = not important, 1.0 = critical)
24
+
25
+ ### Relations
26
+ Each relation now includes:
27
+ - `from`: Source entity name
28
+ - `to`: Target entity name
29
+ - `relationType`: Type of relationship
30
+ - `agentThreadId`: Unique identifier for the agent thread
31
+ - `timestamp`: ISO 8601 timestamp of creation
32
+ - `confidence`: Confidence score (0.0 to 1.0)
33
+ - `importance`: Importance for memory integrity if lost (0.0 = not important, 1.0 = critical)
34
+
35
+ ## Storage Architecture
36
+
37
+ The server implements a **collaborative knowledge graph** where multiple agent threads contribute to a shared graph:
38
+
39
+ ### Design Principles
40
+ - **Shared Entities**: Entity names are globally unique across all threads. If entity "Alice" exists, all threads reference the same entity.
41
+ - **Shared Relations**: Relations are unique by (from, to, relationType) across all threads.
42
+ - **Metadata Tracking**: Each entity and relation tracks which agent thread created it via `agentThreadId`, along with `timestamp` and `confidence`.
43
+ - **Distributed Storage**: Data is physically stored in separate JSONL files per thread for organization and performance.
44
+ - **Aggregated Reads**: Read operations combine data from all thread files to provide a complete view of the knowledge graph.
45
+
46
+ ### File Organization
47
+ The server stores data in separate JSONL files per agent thread:
48
+ - Default location: `./memory-data/thread-{agentThreadId}.jsonl`
49
+ - Custom location: Set `MEMORY_DIR_PATH` environment variable
50
+ - Each file contains entities and relations for one agent thread
51
+ - Read operations aggregate data across all thread files
52
+
53
+ ## Available Tools
54
+
55
+ ### 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)
58
+ 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
66
+
67
+ ### 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)
72
+
73
+ ### 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
76
+
77
+ ### 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
81
+
82
+ ## Usage
83
+
84
+ ### Installation
85
+
86
+ ```bash
87
+ npm install @modelcontextprotocol/server-memory-enhanced
88
+ ```
89
+
90
+ ### Running the Server
91
+
92
+ ```bash
93
+ npx mcp-server-memory-enhanced
94
+ ```
95
+
96
+ ### Configuration
97
+
98
+ Set the `MEMORY_DIR_PATH` environment variable to customize the storage location:
99
+
100
+ ```bash
101
+ MEMORY_DIR_PATH=/path/to/memory/directory npx mcp-server-memory-enhanced
102
+ ```
103
+
104
+ ## Example
105
+
106
+ ```typescript
107
+ // Create entities with metadata including importance
108
+ await createEntities({
109
+ entities: [
110
+ {
111
+ 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
118
+ }
119
+ ]
120
+ });
121
+
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
+ ]
135
+ });
136
+
137
+ // Query nodes with range-based filtering
138
+ await queryNodes({
139
+ timestampStart: "2024-01-20T09:00:00Z",
140
+ timestampEnd: "2024-01-20T11:00:00Z",
141
+ confidenceMin: 0.8,
142
+ importanceMin: 0.7 // Only get important items
143
+ });
144
+
145
+ // Get memory statistics
146
+ await getMemoryStats();
147
+ // Returns: { entityCount, relationCount, threadCount, entityTypes, avgConfidence, avgImportance, recentActivity }
148
+
149
+ // Get recent changes since last interaction
150
+ await getRecentChanges({ since: "2024-01-20T10:00:00Z" });
151
+
152
+ // Find how two entities are connected
153
+ await findRelationPath({ from: "Alice", to: "Charlie", maxDepth: 5 });
154
+
155
+ // Get context around specific entities
156
+ await getContext({ entityNames: ["Alice", "Bob"], depth: 2 });
157
+
158
+ // Detect conflicting observations
159
+ await detectConflicts();
160
+
161
+ // Flag entity for human review
162
+ await flagForReview({ entityName: "Alice", reason: "Uncertain data", reviewer: "John" });
163
+
164
+ // Bulk update multiple entities
165
+ await bulkUpdate({
166
+ updates: [
167
+ { entityName: "Alice", importance: 0.95 },
168
+ { entityName: "Bob", confidence: 0.85, addObservations: ["updated info"] }
169
+ ]
170
+ });
171
+
172
+ // Prune old/unimportant data
173
+ await pruneMemory({ olderThan: "2024-01-01T00:00:00Z", importanceLessThan: 0.3, keepMinEntities: 100 });
174
+ ```
175
+
176
+ ## Development
177
+
178
+ ### Build
179
+
180
+ ```bash
181
+ npm run build
182
+ ```
183
+
184
+ ### Test
185
+
186
+ ```bash
187
+ npm run test
188
+ ```
189
+
190
+ ### Watch Mode
191
+
192
+ ```bash
193
+ npm run watch
194
+ ```
195
+
196
+ ## License
197
+
198
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,1022 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { promises as fs } from 'fs';
6
+ import path from 'path';
7
+ import { fileURLToPath } from 'url';
8
+ // Define memory directory path using environment variable with fallback
9
+ export const defaultMemoryDir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'memory-data');
10
+ export async function ensureMemoryDirectory() {
11
+ const memoryDir = process.env.MEMORY_DIR_PATH
12
+ ? (path.isAbsolute(process.env.MEMORY_DIR_PATH)
13
+ ? process.env.MEMORY_DIR_PATH
14
+ : path.join(path.dirname(fileURLToPath(import.meta.url)), process.env.MEMORY_DIR_PATH))
15
+ : defaultMemoryDir;
16
+ // Ensure directory exists
17
+ try {
18
+ await fs.mkdir(memoryDir, { recursive: true });
19
+ }
20
+ catch (error) {
21
+ // Ignore error if directory already exists
22
+ }
23
+ return memoryDir;
24
+ }
25
+ // Initialize memory directory path (will be set during startup)
26
+ let MEMORY_DIR_PATH;
27
+ // The KnowledgeGraphManager class contains all operations to interact with the knowledge graph
28
+ export class KnowledgeGraphManager {
29
+ memoryDirPath;
30
+ static NEGATION_WORDS = new Set(['not', 'no', 'never', 'neither', 'none', 'doesn\'t', 'don\'t', 'isn\'t', 'aren\'t']);
31
+ constructor(memoryDirPath) {
32
+ this.memoryDirPath = memoryDirPath;
33
+ }
34
+ getThreadFilePath(agentThreadId) {
35
+ return path.join(this.memoryDirPath, `thread-${agentThreadId}.jsonl`);
36
+ }
37
+ async loadGraphFromFile(filePath) {
38
+ try {
39
+ const data = await fs.readFile(filePath, "utf-8");
40
+ const lines = data.split("\n").filter(line => line.trim() !== "");
41
+ return lines.reduce((graph, line) => {
42
+ let item;
43
+ try {
44
+ item = JSON.parse(line);
45
+ }
46
+ catch (parseError) {
47
+ console.warn(`Skipping malformed JSON line in ${filePath} (line length: ${line.length} chars)`);
48
+ return graph;
49
+ }
50
+ if (item.type === "entity") {
51
+ // Validate required fields
52
+ if (!item.name || !item.entityType || !Array.isArray(item.observations) ||
53
+ !item.agentThreadId || !item.timestamp ||
54
+ typeof item.confidence !== 'number' || typeof item.importance !== 'number') {
55
+ console.warn(`Skipping entity with missing required fields in ${filePath}`);
56
+ return graph;
57
+ }
58
+ graph.entities.push({
59
+ name: item.name,
60
+ entityType: item.entityType,
61
+ observations: item.observations,
62
+ agentThreadId: item.agentThreadId,
63
+ timestamp: item.timestamp,
64
+ confidence: item.confidence,
65
+ importance: item.importance
66
+ });
67
+ }
68
+ if (item.type === "relation") {
69
+ // Validate required fields
70
+ if (!item.from || !item.to || !item.relationType ||
71
+ !item.agentThreadId || !item.timestamp ||
72
+ typeof item.confidence !== 'number' || typeof item.importance !== 'number') {
73
+ console.warn(`Skipping relation with missing required fields in ${filePath}`);
74
+ return graph;
75
+ }
76
+ graph.relations.push({
77
+ from: item.from,
78
+ to: item.to,
79
+ relationType: item.relationType,
80
+ agentThreadId: item.agentThreadId,
81
+ timestamp: item.timestamp,
82
+ confidence: item.confidence,
83
+ importance: item.importance
84
+ });
85
+ }
86
+ return graph;
87
+ }, { entities: [], relations: [] });
88
+ }
89
+ catch (error) {
90
+ if (error instanceof Error && 'code' in error && error.code === "ENOENT") {
91
+ return { entities: [], relations: [] };
92
+ }
93
+ throw error;
94
+ }
95
+ }
96
+ async loadGraph() {
97
+ const files = await fs.readdir(this.memoryDirPath).catch(() => []);
98
+ const threadFiles = files.filter(f => f.startsWith('thread-') && f.endsWith('.jsonl'));
99
+ const graphs = await Promise.all(threadFiles.map(f => this.loadGraphFromFile(path.join(this.memoryDirPath, f))));
100
+ return graphs.reduce((acc, graph) => ({
101
+ entities: [...acc.entities, ...graph.entities],
102
+ relations: [...acc.relations, ...graph.relations]
103
+ }), { entities: [], relations: [] });
104
+ }
105
+ async saveGraphForThread(agentThreadId, entities, relations) {
106
+ const threadFilePath = this.getThreadFilePath(agentThreadId);
107
+ const lines = [
108
+ ...entities.map(e => JSON.stringify({
109
+ type: "entity",
110
+ name: e.name,
111
+ entityType: e.entityType,
112
+ observations: e.observations,
113
+ agentThreadId: e.agentThreadId,
114
+ timestamp: e.timestamp,
115
+ confidence: e.confidence,
116
+ importance: e.importance
117
+ })),
118
+ ...relations.map(r => JSON.stringify({
119
+ type: "relation",
120
+ from: r.from,
121
+ to: r.to,
122
+ relationType: r.relationType,
123
+ agentThreadId: r.agentThreadId,
124
+ timestamp: r.timestamp,
125
+ confidence: r.confidence,
126
+ importance: r.importance
127
+ })),
128
+ ];
129
+ // Avoid creating or keeping empty files when there is no data for this thread
130
+ if (lines.length === 0) {
131
+ try {
132
+ await fs.unlink(threadFilePath);
133
+ }
134
+ catch (error) {
135
+ // Only ignore ENOENT errors (file doesn't exist)
136
+ if (error instanceof Error && 'code' in error && error.code !== 'ENOENT') {
137
+ console.warn(`Failed to delete empty thread file ${threadFilePath}:`, error);
138
+ }
139
+ }
140
+ return;
141
+ }
142
+ await fs.writeFile(threadFilePath, lines.join("\n"));
143
+ }
144
+ async saveGraph(graph) {
145
+ // Group entities and relations by agentThreadId
146
+ const threadMap = new Map();
147
+ for (const entity of graph.entities) {
148
+ if (!threadMap.has(entity.agentThreadId)) {
149
+ threadMap.set(entity.agentThreadId, { entities: [], relations: [] });
150
+ }
151
+ threadMap.get(entity.agentThreadId).entities.push(entity);
152
+ }
153
+ for (const relation of graph.relations) {
154
+ if (!threadMap.has(relation.agentThreadId)) {
155
+ threadMap.set(relation.agentThreadId, { entities: [], relations: [] });
156
+ }
157
+ threadMap.get(relation.agentThreadId).relations.push(relation);
158
+ }
159
+ // Save each thread's data to its own file
160
+ await Promise.all(Array.from(threadMap.entries()).map(([threadId, data]) => this.saveGraphForThread(threadId, data.entities, data.relations)));
161
+ // Clean up stale thread files that no longer have data
162
+ try {
163
+ const files = await fs.readdir(this.memoryDirPath).catch(() => []);
164
+ const threadFiles = files.filter(f => f.startsWith('thread-') && f.endsWith('.jsonl'));
165
+ await Promise.all(threadFiles.map(async (fileName) => {
166
+ // Extract threadId from filename: thread-{agentThreadId}.jsonl
167
+ const match = fileName.match(/^thread-(.+)\.jsonl$/);
168
+ if (match) {
169
+ const threadId = match[1];
170
+ if (!threadMap.has(threadId)) {
171
+ const filePath = path.join(this.memoryDirPath, fileName);
172
+ try {
173
+ await fs.unlink(filePath);
174
+ }
175
+ catch (error) {
176
+ // Only log non-ENOENT errors
177
+ if (error instanceof Error && 'code' in error && error.code !== 'ENOENT') {
178
+ console.warn(`Failed to delete stale thread file ${filePath}:`, error);
179
+ }
180
+ }
181
+ }
182
+ }
183
+ }));
184
+ }
185
+ catch (error) {
186
+ // Best-effort cleanup: log but don't fail the save operation
187
+ console.warn('Failed to clean up stale thread files:', error);
188
+ }
189
+ }
190
+ async createEntities(entities) {
191
+ const graph = await this.loadGraph();
192
+ // Entity names are globally unique across all threads in the collaborative knowledge graph
193
+ // This prevents duplicate entities while allowing multiple threads to contribute to the same entity
194
+ const newEntities = entities.filter(e => !graph.entities.some(existingEntity => existingEntity.name === e.name));
195
+ graph.entities.push(...newEntities);
196
+ await this.saveGraph(graph);
197
+ return newEntities;
198
+ }
199
+ async createRelations(relations) {
200
+ const graph = await this.loadGraph();
201
+ // Validate that referenced entities exist
202
+ const entityNames = new Set(graph.entities.map(e => e.name));
203
+ const validRelations = relations.filter(r => {
204
+ if (!entityNames.has(r.from) || !entityNames.has(r.to)) {
205
+ console.warn(`Skipping relation ${r.from} -> ${r.to}: one or both entities do not exist`);
206
+ return false;
207
+ }
208
+ return true;
209
+ });
210
+ // Relations are globally unique by (from, to, relationType) across all threads
211
+ // This enables multiple threads to collaboratively build the knowledge graph
212
+ const newRelations = validRelations.filter(r => !graph.relations.some(existingRelation => existingRelation.from === r.from &&
213
+ existingRelation.to === r.to &&
214
+ existingRelation.relationType === r.relationType));
215
+ graph.relations.push(...newRelations);
216
+ await this.saveGraph(graph);
217
+ return newRelations;
218
+ }
219
+ async addObservations(observations) {
220
+ const graph = await this.loadGraph();
221
+ const results = observations.map(o => {
222
+ const entity = graph.entities.find(e => e.name === o.entityName);
223
+ if (!entity) {
224
+ throw new Error(`Entity with name ${o.entityName} not found`);
225
+ }
226
+ const newObservations = o.contents.filter(content => !entity.observations.includes(content));
227
+ entity.observations.push(...newObservations);
228
+ // Update metadata based on this operation, but keep original agentThreadId
229
+ // to maintain thread file consistency and avoid orphaned data
230
+ entity.timestamp = o.timestamp;
231
+ entity.confidence = o.confidence;
232
+ entity.importance = o.importance;
233
+ return { entityName: o.entityName, addedObservations: newObservations };
234
+ });
235
+ await this.saveGraph(graph);
236
+ return results;
237
+ }
238
+ async deleteEntities(entityNames) {
239
+ const graph = await this.loadGraph();
240
+ graph.entities = graph.entities.filter(e => !entityNames.includes(e.name));
241
+ graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to));
242
+ await this.saveGraph(graph);
243
+ }
244
+ async deleteObservations(deletions) {
245
+ const graph = await this.loadGraph();
246
+ deletions.forEach(d => {
247
+ const entity = graph.entities.find(e => e.name === d.entityName);
248
+ if (entity) {
249
+ entity.observations = entity.observations.filter(o => !d.observations.includes(o));
250
+ }
251
+ });
252
+ await this.saveGraph(graph);
253
+ }
254
+ async deleteRelations(relations) {
255
+ const graph = await this.loadGraph();
256
+ // Delete relations globally across all threads by matching (from, to, relationType)
257
+ // In a collaborative knowledge graph, deletions affect all threads
258
+ graph.relations = graph.relations.filter(r => !relations.some(delRelation => r.from === delRelation.from &&
259
+ r.to === delRelation.to &&
260
+ r.relationType === delRelation.relationType));
261
+ await this.saveGraph(graph);
262
+ }
263
+ async readGraph() {
264
+ return this.loadGraph();
265
+ }
266
+ async searchNodes(query) {
267
+ const graph = await this.loadGraph();
268
+ // Filter entities
269
+ const filteredEntities = graph.entities.filter(e => e.name.toLowerCase().includes(query.toLowerCase()) ||
270
+ e.entityType.toLowerCase().includes(query.toLowerCase()) ||
271
+ e.observations.some(o => o.toLowerCase().includes(query.toLowerCase())));
272
+ // Create a Set of filtered entity names for quick lookup
273
+ const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
274
+ // Filter relations to only include those between filtered entities
275
+ const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
276
+ const filteredGraph = {
277
+ entities: filteredEntities,
278
+ relations: filteredRelations,
279
+ };
280
+ return filteredGraph;
281
+ }
282
+ async openNodes(names) {
283
+ const graph = await this.loadGraph();
284
+ // Filter entities
285
+ const filteredEntities = graph.entities.filter(e => names.includes(e.name));
286
+ // Create a Set of filtered entity names for quick lookup
287
+ const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
288
+ // Filter relations to only include those between filtered entities
289
+ const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
290
+ const filteredGraph = {
291
+ entities: filteredEntities,
292
+ relations: filteredRelations,
293
+ };
294
+ return filteredGraph;
295
+ }
296
+ async queryNodes(filters) {
297
+ const graph = await this.loadGraph();
298
+ // If no filters provided, return entire graph
299
+ if (!filters) {
300
+ return graph;
301
+ }
302
+ // Apply filters to entities
303
+ const filteredEntities = graph.entities.filter(e => {
304
+ // Timestamp range filter
305
+ if (filters.timestampStart && e.timestamp < filters.timestampStart)
306
+ return false;
307
+ if (filters.timestampEnd && e.timestamp > filters.timestampEnd)
308
+ return false;
309
+ // Confidence range filter
310
+ if (filters.confidenceMin !== undefined && e.confidence < filters.confidenceMin)
311
+ return false;
312
+ if (filters.confidenceMax !== undefined && e.confidence > filters.confidenceMax)
313
+ return false;
314
+ // Importance range filter
315
+ if (filters.importanceMin !== undefined && e.importance < filters.importanceMin)
316
+ return false;
317
+ if (filters.importanceMax !== undefined && e.importance > filters.importanceMax)
318
+ return false;
319
+ return true;
320
+ });
321
+ // Create a Set of filtered entity names for quick lookup
322
+ const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
323
+ // Apply filters to relations (and ensure they connect filtered entities)
324
+ const filteredRelations = graph.relations.filter(r => {
325
+ // Must connect filtered entities
326
+ if (!filteredEntityNames.has(r.from) || !filteredEntityNames.has(r.to))
327
+ return false;
328
+ // Timestamp range filter
329
+ if (filters.timestampStart && r.timestamp < filters.timestampStart)
330
+ return false;
331
+ if (filters.timestampEnd && r.timestamp > filters.timestampEnd)
332
+ return false;
333
+ // Confidence range filter
334
+ if (filters.confidenceMin !== undefined && r.confidence < filters.confidenceMin)
335
+ return false;
336
+ if (filters.confidenceMax !== undefined && r.confidence > filters.confidenceMax)
337
+ return false;
338
+ // Importance range filter
339
+ if (filters.importanceMin !== undefined && r.importance < filters.importanceMin)
340
+ return false;
341
+ if (filters.importanceMax !== undefined && r.importance > filters.importanceMax)
342
+ return false;
343
+ return true;
344
+ });
345
+ return {
346
+ entities: filteredEntities,
347
+ relations: filteredRelations,
348
+ };
349
+ }
350
+ // Enhancement 1: Memory Statistics & Insights
351
+ async getMemoryStats() {
352
+ const graph = await this.loadGraph();
353
+ // Count entity types
354
+ const entityTypes = {};
355
+ graph.entities.forEach(e => {
356
+ entityTypes[e.entityType] = (entityTypes[e.entityType] || 0) + 1;
357
+ });
358
+ // Calculate averages
359
+ const avgConfidence = graph.entities.length > 0
360
+ ? graph.entities.reduce((sum, e) => sum + e.confidence, 0) / graph.entities.length
361
+ : 0;
362
+ const avgImportance = graph.entities.length > 0
363
+ ? graph.entities.reduce((sum, e) => sum + e.importance, 0) / graph.entities.length
364
+ : 0;
365
+ // Count unique threads
366
+ const threads = new Set([
367
+ ...graph.entities.map(e => e.agentThreadId),
368
+ ...graph.relations.map(r => r.agentThreadId)
369
+ ]);
370
+ // Recent activity (last 7 days, grouped by day)
371
+ const now = new Date();
372
+ const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
373
+ const recentEntities = graph.entities.filter(e => new Date(e.timestamp) >= sevenDaysAgo);
374
+ // Group by day
375
+ const activityByDay = {};
376
+ recentEntities.forEach(e => {
377
+ const day = e.timestamp.substring(0, 10); // YYYY-MM-DD
378
+ activityByDay[day] = (activityByDay[day] || 0) + 1;
379
+ });
380
+ const recentActivity = Object.entries(activityByDay)
381
+ .map(([timestamp, entityCount]) => ({ timestamp, entityCount }))
382
+ .sort((a, b) => a.timestamp.localeCompare(b.timestamp));
383
+ return {
384
+ entityCount: graph.entities.length,
385
+ relationCount: graph.relations.length,
386
+ threadCount: threads.size,
387
+ entityTypes,
388
+ avgConfidence,
389
+ avgImportance,
390
+ recentActivity
391
+ };
392
+ }
393
+ // Enhancement 2: Get recent changes
394
+ async getRecentChanges(since) {
395
+ const graph = await this.loadGraph();
396
+ const sinceDate = new Date(since);
397
+ // Only return entities and relations that were actually modified since the specified time
398
+ const recentEntities = graph.entities.filter(e => new Date(e.timestamp) >= sinceDate);
399
+ const recentEntityNames = new Set(recentEntities.map(e => e.name));
400
+ // Only include relations that are recent themselves
401
+ const recentRelations = graph.relations.filter(r => new Date(r.timestamp) >= sinceDate);
402
+ return {
403
+ entities: recentEntities,
404
+ relations: recentRelations
405
+ };
406
+ }
407
+ // Enhancement 3: Relationship path finding
408
+ async findRelationPath(from, to, maxDepth = 5) {
409
+ const graph = await this.loadGraph();
410
+ if (from === to) {
411
+ return { found: true, path: [from], relations: [] };
412
+ }
413
+ // BFS to find shortest path
414
+ const queue = [
415
+ { entity: from, path: [from], relations: [] }
416
+ ];
417
+ const visited = new Set([from]);
418
+ while (queue.length > 0) {
419
+ const current = queue.shift();
420
+ if (current.path.length > maxDepth) {
421
+ continue;
422
+ }
423
+ // Find all relations connected to current entity (both outgoing and incoming for bidirectional search)
424
+ const outgoing = graph.relations.filter(r => r.from === current.entity);
425
+ const incoming = graph.relations.filter(r => r.to === current.entity);
426
+ // Check outgoing relations
427
+ for (const rel of outgoing) {
428
+ if (rel.to === to) {
429
+ return {
430
+ found: true,
431
+ path: [...current.path, rel.to],
432
+ relations: [...current.relations, rel]
433
+ };
434
+ }
435
+ if (!visited.has(rel.to)) {
436
+ visited.add(rel.to);
437
+ queue.push({
438
+ entity: rel.to,
439
+ path: [...current.path, rel.to],
440
+ relations: [...current.relations, rel]
441
+ });
442
+ }
443
+ }
444
+ // Check incoming relations (traverse backwards)
445
+ for (const rel of incoming) {
446
+ if (rel.from === to) {
447
+ return {
448
+ found: true,
449
+ path: [...current.path, rel.from],
450
+ relations: [...current.relations, rel]
451
+ };
452
+ }
453
+ if (!visited.has(rel.from)) {
454
+ visited.add(rel.from);
455
+ queue.push({
456
+ entity: rel.from,
457
+ path: [...current.path, rel.from],
458
+ relations: [...current.relations, rel]
459
+ });
460
+ }
461
+ }
462
+ }
463
+ return { found: false, path: [], relations: [] };
464
+ }
465
+ // Enhancement 4: Detect conflicting observations
466
+ async detectConflicts() {
467
+ const graph = await this.loadGraph();
468
+ const conflicts = [];
469
+ for (const entity of graph.entities) {
470
+ const entityConflicts = [];
471
+ for (let i = 0; i < entity.observations.length; i++) {
472
+ for (let j = i + 1; j < entity.observations.length; j++) {
473
+ const obs1 = entity.observations[i].toLowerCase();
474
+ const obs2 = entity.observations[j].toLowerCase();
475
+ // Check for negation patterns
476
+ const obs1HasNegation = Array.from(KnowledgeGraphManager.NEGATION_WORDS).some(word => obs1.includes(word));
477
+ const obs2HasNegation = Array.from(KnowledgeGraphManager.NEGATION_WORDS).some(word => obs2.includes(word));
478
+ // If one has negation and they share key words, might be a conflict
479
+ if (obs1HasNegation !== obs2HasNegation) {
480
+ const words1 = obs1.split(/\s+/).filter(w => w.length > 3);
481
+ const words2Set = new Set(obs2.split(/\s+/).filter(w => w.length > 3));
482
+ const commonWords = words1.filter(w => words2Set.has(w) && !KnowledgeGraphManager.NEGATION_WORDS.has(w));
483
+ if (commonWords.length >= 2) {
484
+ entityConflicts.push({
485
+ obs1: entity.observations[i],
486
+ obs2: entity.observations[j],
487
+ reason: 'Potential contradiction with negation'
488
+ });
489
+ }
490
+ }
491
+ }
492
+ }
493
+ if (entityConflicts.length > 0) {
494
+ conflicts.push({ entityName: entity.name, conflicts: entityConflicts });
495
+ }
496
+ }
497
+ return conflicts;
498
+ }
499
+ // Enhancement 5: Memory pruning
500
+ async pruneMemory(options) {
501
+ const graph = await this.loadGraph();
502
+ const initialEntityCount = graph.entities.length;
503
+ const initialRelationCount = graph.relations.length;
504
+ // Filter entities to remove
505
+ let entitiesToKeep = graph.entities;
506
+ if (options.olderThan) {
507
+ const cutoffDate = new Date(options.olderThan);
508
+ entitiesToKeep = entitiesToKeep.filter(e => new Date(e.timestamp) >= cutoffDate);
509
+ }
510
+ if (options.importanceLessThan !== undefined) {
511
+ entitiesToKeep = entitiesToKeep.filter(e => e.importance >= options.importanceLessThan);
512
+ }
513
+ // Ensure we keep minimum entities
514
+ // If keepMinEntities is set and we need more entities, take from the already-filtered set
515
+ // sorted by importance and recency
516
+ if (options.keepMinEntities && entitiesToKeep.length < options.keepMinEntities) {
517
+ // Sort the filtered entities by importance and timestamp, keep the most important and recent
518
+ const sorted = [...entitiesToKeep].sort((a, b) => {
519
+ if (a.importance !== b.importance)
520
+ return b.importance - a.importance;
521
+ return b.timestamp.localeCompare(a.timestamp);
522
+ });
523
+ // If we still don't have enough, we keep what we have
524
+ entitiesToKeep = sorted.slice(0, Math.min(options.keepMinEntities, sorted.length));
525
+ }
526
+ const keptEntityNames = new Set(entitiesToKeep.map(e => e.name));
527
+ // Remove relations that reference removed entities
528
+ const relationsToKeep = graph.relations.filter(r => keptEntityNames.has(r.from) && keptEntityNames.has(r.to));
529
+ graph.entities = entitiesToKeep;
530
+ graph.relations = relationsToKeep;
531
+ await this.saveGraph(graph);
532
+ return {
533
+ removedEntities: initialEntityCount - entitiesToKeep.length,
534
+ removedRelations: initialRelationCount - relationsToKeep.length
535
+ };
536
+ }
537
+ // Enhancement 6: Batch operations
538
+ async bulkUpdate(updates) {
539
+ const graph = await this.loadGraph();
540
+ let updated = 0;
541
+ const notFound = [];
542
+ for (const update of updates) {
543
+ const entity = graph.entities.find(e => e.name === update.entityName);
544
+ if (!entity) {
545
+ notFound.push(update.entityName);
546
+ continue;
547
+ }
548
+ if (update.confidence !== undefined) {
549
+ entity.confidence = update.confidence;
550
+ }
551
+ if (update.importance !== undefined) {
552
+ entity.importance = update.importance;
553
+ }
554
+ if (update.addObservations) {
555
+ const newObs = update.addObservations.filter(obs => !entity.observations.includes(obs));
556
+ entity.observations.push(...newObs);
557
+ }
558
+ entity.timestamp = new Date().toISOString();
559
+ updated++;
560
+ }
561
+ await this.saveGraph(graph);
562
+ return { updated, notFound };
563
+ }
564
+ // Enhancement 7: Flag for review (Human-in-the-Loop)
565
+ async flagForReview(entityName, reason, reviewer) {
566
+ const graph = await this.loadGraph();
567
+ const entity = graph.entities.find(e => e.name === entityName);
568
+ if (!entity) {
569
+ throw new Error(`Entity with name ${entityName} not found`);
570
+ }
571
+ // Add a special observation to mark for review
572
+ const flagObservation = `[FLAGGED FOR REVIEW: ${reason}${reviewer ? ` - Reviewer: ${reviewer}` : ''}]`;
573
+ if (!entity.observations.includes(flagObservation)) {
574
+ entity.observations.push(flagObservation);
575
+ entity.timestamp = new Date().toISOString();
576
+ await this.saveGraph(graph);
577
+ }
578
+ }
579
+ // Enhancement 8: Get entities flagged for review
580
+ async getFlaggedEntities() {
581
+ const graph = await this.loadGraph();
582
+ return graph.entities.filter(e => e.observations.some(obs => obs.includes('[FLAGGED FOR REVIEW:')));
583
+ }
584
+ // Enhancement 9: Get context (entities related to a topic/entity)
585
+ async getContext(entityNames, depth = 1) {
586
+ const graph = await this.loadGraph();
587
+ const contextEntityNames = new Set(entityNames);
588
+ // Expand to include related entities up to specified depth
589
+ for (let d = 0; d < depth; d++) {
590
+ const currentEntities = Array.from(contextEntityNames);
591
+ for (const entityName of currentEntities) {
592
+ // Find all relations involving this entity
593
+ const relatedRelations = graph.relations.filter(r => r.from === entityName || r.to === entityName);
594
+ // Add related entities
595
+ relatedRelations.forEach(r => {
596
+ contextEntityNames.add(r.from);
597
+ contextEntityNames.add(r.to);
598
+ });
599
+ }
600
+ }
601
+ // Get all entities and relations in context
602
+ const contextEntities = graph.entities.filter(e => contextEntityNames.has(e.name));
603
+ const contextRelations = graph.relations.filter(r => contextEntityNames.has(r.from) && contextEntityNames.has(r.to));
604
+ return {
605
+ entities: contextEntities,
606
+ relations: contextRelations
607
+ };
608
+ }
609
+ }
610
+ let knowledgeGraphManager;
611
+ // Zod schemas for enhanced entities and relations
612
+ const EntitySchema = z.object({
613
+ name: z.string().describe("The name of the entity"),
614
+ entityType: z.string().describe("The type of the entity"),
615
+ observations: z.array(z.string()).describe("An array of observation contents associated with the entity"),
616
+ agentThreadId: z.string().describe("The agent thread ID that created this entity"),
617
+ timestamp: z.string().describe("ISO 8601 timestamp of when the entity was created"),
618
+ confidence: z.number().min(0).max(1).describe("Confidence coefficient from 0 to 1"),
619
+ importance: z.number().min(0).max(1).describe("Importance for memory integrity if lost: 0 (not important) to 1 (critical)")
620
+ });
621
+ const RelationSchema = z.object({
622
+ from: z.string().describe("The name of the entity where the relation starts"),
623
+ to: z.string().describe("The name of the entity where the relation ends"),
624
+ relationType: z.string().describe("The type of the relation"),
625
+ agentThreadId: z.string().describe("The agent thread ID that created this relation"),
626
+ timestamp: z.string().describe("ISO 8601 timestamp of when the relation was created"),
627
+ confidence: z.number().min(0).max(1).describe("Confidence coefficient from 0 to 1"),
628
+ importance: z.number().min(0).max(1).describe("Importance for memory integrity if lost: 0 (not important) to 1 (critical)")
629
+ });
630
+ // The server instance and tools exposed to Claude
631
+ const server = new McpServer({
632
+ name: "memory-enhanced-server",
633
+ version: "0.1.0",
634
+ });
635
+ // Register create_entities tool
636
+ server.registerTool("create_entities", {
637
+ title: "Create Entities",
638
+ description: "Create multiple new entities in the knowledge graph with metadata (agent thread ID, timestamp, confidence, importance)",
639
+ inputSchema: {
640
+ entities: z.array(EntitySchema)
641
+ },
642
+ outputSchema: {
643
+ entities: z.array(EntitySchema)
644
+ }
645
+ }, async ({ entities }) => {
646
+ const result = await knowledgeGraphManager.createEntities(entities);
647
+ return {
648
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
649
+ structuredContent: { entities: result }
650
+ };
651
+ });
652
+ // Register create_relations tool
653
+ server.registerTool("create_relations", {
654
+ title: "Create Relations",
655
+ description: "Create multiple new relations between entities in the knowledge graph with metadata (agent thread ID, timestamp, confidence, importance). Relations should be in active voice",
656
+ inputSchema: {
657
+ relations: z.array(RelationSchema)
658
+ },
659
+ outputSchema: {
660
+ relations: z.array(RelationSchema)
661
+ }
662
+ }, async ({ relations }) => {
663
+ const result = await knowledgeGraphManager.createRelations(relations);
664
+ return {
665
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
666
+ structuredContent: { relations: result }
667
+ };
668
+ });
669
+ // Register add_observations tool
670
+ server.registerTool("add_observations", {
671
+ title: "Add Observations",
672
+ description: "Add new observations to existing entities in the knowledge graph with metadata (agent thread ID, timestamp, confidence, importance)",
673
+ inputSchema: {
674
+ observations: z.array(z.object({
675
+ entityName: z.string().describe("The name of the entity to add the observations to"),
676
+ contents: z.array(z.string()).describe("An array of observation contents to add"),
677
+ agentThreadId: z.string().describe("The agent thread ID adding these observations"),
678
+ timestamp: z.string().describe("ISO 8601 timestamp of when the observations are added"),
679
+ confidence: z.number().min(0).max(1).describe("Confidence coefficient from 0 to 1"),
680
+ importance: z.number().min(0).max(1).describe("Importance for memory integrity if lost: 0 (not important) to 1 (critical)")
681
+ }))
682
+ },
683
+ outputSchema: {
684
+ results: z.array(z.object({
685
+ entityName: z.string(),
686
+ addedObservations: z.array(z.string())
687
+ }))
688
+ }
689
+ }, async ({ observations }) => {
690
+ const result = await knowledgeGraphManager.addObservations(observations);
691
+ return {
692
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
693
+ structuredContent: { results: result }
694
+ };
695
+ });
696
+ // Register delete_entities tool
697
+ server.registerTool("delete_entities", {
698
+ title: "Delete Entities",
699
+ description: "Delete multiple entities and their associated relations from the knowledge graph",
700
+ inputSchema: {
701
+ entityNames: z.array(z.string()).describe("An array of entity names to delete")
702
+ },
703
+ outputSchema: {
704
+ success: z.boolean(),
705
+ message: z.string()
706
+ }
707
+ }, async ({ entityNames }) => {
708
+ await knowledgeGraphManager.deleteEntities(entityNames);
709
+ return {
710
+ content: [{ type: "text", text: "Entities deleted successfully" }],
711
+ structuredContent: { success: true, message: "Entities deleted successfully" }
712
+ };
713
+ });
714
+ // Register delete_observations tool
715
+ server.registerTool("delete_observations", {
716
+ title: "Delete Observations",
717
+ description: "Delete specific observations from entities in the knowledge graph",
718
+ inputSchema: {
719
+ deletions: z.array(z.object({
720
+ entityName: z.string().describe("The name of the entity containing the observations"),
721
+ observations: z.array(z.string()).describe("An array of observations to delete")
722
+ }))
723
+ },
724
+ outputSchema: {
725
+ success: z.boolean(),
726
+ message: z.string()
727
+ }
728
+ }, async ({ deletions }) => {
729
+ await knowledgeGraphManager.deleteObservations(deletions);
730
+ return {
731
+ content: [{ type: "text", text: "Observations deleted successfully" }],
732
+ structuredContent: { success: true, message: "Observations deleted successfully" }
733
+ };
734
+ });
735
+ // Register delete_relations tool
736
+ server.registerTool("delete_relations", {
737
+ title: "Delete Relations",
738
+ description: "Delete multiple relations from the knowledge graph",
739
+ inputSchema: {
740
+ relations: z.array(RelationSchema).describe("An array of relations to delete")
741
+ },
742
+ outputSchema: {
743
+ success: z.boolean(),
744
+ message: z.string()
745
+ }
746
+ }, async ({ relations }) => {
747
+ await knowledgeGraphManager.deleteRelations(relations);
748
+ return {
749
+ content: [{ type: "text", text: "Relations deleted successfully" }],
750
+ structuredContent: { success: true, message: "Relations deleted successfully" }
751
+ };
752
+ });
753
+ // Register read_graph tool
754
+ server.registerTool("read_graph", {
755
+ title: "Read Graph",
756
+ description: "Read the entire knowledge graph",
757
+ inputSchema: {},
758
+ outputSchema: {
759
+ entities: z.array(EntitySchema),
760
+ relations: z.array(RelationSchema)
761
+ }
762
+ }, async () => {
763
+ const graph = await knowledgeGraphManager.readGraph();
764
+ return {
765
+ content: [{ type: "text", text: JSON.stringify(graph, null, 2) }],
766
+ structuredContent: { ...graph }
767
+ };
768
+ });
769
+ // Register search_nodes tool
770
+ server.registerTool("search_nodes", {
771
+ title: "Search Nodes",
772
+ description: "Search for nodes in the knowledge graph based on a query",
773
+ inputSchema: {
774
+ query: z.string().describe("The search query to match against entity names, types, and observation content")
775
+ },
776
+ outputSchema: {
777
+ entities: z.array(EntitySchema),
778
+ relations: z.array(RelationSchema)
779
+ }
780
+ }, async ({ query }) => {
781
+ const graph = await knowledgeGraphManager.searchNodes(query);
782
+ return {
783
+ content: [{ type: "text", text: JSON.stringify(graph, null, 2) }],
784
+ structuredContent: { ...graph }
785
+ };
786
+ });
787
+ // Register open_nodes tool
788
+ server.registerTool("open_nodes", {
789
+ title: "Open Nodes",
790
+ description: "Open specific nodes in the knowledge graph by their names",
791
+ inputSchema: {
792
+ names: z.array(z.string()).describe("An array of entity names to retrieve")
793
+ },
794
+ outputSchema: {
795
+ entities: z.array(EntitySchema),
796
+ relations: z.array(RelationSchema)
797
+ }
798
+ }, async ({ names }) => {
799
+ const graph = await knowledgeGraphManager.openNodes(names);
800
+ return {
801
+ content: [{ type: "text", text: JSON.stringify(graph, null, 2) }],
802
+ structuredContent: { ...graph }
803
+ };
804
+ });
805
+ // Register query_nodes tool for advanced filtering
806
+ server.registerTool("query_nodes", {
807
+ title: "Query Nodes",
808
+ description: "Query nodes and relations in the knowledge graph with advanced filtering by timestamp, confidence, and importance ranges",
809
+ inputSchema: {
810
+ timestampStart: z.string().optional().describe("ISO 8601 timestamp - filter for items created on or after this time"),
811
+ timestampEnd: z.string().optional().describe("ISO 8601 timestamp - filter for items created on or before this time"),
812
+ confidenceMin: z.number().min(0).max(1).optional().describe("Minimum confidence value (0-1)"),
813
+ confidenceMax: z.number().min(0).max(1).optional().describe("Maximum confidence value (0-1)"),
814
+ importanceMin: z.number().min(0).max(1).optional().describe("Minimum importance value (0-1)"),
815
+ importanceMax: z.number().min(0).max(1).optional().describe("Maximum importance value (0-1)")
816
+ },
817
+ outputSchema: {
818
+ entities: z.array(EntitySchema),
819
+ relations: z.array(RelationSchema)
820
+ }
821
+ }, async (filters) => {
822
+ const graph = await knowledgeGraphManager.queryNodes(filters);
823
+ return {
824
+ content: [{ type: "text", text: JSON.stringify(graph, null, 2) }],
825
+ structuredContent: { ...graph }
826
+ };
827
+ });
828
+ // Register get_memory_stats tool
829
+ server.registerTool("get_memory_stats", {
830
+ title: "Get Memory Statistics",
831
+ description: "Get comprehensive statistics about the knowledge graph including entity counts, thread activity, and confidence/importance metrics",
832
+ inputSchema: {},
833
+ outputSchema: {
834
+ entityCount: z.number(),
835
+ relationCount: z.number(),
836
+ threadCount: z.number(),
837
+ entityTypes: z.record(z.number()),
838
+ avgConfidence: z.number(),
839
+ avgImportance: z.number(),
840
+ recentActivity: z.array(z.object({
841
+ timestamp: z.string(),
842
+ entityCount: z.number()
843
+ }))
844
+ }
845
+ }, async () => {
846
+ const stats = await knowledgeGraphManager.getMemoryStats();
847
+ return {
848
+ content: [{ type: "text", text: JSON.stringify(stats, null, 2) }],
849
+ structuredContent: stats
850
+ };
851
+ });
852
+ // Register get_recent_changes tool
853
+ server.registerTool("get_recent_changes", {
854
+ title: "Get Recent Changes",
855
+ description: "Retrieve entities and relations that were created or modified since a specific timestamp",
856
+ inputSchema: {
857
+ since: z.string().describe("ISO 8601 timestamp - return changes since this time")
858
+ },
859
+ outputSchema: {
860
+ entities: z.array(EntitySchema),
861
+ relations: z.array(RelationSchema)
862
+ }
863
+ }, async ({ since }) => {
864
+ const changes = await knowledgeGraphManager.getRecentChanges(since);
865
+ return {
866
+ content: [{ type: "text", text: JSON.stringify(changes, null, 2) }],
867
+ structuredContent: { ...changes }
868
+ };
869
+ });
870
+ // Register find_relation_path tool
871
+ server.registerTool("find_relation_path", {
872
+ title: "Find Relationship Path",
873
+ description: "Find a path of relationships connecting two entities in the knowledge graph",
874
+ inputSchema: {
875
+ from: z.string().describe("Starting entity name"),
876
+ to: z.string().describe("Target entity name"),
877
+ maxDepth: z.number().optional().default(5).describe("Maximum path depth to search (default: 5)")
878
+ },
879
+ outputSchema: {
880
+ found: z.boolean(),
881
+ path: z.array(z.string()),
882
+ relations: z.array(RelationSchema)
883
+ }
884
+ }, async ({ from, to, maxDepth }) => {
885
+ const result = await knowledgeGraphManager.findRelationPath(from, to, maxDepth || 5);
886
+ return {
887
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
888
+ structuredContent: result
889
+ };
890
+ });
891
+ // Register detect_conflicts tool
892
+ server.registerTool("detect_conflicts", {
893
+ title: "Detect Conflicts",
894
+ description: "Detect potentially conflicting observations within entities using pattern matching and negation detection",
895
+ inputSchema: {},
896
+ outputSchema: {
897
+ conflicts: z.array(z.object({
898
+ entityName: z.string(),
899
+ conflicts: z.array(z.object({
900
+ obs1: z.string(),
901
+ obs2: z.string(),
902
+ reason: z.string()
903
+ }))
904
+ }))
905
+ }
906
+ }, async () => {
907
+ const conflicts = await knowledgeGraphManager.detectConflicts();
908
+ return {
909
+ content: [{ type: "text", text: JSON.stringify({ conflicts }, null, 2) }],
910
+ structuredContent: { conflicts }
911
+ };
912
+ });
913
+ // Register prune_memory tool
914
+ server.registerTool("prune_memory", {
915
+ title: "Prune Memory",
916
+ description: "Remove old or low-importance entities to manage memory size, with option to keep minimum number of entities",
917
+ inputSchema: {
918
+ olderThan: z.string().optional().describe("ISO 8601 timestamp - remove entities older than this"),
919
+ importanceLessThan: z.number().min(0).max(1).optional().describe("Remove entities with importance less than this value"),
920
+ keepMinEntities: z.number().optional().describe("Minimum number of entities to keep regardless of filters")
921
+ },
922
+ outputSchema: {
923
+ removedEntities: z.number(),
924
+ removedRelations: z.number()
925
+ }
926
+ }, async (options) => {
927
+ const result = await knowledgeGraphManager.pruneMemory(options);
928
+ return {
929
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
930
+ structuredContent: result
931
+ };
932
+ });
933
+ // Register bulk_update tool
934
+ server.registerTool("bulk_update", {
935
+ title: "Bulk Update",
936
+ description: "Efficiently update multiple entities at once with new confidence, importance, or observations",
937
+ inputSchema: {
938
+ updates: z.array(z.object({
939
+ entityName: z.string(),
940
+ confidence: z.number().min(0).max(1).optional(),
941
+ importance: z.number().min(0).max(1).optional(),
942
+ addObservations: z.array(z.string()).optional()
943
+ }))
944
+ },
945
+ outputSchema: {
946
+ updated: z.number(),
947
+ notFound: z.array(z.string())
948
+ }
949
+ }, async ({ updates }) => {
950
+ const result = await knowledgeGraphManager.bulkUpdate(updates);
951
+ return {
952
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
953
+ structuredContent: result
954
+ };
955
+ });
956
+ // Register flag_for_review tool
957
+ server.registerTool("flag_for_review", {
958
+ title: "Flag Entity for Review",
959
+ description: "Mark an entity for human review with a specific reason (Human-in-the-Loop)",
960
+ inputSchema: {
961
+ entityName: z.string().describe("Name of entity to flag"),
962
+ reason: z.string().describe("Reason for flagging"),
963
+ reviewer: z.string().optional().describe("Optional reviewer name")
964
+ },
965
+ outputSchema: {
966
+ success: z.boolean(),
967
+ message: z.string()
968
+ }
969
+ }, async ({ entityName, reason, reviewer }) => {
970
+ await knowledgeGraphManager.flagForReview(entityName, reason, reviewer);
971
+ return {
972
+ content: [{ type: "text", text: `Entity "${entityName}" flagged for review` }],
973
+ structuredContent: { success: true, message: `Entity "${entityName}" flagged for review` }
974
+ };
975
+ });
976
+ // Register get_flagged_entities tool
977
+ server.registerTool("get_flagged_entities", {
978
+ title: "Get Flagged Entities",
979
+ description: "Retrieve all entities that have been flagged for human review",
980
+ inputSchema: {},
981
+ outputSchema: {
982
+ entities: z.array(EntitySchema)
983
+ }
984
+ }, async () => {
985
+ const entities = await knowledgeGraphManager.getFlaggedEntities();
986
+ return {
987
+ content: [{ type: "text", text: JSON.stringify({ entities }, null, 2) }],
988
+ structuredContent: { entities }
989
+ };
990
+ });
991
+ // Register get_context tool
992
+ server.registerTool("get_context", {
993
+ title: "Get Context",
994
+ description: "Retrieve entities and relations related to specified entities up to a certain depth, useful for understanding context around specific topics",
995
+ inputSchema: {
996
+ entityNames: z.array(z.string()).describe("Names of entities to get context for"),
997
+ depth: z.number().optional().default(1).describe("How many relationship hops to include (default: 1)")
998
+ },
999
+ outputSchema: {
1000
+ entities: z.array(EntitySchema),
1001
+ relations: z.array(RelationSchema)
1002
+ }
1003
+ }, async ({ entityNames, depth }) => {
1004
+ const context = await knowledgeGraphManager.getContext(entityNames, depth || 1);
1005
+ return {
1006
+ content: [{ type: "text", text: JSON.stringify(context, null, 2) }],
1007
+ structuredContent: { ...context }
1008
+ };
1009
+ });
1010
+ async function main() {
1011
+ // Initialize memory directory path
1012
+ MEMORY_DIR_PATH = await ensureMemoryDirectory();
1013
+ // Initialize knowledge graph manager with the memory directory path
1014
+ knowledgeGraphManager = new KnowledgeGraphManager(MEMORY_DIR_PATH);
1015
+ const transport = new StdioServerTransport();
1016
+ await server.connect(transport);
1017
+ console.error("Enhanced Knowledge Graph MCP Server running on stdio");
1018
+ }
1019
+ main().catch((error) => {
1020
+ console.error("Fatal error in main():", error);
1021
+ process.exit(1);
1022
+ });
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "server-memory-enhanced",
3
+ "version": "0.1.0",
4
+ "description": "Enhanced MCP server for memory with agent threading, timestamps, and confidence scoring",
5
+ "license": "MIT",
6
+ "mcpName": "io.github.modelcontextprotocol/server-memory-enhanced",
7
+ "author": "Andriy Shevchenko",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/andriyshevchenko/servers.git"
11
+ },
12
+ "type": "module",
13
+ "bin": {
14
+ "mcp-server-memory-enhanced": "dist/index.js"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc && shx chmod +x dist/*.js",
21
+ "prepare": "npm run build",
22
+ "watch": "tsc --watch",
23
+ "test": "vitest run --coverage"
24
+ },
25
+ "dependencies": {
26
+ "@modelcontextprotocol/sdk": "^1.25.2",
27
+ "zod": "^3.25.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^22",
31
+ "@vitest/coverage-v8": "^2.1.8",
32
+ "shx": "^0.3.4",
33
+ "typescript": "^5.6.2",
34
+ "vitest": "^2.1.8"
35
+ }
36
+ }