@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/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 cors from "cors";
10
- import express from "express";
11
- import { readFileSync } from "fs";
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
- // Type definition for OpenMemory Memory class
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
- content: string,
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
- // Async Storage for Request Context (User ID and Client Name)
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 (error) {
51
- console.error("[MCP] Failed to read package.json version", error);
52
- }
36
+ } catch {}
53
37
 
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
- };
38
+ const VALID_VERSION = (
39
+ PACKAGE_VERSION.match(/^\d+\.\d+\.\d+$/) ? PACKAGE_VERSION : "0.0.0"
40
+ ) as `${number}.${number}.${number}`;
62
41
 
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;
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 dir = path.dirname(dbPath);
91
- if (dir) fs.mkdirSync(dir, { recursive: true });
92
- } catch {}
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
- try {
95
- const { Memory } = await import("openmemory-js/dist/core/memory.js");
96
- const hsg = await import("openmemory-js/dist/memory/hsg.js");
97
- sdk_update_memory = hsg.update_memory;
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
- "[FATAL] CyberMem cannot start without a working database. " +
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
- // PERSISTENT LOGGING DB
127
- let loggingDb: any = null;
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
- let stdioClientName: string | undefined = undefined;
143
-
144
- // Protocol Instructions
145
- const CYBERMEM_INSTRUCTIONS = `CyberMem is a persistent context daemon for AI agents.
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
- // 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
- }),
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
- // access underlying server
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
- },
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
- // TOOLS
263
- server.registerTool(
264
- "add_memory",
265
- {
266
- description:
267
- "Store a new memory. Use for high-quality, stable data. " +
268
- CYBERMEM_INSTRUCTIONS,
269
- inputSchema: z.object({
270
- content: z.string(),
271
- tags: z.array(z.string()).optional(),
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
- 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
- },
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
- server.registerTool(
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
- server.registerTool(
331
- "reinforce_memory",
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
- server.registerTool(
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
- },
383
- );
145
+ // --- SERVER SETUP ---
146
+ interface AuthContext {
147
+ clientName: string;
148
+ [key: string]: unknown;
149
+ }
384
150
 
385
- return server;
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
- // 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 }),
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
- if (memory) {
414
- app.post("/add", async (req, res) => {
415
- try {
416
- const result = await (memory as any)!.add(req.body.content, {
417
- id: req.body.id,
418
- tags: req.body.tags,
419
- });
420
- await logActivity("create", {
421
- method: "POST",
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
- // MULTI-SESSION SSE SUPPORT
510
- const sessions = new Map<
511
- string,
512
- {
513
- server: McpServer;
514
- transport: SSEServerTransport;
515
- clientName?: string;
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
- 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
-
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 newServer.connect(transport);
538
- sessions.set(transport.sessionId, { server: newServer, transport });
539
-
540
- transport.onclose = () => {
541
- console.error(`[MCP] SSE Connection Closed: ${transport.sessionId}`);
542
- sessions.delete(transport.sessionId);
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
- app.post("/message", async (req, res) => {
564
- const sessionId = req.query.sessionId as string;
565
- const session = sessions.get(sessionId);
566
- if (!session) {
567
- res.status(404).send("Session not found");
568
- return;
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
- const clientName = session.clientName || "sse-client";
572
- await requestContext.run({ clientName }, async () => {
573
- await session.transport.handlePostMessage(req, res);
574
- });
575
- } catch (err) {
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");
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
- app.listen(port, () =>
587
- console.error(`CyberMem MCP running on http://localhost:${port}`),
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
- // STDIO
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
+ });