collective-memory-mcp 0.1.0 → 0.2.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 +104 -22
- package/package.json +16 -9
- package/src/models.js +89 -0
- package/src/server.js +793 -0
- package/src/storage.js +354 -0
- package/index.js +0 -127
package/src/storage.js
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage layer for the Collective Memory System using SQLite.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import Database from "better-sqlite3";
|
|
6
|
+
import { promises as fs } from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import os from "os";
|
|
9
|
+
import { Entity, Relation } from "./models.js";
|
|
10
|
+
|
|
11
|
+
const DB_DIR = path.join(os.homedir(), ".collective-memory");
|
|
12
|
+
const DB_PATH = path.join(DB_DIR, "memory.db");
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* SQLite-based storage for the knowledge graph
|
|
16
|
+
*/
|
|
17
|
+
export class Storage {
|
|
18
|
+
constructor(dbPath = DB_PATH) {
|
|
19
|
+
this.dbPath = dbPath;
|
|
20
|
+
this.db = null;
|
|
21
|
+
this.init();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Initialize database and create schema
|
|
26
|
+
*/
|
|
27
|
+
init() {
|
|
28
|
+
// Ensure directory exists
|
|
29
|
+
fs.mkdir(path.dirname(this.dbPath), { recursive: true }).catch(() => {});
|
|
30
|
+
|
|
31
|
+
this.db = new Database(this.dbPath);
|
|
32
|
+
this.db.pragma("journal_mode = WAL");
|
|
33
|
+
|
|
34
|
+
// Create entities table
|
|
35
|
+
this.db.exec(`
|
|
36
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
37
|
+
name TEXT PRIMARY KEY,
|
|
38
|
+
entity_type TEXT NOT NULL,
|
|
39
|
+
observations TEXT NOT NULL DEFAULT '[]',
|
|
40
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
41
|
+
metadata TEXT NOT NULL DEFAULT '{}'
|
|
42
|
+
)
|
|
43
|
+
`);
|
|
44
|
+
|
|
45
|
+
// Create relations table
|
|
46
|
+
this.db.exec(`
|
|
47
|
+
CREATE TABLE IF NOT EXISTS relations (
|
|
48
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
49
|
+
from_entity TEXT NOT NULL,
|
|
50
|
+
to_entity TEXT NOT NULL,
|
|
51
|
+
relation_type TEXT NOT NULL,
|
|
52
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
53
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
54
|
+
UNIQUE(from_entity, to_entity, relation_type)
|
|
55
|
+
)
|
|
56
|
+
`);
|
|
57
|
+
|
|
58
|
+
// Create indexes
|
|
59
|
+
this.db.exec(`
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(entity_type);
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_relations_from ON relations(from_entity);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_relations_to ON relations(to_entity);
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_relations_type ON relations(relation_type);
|
|
64
|
+
`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Prepare statement helper
|
|
69
|
+
*/
|
|
70
|
+
prepare(sql) {
|
|
71
|
+
return this.db.prepare(sql);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ========== Entity Operations ==========
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create a new entity. Returns true if created, false if duplicate.
|
|
78
|
+
*/
|
|
79
|
+
createEntity(entity) {
|
|
80
|
+
try {
|
|
81
|
+
const stmt = this.prepare(`
|
|
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;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get an entity by name
|
|
101
|
+
*/
|
|
102
|
+
getEntity(name) {
|
|
103
|
+
const stmt = this.prepare("SELECT * FROM entities WHERE name = ?");
|
|
104
|
+
const row = stmt.get(name);
|
|
105
|
+
if (row) {
|
|
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
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get all entities
|
|
119
|
+
*/
|
|
120
|
+
getAllEntities() {
|
|
121
|
+
const stmt = this.prepare("SELECT * FROM entities");
|
|
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
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Check if an entity exists
|
|
134
|
+
*/
|
|
135
|
+
entityExists(name) {
|
|
136
|
+
const stmt = this.prepare("SELECT 1 FROM entities WHERE name = ?");
|
|
137
|
+
return stmt.get(name) !== undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Update entity observations
|
|
142
|
+
*/
|
|
143
|
+
updateEntity(name, { observations, metadata } = {}) {
|
|
144
|
+
const updates = [];
|
|
145
|
+
const params = [];
|
|
146
|
+
|
|
147
|
+
if (observations !== undefined) {
|
|
148
|
+
updates.push("observations = ?");
|
|
149
|
+
params.push(JSON.stringify(observations));
|
|
150
|
+
}
|
|
151
|
+
if (metadata !== undefined) {
|
|
152
|
+
updates.push("metadata = ?");
|
|
153
|
+
params.push(JSON.stringify(metadata));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (updates.length === 0) return true;
|
|
157
|
+
|
|
158
|
+
params.push(name);
|
|
159
|
+
const stmt = this.prepare(`UPDATE entities SET ${updates.join(", ")} WHERE name = ?`);
|
|
160
|
+
return stmt.run(...params).changes > 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Delete an entity and its relations
|
|
165
|
+
*/
|
|
166
|
+
deleteEntity(name) {
|
|
167
|
+
const deleteRelations = this.prepare(
|
|
168
|
+
"DELETE FROM relations WHERE from_entity = ? OR to_entity = ?"
|
|
169
|
+
);
|
|
170
|
+
deleteRelations.run(name, name);
|
|
171
|
+
|
|
172
|
+
const deleteEntity = this.prepare("DELETE FROM entities WHERE name = ?");
|
|
173
|
+
return deleteEntity.run(name).changes > 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Delete multiple entities
|
|
178
|
+
*/
|
|
179
|
+
deleteEntities(names) {
|
|
180
|
+
let count = 0;
|
|
181
|
+
for (const name of names) {
|
|
182
|
+
if (this.deleteEntity(name)) count++;
|
|
183
|
+
}
|
|
184
|
+
return count;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ========== Relation Operations ==========
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Create a new relation. Returns true if created, false if duplicate.
|
|
191
|
+
*/
|
|
192
|
+
createRelation(relation) {
|
|
193
|
+
try {
|
|
194
|
+
const stmt = this.prepare(`
|
|
195
|
+
INSERT INTO relations (from_entity, to_entity, relation_type, created_at, metadata)
|
|
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;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get relations with optional filters
|
|
214
|
+
*/
|
|
215
|
+
getRelations({ fromEntity, toEntity, relationType } = {}) {
|
|
216
|
+
let sql = "SELECT * FROM relations WHERE 1=1";
|
|
217
|
+
const params = [];
|
|
218
|
+
|
|
219
|
+
if (fromEntity) {
|
|
220
|
+
sql += " AND from_entity = ?";
|
|
221
|
+
params.push(fromEntity);
|
|
222
|
+
}
|
|
223
|
+
if (toEntity) {
|
|
224
|
+
sql += " AND to_entity = ?";
|
|
225
|
+
params.push(toEntity);
|
|
226
|
+
}
|
|
227
|
+
if (relationType) {
|
|
228
|
+
sql += " AND relation_type = ?";
|
|
229
|
+
params.push(relationType);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const stmt = this.prepare(sql);
|
|
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
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get all relations
|
|
245
|
+
*/
|
|
246
|
+
getAllRelations() {
|
|
247
|
+
return this.getRelations();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Check if a relation exists
|
|
252
|
+
*/
|
|
253
|
+
relationExists(fromEntity, toEntity, relationType) {
|
|
254
|
+
const stmt = this.prepare(
|
|
255
|
+
"SELECT 1 FROM relations WHERE from_entity = ? AND to_entity = ? AND relation_type = ?"
|
|
256
|
+
);
|
|
257
|
+
return stmt.get(fromEntity, toEntity, relationType) !== undefined;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Delete a specific relation
|
|
262
|
+
*/
|
|
263
|
+
deleteRelation(fromEntity, toEntity, relationType) {
|
|
264
|
+
const stmt = this.prepare(
|
|
265
|
+
"DELETE FROM relations WHERE from_entity = ? AND to_entity = ? AND relation_type = ?"
|
|
266
|
+
);
|
|
267
|
+
return stmt.run(fromEntity, toEntity, relationType).changes > 0;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Delete multiple relations
|
|
272
|
+
*/
|
|
273
|
+
deleteRelations(relations) {
|
|
274
|
+
let count = 0;
|
|
275
|
+
for (const [fromEntity, toEntity, relationType] of relations) {
|
|
276
|
+
if (this.deleteRelation(fromEntity, toEntity, relationType)) count++;
|
|
277
|
+
}
|
|
278
|
+
return count;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ========== Search ==========
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Search entities by name, type, or observations
|
|
285
|
+
*/
|
|
286
|
+
searchEntities(query) {
|
|
287
|
+
const pattern = `%${query}%`;
|
|
288
|
+
const stmt = this.prepare(`
|
|
289
|
+
SELECT DISTINCT e.* FROM entities e
|
|
290
|
+
WHERE e.name LIKE ?
|
|
291
|
+
OR e.entity_type LIKE ?
|
|
292
|
+
OR e.observations LIKE ?
|
|
293
|
+
ORDER BY e.created_at DESC
|
|
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
|
+
}));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Get entities related to a given entity
|
|
307
|
+
*/
|
|
308
|
+
getRelatedEntities(entityName) {
|
|
309
|
+
const result = { connected: [], incoming: [], outgoing: [] };
|
|
310
|
+
|
|
311
|
+
// Get connected entity names
|
|
312
|
+
const outgoingStmt = this.prepare(
|
|
313
|
+
"SELECT DISTINCT to_entity FROM relations WHERE from_entity = ?"
|
|
314
|
+
);
|
|
315
|
+
const incomingStmt = this.prepare(
|
|
316
|
+
"SELECT DISTINCT from_entity FROM relations WHERE to_entity = ?"
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const outgoing = outgoingStmt.all(entityName);
|
|
320
|
+
const incoming = incomingStmt.all(entityName);
|
|
321
|
+
|
|
322
|
+
const connectedNames = new Set([
|
|
323
|
+
...outgoing.map(r => r.to_entity),
|
|
324
|
+
...incoming.map(r => r.from_entity)
|
|
325
|
+
]);
|
|
326
|
+
|
|
327
|
+
for (const name of connectedNames) {
|
|
328
|
+
const entity = this.getEntity(name);
|
|
329
|
+
if (entity) result.connected.push(entity);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return result;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Close database connection
|
|
337
|
+
*/
|
|
338
|
+
close() {
|
|
339
|
+
if (this.db) {
|
|
340
|
+
this.db.close();
|
|
341
|
+
this.db = null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Export singleton instance
|
|
347
|
+
let storageInstance = null;
|
|
348
|
+
|
|
349
|
+
export function getStorage(dbPath) {
|
|
350
|
+
if (!storageInstance) {
|
|
351
|
+
storageInstance = new Storage(dbPath);
|
|
352
|
+
}
|
|
353
|
+
return storageInstance;
|
|
354
|
+
}
|
package/index.js
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Collective Memory MCP Server - npx Wrapper
|
|
5
|
-
*
|
|
6
|
-
* This wrapper spawns the Python MCP server, making it available via npx:
|
|
7
|
-
* npx collective-memory-mcp
|
|
8
|
-
*
|
|
9
|
-
* The Python package will be automatically installed on first run.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { spawn, spawnSync } from 'child_process';
|
|
13
|
-
import { fileURLToPath } from 'url';
|
|
14
|
-
import { dirname, join } from 'path';
|
|
15
|
-
import { existsSync, readFileSync } from 'fs';
|
|
16
|
-
import { homedir } from 'os';
|
|
17
|
-
|
|
18
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
-
const __dirname = dirname(__filename);
|
|
20
|
-
|
|
21
|
-
// ANSI color codes for better output
|
|
22
|
-
const colors = {
|
|
23
|
-
reset: '\x1b[0m',
|
|
24
|
-
red: '\x1b[31m',
|
|
25
|
-
green: '\x1b[32m',
|
|
26
|
-
yellow: '\x1b[33m',
|
|
27
|
-
blue: '\x1b[34m',
|
|
28
|
-
cyan: '\x1b[36m',
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
function log(message, color = 'reset') {
|
|
32
|
-
console.error(`${colors[color]}${message}${colors.reset}`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function findPython() {
|
|
36
|
-
// Try different Python commands
|
|
37
|
-
const pythonCommands = ['python3', 'python', 'python3.12', 'python3.11', 'python3.10'];
|
|
38
|
-
|
|
39
|
-
for (const cmd of pythonCommands) {
|
|
40
|
-
const result = spawnSync(cmd, ['--version'], { stdio: 'pipe' });
|
|
41
|
-
if (result.status === 0) {
|
|
42
|
-
return cmd;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function getDbPath() {
|
|
50
|
-
// Allow override via environment variable
|
|
51
|
-
if (process.env.COLLECTIVE_MEMORY_DB_PATH) {
|
|
52
|
-
return process.env.COLLECTIVE_MEMORY_DB_PATH;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Default to ~/.collective-memory/memory.db
|
|
56
|
-
return join(homedir(), '.collective-memory', 'memory.db');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function main() {
|
|
60
|
-
const pythonCmd = findPython();
|
|
61
|
-
|
|
62
|
-
if (!pythonCmd) {
|
|
63
|
-
log('Error: Python 3.10+ is required but not found.', 'red');
|
|
64
|
-
log('Please install Python from https://python.org', 'yellow');
|
|
65
|
-
process.exit(1);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Check if collective_memory module is available
|
|
69
|
-
const checkModule = spawnSync(pythonCmd, ['-c', 'import collective_memory'], {
|
|
70
|
-
stdio: 'pipe'
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
if (checkModule.status !== 0) {
|
|
74
|
-
log('Collective Memory MCP Server not found. Installing...', 'yellow');
|
|
75
|
-
log('', 'reset');
|
|
76
|
-
|
|
77
|
-
const installArgs = ['-m', 'pip', 'install', '-U', 'collective-memory-mcp'];
|
|
78
|
-
const install = spawn(pythonCmd, installArgs, {
|
|
79
|
-
stdio: 'inherit'
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
install.on('close', (code) => {
|
|
83
|
-
if (code !== 0) {
|
|
84
|
-
log('', 'reset');
|
|
85
|
-
log('Failed to install collective-memory-mcp Python package.', 'red');
|
|
86
|
-
log('You can install it manually:', 'yellow');
|
|
87
|
-
log(` ${pythonCmd} -m pip install collective-memory-mcp`, 'cyan');
|
|
88
|
-
process.exit(1);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
log('', 'reset');
|
|
92
|
-
log('Installation complete. Starting server...', 'green');
|
|
93
|
-
startServer(pythonCmd);
|
|
94
|
-
});
|
|
95
|
-
} else {
|
|
96
|
-
startServer(pythonCmd);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function startServer(pythonCmd) {
|
|
101
|
-
const dbPath = getDbPath();
|
|
102
|
-
|
|
103
|
-
// Spawn the Python MCP server
|
|
104
|
-
const server = spawn(pythonCmd, ['-m', 'collective_memory'], {
|
|
105
|
-
stdio: 'inherit',
|
|
106
|
-
env: {
|
|
107
|
-
...process.env,
|
|
108
|
-
COLLECTIVE_MEMORY_DB_PATH: dbPath,
|
|
109
|
-
PYTHONUNBUFFERED: '1',
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
server.on('error', (err) => {
|
|
114
|
-
log(`Error starting server: ${err.message}`, 'red');
|
|
115
|
-
process.exit(1);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
server.on('exit', (code) => {
|
|
119
|
-
process.exit(code ?? 0);
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Handle signals
|
|
124
|
-
process.on('SIGINT', () => process.exit(0));
|
|
125
|
-
process.on('SIGTERM', () => process.exit(0));
|
|
126
|
-
|
|
127
|
-
main();
|