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.
- package/README.md +198 -0
- package/dist/index.js +1022 -0
- 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
|
+
}
|