@cybermem/mcp 0.14.15 → 0.16.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 +240 -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/stdio_attribution.spec.ts +74 -0
- package/e2e/utils/FastMCPHandshakeTransport.ts +183 -0
- package/package.json +6 -9
- package/scripts/postbuild.js +24 -0
- package/src/index.ts +317 -542
- package/src/openmemory-js.d.ts +6 -0
- package/test-handshake.ts +26 -0
package/src/index.ts
CHANGED
|
@@ -1,597 +1,372 @@
|
|
|
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
|
-
}
|
|
36
|
+
} catch {}
|
|
53
37
|
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
};
|
|
38
|
+
const VALID_VERSION = (
|
|
39
|
+
PACKAGE_VERSION.match(/^\d+\.\d+\.\d+$/) ? PACKAGE_VERSION : "0.0.0"
|
|
40
|
+
) as `${number}.${number}.${number}`;
|
|
62
41
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
let sdk_reinforce_memory:
|
|
82
|
-
| ((id: string, boost?: number) => Promise<unknown>)
|
|
83
|
-
| null = null;
|
|
84
|
-
|
|
85
|
-
// LOCAL SDK MODE
|
|
86
|
-
const dbPath = process.env.OM_DB_PATH!;
|
|
87
|
-
const fs = await import("fs");
|
|
88
|
-
const path = await import("path");
|
|
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) => {
|
|
89
60
|
try {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
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
|
+
};
|
|
93
79
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
sdk_reinforce_memory = hsg.reinforce_memory;
|
|
99
|
-
memory = new Memory() as IMemory;
|
|
100
|
-
|
|
101
|
-
// Initialize Tables
|
|
102
|
-
const sqlite3 = await import("sqlite3");
|
|
103
|
-
const db = new sqlite3.default.Database(dbPath);
|
|
104
|
-
db.configure("busyTimeout", 5000);
|
|
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
|
-
};
|
|
90
|
+
const dir = dirname(dbPath);
|
|
91
|
+
if (dir && !existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
141
92
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
PROTOCOL:
|
|
147
|
-
1. On session start: call query_memory("user context profile")
|
|
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
|
-
},
|
|
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));",
|
|
212
97
|
);
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
}),
|
|
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);",
|
|
231
100
|
);
|
|
232
101
|
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
},
|
|
260
|
-
);
|
|
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
|
+
}
|
|
261
115
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
async (args: { content: string; tags?: string[] }) => {
|
|
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
|
-
);
|
|
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
|
+
}
|
|
284
128
|
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
},
|
|
129
|
+
// Backfill NULL tool values for SQLite safety
|
|
130
|
+
await run_async(
|
|
131
|
+
"UPDATE cybermem_access_log SET tool = 'unknown' WHERE tool IS NULL;",
|
|
300
132
|
);
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
"update_memory",
|
|
304
|
-
{
|
|
305
|
-
description:
|
|
306
|
-
"Mutate existing memory (content/tags). HIGH COST: re-embeds and re-links. Use for corrections.",
|
|
307
|
-
inputSchema: z.object({
|
|
308
|
-
id: z.string(),
|
|
309
|
-
content: z.string().optional(),
|
|
310
|
-
tags: z.array(z.string()).optional(),
|
|
311
|
-
}),
|
|
312
|
-
},
|
|
313
|
-
async (args: { id: string; content?: string; tags?: string[] }) => {
|
|
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
|
-
},
|
|
133
|
+
await run_async(
|
|
134
|
+
"UPDATE cybermem_stats SET tool = 'unknown' WHERE tool IS NULL;",
|
|
328
135
|
);
|
|
329
|
-
|
|
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
|
-
},
|
|
136
|
+
} catch (e: any) {
|
|
137
|
+
console.error(
|
|
138
|
+
"[INIT] Migration Error: Failed to apply database migrations:",
|
|
139
|
+
e?.message ?? e,
|
|
351
140
|
);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
352
144
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
},
|
|
383
|
-
);
|
|
145
|
+
// --- SERVER SETUP ---
|
|
146
|
+
interface AuthContext {
|
|
147
|
+
clientName: string;
|
|
148
|
+
[key: string]: unknown;
|
|
149
|
+
}
|
|
384
150
|
|
|
385
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Extended context for MCP tools to handle STDIO client attribution.
|
|
153
|
+
*/
|
|
154
|
+
interface ToolContext {
|
|
155
|
+
session?: AuthContext;
|
|
156
|
+
client?: {
|
|
157
|
+
version: {
|
|
158
|
+
name: string;
|
|
159
|
+
};
|
|
386
160
|
};
|
|
161
|
+
}
|
|
387
162
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
express.json()(req, res, next);
|
|
402
|
-
});
|
|
403
|
-
app.get("/health", (req, res) =>
|
|
404
|
-
res.json({ ok: true, version: PACKAGE_VERSION }),
|
|
405
|
-
);
|
|
406
|
-
|
|
407
|
-
app.use((req, res, next) => {
|
|
408
|
-
const clientName =
|
|
409
|
-
(req.headers["x-client-name"] as string) || "antigravity-client";
|
|
410
|
-
requestContext.run({ clientName }, next);
|
|
411
|
-
});
|
|
163
|
+
/**
|
|
164
|
+
* Derives the client name from the tool execution context.
|
|
165
|
+
* Falls back to handshake client name if session is 'stdio'.
|
|
166
|
+
*/
|
|
167
|
+
function getClientName(context: any): string {
|
|
168
|
+
const ctx = context as ToolContext;
|
|
169
|
+
const sessionName = ctx.session?.clientName;
|
|
170
|
+
if (sessionName === "stdio" && ctx.client?.version?.name) {
|
|
171
|
+
return ctx.client.version.name;
|
|
172
|
+
}
|
|
173
|
+
return sessionName || "unknown";
|
|
174
|
+
}
|
|
412
175
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
endpoint: "/add",
|
|
423
|
-
status: 200,
|
|
424
|
-
});
|
|
425
|
-
res.json(result);
|
|
426
|
-
} catch (e: any) {
|
|
427
|
-
res.status(500).json({ error: e.message });
|
|
428
|
-
}
|
|
429
|
-
});
|
|
430
|
-
app.post("/query", async (req, res) => {
|
|
431
|
-
try {
|
|
432
|
-
const result = await memory!.search(req.body.query || "", {
|
|
433
|
-
limit: req.body.k || 5,
|
|
434
|
-
});
|
|
435
|
-
await logActivity("read", {
|
|
436
|
-
method: "POST",
|
|
437
|
-
endpoint: "/query",
|
|
438
|
-
status: 200,
|
|
439
|
-
});
|
|
440
|
-
res.json(result);
|
|
441
|
-
} catch (e: any) {
|
|
442
|
-
res.status(500).json({ error: e.message });
|
|
443
|
-
}
|
|
444
|
-
});
|
|
445
|
-
app.get("/all", async (req, res) => {
|
|
446
|
-
try {
|
|
447
|
-
const result = await memory!.search("", { limit: 10 });
|
|
448
|
-
await logActivity("read", {
|
|
449
|
-
method: "GET",
|
|
450
|
-
endpoint: "/all",
|
|
451
|
-
status: 200,
|
|
452
|
-
});
|
|
453
|
-
res.json(result);
|
|
454
|
-
} catch (e: any) {
|
|
455
|
-
res.status(500).json({ error: e.message });
|
|
456
|
-
}
|
|
457
|
-
});
|
|
458
|
-
app.patch("/memory/:id", async (req, res) => {
|
|
459
|
-
try {
|
|
460
|
-
const result = await sdk_update_memory(
|
|
461
|
-
req.params.id,
|
|
462
|
-
req.body.content,
|
|
463
|
-
req.body.tags,
|
|
464
|
-
req.body.metadata,
|
|
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 });
|
|
474
|
-
}
|
|
475
|
-
});
|
|
476
|
-
app.post("/memory/:id/reinforce", async (req, res) => {
|
|
477
|
-
try {
|
|
478
|
-
await sdk_reinforce_memory(req.params.id, req.body.boost);
|
|
479
|
-
await logActivity("update", {
|
|
480
|
-
method: "POST",
|
|
481
|
-
endpoint: `/memory/${req.params.id}/reinforce`,
|
|
482
|
-
status: 200,
|
|
483
|
-
});
|
|
484
|
-
res.json({ ok: true });
|
|
485
|
-
} catch (e: any) {
|
|
486
|
-
res.status(500).json({ error: e.message });
|
|
487
|
-
}
|
|
488
|
-
});
|
|
489
|
-
app.delete("/memory/:id", async (req, res) => {
|
|
490
|
-
const dbPath = process.env.OM_DB_PATH!;
|
|
491
|
-
const sqlite3 = await import("sqlite3");
|
|
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
|
-
});
|
|
176
|
+
const server = new FastMCP<AuthContext>({
|
|
177
|
+
name: "cybermem",
|
|
178
|
+
version: VALID_VERSION,
|
|
179
|
+
instructions: CYBERMEM_INSTRUCTIONS,
|
|
180
|
+
health: { enabled: true, path: "/health" },
|
|
181
|
+
authenticate: async (req) => {
|
|
182
|
+
// STDIO transport doesn't provide an HTTP request object
|
|
183
|
+
if (!req?.headers) {
|
|
184
|
+
return { clientName: "stdio" };
|
|
507
185
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
186
|
+
const clientName = (req.headers["x-client-name"] ||
|
|
187
|
+
req.headers["X-Client-Name"] ||
|
|
188
|
+
"unknown") as string;
|
|
189
|
+
// Extract versioned naming if present (e.g. "antigravity/v1.0.0" -> "antigravity")
|
|
190
|
+
return { clientName: clientName.split("/")[0] };
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const memory = new Memory() as IMemory;
|
|
195
|
+
|
|
196
|
+
const app = server.getApp();
|
|
197
|
+
// Keep Hono middleware for custom routes if any, though FastMCP transport bypasses it
|
|
198
|
+
app.use("*", async (c, next) => {
|
|
199
|
+
const clientName = (
|
|
200
|
+
c.req.header("X-Client-Name") ||
|
|
201
|
+
c.req.header("x-client-name") ||
|
|
202
|
+
"unknown"
|
|
203
|
+
).split("/")[0];
|
|
204
|
+
return requestContext.run({ clientName }, next);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// TOOLS
|
|
208
|
+
server.addTool({
|
|
209
|
+
name: "add_memory",
|
|
210
|
+
description: "Store a new memory with optional tags for semantic retrieval.",
|
|
211
|
+
parameters: z.object({
|
|
212
|
+
content: z.string().describe("The text content of the memory"),
|
|
213
|
+
tags: z.array(z.string()).optional().describe("Category tags"),
|
|
214
|
+
}),
|
|
215
|
+
execute: async (args, context) => {
|
|
216
|
+
const clientName = getClientName(context);
|
|
217
|
+
|
|
218
|
+
return requestContext.run({ clientName }, async () => {
|
|
219
|
+
try {
|
|
220
|
+
const res = await memory.add(args.content, { tags: args.tags });
|
|
221
|
+
await logActivity("add_memory");
|
|
222
|
+
return JSON.stringify(res);
|
|
223
|
+
} catch (err: any) {
|
|
224
|
+
await logActivity("add_memory", 500);
|
|
225
|
+
throw err;
|
|
516
226
|
}
|
|
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
227
|
});
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
server.addTool({
|
|
232
|
+
name: "query_memory",
|
|
233
|
+
description: "Retrieve relevant memories using semantic search.",
|
|
234
|
+
parameters: z.object({
|
|
235
|
+
query: z.string().describe("Search query string"),
|
|
236
|
+
k: z.number().default(5).describe("Number of results"),
|
|
237
|
+
}),
|
|
238
|
+
execute: async (args, context) => {
|
|
239
|
+
const clientName = getClientName(context);
|
|
240
|
+
|
|
241
|
+
return requestContext.run({ clientName }, async () => {
|
|
536
242
|
try {
|
|
537
|
-
await
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
};
|
|
544
|
-
transport.onerror = (err: Error) => {
|
|
545
|
-
console.error(
|
|
546
|
-
`[MCP] SSE Connection Error: ${transport.sessionId}`,
|
|
547
|
-
err,
|
|
548
|
-
);
|
|
549
|
-
sessions.delete(transport.sessionId);
|
|
550
|
-
};
|
|
551
|
-
|
|
552
|
-
// await transport.start(); // FIXED: connect() starts it automatically
|
|
553
|
-
} catch (err) {
|
|
554
|
-
console.error("[MCP] Failed to start SSE transport:", err);
|
|
555
|
-
sessions.delete(transport.sessionId);
|
|
556
|
-
// If headers haven't been sent, send 500
|
|
557
|
-
if (!res.headersSent) {
|
|
558
|
-
res.status(500).send("Internal Server Error during SSE handshake");
|
|
559
|
-
}
|
|
243
|
+
const res = await memory.search(args.query, { limit: args.k });
|
|
244
|
+
await logActivity("query_memory");
|
|
245
|
+
return JSON.stringify(res);
|
|
246
|
+
} catch (err: any) {
|
|
247
|
+
await logActivity("query_memory", 500);
|
|
248
|
+
throw err;
|
|
560
249
|
}
|
|
561
250
|
});
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
server.addTool({
|
|
255
|
+
name: "update_memory",
|
|
256
|
+
description:
|
|
257
|
+
"Update an existing memory's content or tags. At least one must be provided.",
|
|
258
|
+
parameters: z
|
|
259
|
+
.object({
|
|
260
|
+
id: z.string().describe("Memory ID"),
|
|
261
|
+
content: z.string().optional().describe("New content"),
|
|
262
|
+
tags: z.array(z.string()).optional().describe("New tags"),
|
|
263
|
+
})
|
|
264
|
+
.refine((data) => data.content !== undefined || data.tags !== undefined, {
|
|
265
|
+
message: "Either content or tags must be provided for update",
|
|
266
|
+
path: ["content"],
|
|
267
|
+
}),
|
|
268
|
+
execute: async (args, context) => {
|
|
269
|
+
const clientName = getClientName(context);
|
|
270
|
+
|
|
271
|
+
return requestContext.run({ clientName }, async () => {
|
|
272
|
+
try {
|
|
273
|
+
const res = await update_memory(args.id, args.content, args.tags);
|
|
274
|
+
await logActivity("update_memory");
|
|
275
|
+
return JSON.stringify(res);
|
|
276
|
+
} catch (err: any) {
|
|
277
|
+
await logActivity("update_memory", 500);
|
|
278
|
+
throw err;
|
|
569
279
|
}
|
|
280
|
+
});
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
server.addTool({
|
|
285
|
+
name: "reinforce_memory",
|
|
286
|
+
description: "Boost a memory's relevance score to prevent decay.",
|
|
287
|
+
parameters: z.object({
|
|
288
|
+
id: z.string().describe("Memory ID"),
|
|
289
|
+
boost: z
|
|
290
|
+
.number()
|
|
291
|
+
.default(0.1)
|
|
292
|
+
.describe("Relevance boost amount (0.0 to 1.0)"),
|
|
293
|
+
}),
|
|
294
|
+
execute: async (args, context) => {
|
|
295
|
+
const clientName = getClientName(context);
|
|
296
|
+
|
|
297
|
+
return requestContext.run({ clientName }, async () => {
|
|
570
298
|
try {
|
|
571
|
-
|
|
572
|
-
await
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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");
|
|
582
|
-
}
|
|
299
|
+
await reinforce_memory(args.id, args.boost);
|
|
300
|
+
await logActivity("reinforce_memory");
|
|
301
|
+
return `Memory reinforced: ${args.id}`;
|
|
302
|
+
} catch (err: any) {
|
|
303
|
+
await logActivity("reinforce_memory", 500);
|
|
304
|
+
throw err;
|
|
583
305
|
}
|
|
584
306
|
});
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
server.addTool({
|
|
311
|
+
name: "delete_memory",
|
|
312
|
+
description: "Permanently delete a memory and its associated vectors.",
|
|
313
|
+
parameters: z.object({
|
|
314
|
+
id: z.string().describe("Memory ID"),
|
|
315
|
+
}),
|
|
316
|
+
execute: async (args, context) => {
|
|
317
|
+
const clientName = getClientName(context);
|
|
318
|
+
|
|
319
|
+
return requestContext.run({ clientName }, async () => {
|
|
320
|
+
try {
|
|
321
|
+
await run_async("DELETE FROM memories WHERE id=?", [args.id]);
|
|
322
|
+
await run_async("DELETE FROM vectors WHERE id=?", [args.id]);
|
|
323
|
+
await logActivity("delete_memory");
|
|
324
|
+
return "Deleted";
|
|
325
|
+
} catch (err: any) {
|
|
326
|
+
await logActivity("delete_memory", 500);
|
|
327
|
+
throw err;
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
},
|
|
331
|
+
});
|
|
585
332
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
333
|
+
// START
|
|
334
|
+
async function main() {
|
|
335
|
+
console.error("[INIT] Starting CyberMem MCP...");
|
|
336
|
+
await initialize();
|
|
337
|
+
console.error("[INIT] Database initialized.");
|
|
338
|
+
|
|
339
|
+
const argsArr = process.argv.slice(2);
|
|
340
|
+
const getArg = (name: string) => {
|
|
341
|
+
const idx = argsArr.indexOf(name);
|
|
342
|
+
return idx !== -1 ? argsArr[idx + 1] : undefined;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const port = parseInt(getArg("--port") || "3100", 10);
|
|
346
|
+
const useHttp = argsArr.includes("--http") || argsArr.includes("--port");
|
|
347
|
+
|
|
348
|
+
console.error(`[INIT] Starting ${useHttp ? "HTTP" : "STDIO"} server...`);
|
|
349
|
+
await server.start({
|
|
350
|
+
transportType: useHttp ? "httpStream" : "stdio",
|
|
351
|
+
httpStream: useHttp
|
|
352
|
+
? {
|
|
353
|
+
port,
|
|
354
|
+
host: "0.0.0.0",
|
|
355
|
+
endpoint: "/mcp",
|
|
356
|
+
stateless: false,
|
|
357
|
+
enableJsonResponse: true,
|
|
358
|
+
}
|
|
359
|
+
: undefined,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
if (useHttp) {
|
|
363
|
+
console.error(`CyberMem MCP running on http://localhost:${port}/mcp`);
|
|
589
364
|
} 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"));
|
|
365
|
+
console.error(`CyberMem MCP ${VALID_VERSION} [STDIO]`);
|
|
596
366
|
}
|
|
597
367
|
}
|
|
368
|
+
|
|
369
|
+
main().catch((err) => {
|
|
370
|
+
console.error("[CRITICAL] Main Failure:", err);
|
|
371
|
+
process.exit(1);
|
|
372
|
+
});
|