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/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();