@cybermem/mcp 0.14.14 → 0.15.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/CHANGELOG.md +12 -0
- package/dist/index.js +219 -413
- package/e2e/api.spec.ts +132 -186
- package/e2e/sse_transport.spec.ts +69 -29
- package/e2e/sse_transport_multi.spec.ts +73 -137
- package/e2e/utils/FastMCPHandshakeTransport.ts +183 -0
- package/package.json +6 -9
- package/scripts/postbuild.js +24 -0
- package/src/index.ts +292 -541
- package/src/openmemory-js.d.ts +6 -0
- package/test-handshake.ts +26 -0
package/src/index.ts
CHANGED
|
@@ -1,597 +1,348 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
import "./console-fix.js";
|
|
2
3
|
import "./env.js";
|
|
3
4
|
|
|
4
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
-
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
6
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
-
import { InitializeRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
8
5
|
import { AsyncLocalStorage } from "async_hooks";
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
12
|
-
import { join } from "path";
|
|
6
|
+
import { FastMCP } from "fastmcp";
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync } from "fs";
|
|
8
|
+
import { dirname, join } from "path";
|
|
13
9
|
import { z } from "zod";
|
|
14
10
|
|
|
15
|
-
//
|
|
11
|
+
// --- CORE SDK IMPORTS ---
|
|
12
|
+
import { all_async, run_async } from "openmemory-js/dist/core/db.js";
|
|
13
|
+
import { Memory } from "openmemory-js/dist/core/memory.js";
|
|
14
|
+
import {
|
|
15
|
+
reinforce_memory,
|
|
16
|
+
update_memory,
|
|
17
|
+
} from "openmemory-js/dist/memory/hsg.js";
|
|
18
|
+
|
|
19
|
+
// --- TYPES ---
|
|
16
20
|
interface IMemory {
|
|
17
|
-
add(
|
|
18
|
-
|
|
19
|
-
opts?: { tags?: string[]; user_id?: string; [key: string]: unknown },
|
|
20
|
-
): Promise<unknown>;
|
|
21
|
-
search(
|
|
22
|
-
query: string,
|
|
23
|
-
opts?: {
|
|
24
|
-
limit?: number;
|
|
25
|
-
user_id?: string;
|
|
26
|
-
sectors?: unknown;
|
|
27
|
-
[key: string]: unknown;
|
|
28
|
-
},
|
|
29
|
-
): Promise<unknown>;
|
|
21
|
+
add(content: string, opts?: any): Promise<unknown>;
|
|
22
|
+
search(query: string, opts?: any): Promise<unknown>;
|
|
30
23
|
get(id: string): Promise<unknown>;
|
|
31
24
|
delete_all(user_id: string): Promise<unknown>;
|
|
32
25
|
wipe(): Promise<unknown>;
|
|
33
26
|
}
|
|
34
27
|
|
|
35
|
-
//
|
|
36
|
-
const requestContext = new AsyncLocalStorage<{
|
|
37
|
-
userId?: string;
|
|
38
|
-
clientName?: string;
|
|
39
|
-
}>();
|
|
40
|
-
|
|
41
|
-
// CLI args processing
|
|
42
|
-
const args = process.argv.slice(2);
|
|
28
|
+
// --- GLOBALS & CONTEXT ---
|
|
29
|
+
const requestContext = new AsyncLocalStorage<{ clientName?: string }>();
|
|
43
30
|
|
|
44
|
-
// Read version from package.json
|
|
45
31
|
let PACKAGE_VERSION = "0.0.0";
|
|
46
32
|
try {
|
|
47
33
|
const packageJsonPath = join(__dirname, "../package.json");
|
|
48
34
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
49
35
|
PACKAGE_VERSION = packageJson.version;
|
|
50
|
-
} catch
|
|
51
|
-
console.error("[MCP] Failed to read package.json version", error);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Start the server
|
|
55
|
-
startServer();
|
|
56
|
-
|
|
57
|
-
async function startServer() {
|
|
58
|
-
const getArg = (name: string) => {
|
|
59
|
-
const idx = args.indexOf(name);
|
|
60
|
-
return idx !== -1 && args[idx + 1] ? args[idx + 1] : undefined;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const cliEnv = getArg("--env");
|
|
64
|
-
|
|
65
|
-
if (cliEnv === "staging") {
|
|
66
|
-
console.error("[MCP] Running in Staging environment");
|
|
67
|
-
process.env.CYBERMEM_ENV = "staging";
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// --- IMPLEMENTATION LOGIC ---
|
|
71
|
-
|
|
72
|
-
let memory: IMemory | null = null;
|
|
73
|
-
let sdk_update_memory:
|
|
74
|
-
| ((
|
|
75
|
-
id: string,
|
|
76
|
-
content?: string,
|
|
77
|
-
tags?: string[],
|
|
78
|
-
metadata?: Record<string, unknown>,
|
|
79
|
-
) => Promise<unknown>)
|
|
80
|
-
| null = null;
|
|
81
|
-
let sdk_reinforce_memory:
|
|
82
|
-
| ((id: string, boost?: number) => Promise<unknown>)
|
|
83
|
-
| null = null;
|
|
36
|
+
} catch {}
|
|
84
37
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const path = await import("path");
|
|
89
|
-
try {
|
|
90
|
-
const dir = path.dirname(dbPath);
|
|
91
|
-
if (dir) fs.mkdirSync(dir, { recursive: true });
|
|
92
|
-
} catch {}
|
|
38
|
+
const VALID_VERSION = (
|
|
39
|
+
PACKAGE_VERSION.match(/^\d+\.\d+\.\d+$/) ? PACKAGE_VERSION : "0.0.0"
|
|
40
|
+
) as `${number}.${number}.${number}`;
|
|
93
41
|
|
|
42
|
+
const CYBERMEM_INSTRUCTIONS = `CyberMem is a persistent context daemon for AI agents.
|
|
43
|
+
PROTOCOL:
|
|
44
|
+
1. On session start: call query_memory("user context profile")
|
|
45
|
+
2. Store insights immediately with add_memory
|
|
46
|
+
3. Corrections: update_memory
|
|
47
|
+
4. Decay prevention: reinforce_memory
|
|
48
|
+
Full protocol: https://docs.cybermem.dev/agent-protocol`;
|
|
49
|
+
|
|
50
|
+
// --- ERROR TRAPPING ---
|
|
51
|
+
process.on("uncaughtException", (err) => {
|
|
52
|
+
console.error("[CRITICAL] Uncaught Exception:", err);
|
|
53
|
+
});
|
|
54
|
+
process.on("unhandledRejection", (reason) => {
|
|
55
|
+
console.error("[CRITICAL] Unhandled Rejection:", reason);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// --- LOGGING ---
|
|
59
|
+
const logActivity = async (tool: string, status: number = 200) => {
|
|
94
60
|
try {
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
61
|
+
const ctx = requestContext.getStore();
|
|
62
|
+
const client = ctx?.clientName || "unknown";
|
|
63
|
+
console.log(`[MCP-LOG] client=${client} tool=${tool} status=${status}`);
|
|
64
|
+
const ts = Date.now();
|
|
65
|
+
const isError = status >= 400 ? 1 : 0;
|
|
66
|
+
|
|
67
|
+
await run_async(
|
|
68
|
+
"INSERT INTO cybermem_access_log (timestamp, client_name, client_version, method, endpoint, tool, status, is_error) VALUES (?, ?, ?, 'POST', '/mcp', ?, ?, ?)",
|
|
69
|
+
[ts, client, PACKAGE_VERSION, tool, status.toString(), isError],
|
|
70
|
+
);
|
|
71
|
+
await run_async(
|
|
72
|
+
"INSERT INTO cybermem_stats (client_name, tool, count, errors, last_updated) VALUES (?, ?, 1, ?, ?) ON CONFLICT(client_name, tool) DO UPDATE SET count=count+1, errors=errors+?, last_updated=?",
|
|
73
|
+
[client, tool, isError, ts, isError, ts],
|
|
74
|
+
);
|
|
75
|
+
} catch (err: any) {
|
|
76
|
+
console.error("[MCP] Log Error:", err.message);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
100
79
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
db.serialize(() => {
|
|
106
|
-
db.run(
|
|
107
|
-
"CREATE TABLE IF NOT EXISTS cybermem_stats (id INTEGER PRIMARY KEY AUTOINCREMENT, client_name TEXT NOT NULL, operation TEXT NOT NULL, count INTEGER DEFAULT 0, errors INTEGER DEFAULT 0, last_updated INTEGER NOT NULL, UNIQUE(client_name, operation));",
|
|
108
|
-
);
|
|
109
|
-
db.run(
|
|
110
|
-
"CREATE TABLE IF NOT EXISTS cybermem_access_log (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER NOT NULL, client_name TEXT NOT NULL, client_version TEXT, method TEXT NOT NULL, endpoint TEXT NOT NULL, operation TEXT NOT NULL, status TEXT NOT NULL, is_error INTEGER DEFAULT 0);",
|
|
111
|
-
);
|
|
112
|
-
db.run(
|
|
113
|
-
"CREATE TABLE IF NOT EXISTS access_keys (id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), key_hash TEXT NOT NULL, name TEXT DEFAULT 'default', user_id TEXT DEFAULT 'default', created_at TEXT DEFAULT (datetime('now')), last_used_at TEXT, is_active INTEGER DEFAULT 1);",
|
|
114
|
-
);
|
|
115
|
-
});
|
|
116
|
-
db.close();
|
|
117
|
-
} catch (e) {
|
|
118
|
-
console.error("Failed to initialize OpenMemory SDK:", e);
|
|
80
|
+
// --- INITIALIZATION ---
|
|
81
|
+
async function initialize() {
|
|
82
|
+
const dbPath = process.env.OM_DB_PATH;
|
|
83
|
+
if (!dbPath) {
|
|
119
84
|
console.error(
|
|
120
|
-
"[
|
|
121
|
-
"Check OM_DB_PATH and ensure sqlite3 native bindings are installed.",
|
|
85
|
+
"[INIT] Environment variable OM_DB_PATH is not set. Please configure OM_DB_PATH to point to the OpenMemory database file (e.g., /path/to/openmemory.db).",
|
|
122
86
|
);
|
|
123
87
|
process.exit(1);
|
|
124
88
|
}
|
|
125
89
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const initLoggingDb = async () => {
|
|
129
|
-
if (loggingDb) return loggingDb;
|
|
130
|
-
const dbPath = process.env.OM_DB_PATH!;
|
|
131
|
-
const sqlite3 = await import("sqlite3");
|
|
132
|
-
loggingDb = new sqlite3.default.Database(dbPath);
|
|
133
|
-
loggingDb.configure("busyTimeout", 10000);
|
|
134
|
-
return new Promise((resolve) => {
|
|
135
|
-
loggingDb.serialize(() => {
|
|
136
|
-
loggingDb.run("PRAGMA journal_mode=WAL;");
|
|
137
|
-
loggingDb.run("PRAGMA synchronous=NORMAL;", () => resolve(loggingDb));
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
let stdioClientName: string | undefined = undefined;
|
|
90
|
+
const dir = dirname(dbPath);
|
|
91
|
+
if (dir && !existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
143
92
|
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
2. Store new insights immediately with add_memory (STABLE data)
|
|
149
|
-
3. For corrections: use update_memory (STRUCTURAL mutation, high cost)
|
|
150
|
-
4. To prevent decay: use reinforce_memory (METABOLIC boost, low cost)
|
|
151
|
-
5. Always include tags: [topic, year, source:your-client-name]
|
|
152
|
-
For full protocol: https://docs.cybermem.dev/agent-protocol`;
|
|
153
|
-
|
|
154
|
-
const logActivity = async (
|
|
155
|
-
operation: string,
|
|
156
|
-
opts: {
|
|
157
|
-
method?: string;
|
|
158
|
-
endpoint?: string;
|
|
159
|
-
status?: number;
|
|
160
|
-
sessionId?: string;
|
|
161
|
-
} = {},
|
|
162
|
-
) => {
|
|
163
|
-
// Determine client name (priority: specific > store > default)
|
|
164
|
-
let client: string;
|
|
165
|
-
const ctx = requestContext.getStore();
|
|
166
|
-
|
|
167
|
-
if (opts.sessionId) {
|
|
168
|
-
// For SSE sessions, prefer the real client name from the request context when available
|
|
169
|
-
client = ctx?.clientName || "sse-client";
|
|
170
|
-
} else if (ctx) {
|
|
171
|
-
client = ctx.clientName || stdioClientName || "unknown";
|
|
172
|
-
} else {
|
|
173
|
-
client = stdioClientName || "unknown";
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const { method = "POST", endpoint = "/mcp", status = 200 } = opts;
|
|
177
|
-
try {
|
|
178
|
-
const db = await initLoggingDb();
|
|
179
|
-
const ts = Date.now();
|
|
180
|
-
const is_error = status >= 400 ? 1 : 0;
|
|
181
|
-
db.serialize(() => {
|
|
182
|
-
db.run(
|
|
183
|
-
"INSERT INTO cybermem_access_log (timestamp, client_name, client_version, method, endpoint, operation, status, is_error) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
184
|
-
[
|
|
185
|
-
ts,
|
|
186
|
-
client,
|
|
187
|
-
PACKAGE_VERSION,
|
|
188
|
-
method,
|
|
189
|
-
endpoint,
|
|
190
|
-
operation,
|
|
191
|
-
status.toString(),
|
|
192
|
-
is_error,
|
|
193
|
-
],
|
|
194
|
-
);
|
|
195
|
-
db.run(
|
|
196
|
-
"INSERT INTO cybermem_stats (client_name, operation, count, errors, last_updated) VALUES (?, ?, 1, ?, ?) ON CONFLICT(client_name, operation) DO UPDATE SET count = count + 1, errors = errors + ?, last_updated = ?",
|
|
197
|
-
[client, operation, is_error, ts, is_error, ts],
|
|
198
|
-
);
|
|
199
|
-
});
|
|
200
|
-
} catch {}
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
// Factory to create configured McpServer instance
|
|
204
|
-
const createConfiguredServer = (
|
|
205
|
-
onClientConnected?: (name: string) => void,
|
|
206
|
-
) => {
|
|
207
|
-
const server = new McpServer(
|
|
208
|
-
{ name: "cybermem", version: PACKAGE_VERSION },
|
|
209
|
-
{
|
|
210
|
-
instructions: CYBERMEM_INSTRUCTIONS,
|
|
211
|
-
},
|
|
212
|
-
);
|
|
213
|
-
|
|
214
|
-
// access underlying server to set internal state for direct memory access
|
|
215
|
-
// Casting to unknown first to allow adding private property
|
|
216
|
-
(server as unknown as { _memoryReady: boolean })._memoryReady = true;
|
|
217
|
-
|
|
218
|
-
server.registerResource(
|
|
219
|
-
"CyberMem Agent Protocol",
|
|
220
|
-
"cybermem://protocol",
|
|
221
|
-
{ description: "Instructions for AI agents", mimeType: "text/plain" },
|
|
222
|
-
async () => ({
|
|
223
|
-
contents: [
|
|
224
|
-
{
|
|
225
|
-
uri: "cybermem://protocol",
|
|
226
|
-
mimeType: "text/plain",
|
|
227
|
-
text: CYBERMEM_INSTRUCTIONS,
|
|
228
|
-
},
|
|
229
|
-
],
|
|
230
|
-
}),
|
|
93
|
+
// Migrations
|
|
94
|
+
try {
|
|
95
|
+
await run_async(
|
|
96
|
+
"CREATE TABLE IF NOT EXISTS cybermem_stats (id INTEGER PRIMARY KEY AUTOINCREMENT, client_name TEXT NOT NULL, tool TEXT NOT NULL, count INTEGER DEFAULT 0, errors INTEGER DEFAULT 0, last_updated INTEGER NOT NULL, UNIQUE(client_name, tool));",
|
|
231
97
|
);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
server.server.setRequestHandler(
|
|
235
|
-
InitializeRequestSchema,
|
|
236
|
-
async (request) => {
|
|
237
|
-
// For SSE multiple clients, stdioClientName global is less useful,
|
|
238
|
-
// but we can set it for context if running in single-user mode.
|
|
239
|
-
// For multi-user, rely on requestContext.
|
|
240
|
-
// For SSE multiple clients, rely on per-request context instead of a global.
|
|
241
|
-
const clientName = request.params.clientInfo.name;
|
|
242
|
-
if (onClientConnected) {
|
|
243
|
-
onClientConnected(clientName);
|
|
244
|
-
} else {
|
|
245
|
-
stdioClientName = clientName;
|
|
246
|
-
}
|
|
247
|
-
console.error(`[MCP] Client identified via handshake: ${clientName}`);
|
|
248
|
-
return {
|
|
249
|
-
protocolVersion: "2024-11-05",
|
|
250
|
-
capabilities: {
|
|
251
|
-
tools: { listChanged: true },
|
|
252
|
-
resources: { subscribe: true },
|
|
253
|
-
},
|
|
254
|
-
serverInfo: {
|
|
255
|
-
name: "cybermem",
|
|
256
|
-
version: PACKAGE_VERSION,
|
|
257
|
-
},
|
|
258
|
-
};
|
|
259
|
-
},
|
|
98
|
+
await run_async(
|
|
99
|
+
"CREATE TABLE IF NOT EXISTS cybermem_access_log (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER NOT NULL, client_name TEXT NOT NULL, client_version TEXT, method TEXT NOT NULL, endpoint TEXT NOT NULL, tool TEXT NOT NULL, status TEXT NOT NULL, is_error INTEGER DEFAULT 0);",
|
|
260
100
|
);
|
|
261
101
|
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
"
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const res = await memory!.add(args.content, { tags: args.tags });
|
|
276
|
-
await logActivity("create", {
|
|
277
|
-
method: "POST",
|
|
278
|
-
endpoint: "/memory/add",
|
|
279
|
-
status: 200,
|
|
280
|
-
});
|
|
281
|
-
return { content: [{ type: "text", text: JSON.stringify(res) }] };
|
|
282
|
-
},
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
server.registerTool(
|
|
286
|
-
"query_memory",
|
|
287
|
-
{
|
|
288
|
-
description: "Search memories.",
|
|
289
|
-
inputSchema: z.object({ query: z.string(), k: z.number().default(5) }),
|
|
290
|
-
},
|
|
291
|
-
async (args: { query: string; k?: number }) => {
|
|
292
|
-
const res = await memory!.search(args.query, { limit: args.k });
|
|
293
|
-
await logActivity("read", {
|
|
294
|
-
method: "POST",
|
|
295
|
-
endpoint: "/memory/query",
|
|
296
|
-
status: 200,
|
|
297
|
-
});
|
|
298
|
-
return { content: [{ type: "text", text: JSON.stringify(res) }] };
|
|
299
|
-
},
|
|
300
|
-
);
|
|
102
|
+
// Robustly check for and rename 'operation' to 'tool'
|
|
103
|
+
const statsInfo = (await all_async(
|
|
104
|
+
"PRAGMA table_info(cybermem_stats);",
|
|
105
|
+
)) as any[];
|
|
106
|
+
if (statsInfo.some((col: any) => col.name === "operation")) {
|
|
107
|
+
await run_async(
|
|
108
|
+
"ALTER TABLE cybermem_stats RENAME COLUMN operation TO tool;",
|
|
109
|
+
);
|
|
110
|
+
} else if (!statsInfo.some((col: any) => col.name === "tool")) {
|
|
111
|
+
await run_async(
|
|
112
|
+
"ALTER TABLE cybermem_stats ADD COLUMN tool TEXT DEFAULT 'unknown';",
|
|
113
|
+
);
|
|
114
|
+
}
|
|
301
115
|
|
|
302
|
-
|
|
303
|
-
"
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
if (!sdk_update_memory) throw new Error("Update not available in SDK");
|
|
315
|
-
if (args.content === undefined && args.tags === undefined) {
|
|
316
|
-
throw new Error(
|
|
317
|
-
"At least one of 'content' or 'tags' must be provided to update_memory",
|
|
318
|
-
);
|
|
319
|
-
}
|
|
320
|
-
const res = await sdk_update_memory(args.id, args.content, args.tags);
|
|
321
|
-
await logActivity("update", {
|
|
322
|
-
method: "PATCH",
|
|
323
|
-
endpoint: `/memory/${args.id}`,
|
|
324
|
-
status: 200,
|
|
325
|
-
});
|
|
326
|
-
return { content: [{ type: "text", text: JSON.stringify(res) }] };
|
|
327
|
-
},
|
|
328
|
-
);
|
|
116
|
+
const logInfo = (await all_async(
|
|
117
|
+
"PRAGMA table_info(cybermem_access_log);",
|
|
118
|
+
)) as any[];
|
|
119
|
+
if (logInfo.some((col: any) => col.name === "operation")) {
|
|
120
|
+
await run_async(
|
|
121
|
+
"ALTER TABLE cybermem_access_log RENAME COLUMN operation TO tool;",
|
|
122
|
+
);
|
|
123
|
+
} else if (!logInfo.some((col: any) => col.name === "tool")) {
|
|
124
|
+
await run_async(
|
|
125
|
+
"ALTER TABLE cybermem_access_log ADD COLUMN tool TEXT DEFAULT 'unknown';",
|
|
126
|
+
);
|
|
127
|
+
}
|
|
329
128
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
description:
|
|
334
|
-
"Metabolic boost (salience). LOW COST: prevents decay without mutation. Use for active topics.",
|
|
335
|
-
inputSchema: z.object({
|
|
336
|
-
id: z.string(),
|
|
337
|
-
boost: z.number().default(0.1),
|
|
338
|
-
}),
|
|
339
|
-
},
|
|
340
|
-
async (args: { id: string; boost?: number }) => {
|
|
341
|
-
if (!sdk_reinforce_memory)
|
|
342
|
-
throw new Error("Reinforce not available in SDK");
|
|
343
|
-
const res = await sdk_reinforce_memory(args.id, args.boost);
|
|
344
|
-
await logActivity("update", {
|
|
345
|
-
method: "POST",
|
|
346
|
-
endpoint: `/memory/${args.id}/reinforce`,
|
|
347
|
-
status: 200,
|
|
348
|
-
});
|
|
349
|
-
return { content: [{ type: "text", text: JSON.stringify(res) }] };
|
|
350
|
-
},
|
|
129
|
+
// Backfill NULL tool values for SQLite safety
|
|
130
|
+
await run_async(
|
|
131
|
+
"UPDATE cybermem_access_log SET tool = 'unknown' WHERE tool IS NULL;",
|
|
351
132
|
);
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
"delete_memory",
|
|
355
|
-
{
|
|
356
|
-
description: "Delete memory",
|
|
357
|
-
inputSchema: z.object({ id: z.string() }),
|
|
358
|
-
},
|
|
359
|
-
async (args: { id: string }) => {
|
|
360
|
-
const dbPath = process.env.OM_DB_PATH!;
|
|
361
|
-
const sqlite3 = await import("sqlite3");
|
|
362
|
-
const db = new sqlite3.default.Database(dbPath);
|
|
363
|
-
return new Promise((resolve, reject) => {
|
|
364
|
-
db.serialize(() => {
|
|
365
|
-
db.run("DELETE FROM memories WHERE id = ?", [args.id]);
|
|
366
|
-
db.run(
|
|
367
|
-
"DELETE FROM vectors WHERE id = ?",
|
|
368
|
-
[args.id],
|
|
369
|
-
async (err: Error | null) => {
|
|
370
|
-
db.close();
|
|
371
|
-
await logActivity("delete", {
|
|
372
|
-
method: "DELETE",
|
|
373
|
-
endpoint: `/memory/${args.id}`,
|
|
374
|
-
status: err ? 500 : 200,
|
|
375
|
-
});
|
|
376
|
-
if (err) reject(err);
|
|
377
|
-
else resolve({ content: [{ type: "text", text: "Deleted" }] });
|
|
378
|
-
},
|
|
379
|
-
);
|
|
380
|
-
});
|
|
381
|
-
});
|
|
382
|
-
},
|
|
133
|
+
await run_async(
|
|
134
|
+
"UPDATE cybermem_stats SET tool = 'unknown' WHERE tool IS NULL;",
|
|
383
135
|
);
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
// EXPRESS SERVER
|
|
389
|
-
// HTTP server mode for Docker/Traefik deployment
|
|
390
|
-
const useHttp = args.includes("--http") || args.includes("--port");
|
|
391
|
-
if (useHttp) {
|
|
392
|
-
const port = parseInt(getArg("--port") || "3100", 10);
|
|
393
|
-
const app = express();
|
|
394
|
-
app.use(cors());
|
|
395
|
-
app.use((req, res, next) => {
|
|
396
|
-
// Skip JSON parsing for SSE message endpoint - it needs raw body stream
|
|
397
|
-
// Use req.url to handle query params like /message?sessionId=...
|
|
398
|
-
if (req.url.startsWith("/message")) {
|
|
399
|
-
return next();
|
|
400
|
-
}
|
|
401
|
-
express.json()(req, res, next);
|
|
402
|
-
});
|
|
403
|
-
app.get("/health", (req, res) =>
|
|
404
|
-
res.json({ ok: true, version: PACKAGE_VERSION }),
|
|
136
|
+
} catch (e: any) {
|
|
137
|
+
console.error(
|
|
138
|
+
"[INIT] Migration Error: Failed to apply database migrations:",
|
|
139
|
+
e?.message ?? e,
|
|
405
140
|
);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
406
144
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
145
|
+
// --- SERVER SETUP ---
|
|
146
|
+
interface AuthContext {
|
|
147
|
+
clientName: string;
|
|
148
|
+
[key: string]: unknown;
|
|
149
|
+
}
|
|
412
150
|
|
|
413
|
-
|
|
414
|
-
|
|
151
|
+
const server = new FastMCP<AuthContext>({
|
|
152
|
+
name: "cybermem",
|
|
153
|
+
version: VALID_VERSION,
|
|
154
|
+
instructions: CYBERMEM_INSTRUCTIONS,
|
|
155
|
+
health: { enabled: true, path: "/health" },
|
|
156
|
+
authenticate: async (req) => {
|
|
157
|
+
const clientName = (req.headers["x-client-name"] ||
|
|
158
|
+
req.headers["X-Client-Name"] ||
|
|
159
|
+
"unknown") as string;
|
|
160
|
+
// Extract versioned naming if present (e.g. "antigravity/v1.0.0" -> "antigravity")
|
|
161
|
+
return { clientName: clientName.split("/")[0] };
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const memory = new Memory() as IMemory;
|
|
166
|
+
|
|
167
|
+
const app = server.getApp();
|
|
168
|
+
// Keep Hono middleware for custom routes if any, though FastMCP transport bypasses it
|
|
169
|
+
app.use("*", async (c, next) => {
|
|
170
|
+
const clientName = (
|
|
171
|
+
c.req.header("X-Client-Name") ||
|
|
172
|
+
c.req.header("x-client-name") ||
|
|
173
|
+
"unknown"
|
|
174
|
+
).split("/")[0];
|
|
175
|
+
return requestContext.run({ clientName }, next);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// TOOLS
|
|
179
|
+
server.addTool({
|
|
180
|
+
name: "add_memory",
|
|
181
|
+
description: "Store a new memory with optional tags for semantic retrieval.",
|
|
182
|
+
parameters: z.object({
|
|
183
|
+
content: z.string().describe("The text content of the memory"),
|
|
184
|
+
tags: z.array(z.string()).optional().describe("Category tags"),
|
|
185
|
+
}),
|
|
186
|
+
execute: async (args, context) => {
|
|
187
|
+
return requestContext.run(
|
|
188
|
+
{ clientName: context.session?.clientName },
|
|
189
|
+
async () => {
|
|
415
190
|
try {
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
await logActivity("
|
|
421
|
-
|
|
422
|
-
endpoint: "/add",
|
|
423
|
-
status: 200,
|
|
424
|
-
});
|
|
425
|
-
res.json(result);
|
|
426
|
-
} catch (e: any) {
|
|
427
|
-
res.status(500).json({ error: e.message });
|
|
191
|
+
const res = await memory.add(args.content, { tags: args.tags });
|
|
192
|
+
await logActivity("add_memory");
|
|
193
|
+
return JSON.stringify(res);
|
|
194
|
+
} catch (err: any) {
|
|
195
|
+
await logActivity("add_memory", 500);
|
|
196
|
+
throw err;
|
|
428
197
|
}
|
|
429
|
-
}
|
|
430
|
-
|
|
198
|
+
},
|
|
199
|
+
);
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
server.addTool({
|
|
204
|
+
name: "query_memory",
|
|
205
|
+
description: "Retrieve relevant memories using semantic search.",
|
|
206
|
+
parameters: z.object({
|
|
207
|
+
query: z.string().describe("Search query string"),
|
|
208
|
+
k: z.number().default(5).describe("Number of results"),
|
|
209
|
+
}),
|
|
210
|
+
execute: async (args, context) => {
|
|
211
|
+
return requestContext.run(
|
|
212
|
+
{ clientName: context.session?.clientName },
|
|
213
|
+
async () => {
|
|
431
214
|
try {
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
status: 200,
|
|
439
|
-
});
|
|
440
|
-
res.json(result);
|
|
441
|
-
} catch (e: any) {
|
|
442
|
-
res.status(500).json({ error: e.message });
|
|
215
|
+
const res = await memory.search(args.query, { limit: args.k });
|
|
216
|
+
await logActivity("query_memory");
|
|
217
|
+
return JSON.stringify(res);
|
|
218
|
+
} catch (err: any) {
|
|
219
|
+
await logActivity("query_memory", 500);
|
|
220
|
+
throw err;
|
|
443
221
|
}
|
|
444
|
-
}
|
|
445
|
-
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
server.addTool({
|
|
228
|
+
name: "update_memory",
|
|
229
|
+
description:
|
|
230
|
+
"Update an existing memory's content or tags. At least one must be provided.",
|
|
231
|
+
parameters: z
|
|
232
|
+
.object({
|
|
233
|
+
id: z.string().describe("Memory ID"),
|
|
234
|
+
content: z.string().optional().describe("New content"),
|
|
235
|
+
tags: z.array(z.string()).optional().describe("New tags"),
|
|
236
|
+
})
|
|
237
|
+
.refine((data) => data.content !== undefined || data.tags !== undefined, {
|
|
238
|
+
message: "Either content or tags must be provided for update",
|
|
239
|
+
path: ["content"],
|
|
240
|
+
}),
|
|
241
|
+
execute: async (args, context) => {
|
|
242
|
+
return requestContext.run(
|
|
243
|
+
{ clientName: context.session?.clientName },
|
|
244
|
+
async () => {
|
|
446
245
|
try {
|
|
447
|
-
const
|
|
448
|
-
await logActivity("
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
res.json(result);
|
|
454
|
-
} catch (e: any) {
|
|
455
|
-
res.status(500).json({ error: e.message });
|
|
246
|
+
const res = await update_memory(args.id, args.content, args.tags);
|
|
247
|
+
await logActivity("update_memory");
|
|
248
|
+
return JSON.stringify(res);
|
|
249
|
+
} catch (err: any) {
|
|
250
|
+
await logActivity("update_memory", 500);
|
|
251
|
+
throw err;
|
|
456
252
|
}
|
|
457
|
-
}
|
|
458
|
-
|
|
253
|
+
},
|
|
254
|
+
);
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
server.addTool({
|
|
259
|
+
name: "reinforce_memory",
|
|
260
|
+
description: "Boost a memory's relevance score to prevent decay.",
|
|
261
|
+
parameters: z.object({
|
|
262
|
+
id: z.string().describe("Memory ID"),
|
|
263
|
+
boost: z
|
|
264
|
+
.number()
|
|
265
|
+
.default(0.1)
|
|
266
|
+
.describe("Relevance boost amount (0.0 to 1.0)"),
|
|
267
|
+
}),
|
|
268
|
+
execute: async (args, context) => {
|
|
269
|
+
return requestContext.run(
|
|
270
|
+
{ clientName: context.session?.clientName },
|
|
271
|
+
async () => {
|
|
459
272
|
try {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
await logActivity("update", {
|
|
467
|
-
method: "PATCH",
|
|
468
|
-
endpoint: `/memory/${req.params.id}`,
|
|
469
|
-
status: 200,
|
|
470
|
-
});
|
|
471
|
-
res.json(result);
|
|
472
|
-
} catch (e: any) {
|
|
473
|
-
res.status(500).json({ error: e.message });
|
|
273
|
+
await reinforce_memory(args.id, args.boost);
|
|
274
|
+
await logActivity("reinforce_memory");
|
|
275
|
+
return `Memory reinforced: ${args.id}`;
|
|
276
|
+
} catch (err: any) {
|
|
277
|
+
await logActivity("reinforce_memory", 500);
|
|
278
|
+
throw err;
|
|
474
279
|
}
|
|
475
|
-
}
|
|
476
|
-
|
|
280
|
+
},
|
|
281
|
+
);
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
server.addTool({
|
|
286
|
+
name: "delete_memory",
|
|
287
|
+
description: "Permanently delete a memory and its associated vectors.",
|
|
288
|
+
parameters: z.object({
|
|
289
|
+
id: z.string().describe("Memory ID"),
|
|
290
|
+
}),
|
|
291
|
+
execute: async (args, context) => {
|
|
292
|
+
return requestContext.run(
|
|
293
|
+
{ clientName: context.session?.clientName },
|
|
294
|
+
async () => {
|
|
477
295
|
try {
|
|
478
|
-
await
|
|
479
|
-
await
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
} catch (e: any) {
|
|
486
|
-
res.status(500).json({ error: e.message });
|
|
296
|
+
await run_async("DELETE FROM memories WHERE id=?", [args.id]);
|
|
297
|
+
await run_async("DELETE FROM vectors WHERE id=?", [args.id]);
|
|
298
|
+
await logActivity("delete_memory");
|
|
299
|
+
return "Deleted";
|
|
300
|
+
} catch (err: any) {
|
|
301
|
+
await logActivity("delete_memory", 500);
|
|
302
|
+
throw err;
|
|
487
303
|
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
const db = new sqlite3.default.Database(dbPath);
|
|
493
|
-
db.run(
|
|
494
|
-
"DELETE FROM memories WHERE id = ?",
|
|
495
|
-
[req.params.id],
|
|
496
|
-
async () => {
|
|
497
|
-
db.close();
|
|
498
|
-
await logActivity("delete", {
|
|
499
|
-
method: "DELETE",
|
|
500
|
-
endpoint: `/memory/${req.params.id}`,
|
|
501
|
-
status: 200,
|
|
502
|
-
});
|
|
503
|
-
res.json({ ok: true });
|
|
504
|
-
},
|
|
505
|
-
);
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// MULTI-SESSION SSE SUPPORT
|
|
510
|
-
const sessions = new Map<
|
|
511
|
-
string,
|
|
512
|
-
{
|
|
513
|
-
server: McpServer;
|
|
514
|
-
transport: SSEServerTransport;
|
|
515
|
-
clientName?: string;
|
|
516
|
-
}
|
|
517
|
-
>();
|
|
518
|
-
|
|
519
|
-
// Legacy MCP endpoint - 410 Gone
|
|
520
|
-
app.all("/mcp", (req, res) => {
|
|
521
|
-
res
|
|
522
|
-
.status(410)
|
|
523
|
-
.send(
|
|
524
|
-
"Endpoint /mcp is deprecated. Please update your client configuration to use /sse for Server-Sent Events.",
|
|
525
|
-
);
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
app.get("/sse", async (req, res) => {
|
|
529
|
-
console.error("[MCP] Attempting SSE Connection...");
|
|
530
|
-
const transport = new SSEServerTransport("/message", res);
|
|
531
|
-
const newServer = createConfiguredServer((name) => {
|
|
532
|
-
const session = sessions.get(transport.sessionId);
|
|
533
|
-
if (session) session.clientName = name;
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
try {
|
|
537
|
-
await newServer.connect(transport);
|
|
538
|
-
sessions.set(transport.sessionId, { server: newServer, transport });
|
|
304
|
+
},
|
|
305
|
+
);
|
|
306
|
+
},
|
|
307
|
+
});
|
|
539
308
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
console.error(
|
|
546
|
-
`[MCP] SSE Connection Error: ${transport.sessionId}`,
|
|
547
|
-
err,
|
|
548
|
-
);
|
|
549
|
-
sessions.delete(transport.sessionId);
|
|
550
|
-
};
|
|
309
|
+
// START
|
|
310
|
+
async function main() {
|
|
311
|
+
console.error("[INIT] Starting CyberMem MCP...");
|
|
312
|
+
await initialize();
|
|
313
|
+
console.error("[INIT] Database initialized.");
|
|
551
314
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
if (!res.headersSent) {
|
|
558
|
-
res.status(500).send("Internal Server Error during SSE handshake");
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
});
|
|
315
|
+
const argsArr = process.argv.slice(2);
|
|
316
|
+
const getArg = (name: string) => {
|
|
317
|
+
const idx = argsArr.indexOf(name);
|
|
318
|
+
return idx !== -1 ? argsArr[idx + 1] : undefined;
|
|
319
|
+
};
|
|
562
320
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
console.error(
|
|
577
|
-
`[MCP] Error handling message for session ${sessionId}:`,
|
|
578
|
-
err,
|
|
579
|
-
);
|
|
580
|
-
if (!res.headersSent) {
|
|
581
|
-
res.status(500).send("Internal Server Error processing message");
|
|
321
|
+
const port = parseInt(getArg("--port") || "3100", 10);
|
|
322
|
+
const useHttp = argsArr.includes("--http") || argsArr.includes("--port");
|
|
323
|
+
|
|
324
|
+
console.error(`[INIT] Starting ${useHttp ? "HTTP" : "STDIO"} server...`);
|
|
325
|
+
await server.start({
|
|
326
|
+
transportType: useHttp ? "httpStream" : "stdio",
|
|
327
|
+
httpStream: useHttp
|
|
328
|
+
? {
|
|
329
|
+
port,
|
|
330
|
+
host: "0.0.0.0",
|
|
331
|
+
endpoint: "/mcp",
|
|
332
|
+
stateless: false,
|
|
333
|
+
enableJsonResponse: true,
|
|
582
334
|
}
|
|
583
|
-
|
|
584
|
-
|
|
335
|
+
: undefined,
|
|
336
|
+
});
|
|
585
337
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
);
|
|
338
|
+
if (useHttp) {
|
|
339
|
+
console.error(`CyberMem MCP running on http://localhost:${port}/mcp`);
|
|
589
340
|
} else {
|
|
590
|
-
|
|
591
|
-
const transport = new StdioServerTransport();
|
|
592
|
-
const server = createConfiguredServer();
|
|
593
|
-
server
|
|
594
|
-
.connect(transport)
|
|
595
|
-
.then(() => console.error("CyberMem MCP connected via STDIO"));
|
|
341
|
+
console.error(`CyberMem MCP ${VALID_VERSION} [STDIO]`);
|
|
596
342
|
}
|
|
597
343
|
}
|
|
344
|
+
|
|
345
|
+
main().catch((err) => {
|
|
346
|
+
console.error("[CRITICAL] Main Failure:", err);
|
|
347
|
+
process.exit(1);
|
|
348
|
+
});
|