collective-memory-mcp 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -6
- package/src/storage.js +150 -188
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "collective-memory-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "A persistent, graph-based memory system for AI agents (MCP Server)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/server.js",
|
|
@@ -37,10 +37,6 @@
|
|
|
37
37
|
"win32"
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@modelcontextprotocol/sdk": "^0.6.0"
|
|
41
|
-
"better-sqlite3": "^11.0.0"
|
|
42
|
-
},
|
|
43
|
-
"devDependencies": {
|
|
44
|
-
"@types/better-sqlite3": "^7.6.0"
|
|
40
|
+
"@modelcontextprotocol/sdk": "^0.6.0"
|
|
45
41
|
}
|
|
46
42
|
}
|
package/src/storage.js
CHANGED
|
@@ -1,74 +1,84 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Storage layer for the Collective Memory System using
|
|
2
|
+
* Storage layer for the Collective Memory System using JSON file.
|
|
3
|
+
* Pure JavaScript - no native dependencies required.
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
|
-
import Database from "better-sqlite3";
|
|
6
6
|
import { promises as fs } from "fs";
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
7
8
|
import path from "path";
|
|
8
9
|
import os from "os";
|
|
9
10
|
import { Entity, Relation } from "./models.js";
|
|
10
11
|
|
|
11
12
|
const DB_DIR = path.join(os.homedir(), ".collective-memory");
|
|
12
|
-
const DB_PATH = path.join(DB_DIR, "memory.
|
|
13
|
+
const DB_PATH = path.join(DB_DIR, "memory.json");
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
|
-
*
|
|
16
|
+
* Simple file-based storage
|
|
16
17
|
*/
|
|
17
18
|
export class Storage {
|
|
18
19
|
constructor(dbPath = DB_PATH) {
|
|
19
20
|
this.dbPath = dbPath;
|
|
20
|
-
this.
|
|
21
|
+
this.data = null;
|
|
22
|
+
// Initialize synchronously
|
|
21
23
|
this.init();
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
|
-
* Initialize
|
|
27
|
+
* Initialize storage (synchronous)
|
|
26
28
|
*/
|
|
27
29
|
init() {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
30
|
+
try {
|
|
31
|
+
// Ensure directory exists
|
|
32
|
+
const dir = path.dirname(this.dbPath);
|
|
33
|
+
mkdirSync(dir, { recursive: true });
|
|
34
|
+
|
|
35
|
+
// Try to load existing data
|
|
36
|
+
if (existsSync(this.dbPath)) {
|
|
37
|
+
const content = readFileSync(this.dbPath, "utf-8");
|
|
38
|
+
this.data = JSON.parse(content);
|
|
39
|
+
} else {
|
|
40
|
+
// Create new empty data
|
|
41
|
+
this.data = {
|
|
42
|
+
entities: {},
|
|
43
|
+
relations: [],
|
|
44
|
+
version: "1.0",
|
|
45
|
+
};
|
|
46
|
+
this.saveSync();
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
// If anything fails, start with empty data
|
|
50
|
+
this.data = {
|
|
51
|
+
entities: {},
|
|
52
|
+
relations: [],
|
|
53
|
+
version: "1.0",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Save data synchronously
|
|
60
|
+
*/
|
|
61
|
+
saveSync() {
|
|
62
|
+
try {
|
|
63
|
+
const dir = path.dirname(this.dbPath);
|
|
64
|
+
mkdirSync(dir, { recursive: true });
|
|
65
|
+
writeFileSync(this.dbPath, JSON.stringify(this.data, null, 2), "utf-8");
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error("Failed to save:", error.message);
|
|
68
|
+
}
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
/**
|
|
68
|
-
*
|
|
72
|
+
* Save data asynchronously
|
|
69
73
|
*/
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
async save() {
|
|
75
|
+
const dir = path.dirname(this.dbPath);
|
|
76
|
+
try {
|
|
77
|
+
await fs.mkdir(dir, { recursive: true });
|
|
78
|
+
} catch {
|
|
79
|
+
// Ignore
|
|
80
|
+
}
|
|
81
|
+
await fs.writeFile(this.dbPath, JSON.stringify(this.data, null, 2), "utf-8");
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
// ========== Entity Operations ==========
|
|
@@ -76,40 +86,22 @@ export class Storage {
|
|
|
76
86
|
/**
|
|
77
87
|
* Create a new entity. Returns true if created, false if duplicate.
|
|
78
88
|
*/
|
|
79
|
-
createEntity(entity) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
INSERT INTO entities (name, entity_type, observations, created_at, metadata)
|
|
83
|
-
VALUES (?, ?, ?, ?, ?)
|
|
84
|
-
`);
|
|
85
|
-
stmt.run(
|
|
86
|
-
entity.name,
|
|
87
|
-
entity.entityType,
|
|
88
|
-
JSON.stringify(entity.observations),
|
|
89
|
-
entity.createdAt,
|
|
90
|
-
JSON.stringify(entity.metadata)
|
|
91
|
-
);
|
|
92
|
-
return true;
|
|
93
|
-
} catch (err) {
|
|
94
|
-
if (err.code === "SQLITE_CONSTRAINT") return false;
|
|
95
|
-
throw err;
|
|
89
|
+
async createEntity(entity) {
|
|
90
|
+
if (this.data.entities[entity.name]) {
|
|
91
|
+
return false;
|
|
96
92
|
}
|
|
93
|
+
this.data.entities[entity.name] = entity.toJSON();
|
|
94
|
+
await this.save();
|
|
95
|
+
return true;
|
|
97
96
|
}
|
|
98
97
|
|
|
99
98
|
/**
|
|
100
99
|
* Get an entity by name
|
|
101
100
|
*/
|
|
102
101
|
getEntity(name) {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return new Entity({
|
|
107
|
-
name: row.name,
|
|
108
|
-
entityType: row.entity_type,
|
|
109
|
-
observations: JSON.parse(row.observations),
|
|
110
|
-
createdAt: row.created_at,
|
|
111
|
-
metadata: JSON.parse(row.metadata),
|
|
112
|
-
});
|
|
102
|
+
const data = this.data.entities[name];
|
|
103
|
+
if (data) {
|
|
104
|
+
return new Entity(data);
|
|
113
105
|
}
|
|
114
106
|
return null;
|
|
115
107
|
}
|
|
@@ -118,68 +110,60 @@ export class Storage {
|
|
|
118
110
|
* Get all entities
|
|
119
111
|
*/
|
|
120
112
|
getAllEntities() {
|
|
121
|
-
|
|
122
|
-
const rows = stmt.all();
|
|
123
|
-
return rows.map(row => new Entity({
|
|
124
|
-
name: row.name,
|
|
125
|
-
entityType: row.entity_type,
|
|
126
|
-
observations: JSON.parse(row.observations),
|
|
127
|
-
createdAt: row.created_at,
|
|
128
|
-
metadata: JSON.parse(row.metadata),
|
|
129
|
-
}));
|
|
113
|
+
return Object.values(this.data.entities).map(data => new Entity(data));
|
|
130
114
|
}
|
|
131
115
|
|
|
132
116
|
/**
|
|
133
117
|
* Check if an entity exists
|
|
134
118
|
*/
|
|
135
119
|
entityExists(name) {
|
|
136
|
-
|
|
137
|
-
return stmt.get(name) !== undefined;
|
|
120
|
+
return name in this.data.entities;
|
|
138
121
|
}
|
|
139
122
|
|
|
140
123
|
/**
|
|
141
|
-
* Update entity
|
|
124
|
+
* Update entity
|
|
142
125
|
*/
|
|
143
|
-
updateEntity(name, { observations, metadata } = {}) {
|
|
144
|
-
const
|
|
145
|
-
|
|
126
|
+
async updateEntity(name, { observations, metadata } = {}) {
|
|
127
|
+
const entity = this.data.entities[name];
|
|
128
|
+
if (!entity) return false;
|
|
146
129
|
|
|
147
130
|
if (observations !== undefined) {
|
|
148
|
-
|
|
149
|
-
params.push(JSON.stringify(observations));
|
|
131
|
+
entity.observations = observations;
|
|
150
132
|
}
|
|
151
133
|
if (metadata !== undefined) {
|
|
152
|
-
|
|
153
|
-
params.push(JSON.stringify(metadata));
|
|
134
|
+
entity.metadata = metadata;
|
|
154
135
|
}
|
|
155
136
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
params.push(name);
|
|
159
|
-
const stmt = this.prepare(`UPDATE entities SET ${updates.join(", ")} WHERE name = ?`);
|
|
160
|
-
return stmt.run(...params).changes > 0;
|
|
137
|
+
await this.save();
|
|
138
|
+
return true;
|
|
161
139
|
}
|
|
162
140
|
|
|
163
141
|
/**
|
|
164
142
|
* Delete an entity and its relations
|
|
165
143
|
*/
|
|
166
|
-
deleteEntity(name) {
|
|
167
|
-
|
|
168
|
-
|
|
144
|
+
async deleteEntity(name) {
|
|
145
|
+
if (!this.data.entities[name]) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
delete this.data.entities[name];
|
|
150
|
+
|
|
151
|
+
// Remove relations involving this entity
|
|
152
|
+
this.data.relations = this.data.relations.filter(
|
|
153
|
+
r => r.from !== name && r.to !== name
|
|
169
154
|
);
|
|
170
|
-
deleteRelations.run(name, name);
|
|
171
155
|
|
|
172
|
-
|
|
173
|
-
return
|
|
156
|
+
await this.save();
|
|
157
|
+
return true;
|
|
174
158
|
}
|
|
175
159
|
|
|
176
160
|
/**
|
|
177
161
|
* Delete multiple entities
|
|
178
162
|
*/
|
|
179
|
-
deleteEntities(names) {
|
|
163
|
+
async deleteEntities(names) {
|
|
180
164
|
let count = 0;
|
|
181
165
|
for (const name of names) {
|
|
182
|
-
if (this.deleteEntity(name)) count++;
|
|
166
|
+
if (await this.deleteEntity(name)) count++;
|
|
183
167
|
}
|
|
184
168
|
return count;
|
|
185
169
|
}
|
|
@@ -189,91 +173,82 @@ export class Storage {
|
|
|
189
173
|
/**
|
|
190
174
|
* Create a new relation. Returns true if created, false if duplicate.
|
|
191
175
|
*/
|
|
192
|
-
createRelation(relation) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
VALUES (?, ?, ?, ?, ?)
|
|
197
|
-
`);
|
|
198
|
-
stmt.run(
|
|
199
|
-
relation.from,
|
|
200
|
-
relation.to,
|
|
201
|
-
relation.relationType,
|
|
202
|
-
relation.createdAt,
|
|
203
|
-
JSON.stringify(relation.metadata)
|
|
204
|
-
);
|
|
205
|
-
return true;
|
|
206
|
-
} catch (err) {
|
|
207
|
-
if (err.code === "SQLITE_CONSTRAINT") return false;
|
|
208
|
-
throw err;
|
|
176
|
+
async createRelation(relation) {
|
|
177
|
+
const key = this.relationKey(relation.from, relation.to, relation.relationType);
|
|
178
|
+
if (this.data.relations.some(r => this.relationKey(r.from, r.to, r.relationType) === key)) {
|
|
179
|
+
return false;
|
|
209
180
|
}
|
|
181
|
+
|
|
182
|
+
this.data.relations.push(relation.toJSON());
|
|
183
|
+
await this.save();
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Generate unique key for relation
|
|
189
|
+
*/
|
|
190
|
+
relationKey(from, to, type) {
|
|
191
|
+
return `${from}|${to}|${type}`;
|
|
210
192
|
}
|
|
211
193
|
|
|
212
194
|
/**
|
|
213
195
|
* Get relations with optional filters
|
|
214
196
|
*/
|
|
215
197
|
getRelations({ fromEntity, toEntity, relationType } = {}) {
|
|
216
|
-
let
|
|
217
|
-
const params = [];
|
|
198
|
+
let results = this.data.relations.map(r => new Relation(r));
|
|
218
199
|
|
|
219
200
|
if (fromEntity) {
|
|
220
|
-
|
|
221
|
-
params.push(fromEntity);
|
|
201
|
+
results = results.filter(r => r.from === fromEntity);
|
|
222
202
|
}
|
|
223
203
|
if (toEntity) {
|
|
224
|
-
|
|
225
|
-
params.push(toEntity);
|
|
204
|
+
results = results.filter(r => r.to === toEntity);
|
|
226
205
|
}
|
|
227
206
|
if (relationType) {
|
|
228
|
-
|
|
229
|
-
params.push(relationType);
|
|
207
|
+
results = results.filter(r => r.relationType === relationType);
|
|
230
208
|
}
|
|
231
209
|
|
|
232
|
-
|
|
233
|
-
const rows = stmt.all(...params);
|
|
234
|
-
return rows.map(row => new Relation({
|
|
235
|
-
from: row.from_entity,
|
|
236
|
-
to: row.to_entity,
|
|
237
|
-
relationType: row.relation_type,
|
|
238
|
-
createdAt: row.created_at,
|
|
239
|
-
metadata: JSON.parse(row.metadata),
|
|
240
|
-
}));
|
|
210
|
+
return results;
|
|
241
211
|
}
|
|
242
212
|
|
|
243
213
|
/**
|
|
244
214
|
* Get all relations
|
|
245
215
|
*/
|
|
246
216
|
getAllRelations() {
|
|
247
|
-
return this.
|
|
217
|
+
return this.data.relations.map(r => new Relation(r));
|
|
248
218
|
}
|
|
249
219
|
|
|
250
220
|
/**
|
|
251
221
|
* Check if a relation exists
|
|
252
222
|
*/
|
|
253
223
|
relationExists(fromEntity, toEntity, relationType) {
|
|
254
|
-
|
|
255
|
-
|
|
224
|
+
return this.data.relations.some(
|
|
225
|
+
r => r.from === fromEntity && r.to === toEntity && r.relationType === relationType
|
|
256
226
|
);
|
|
257
|
-
return stmt.get(fromEntity, toEntity, relationType) !== undefined;
|
|
258
227
|
}
|
|
259
228
|
|
|
260
229
|
/**
|
|
261
230
|
* Delete a specific relation
|
|
262
231
|
*/
|
|
263
|
-
deleteRelation(fromEntity, toEntity, relationType) {
|
|
264
|
-
const
|
|
265
|
-
|
|
232
|
+
async deleteRelation(fromEntity, toEntity, relationType) {
|
|
233
|
+
const before = this.data.relations.length;
|
|
234
|
+
this.data.relations = this.data.relations.filter(
|
|
235
|
+
r => !(r.from === fromEntity && r.to === toEntity && r.relationType === relationType)
|
|
266
236
|
);
|
|
267
|
-
|
|
237
|
+
|
|
238
|
+
if (this.data.relations.length < before) {
|
|
239
|
+
await this.save();
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
return false;
|
|
268
243
|
}
|
|
269
244
|
|
|
270
245
|
/**
|
|
271
246
|
* Delete multiple relations
|
|
272
247
|
*/
|
|
273
|
-
deleteRelations(relations) {
|
|
248
|
+
async deleteRelations(relations) {
|
|
274
249
|
let count = 0;
|
|
275
250
|
for (const [fromEntity, toEntity, relationType] of relations) {
|
|
276
|
-
if (this.deleteRelation(fromEntity, toEntity, relationType)) count++;
|
|
251
|
+
if (await this.deleteRelation(fromEntity, toEntity, relationType)) count++;
|
|
277
252
|
}
|
|
278
253
|
return count;
|
|
279
254
|
}
|
|
@@ -284,22 +259,13 @@ export class Storage {
|
|
|
284
259
|
* Search entities by name, type, or observations
|
|
285
260
|
*/
|
|
286
261
|
searchEntities(query) {
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
`);
|
|
295
|
-
const rows = stmt.all(pattern, pattern, pattern);
|
|
296
|
-
return rows.map(row => new Entity({
|
|
297
|
-
name: row.name,
|
|
298
|
-
entityType: row.entity_type,
|
|
299
|
-
observations: JSON.parse(row.observations),
|
|
300
|
-
createdAt: row.created_at,
|
|
301
|
-
metadata: JSON.parse(row.metadata),
|
|
302
|
-
}));
|
|
262
|
+
const lowerQuery = query.toLowerCase();
|
|
263
|
+
return this.getAllEntities().filter(e => {
|
|
264
|
+
if (e.name.toLowerCase().includes(lowerQuery)) return true;
|
|
265
|
+
if (e.entityType.toLowerCase().includes(lowerQuery)) return true;
|
|
266
|
+
if (e.observations.some(o => o.toLowerCase().includes(lowerQuery))) return true;
|
|
267
|
+
return false;
|
|
268
|
+
});
|
|
303
269
|
}
|
|
304
270
|
|
|
305
271
|
/**
|
|
@@ -308,42 +274,38 @@ export class Storage {
|
|
|
308
274
|
getRelatedEntities(entityName) {
|
|
309
275
|
const result = { connected: [], incoming: [], outgoing: [] };
|
|
310
276
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
...outgoing.map(r => r.to_entity),
|
|
324
|
-
...incoming.map(r => r.from_entity)
|
|
325
|
-
]);
|
|
277
|
+
const connectedNames = new Set();
|
|
278
|
+
|
|
279
|
+
for (const rel of this.data.relations) {
|
|
280
|
+
if (rel.from === entityName) {
|
|
281
|
+
connectedNames.add(rel.to);
|
|
282
|
+
result.outgoing.push(rel.to);
|
|
283
|
+
}
|
|
284
|
+
if (rel.to === entityName) {
|
|
285
|
+
connectedNames.add(rel.from);
|
|
286
|
+
result.incoming.push(rel.from);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
326
289
|
|
|
327
290
|
for (const name of connectedNames) {
|
|
328
291
|
const entity = this.getEntity(name);
|
|
329
|
-
if (entity)
|
|
292
|
+
if (entity) {
|
|
293
|
+
result.connected.push(entity);
|
|
294
|
+
}
|
|
330
295
|
}
|
|
331
296
|
|
|
332
297
|
return result;
|
|
333
298
|
}
|
|
334
299
|
|
|
335
300
|
/**
|
|
336
|
-
* Close
|
|
301
|
+
* Close storage
|
|
337
302
|
*/
|
|
338
|
-
close() {
|
|
339
|
-
|
|
340
|
-
this.db.close();
|
|
341
|
-
this.db = null;
|
|
342
|
-
}
|
|
303
|
+
async close() {
|
|
304
|
+
await this.save();
|
|
343
305
|
}
|
|
344
306
|
}
|
|
345
307
|
|
|
346
|
-
//
|
|
308
|
+
// Singleton instance
|
|
347
309
|
let storageInstance = null;
|
|
348
310
|
|
|
349
311
|
export function getStorage(dbPath) {
|