@cybermem/mcp 0.8.3 → 0.8.5

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/dist/env.js ADDED
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ /**
7
+ * Environment Initialization for CyberMem MCP
8
+ * Must be imported first to set side-effect vars.
9
+ */
10
+ const dotenv_1 = __importDefault(require("dotenv"));
11
+ dotenv_1.default.config();
12
+ process.env.OM_TIER = process.env.OM_TIER || "hybrid";
13
+ process.env.OM_PORT = process.env.OM_PORT || "0";
14
+ process.env.PORT = process.env.PORT || "0";
package/dist/index.js CHANGED
@@ -1,5 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ require("./env.js");
3
8
  /**
4
9
  * CyberMem MCP Server
5
10
  *
@@ -7,25 +12,35 @@
7
12
  * 1. Local/Server Mode (default): Uses openmemory-js SDK directly.
8
13
  * 2. Remote Client Mode (with --url): Proxies requests to a remote CyberMem server via HTTP.
9
14
  */
10
- var __importDefault = (this && this.__importDefault) || function (mod) {
11
- return (mod && mod.__esModule) ? mod : { "default": mod };
12
- };
13
- Object.defineProperty(exports, "__esModule", { value: true });
14
15
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
15
16
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
16
17
  const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
18
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
19
+ const async_hooks_1 = require("async_hooks");
17
20
  const axios_1 = __importDefault(require("axios"));
18
21
  const cors_1 = __importDefault(require("cors"));
19
- const dotenv_1 = __importDefault(require("dotenv"));
20
22
  const express_1 = __importDefault(require("express"));
21
- const openmemory_js_1 = require("openmemory-js");
22
23
  const zod_1 = require("zod");
23
- const auth_js_1 = require("./auth.js");
24
- dotenv_1.default.config();
24
+ const auth_1 = require("./auth");
25
+ // Redirect all stdout to stderr IMMEDIATELY to protect Stdio protocol
26
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
27
+ process.stdout.write = (chunk, encoding, callback) => {
28
+ const str = typeof chunk === "string" ? chunk : chunk.toString();
29
+ // Allow ONLY protocol messages (must be JSON-RPC)
30
+ if (str.includes('"jsonrpc":')) {
31
+ return originalStdoutWrite(chunk, encoding, callback);
32
+ }
33
+ return process.stderr.write(chunk, encoding, callback);
34
+ };
35
+ // Also redirect console outputs
36
+ console.log = console.error;
37
+ console.info = console.error;
38
+ // Async Storage for Request Context (User ID and Client Name)
39
+ const requestContext = new async_hooks_1.AsyncLocalStorage();
25
40
  // Handle CLI auth commands first
26
41
  const args = process.argv.slice(2);
27
42
  if (args.includes("--login")) {
28
- (0, auth_js_1.login)()
43
+ (0, auth_1.login)()
29
44
  .then(() => process.exit(0))
30
45
  .catch((err) => {
31
46
  console.error("Login failed:", err.message);
@@ -33,11 +48,11 @@ if (args.includes("--login")) {
33
48
  });
34
49
  }
35
50
  else if (args.includes("--logout")) {
36
- (0, auth_js_1.logout)();
51
+ (0, auth_1.logout)();
37
52
  process.exit(0);
38
53
  }
39
54
  else if (args.includes("--status")) {
40
- (0, auth_js_1.showStatus)();
55
+ (0, auth_1.showStatus)();
41
56
  process.exit(0);
42
57
  }
43
58
  else {
@@ -48,9 +63,9 @@ async function startServer() {
48
63
  const idx = args.indexOf(name);
49
64
  return idx !== -1 && args[idx + 1] ? args[idx + 1] : undefined;
50
65
  };
51
- const cliClientName = getArg("--client-name") || "cybermem-mcp";
52
66
  const cliUrl = getArg("--url");
53
- const cliApiKey = getArg("--api-key");
67
+ const cliToken = getArg("--token") || getArg("--api-key") || (cliUrl ? (0, auth_1.getToken)() : undefined);
68
+ let stdioClientName = undefined;
54
69
  // Protocol Instructions
55
70
  const CYBERMEM_INSTRUCTIONS = `CyberMem is a persistent context daemon for AI agents.
56
71
  PROTOCOL:
@@ -58,7 +73,7 @@ PROTOCOL:
58
73
  2. Store new insights immediately with add_memory (FULL content)
59
74
  3. Always include tags: [topic, year, source:your-client-name]
60
75
  For full protocol: https://docs.cybermem.dev/agent-protocol`;
61
- const server = new mcp_js_1.McpServer({ name: "cybermem", version: "0.8.2" }, {
76
+ const server = new mcp_js_1.McpServer({ name: "cybermem", version: "0.7.5" }, {
62
77
  instructions: CYBERMEM_INSTRUCTIONS,
63
78
  });
64
79
  server.registerResource("CyberMem Agent Protocol", "cybermem://protocol", { description: "Instructions for AI agents", mimeType: "text/plain" }, async () => ({
@@ -70,6 +85,23 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
70
85
  },
71
86
  ],
72
87
  }));
88
+ // Capture client info from handshake
89
+ // @ts-ignore - access underlying server
90
+ server.server.setRequestHandler(types_js_1.InitializeRequestSchema, async (request) => {
91
+ stdioClientName = request.params.clientInfo.name;
92
+ console.error(`[MCP] Client identified via handshake: ${stdioClientName}`);
93
+ return {
94
+ protocolVersion: "2024-11-05",
95
+ capabilities: {
96
+ tools: { listChanged: true },
97
+ resources: { subscribe: true },
98
+ },
99
+ serverInfo: {
100
+ name: "cybermem",
101
+ version: "0.7.5",
102
+ },
103
+ };
104
+ });
73
105
  // --- IMPLEMENTATION LOGIC ---
74
106
  let memory = null;
75
107
  let apiClient = null;
@@ -79,35 +111,145 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
79
111
  apiClient = axios_1.default.create({
80
112
  baseURL: cliUrl,
81
113
  headers: {
82
- Authorization: `Bearer ${cliApiKey}`,
83
- "X-Client-Name": cliClientName,
114
+ "X-API-Key": cliToken,
115
+ Accept: "application/json, text/event-stream",
116
+ "Content-Type": "application/json",
84
117
  },
85
118
  });
119
+ // Dynamically inject client name from context or discovery
120
+ apiClient.interceptors.request.use((config) => {
121
+ const ctx = requestContext.getStore();
122
+ config.headers["X-Client-Name"] =
123
+ ctx?.clientName || stdioClientName || "unknown-mcp-client";
124
+ return config;
125
+ });
86
126
  }
87
127
  else {
88
128
  // LOCAL SDK MODE
89
129
  const homedir = process.env.HOME || process.env.USERPROFILE || "";
90
- // Default to ~/.cybermem/data if OM_DB_PATH not set
91
- if (!process.env.OM_DB_PATH) {
92
- process.env.OM_DB_PATH = `${homedir}/.cybermem/data/openmemory.sqlite`;
93
- }
130
+ // FORCE absolute standardized path for consistency across components
131
+ const path = await import("path");
132
+ const dbPath = path.resolve(homedir, ".cybermem/data/openmemory.sqlite");
133
+ process.env.OM_DB_PATH = dbPath;
94
134
  // Ensure directory exists
95
- const fs = require("fs");
135
+ const fs = await import("fs");
96
136
  try {
97
- const dbPath = process.env.OM_DB_PATH;
98
- const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
137
+ const dir = path.dirname(dbPath);
99
138
  if (dir)
100
139
  fs.mkdirSync(dir, { recursive: true });
101
140
  }
102
141
  catch { }
103
- memory = new openmemory_js_1.Memory();
142
+ try {
143
+ // Dynamic import to ensure env vars are set before loading SDK
144
+ // We import from dist/core/memory directly to avoid triggering the server side-effects in openmemory-js/dist/index.js
145
+ // @ts-ignore
146
+ const { Memory } = await import("openmemory-js/dist/core/memory.js");
147
+ memory = new Memory();
148
+ server._memoryReady = true;
149
+ // --- INITIALIZE LOGGING TABLES ---
150
+ const sqlite3 = await import("sqlite3");
151
+ const db = new sqlite3.default.Database(dbPath);
152
+ db.configure("busyTimeout", 5000);
153
+ db.serialize(() => {
154
+ db.run("PRAGMA journal_mode=WAL;", (err) => {
155
+ if (err)
156
+ console.error("[MCP] Init WAL error:", err.message);
157
+ });
158
+ db.run(`CREATE TABLE IF NOT EXISTS cybermem_stats (
159
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
160
+ client_name TEXT NOT NULL,
161
+ operation TEXT NOT NULL,
162
+ count INTEGER DEFAULT 0,
163
+ errors INTEGER DEFAULT 0,
164
+ last_updated INTEGER NOT NULL,
165
+ UNIQUE(client_name, operation)
166
+ );`, (err) => {
167
+ if (err)
168
+ console.error("[MCP] Init stats table error:", err.message);
169
+ });
170
+ db.run(`CREATE TABLE IF NOT EXISTS cybermem_access_log (
171
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
172
+ timestamp INTEGER NOT NULL,
173
+ client_name TEXT NOT NULL,
174
+ client_version TEXT,
175
+ method TEXT NOT NULL,
176
+ endpoint TEXT NOT NULL,
177
+ operation TEXT NOT NULL,
178
+ status TEXT NOT NULL,
179
+ is_error INTEGER DEFAULT 0
180
+ );`, (err) => {
181
+ if (err)
182
+ console.error("[MCP] Init access_log table error:", err.message);
183
+ });
184
+ });
185
+ db.close();
186
+ }
187
+ catch (e) {
188
+ console.error("Failed to initialize OpenMemory SDK:", e);
189
+ server._memoryReady = false;
190
+ }
104
191
  }
105
- // Helper to add source tag
192
+ // Helper to log activity to SQLite (Local SDK Mode only)
193
+ const logActivity = async (operation, opts = {}) => {
194
+ if (cliUrl || !memory)
195
+ return;
196
+ const { client: providedClient, method = "POST", endpoint = "/mcp", status = 200, } = opts;
197
+ const ctx = requestContext.getStore();
198
+ const client = providedClient || ctx?.clientName || stdioClientName || "unknown-client";
199
+ try {
200
+ const dbPath = process.env.OM_DB_PATH;
201
+ const sqlite3 = await import("sqlite3");
202
+ const db = new sqlite3.default.Database(dbPath);
203
+ db.configure("busyTimeout", 5000);
204
+ const ts = Date.now();
205
+ const is_error = status >= 400 ? 1 : 0;
206
+ console.error(`[MCP] Logging ${operation} for ${client} (status: ${status})`);
207
+ db.serialize(() => {
208
+ // Log to access_log
209
+ db.run(`INSERT INTO cybermem_access_log
210
+ (timestamp, client_name, client_version, method, endpoint, operation, status, is_error)
211
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
212
+ ts,
213
+ client,
214
+ "0.7.0",
215
+ method,
216
+ endpoint,
217
+ operation,
218
+ status.toString(),
219
+ is_error,
220
+ ], (err) => {
221
+ if (err)
222
+ console.error("[MCP] Log access error:", err.message);
223
+ });
224
+ // Log to stats (Upsert)
225
+ db.run(`INSERT INTO cybermem_stats (client_name, operation, count, errors, last_updated)
226
+ VALUES (?, ?, 1, ?, ?)
227
+ ON CONFLICT(client_name, operation) DO UPDATE SET
228
+ count = count + 1,
229
+ errors = errors + ?,
230
+ last_updated = ?`, [client, operation, is_error, ts, is_error, ts], (err) => {
231
+ if (err)
232
+ console.error("[MCP] Log stats error:", err.message);
233
+ });
234
+ });
235
+ db.close();
236
+ }
237
+ catch (e) {
238
+ console.error("Failed to log activity to SQLite:", e);
239
+ }
240
+ };
106
241
  const addSourceTag = (tags = []) => {
107
- if (!tags.some((t) => t.startsWith("source:")))
108
- tags.push(`source:${cliClientName}`);
242
+ if (!tags.some((t) => t.startsWith("source:"))) {
243
+ const clientName = requestContext.getStore()?.clientName || stdioClientName || "unknown";
244
+ tags.push(`source:${clientName}`);
245
+ }
109
246
  return tags;
110
247
  };
248
+ // Helper to get current User ID from context or args
249
+ const getContextUserId = (argsUserId) => {
250
+ const store = requestContext.getStore();
251
+ return argsUserId || store?.userId;
252
+ };
111
253
  // --- TOOLS ---
112
254
  server.registerTool("add_memory", {
113
255
  description: "Store a new memory. " + CYBERMEM_INSTRUCTIONS,
@@ -118,38 +260,76 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
118
260
  }),
119
261
  }, async (args) => {
120
262
  const tags = addSourceTag(args.tags);
263
+ const userId = getContextUserId(args.user_id);
121
264
  if (cliUrl) {
122
- const res = await apiClient.post("/add", { ...args, tags });
265
+ const res = await apiClient.post("/add", {
266
+ ...args,
267
+ user_id: userId,
268
+ tags,
269
+ });
123
270
  return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
124
271
  }
125
272
  else {
126
- const res = await memory.add(args.content, {
127
- user_id: args.user_id,
128
- tags,
129
- });
130
- return { content: [{ type: "text", text: JSON.stringify(res) }] };
273
+ try {
274
+ const res = await memory.add(args.content, {
275
+ user_id: userId,
276
+ tags,
277
+ });
278
+ await logActivity("create", {
279
+ method: "POST",
280
+ endpoint: "/memory/add",
281
+ status: 200,
282
+ });
283
+ return { content: [{ type: "text", text: JSON.stringify(res) }] };
284
+ }
285
+ catch (e) {
286
+ await logActivity("create", {
287
+ method: "POST",
288
+ endpoint: "/memory/add",
289
+ status: 500,
290
+ });
291
+ throw e;
292
+ }
131
293
  }
132
294
  });
133
295
  server.registerTool("query_memory", {
134
296
  description: "Search memories.",
135
297
  inputSchema: zod_1.z.object({ query: zod_1.z.string(), k: zod_1.z.number().default(5) }),
136
298
  }, async (args) => {
299
+ const userId = getContextUserId(); // Search is scoped to user if provided
137
300
  if (cliUrl) {
138
301
  const res = await apiClient.post("/query", args);
139
302
  return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
140
303
  }
141
304
  else {
142
- const res = await memory.search(args.query, { limit: args.k });
143
- return { content: [{ type: "text", text: JSON.stringify(res) }] };
305
+ try {
306
+ const res = await memory.search(args.query, {
307
+ limit: args.k,
308
+ user_id: userId,
309
+ });
310
+ await logActivity("read", {
311
+ method: "POST",
312
+ endpoint: "/memory/query",
313
+ status: 200,
314
+ });
315
+ return { content: [{ type: "text", text: JSON.stringify(res) }] };
316
+ }
317
+ catch (e) {
318
+ await logActivity("read", {
319
+ method: "POST",
320
+ endpoint: "/memory/query",
321
+ status: 500,
322
+ });
323
+ throw e;
324
+ }
144
325
  }
145
326
  });
146
327
  server.registerTool("list_memories", {
147
328
  description: "List recent memories",
148
329
  inputSchema: zod_1.z.object({ limit: zod_1.z.number().default(10) }),
149
330
  }, async (args) => {
331
+ const userId = getContextUserId();
150
332
  if (cliUrl) {
151
- // Fallback to /query with empty string if /list not available, or use /all
152
- // Old API had /all
153
333
  try {
154
334
  const res = await apiClient.get(`/all?limit=${args.limit}`);
155
335
  return {
@@ -167,7 +347,15 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
167
347
  }
168
348
  }
169
349
  else {
170
- const res = await memory.search("", { limit: args.limit });
350
+ const res = await memory.search("", {
351
+ limit: args.limit,
352
+ user_id: userId,
353
+ });
354
+ await logActivity("read", {
355
+ method: "GET",
356
+ endpoint: "/memory/all",
357
+ status: 200,
358
+ });
171
359
  return { content: [{ type: "text", text: JSON.stringify(res) }] };
172
360
  }
173
361
  });
@@ -180,17 +368,55 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
180
368
  return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
181
369
  }
182
370
  else {
183
- return {
184
- content: [
185
- { type: "text", text: "Delete not implemented in SDK yet" },
186
- ],
187
- };
371
+ const dbPath = process.env.OM_DB_PATH;
372
+ const sqlite3 = await import("sqlite3");
373
+ const db = new sqlite3.default.Database(dbPath);
374
+ db.configure("busyTimeout", 5000);
375
+ return new Promise((resolve, reject) => {
376
+ db.serialize(() => {
377
+ db.run("BEGIN TRANSACTION");
378
+ db.run("DELETE FROM memories WHERE id = ?", [args.id]);
379
+ db.run("DELETE FROM vectors WHERE id = ?", [args.id]);
380
+ db.run("DELETE FROM waypoints WHERE src_id = ? OR dst_id = ?", [
381
+ args.id,
382
+ args.id,
383
+ ]);
384
+ db.run("COMMIT", async (err) => {
385
+ db.close();
386
+ if (err) {
387
+ await logActivity("delete", {
388
+ method: "DELETE",
389
+ endpoint: `/memory/${args.id}`,
390
+ status: 500,
391
+ });
392
+ reject(new Error(`Failed to delete memory ${args.id}: ${err.message}`));
393
+ }
394
+ else {
395
+ await logActivity("delete", {
396
+ method: "DELETE",
397
+ endpoint: `/memory/${args.id}`,
398
+ status: 200,
399
+ });
400
+ resolve({
401
+ content: [
402
+ { type: "text", text: `Memory ${args.id} deleted` },
403
+ ],
404
+ });
405
+ }
406
+ });
407
+ });
408
+ });
188
409
  }
189
410
  });
190
411
  server.registerTool("update_memory", {
191
412
  description: "Update memory",
192
413
  inputSchema: zod_1.z.object({ id: zod_1.z.string(), content: zod_1.z.string().optional() }),
193
414
  }, async (args) => {
415
+ await logActivity("update", {
416
+ method: "PATCH",
417
+ endpoint: `/memory/${args.id}`,
418
+ status: 501,
419
+ });
194
420
  return { content: [{ type: "text", text: "Update not implemented" }] };
195
421
  });
196
422
  // --- TRANSPORT ---
@@ -200,41 +426,130 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
200
426
  const app = (0, express_1.default)();
201
427
  app.use((0, cors_1.default)());
202
428
  app.use(express_1.default.json());
203
- app.get("/health", (req, res) => res.json({ ok: true, version: "0.8.2", mode: cliUrl ? "proxy" : "sdk" }));
204
- // REST API Compatibility (for Remote Clients)
205
- // Only enable if in SDK mode (Server)
429
+ app.get("/health", (req, res) => res.json({
430
+ ok: server._memoryReady,
431
+ version: "0.7.5",
432
+ mode: cliUrl ? "proxy" : "sdk",
433
+ ready: server._memoryReady,
434
+ }));
435
+ app.get("/metrics", async (req, res) => {
436
+ try {
437
+ const dbPath = process.env.OM_DB_PATH;
438
+ const sqlite3 = await import("sqlite3");
439
+ const db = new sqlite3.default.Database(dbPath);
440
+ db.configure("busyTimeout", 5000);
441
+ const getCount = (query) => new Promise((resolve) => db.get(query, (err, row) => resolve(row?.count || 0)));
442
+ const memoriesCount = await getCount("SELECT COUNT(*) as count FROM memories");
443
+ const totalRequests = await getCount("SELECT COUNT(*) as count FROM cybermem_access_log");
444
+ const errorRequests = await getCount("SELECT COUNT(*) as count FROM cybermem_access_log WHERE is_error = 1");
445
+ const uniqueClients = await getCount("SELECT COUNT(DISTINCT client_name) as count FROM cybermem_access_log");
446
+ db.close();
447
+ const metrics = [
448
+ "# HELP openmemory_memories_total Total number of memories",
449
+ "# TYPE openmemory_memories_total gauge",
450
+ `openmemory_memories_total ${memoriesCount}`,
451
+ "# HELP openmemory_requests_aggregate_total Total requests logged in SQLite",
452
+ "# TYPE openmemory_requests_aggregate_total counter",
453
+ `openmemory_requests_aggregate_total ${totalRequests}`,
454
+ "# HELP openmemory_errors_total Total errors logged in SQLite",
455
+ "# TYPE openmemory_errors_total counter",
456
+ `openmemory_errors_total ${errorRequests}`,
457
+ "# HELP openmemory_clients_total Total unique clients logged in SQLite",
458
+ "# TYPE openmemory_clients_total gauge",
459
+ `openmemory_clients_total ${uniqueClients}`,
460
+ "# HELP openmemory_success_rate_aggregate Success rate from SQLite logs",
461
+ "# TYPE openmemory_success_rate_aggregate gauge",
462
+ `openmemory_success_rate_aggregate ${totalRequests > 0 ? ((totalRequests - errorRequests) / totalRequests) * 100 : 100}`,
463
+ ].join("\n");
464
+ res.set("Content-Type", "text/plain").send(metrics);
465
+ }
466
+ catch (e) {
467
+ res.status(500).send(`# Error: ${e.message}`);
468
+ }
469
+ });
470
+ app.use((req, res, next) => {
471
+ const userId = req.headers["x-user-id"];
472
+ const clientName = req.headers["x-client-name"] ||
473
+ req.headers["user-agent"];
474
+ requestContext.run({ userId, clientName }, next);
475
+ });
206
476
  if (!cliUrl && memory) {
207
477
  app.post("/add", async (req, res) => {
208
478
  try {
479
+ const contextUserId = requestContext.getStore()?.userId;
209
480
  const { content, user_id, tags } = req.body;
210
481
  const finalTags = addSourceTag(tags);
211
482
  const result = await memory.add(content, {
212
- user_id,
483
+ user_id: user_id || contextUserId,
213
484
  tags: finalTags,
214
485
  });
486
+ await logActivity("create", {
487
+ client: "rest-api",
488
+ method: "POST",
489
+ endpoint: "/add",
490
+ status: 200,
491
+ });
215
492
  res.json(result);
216
493
  }
217
494
  catch (e) {
495
+ await logActivity("create", {
496
+ client: "rest-api",
497
+ method: "POST",
498
+ endpoint: "/add",
499
+ status: 500,
500
+ });
218
501
  res.status(500).json({ error: e.message });
219
502
  }
220
503
  });
221
504
  app.post("/query", async (req, res) => {
222
505
  try {
506
+ const contextUserId = requestContext.getStore()?.userId;
223
507
  const { query, k } = req.body;
224
- const result = await memory.search(query || "", { limit: k || 5 });
508
+ const result = await memory.search(query || "", {
509
+ limit: k || 5,
510
+ user_id: contextUserId,
511
+ });
512
+ await logActivity("read", {
513
+ client: "rest-api",
514
+ method: "POST",
515
+ endpoint: "/query",
516
+ status: 200,
517
+ });
225
518
  res.json(result);
226
519
  }
227
520
  catch (e) {
521
+ await logActivity("read", {
522
+ client: "rest-api",
523
+ method: "POST",
524
+ endpoint: "/query",
525
+ status: 500,
526
+ });
228
527
  res.status(500).json({ error: e.message });
229
528
  }
230
529
  });
231
530
  app.get("/all", async (req, res) => {
232
531
  try {
532
+ const contextUserId = requestContext.getStore()?.userId;
233
533
  const limit = parseInt(req.query.limit) || 10;
234
- const result = await memory.search("", { limit });
534
+ const result = await memory.search("", {
535
+ limit,
536
+ user_id: contextUserId,
537
+ });
538
+ await logActivity("read", {
539
+ client: "rest-api",
540
+ method: "GET",
541
+ endpoint: "/all",
542
+ status: 200,
543
+ });
235
544
  res.json(result);
236
545
  }
237
546
  catch (e) {
547
+ await logActivity("read", {
548
+ client: "rest-api",
549
+ method: "GET",
550
+ endpoint: "/all",
551
+ status: 500,
552
+ });
238
553
  res.status(500).json({ error: e.message });
239
554
  }
240
555
  });
@@ -246,7 +561,7 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
246
561
  app.all("/sse", async (req, res) => await transport.handleRequest(req, res, req.body));
247
562
  server.connect(transport).then(() => {
248
563
  app.listen(port, () => {
249
- console.log(`CyberMem MCP running on http://localhost:${port}`);
564
+ console.error(`CyberMem MCP (ready: ${server._memoryReady}) running on http://localhost:${port}`);
250
565
  });
251
566
  });
252
567
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cybermem/mcp",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "description": "CyberMem MCP Server - AI Memory with openmemory-js SDK",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -25,7 +25,6 @@
25
25
  "ai-memory",
26
26
  "claude",
27
27
  "cursor",
28
- "antigravity",
29
28
  "openmemory",
30
29
  "llm"
31
30
  ],
@@ -40,7 +39,11 @@
40
39
  "cors": "^2.8.5",
41
40
  "dotenv": "^16.0.0",
42
41
  "express": "^5.2.1",
42
+ "keytar": "^7.9.0",
43
+ "open": "^11.0.0",
43
44
  "openmemory-js": "^1.3.2",
45
+ "sqlite": "^5.1.1",
46
+ "sqlite3": "^5.1.7",
44
47
  "zod": "^3.25.76"
45
48
  },
46
49
  "devDependencies": {
package/src/env.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Environment Initialization for CyberMem MCP
3
+ * Must be imported first to set side-effect vars.
4
+ */
5
+ import dotenv from "dotenv";
6
+ dotenv.config();
7
+
8
+ process.env.OM_TIER = process.env.OM_TIER || "hybrid";
9
+ process.env.OM_PORT = process.env.OM_PORT || "0";
10
+ process.env.PORT = process.env.PORT || "0";
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import "./env.js";
2
3
  /**
3
4
  * CyberMem MCP Server
4
5
  *
@@ -10,15 +11,34 @@
10
11
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
12
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
13
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
14
+ import { InitializeRequestSchema } from "@modelcontextprotocol/sdk/types.js";
15
+ import { AsyncLocalStorage } from "async_hooks";
13
16
  import axios from "axios";
14
17
  import cors from "cors";
15
- import dotenv from "dotenv";
16
18
  import express from "express";
17
- import { Memory } from "openmemory-js";
18
19
  import { z } from "zod";
19
- import { login, logout, showStatus } from "./auth.js";
20
+ import { getToken, login, logout, showStatus } from "./auth";
20
21
 
21
- dotenv.config();
22
+ // Redirect all stdout to stderr IMMEDIATELY to protect Stdio protocol
23
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
24
+ (process.stdout as any).write = (chunk: any, encoding: any, callback: any) => {
25
+ const str = typeof chunk === "string" ? chunk : chunk.toString();
26
+ // Allow ONLY protocol messages (must be JSON-RPC)
27
+ if (str.includes('"jsonrpc":')) {
28
+ return originalStdoutWrite(chunk, encoding, callback);
29
+ }
30
+ return process.stderr.write(chunk, encoding, callback);
31
+ };
32
+
33
+ // Also redirect console outputs
34
+ console.log = console.error;
35
+ console.info = console.error;
36
+
37
+ // Async Storage for Request Context (User ID and Client Name)
38
+ const requestContext = new AsyncLocalStorage<{
39
+ userId?: string;
40
+ clientName?: string;
41
+ }>();
22
42
 
23
43
  // Handle CLI auth commands first
24
44
  const args = process.argv.slice(2);
@@ -46,9 +66,9 @@ async function startServer() {
46
66
  return idx !== -1 && args[idx + 1] ? args[idx + 1] : undefined;
47
67
  };
48
68
 
49
- const cliClientName = getArg("--client-name") || "cybermem-mcp";
50
69
  const cliUrl = getArg("--url");
51
- const cliApiKey = getArg("--api-key");
70
+ const cliToken = getArg("--token") || getArg("--api-key") || (cliUrl ? getToken() : undefined);
71
+ let stdioClientName: string | undefined = undefined;
52
72
 
53
73
  // Protocol Instructions
54
74
  const CYBERMEM_INSTRUCTIONS = `CyberMem is a persistent context daemon for AI agents.
@@ -59,7 +79,7 @@ PROTOCOL:
59
79
  For full protocol: https://docs.cybermem.dev/agent-protocol`;
60
80
 
61
81
  const server = new McpServer(
62
- { name: "cybermem", version: "0.8.2" },
82
+ { name: "cybermem", version: "0.7.5" },
63
83
  {
64
84
  instructions: CYBERMEM_INSTRUCTIONS,
65
85
  },
@@ -80,9 +100,27 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
80
100
  }),
81
101
  );
82
102
 
103
+ // Capture client info from handshake
104
+ // @ts-ignore - access underlying server
105
+ server.server.setRequestHandler(InitializeRequestSchema, async (request) => {
106
+ stdioClientName = request.params.clientInfo.name;
107
+ console.error(`[MCP] Client identified via handshake: ${stdioClientName}`);
108
+ return {
109
+ protocolVersion: "2024-11-05",
110
+ capabilities: {
111
+ tools: { listChanged: true },
112
+ resources: { subscribe: true },
113
+ },
114
+ serverInfo: {
115
+ name: "cybermem",
116
+ version: "0.7.5",
117
+ },
118
+ };
119
+ });
120
+
83
121
  // --- IMPLEMENTATION LOGIC ---
84
122
 
85
- let memory: Memory | null = null;
123
+ let memory: any = null;
86
124
  let apiClient: any = null;
87
125
 
88
126
  if (cliUrl) {
@@ -91,36 +129,182 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
91
129
  apiClient = axios.create({
92
130
  baseURL: cliUrl,
93
131
  headers: {
94
- Authorization: `Bearer ${cliApiKey}`,
95
- "X-Client-Name": cliClientName,
132
+ "X-API-Key": cliToken,
133
+ Accept: "application/json, text/event-stream",
134
+ "Content-Type": "application/json",
96
135
  },
97
136
  });
137
+ // Dynamically inject client name from context or discovery
138
+ apiClient.interceptors.request.use((config: any) => {
139
+ const ctx = requestContext.getStore();
140
+ config.headers["X-Client-Name"] =
141
+ ctx?.clientName || stdioClientName || "unknown-mcp-client";
142
+ return config;
143
+ });
98
144
  } else {
99
145
  // LOCAL SDK MODE
100
146
  const homedir = process.env.HOME || process.env.USERPROFILE || "";
101
- // Default to ~/.cybermem/data if OM_DB_PATH not set
102
- if (!process.env.OM_DB_PATH) {
103
- process.env.OM_DB_PATH = `${homedir}/.cybermem/data/openmemory.sqlite`;
104
- }
147
+ // FORCE absolute standardized path for consistency across components
148
+ const path = await import("path");
149
+ const dbPath = path.resolve(homedir, ".cybermem/data/openmemory.sqlite");
150
+ process.env.OM_DB_PATH = dbPath;
105
151
 
106
152
  // Ensure directory exists
107
- const fs = require("fs");
153
+ const fs = await import("fs");
108
154
  try {
109
- const dbPath = process.env.OM_DB_PATH;
110
- const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
155
+ const dir = path.dirname(dbPath);
111
156
  if (dir) fs.mkdirSync(dir, { recursive: true });
112
- } catch {}
157
+ } catch { }
113
158
 
114
- memory = new Memory();
159
+ try {
160
+ // Dynamic import to ensure env vars are set before loading SDK
161
+ // We import from dist/core/memory directly to avoid triggering the server side-effects in openmemory-js/dist/index.js
162
+ // @ts-ignore
163
+ const { Memory } = await import("openmemory-js/dist/core/memory.js");
164
+ memory = new Memory();
165
+ (server as any)._memoryReady = true;
166
+
167
+ // --- INITIALIZE LOGGING TABLES ---
168
+ const sqlite3 = await import("sqlite3");
169
+ const db = new sqlite3.default.Database(dbPath);
170
+ db.configure("busyTimeout", 5000);
171
+ db.serialize(() => {
172
+ db.run("PRAGMA journal_mode=WAL;", (err: any) => {
173
+ if (err) console.error("[MCP] Init WAL error:", err.message);
174
+ });
175
+ db.run(
176
+ `CREATE TABLE IF NOT EXISTS cybermem_stats (
177
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
178
+ client_name TEXT NOT NULL,
179
+ operation TEXT NOT NULL,
180
+ count INTEGER DEFAULT 0,
181
+ errors INTEGER DEFAULT 0,
182
+ last_updated INTEGER NOT NULL,
183
+ UNIQUE(client_name, operation)
184
+ );`,
185
+ (err: any) => {
186
+ if (err)
187
+ console.error("[MCP] Init stats table error:", err.message);
188
+ },
189
+ );
190
+ db.run(
191
+ `CREATE TABLE IF NOT EXISTS cybermem_access_log (
192
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
193
+ timestamp INTEGER NOT NULL,
194
+ client_name TEXT NOT NULL,
195
+ client_version TEXT,
196
+ method TEXT NOT NULL,
197
+ endpoint TEXT NOT NULL,
198
+ operation TEXT NOT NULL,
199
+ status TEXT NOT NULL,
200
+ is_error INTEGER DEFAULT 0
201
+ );`,
202
+ (err: any) => {
203
+ if (err)
204
+ console.error("[MCP] Init access_log table error:", err.message);
205
+ },
206
+ );
207
+ });
208
+ db.close();
209
+ } catch (e) {
210
+ console.error("Failed to initialize OpenMemory SDK:", e);
211
+ (server as any)._memoryReady = false;
212
+ }
115
213
  }
116
214
 
117
- // Helper to add source tag
215
+ // Helper to log activity to SQLite (Local SDK Mode only)
216
+ const logActivity = async (
217
+ operation: string,
218
+ opts: {
219
+ client?: string;
220
+ method?: string;
221
+ endpoint?: string;
222
+ status?: number;
223
+ } = {},
224
+ ) => {
225
+ if (cliUrl || !memory) return;
226
+
227
+ const {
228
+ client: providedClient,
229
+ method = "POST",
230
+ endpoint = "/mcp",
231
+ status = 200,
232
+ } = opts;
233
+
234
+ const ctx = requestContext.getStore();
235
+ const client =
236
+ providedClient || ctx?.clientName || stdioClientName || "unknown-client";
237
+
238
+ try {
239
+ const dbPath = process.env.OM_DB_PATH!;
240
+ const sqlite3 = await import("sqlite3");
241
+ const db = new sqlite3.default.Database(dbPath);
242
+ db.configure("busyTimeout", 5000);
243
+
244
+ const ts = Date.now();
245
+ const is_error = status >= 400 ? 1 : 0;
246
+
247
+ console.error(
248
+ `[MCP] Logging ${operation} for ${client} (status: ${status})`,
249
+ );
250
+
251
+ db.serialize(() => {
252
+ // Log to access_log
253
+ db.run(
254
+ `INSERT INTO cybermem_access_log
255
+ (timestamp, client_name, client_version, method, endpoint, operation, status, is_error)
256
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
257
+ [
258
+ ts,
259
+ client,
260
+ "0.7.0",
261
+ method,
262
+ endpoint,
263
+ operation,
264
+ status.toString(),
265
+ is_error,
266
+ ],
267
+ (err: any) => {
268
+ if (err) console.error("[MCP] Log access error:", err.message);
269
+ },
270
+ );
271
+
272
+ // Log to stats (Upsert)
273
+ db.run(
274
+ `INSERT INTO cybermem_stats (client_name, operation, count, errors, last_updated)
275
+ VALUES (?, ?, 1, ?, ?)
276
+ ON CONFLICT(client_name, operation) DO UPDATE SET
277
+ count = count + 1,
278
+ errors = errors + ?,
279
+ last_updated = ?`,
280
+ [client, operation, is_error, ts, is_error, ts],
281
+ (err: any) => {
282
+ if (err) console.error("[MCP] Log stats error:", err.message);
283
+ },
284
+ );
285
+ });
286
+
287
+ db.close();
288
+ } catch (e) {
289
+ console.error("Failed to log activity to SQLite:", e);
290
+ }
291
+ };
292
+
118
293
  const addSourceTag = (tags: string[] = []) => {
119
- if (!tags.some((t) => t.startsWith("source:")))
120
- tags.push(`source:${cliClientName}`);
294
+ if (!tags.some((t) => t.startsWith("source:"))) {
295
+ const clientName =
296
+ requestContext.getStore()?.clientName || stdioClientName || "unknown";
297
+ tags.push(`source:${clientName}`);
298
+ }
121
299
  return tags;
122
300
  };
123
301
 
302
+ // Helper to get current User ID from context or args
303
+ const getContextUserId = (argsUserId?: string) => {
304
+ const store = requestContext.getStore();
305
+ return argsUserId || store?.userId;
306
+ };
307
+
124
308
  // --- TOOLS ---
125
309
 
126
310
  server.registerTool(
@@ -133,17 +317,37 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
133
317
  tags: z.array(z.string()).optional(),
134
318
  }),
135
319
  },
136
- async (args) => {
320
+ async (args: any) => {
137
321
  const tags = addSourceTag(args.tags);
322
+ const userId = getContextUserId(args.user_id);
323
+
138
324
  if (cliUrl) {
139
- const res = await apiClient.post("/add", { ...args, tags });
140
- return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
141
- } else {
142
- const res = await memory!.add(args.content, {
143
- user_id: args.user_id,
325
+ const res = await apiClient.post("/add", {
326
+ ...args,
327
+ user_id: userId,
144
328
  tags,
145
329
  });
146
- return { content: [{ type: "text", text: JSON.stringify(res) }] };
330
+ return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
331
+ } else {
332
+ try {
333
+ const res = await memory!.add(args.content, {
334
+ user_id: userId,
335
+ tags,
336
+ });
337
+ await logActivity("create", {
338
+ method: "POST",
339
+ endpoint: "/memory/add",
340
+ status: 200,
341
+ });
342
+ return { content: [{ type: "text", text: JSON.stringify(res) }] };
343
+ } catch (e: any) {
344
+ await logActivity("create", {
345
+ method: "POST",
346
+ endpoint: "/memory/add",
347
+ status: 500,
348
+ });
349
+ throw e;
350
+ }
147
351
  }
148
352
  },
149
353
  );
@@ -154,13 +358,32 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
154
358
  description: "Search memories.",
155
359
  inputSchema: z.object({ query: z.string(), k: z.number().default(5) }),
156
360
  },
157
- async (args) => {
361
+ async (args: any) => {
362
+ const userId = getContextUserId(); // Search is scoped to user if provided
363
+
158
364
  if (cliUrl) {
159
365
  const res = await apiClient.post("/query", args);
160
366
  return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
161
367
  } else {
162
- const res = await memory!.search(args.query, { limit: args.k });
163
- return { content: [{ type: "text", text: JSON.stringify(res) }] };
368
+ try {
369
+ const res = await memory!.search(args.query, {
370
+ limit: args.k,
371
+ user_id: userId,
372
+ });
373
+ await logActivity("read", {
374
+ method: "POST",
375
+ endpoint: "/memory/query",
376
+ status: 200,
377
+ });
378
+ return { content: [{ type: "text", text: JSON.stringify(res) }] };
379
+ } catch (e: any) {
380
+ await logActivity("read", {
381
+ method: "POST",
382
+ endpoint: "/memory/query",
383
+ status: 500,
384
+ });
385
+ throw e;
386
+ }
164
387
  }
165
388
  },
166
389
  );
@@ -172,9 +395,9 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
172
395
  inputSchema: z.object({ limit: z.number().default(10) }),
173
396
  },
174
397
  async (args) => {
398
+ const userId = getContextUserId();
399
+
175
400
  if (cliUrl) {
176
- // Fallback to /query with empty string if /list not available, or use /all
177
- // Old API had /all
178
401
  try {
179
402
  const res = await apiClient.get(`/all?limit=${args.limit}`);
180
403
  return {
@@ -190,7 +413,15 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
190
413
  };
191
414
  }
192
415
  } else {
193
- const res = await memory!.search("", { limit: args.limit });
416
+ const res = await memory!.search("", {
417
+ limit: args.limit,
418
+ user_id: userId,
419
+ });
420
+ await logActivity("read", {
421
+ method: "GET",
422
+ endpoint: "/memory/all",
423
+ status: 200,
424
+ });
194
425
  return { content: [{ type: "text", text: JSON.stringify(res) }] };
195
426
  }
196
427
  },
@@ -202,16 +433,53 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
202
433
  description: "Delete memory by ID",
203
434
  inputSchema: z.object({ id: z.string() }),
204
435
  },
205
- async (args) => {
436
+ async (args: any) => {
206
437
  if (cliUrl) {
207
438
  const res = await apiClient.delete(`/${args.id}`);
208
439
  return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
209
440
  } else {
210
- return {
211
- content: [
212
- { type: "text", text: "Delete not implemented in SDK yet" },
213
- ],
214
- };
441
+ const dbPath = process.env.OM_DB_PATH!;
442
+ const sqlite3 = await import("sqlite3");
443
+ const db = new sqlite3.default.Database(dbPath);
444
+ db.configure("busyTimeout", 5000);
445
+
446
+ return new Promise((resolve, reject) => {
447
+ db.serialize(() => {
448
+ db.run("BEGIN TRANSACTION");
449
+ db.run("DELETE FROM memories WHERE id = ?", [args.id]);
450
+ db.run("DELETE FROM vectors WHERE id = ?", [args.id]);
451
+ db.run("DELETE FROM waypoints WHERE src_id = ? OR dst_id = ?", [
452
+ args.id,
453
+ args.id,
454
+ ]);
455
+ db.run("COMMIT", async (err: any) => {
456
+ db.close();
457
+ if (err) {
458
+ await logActivity("delete", {
459
+ method: "DELETE",
460
+ endpoint: `/memory/${args.id}`,
461
+ status: 500,
462
+ });
463
+ reject(
464
+ new Error(
465
+ `Failed to delete memory ${args.id}: ${err.message}`,
466
+ ),
467
+ );
468
+ } else {
469
+ await logActivity("delete", {
470
+ method: "DELETE",
471
+ endpoint: `/memory/${args.id}`,
472
+ status: 200,
473
+ });
474
+ resolve({
475
+ content: [
476
+ { type: "text", text: `Memory ${args.id} deleted` },
477
+ ],
478
+ });
479
+ }
480
+ });
481
+ });
482
+ });
215
483
  }
216
484
  },
217
485
  );
@@ -222,7 +490,12 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
222
490
  description: "Update memory",
223
491
  inputSchema: z.object({ id: z.string(), content: z.string().optional() }),
224
492
  },
225
- async (args) => {
493
+ async (args: any) => {
494
+ await logActivity("update", {
495
+ method: "PATCH",
496
+ endpoint: `/memory/${args.id}`,
497
+ status: 501,
498
+ });
226
499
  return { content: [{ type: "text", text: "Update not implemented" }] };
227
500
  },
228
501
  );
@@ -237,43 +510,159 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
237
510
  app.use(cors());
238
511
  app.use(express.json());
239
512
 
240
- app.get("/health", (req, res) =>
241
- res.json({ ok: true, version: "0.8.2", mode: cliUrl ? "proxy" : "sdk" }),
513
+ app.get("/health", (req: express.Request, res: express.Response) =>
514
+ res.json({
515
+ ok: (server as any)._memoryReady,
516
+ version: "0.7.5",
517
+ mode: cliUrl ? "proxy" : "sdk",
518
+ ready: (server as any)._memoryReady,
519
+ }),
520
+ );
521
+
522
+ app.get("/metrics", async (req: express.Request, res: express.Response) => {
523
+ try {
524
+ const dbPath = process.env.OM_DB_PATH!;
525
+ const sqlite3 = await import("sqlite3");
526
+ const db = new sqlite3.default.Database(dbPath);
527
+ db.configure("busyTimeout", 5000);
528
+
529
+ const getCount = (query: string): Promise<number> =>
530
+ new Promise((resolve) =>
531
+ db.get(query, (err, row: any) => resolve(row?.count || 0)),
532
+ );
533
+
534
+ const memoriesCount = await getCount(
535
+ "SELECT COUNT(*) as count FROM memories",
536
+ );
537
+ const totalRequests = await getCount(
538
+ "SELECT COUNT(*) as count FROM cybermem_access_log",
539
+ );
540
+ const errorRequests = await getCount(
541
+ "SELECT COUNT(*) as count FROM cybermem_access_log WHERE is_error = 1",
542
+ );
543
+ const uniqueClients = await getCount(
544
+ "SELECT COUNT(DISTINCT client_name) as count FROM cybermem_access_log",
545
+ );
546
+
547
+ db.close();
548
+
549
+ const metrics = [
550
+ "# HELP openmemory_memories_total Total number of memories",
551
+ "# TYPE openmemory_memories_total gauge",
552
+ `openmemory_memories_total ${memoriesCount}`,
553
+ "# HELP openmemory_requests_aggregate_total Total requests logged in SQLite",
554
+ "# TYPE openmemory_requests_aggregate_total counter",
555
+ `openmemory_requests_aggregate_total ${totalRequests}`,
556
+ "# HELP openmemory_errors_total Total errors logged in SQLite",
557
+ "# TYPE openmemory_errors_total counter",
558
+ `openmemory_errors_total ${errorRequests}`,
559
+ "# HELP openmemory_clients_total Total unique clients logged in SQLite",
560
+ "# TYPE openmemory_clients_total gauge",
561
+ `openmemory_clients_total ${uniqueClients}`,
562
+ "# HELP openmemory_success_rate_aggregate Success rate from SQLite logs",
563
+ "# TYPE openmemory_success_rate_aggregate gauge",
564
+ `openmemory_success_rate_aggregate ${totalRequests > 0 ? ((totalRequests - errorRequests) / totalRequests) * 100 : 100}`,
565
+ ].join("\n");
566
+
567
+ res.set("Content-Type", "text/plain").send(metrics);
568
+ } catch (e: any) {
569
+ res.status(500).send(`# Error: ${e.message}`);
570
+ }
571
+ });
572
+
573
+ app.use(
574
+ (
575
+ req: express.Request,
576
+ res: express.Response,
577
+ next: express.NextFunction,
578
+ ) => {
579
+ const userId = req.headers["x-user-id"] as string | undefined;
580
+ const clientName =
581
+ (req.headers["x-client-name"] as string) ||
582
+ (req.headers["user-agent"] as string);
583
+ requestContext.run({ userId, clientName }, next);
584
+ },
242
585
  );
243
586
 
244
- // REST API Compatibility (for Remote Clients)
245
- // Only enable if in SDK mode (Server)
246
587
  if (!cliUrl && memory) {
247
- app.post("/add", async (req, res) => {
588
+ app.post("/add", async (req: express.Request, res: express.Response) => {
248
589
  try {
590
+ const contextUserId = requestContext.getStore()?.userId;
249
591
  const { content, user_id, tags } = req.body;
250
592
  const finalTags = addSourceTag(tags);
251
593
  const result = await memory!.add(content, {
252
- user_id,
594
+ user_id: user_id || contextUserId,
253
595
  tags: finalTags,
254
596
  });
597
+ await logActivity("create", {
598
+ client: "rest-api",
599
+ method: "POST",
600
+ endpoint: "/add",
601
+ status: 200,
602
+ });
255
603
  res.json(result);
256
604
  } catch (e: any) {
605
+ await logActivity("create", {
606
+ client: "rest-api",
607
+ method: "POST",
608
+ endpoint: "/add",
609
+ status: 500,
610
+ });
257
611
  res.status(500).json({ error: e.message });
258
612
  }
259
613
  });
260
614
 
261
- app.post("/query", async (req, res) => {
262
- try {
263
- const { query, k } = req.body;
264
- const result = await memory!.search(query || "", { limit: k || 5 });
265
- res.json(result);
266
- } catch (e: any) {
267
- res.status(500).json({ error: e.message });
268
- }
269
- });
615
+ app.post(
616
+ "/query",
617
+ async (req: express.Request, res: express.Response) => {
618
+ try {
619
+ const contextUserId = requestContext.getStore()?.userId;
620
+ const { query, k } = req.body;
621
+ const result = await memory!.search(query || "", {
622
+ limit: k || 5,
623
+ user_id: contextUserId,
624
+ });
625
+ await logActivity("read", {
626
+ client: "rest-api",
627
+ method: "POST",
628
+ endpoint: "/query",
629
+ status: 200,
630
+ });
631
+ res.json(result);
632
+ } catch (e: any) {
633
+ await logActivity("read", {
634
+ client: "rest-api",
635
+ method: "POST",
636
+ endpoint: "/query",
637
+ status: 500,
638
+ });
639
+ res.status(500).json({ error: e.message });
640
+ }
641
+ },
642
+ );
270
643
 
271
- app.get("/all", async (req, res) => {
644
+ app.get("/all", async (req: express.Request, res: express.Response) => {
272
645
  try {
646
+ const contextUserId = requestContext.getStore()?.userId;
273
647
  const limit = parseInt(req.query.limit as string) || 10;
274
- const result = await memory!.search("", { limit });
648
+ const result = await memory!.search("", {
649
+ limit,
650
+ user_id: contextUserId,
651
+ });
652
+ await logActivity("read", {
653
+ client: "rest-api",
654
+ method: "GET",
655
+ endpoint: "/all",
656
+ status: 200,
657
+ });
275
658
  res.json(result);
276
659
  } catch (e: any) {
660
+ await logActivity("read", {
661
+ client: "rest-api",
662
+ method: "GET",
663
+ endpoint: "/all",
664
+ status: 500,
665
+ });
277
666
  res.status(500).json({ error: e.message });
278
667
  }
279
668
  });
@@ -282,18 +671,24 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
282
671
  const transport = new StreamableHTTPServerTransport({
283
672
  sessionIdGenerator: () => crypto.randomUUID(),
284
673
  });
674
+
285
675
  app.all(
286
676
  "/mcp",
287
- async (req, res) => await transport.handleRequest(req, res, req.body),
677
+ async (req: express.Request, res: express.Response) =>
678
+ await transport.handleRequest(req, res, req.body),
288
679
  );
680
+
289
681
  app.all(
290
682
  "/sse",
291
- async (req, res) => await transport.handleRequest(req, res, req.body),
683
+ async (req: express.Request, res: express.Response) =>
684
+ await transport.handleRequest(req, res, req.body),
292
685
  );
293
686
 
294
687
  server.connect(transport).then(() => {
295
688
  app.listen(port, () => {
296
- console.log(`CyberMem MCP running on http://localhost:${port}`);
689
+ console.error(
690
+ `CyberMem MCP (ready: ${(server as any)._memoryReady}) running on http://localhost:${port}`,
691
+ );
297
692
  });
298
693
  });
299
694
  } else {