@cybermem/mcp 0.8.1 → 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/src/index.ts CHANGED
@@ -1,22 +1,44 @@
1
1
  #!/usr/bin/env node
2
+ import "./env.js";
2
3
  /**
3
4
  * CyberMem MCP Server
4
5
  *
5
- * MCP server for AI agents to interact with CyberMem memory system.
6
- * Uses openmemory-js SDK directly (no HTTP, embedded SQLite).
6
+ * Supports two modes:
7
+ * 1. Local/Server Mode (default): Uses openmemory-js SDK directly.
8
+ * 2. Remote Client Mode (with --url): Proxies requests to a remote CyberMem server via HTTP.
7
9
  */
8
10
 
9
11
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
12
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
11
13
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
14
+ import { InitializeRequestSchema } from "@modelcontextprotocol/sdk/types.js";
15
+ import { AsyncLocalStorage } from "async_hooks";
16
+ import axios from "axios";
12
17
  import cors from "cors";
13
- import dotenv from "dotenv";
14
18
  import express from "express";
15
- import { Memory } from "openmemory-js";
16
19
  import { z } from "zod";
17
- import { login, logout, showStatus } from "./auth.js";
20
+ import { getToken, login, logout, showStatus } from "./auth";
21
+
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;
18
36
 
19
- dotenv.config();
37
+ // Async Storage for Request Context (User ID and Client Name)
38
+ const requestContext = new AsyncLocalStorage<{
39
+ userId?: string;
40
+ clientName?: string;
41
+ }>();
20
42
 
21
43
  // Handle CLI auth commands first
22
44
  const args = process.argv.slice(2);
@@ -35,86 +57,38 @@ if (args.includes("--login")) {
35
57
  showStatus();
36
58
  process.exit(0);
37
59
  } else {
38
- // Continue with MCP server startup
39
60
  startServer();
40
61
  }
41
62
 
42
63
  async function startServer() {
43
- // Parse CLI args
44
- const getArg = (name: string): string | undefined => {
64
+ const getArg = (name: string) => {
45
65
  const idx = args.indexOf(name);
46
66
  return idx !== -1 && args[idx + 1] ? args[idx + 1] : undefined;
47
67
  };
48
68
 
49
- const cliClientName = getArg("--client-name");
50
-
51
- // Track client name per session (used in tags)
52
- const currentClientName = cliClientName || "cybermem-mcp";
53
-
54
- // Configure openmemory-js SDK data path
55
- // Use ~/.cybermem/data/ so db-exporter can mount it
56
- const homedir = process.env.HOME || process.env.USERPROFILE || "";
57
- const dataDir = `${homedir}/.cybermem/data`;
58
- process.env.OM_DB_PATH = `${dataDir}/openmemory.sqlite`;
59
-
60
- // Ensure data directory exists
61
- const fs = require("fs");
62
- try {
63
- fs.mkdirSync(dataDir, { recursive: true });
64
- } catch {}
65
-
66
- // Initialize openmemory-js SDK (embedded SQLite)
67
- const memory = new Memory();
69
+ const cliUrl = getArg("--url");
70
+ const cliToken = getArg("--token") || getArg("--api-key") || (cliUrl ? getToken() : undefined);
71
+ let stdioClientName: string | undefined = undefined;
68
72
 
69
- // CyberMem Agent Protocol - instructions sent to clients on handshake
73
+ // Protocol Instructions
70
74
  const CYBERMEM_INSTRUCTIONS = `CyberMem is a persistent context daemon for AI agents.
71
-
72
75
  PROTOCOL:
73
- 1. On session start: call query_memory("user context profile") to load persona
74
- 2. Store new insights immediately with add_memory - include FULL content, not summaries
75
- 3. Refresh context: 6h for active topics, 24h for projects, 7d for insights
76
- 4. Always include tags: [topic, year, source:your-client-name]
77
- 5. Priority: CyberMem context > session context > training data
78
-
79
- MEMORY FORMAT:
80
- - content: Full text with all details, metrics, dates. NO truncation.
81
- - tags: Always include topic category + year + source:client-name
82
-
83
- INTEGRITY RULES:
84
- - Never overwrite without reading first
85
- - Always include metadata (tags, source)
86
- - Sync before critical decisions
87
- - Last-write-wins for conflicts
88
-
76
+ 1. On session start: call query_memory("user context profile")
77
+ 2. Store new insights immediately with add_memory (FULL content)
78
+ 3. Always include tags: [topic, year, source:your-client-name]
89
79
  For full protocol: https://docs.cybermem.dev/agent-protocol`;
90
80
 
91
- // Short protocol reminder for tool descriptions
92
- const PROTOCOL_REMINDER =
93
- "CyberMem Protocol: Store FULL content (no summaries), always include tags [topic, year, source:client-name]. Query 'user context profile' on session start.";
94
-
95
- // Create McpServer instance
96
81
  const server = new McpServer(
82
+ { name: "cybermem", version: "0.7.5" },
97
83
  {
98
- name: "cybermem",
99
- version: "0.8.0",
100
- },
101
- {
102
- capabilities: {
103
- tools: {},
104
- resources: {},
105
- },
106
84
  instructions: CYBERMEM_INSTRUCTIONS,
107
85
  },
108
86
  );
109
87
 
110
- // Register resources
111
88
  server.registerResource(
112
89
  "CyberMem Agent Protocol",
113
90
  "cybermem://protocol",
114
- {
115
- description: "Instructions for AI agents using CyberMem memory system",
116
- mimeType: "text/plain",
117
- },
91
+ { description: "Instructions for AI agents", mimeType: "text/plain" },
118
92
  async () => ({
119
93
  contents: [
120
94
  {
@@ -126,56 +100,291 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
126
100
  }),
127
101
  );
128
102
 
129
- // Register tools using openmemory-js SDK
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
+
121
+ // --- IMPLEMENTATION LOGIC ---
122
+
123
+ let memory: any = null;
124
+ let apiClient: any = null;
125
+
126
+ if (cliUrl) {
127
+ // REMOTE CLIENT MODE
128
+ console.error(`Connecting to remote CyberMem at ${cliUrl}`);
129
+ apiClient = axios.create({
130
+ baseURL: cliUrl,
131
+ headers: {
132
+ "X-API-Key": cliToken,
133
+ Accept: "application/json, text/event-stream",
134
+ "Content-Type": "application/json",
135
+ },
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
+ });
144
+ } else {
145
+ // LOCAL SDK MODE
146
+ const homedir = process.env.HOME || process.env.USERPROFILE || "";
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;
151
+
152
+ // Ensure directory exists
153
+ const fs = await import("fs");
154
+ try {
155
+ const dir = path.dirname(dbPath);
156
+ if (dir) fs.mkdirSync(dir, { recursive: true });
157
+ } catch { }
158
+
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
+ }
213
+ }
214
+
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
+
293
+ const addSourceTag = (tags: string[] = []) => {
294
+ if (!tags.some((t) => t.startsWith("source:"))) {
295
+ const clientName =
296
+ requestContext.getStore()?.clientName || stdioClientName || "unknown";
297
+ tags.push(`source:${clientName}`);
298
+ }
299
+ return tags;
300
+ };
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
+
308
+ // --- TOOLS ---
309
+
130
310
  server.registerTool(
131
311
  "add_memory",
132
312
  {
133
- description: `Store a new memory in CyberMem. ${PROTOCOL_REMINDER}`,
313
+ description: "Store a new memory. " + CYBERMEM_INSTRUCTIONS,
134
314
  inputSchema: z.object({
135
- content: z
136
- .string()
137
- .describe(
138
- "Full content with all details - NO truncation or summarization",
139
- ),
315
+ content: z.string(),
140
316
  user_id: z.string().optional(),
141
- tags: z
142
- .array(z.string())
143
- .optional()
144
- .describe("Always include [topic, year, source:your-client-name]"),
317
+ tags: z.array(z.string()).optional(),
145
318
  }),
146
319
  },
147
- async (args) => {
148
- // Add source tag automatically
149
- const tags = args.tags || [];
150
- if (!tags.some((t) => t.startsWith("source:"))) {
151
- tags.push(`source:${currentClientName}`);
320
+ async (args: any) => {
321
+ const tags = addSourceTag(args.tags);
322
+ const userId = getContextUserId(args.user_id);
323
+
324
+ if (cliUrl) {
325
+ const res = await apiClient.post("/add", {
326
+ ...args,
327
+ user_id: userId,
328
+ tags,
329
+ });
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
+ }
152
351
  }
153
-
154
- const result = await memory.add(args.content, {
155
- user_id: args.user_id,
156
- tags,
157
- });
158
-
159
- return {
160
- content: [{ type: "text", text: JSON.stringify(result) }],
161
- };
162
352
  },
163
353
  );
164
354
 
165
355
  server.registerTool(
166
356
  "query_memory",
167
357
  {
168
- description: `Search for relevant memories. On session start, call query_memory("user context profile") first.`,
169
- inputSchema: z.object({
170
- query: z.string(),
171
- k: z.number().default(5),
172
- }),
358
+ description: "Search memories.",
359
+ inputSchema: z.object({ query: z.string(), k: z.number().default(5) }),
173
360
  },
174
- async (args) => {
175
- const results = await memory.search(args.query, { limit: args.k });
176
- return {
177
- content: [{ type: "text", text: JSON.stringify(results) }],
178
- };
361
+ async (args: any) => {
362
+ const userId = getContextUserId(); // Search is scoped to user if provided
363
+
364
+ if (cliUrl) {
365
+ const res = await apiClient.post("/query", args);
366
+ return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
367
+ } else {
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
+ }
387
+ }
179
388
  },
180
389
  );
181
390
 
@@ -183,107 +392,309 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
183
392
  "list_memories",
184
393
  {
185
394
  description: "List recent memories",
186
- inputSchema: z.object({
187
- limit: z.number().default(10),
188
- }),
395
+ inputSchema: z.object({ limit: z.number().default(10) }),
189
396
  },
190
397
  async (args) => {
191
- // Use search with empty query to list recent
192
- const results = await memory.search("", { limit: args.limit || 10 });
193
- return {
194
- content: [{ type: "text", text: JSON.stringify(results) }],
195
- };
398
+ const userId = getContextUserId();
399
+
400
+ if (cliUrl) {
401
+ try {
402
+ const res = await apiClient.get(`/all?limit=${args.limit}`);
403
+ return {
404
+ content: [{ type: "text", text: JSON.stringify(res.data) }],
405
+ };
406
+ } catch {
407
+ const res = await apiClient.post("/query", {
408
+ query: "",
409
+ k: args.limit,
410
+ });
411
+ return {
412
+ content: [{ type: "text", text: JSON.stringify(res.data) }],
413
+ };
414
+ }
415
+ } else {
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
+ });
425
+ return { content: [{ type: "text", text: JSON.stringify(res) }] };
426
+ }
196
427
  },
197
428
  );
198
429
 
199
430
  server.registerTool(
200
431
  "delete_memory",
201
432
  {
202
- description: "Delete a memory by ID",
203
- inputSchema: z.object({
204
- id: z.string(),
205
- }),
433
+ description: "Delete memory by ID",
434
+ inputSchema: z.object({ id: z.string() }),
206
435
  },
207
- async (args) => {
208
- // openmemory-js doesn't have delete by ID, use wipe for now
209
- // TODO: Implement delete_by_id in SDK or via direct DB query
210
- return {
211
- content: [
212
- {
213
- type: "text",
214
- text: `Delete not yet implemented in SDK. Memory ID: ${args.id}`,
215
- },
216
- ],
217
- };
436
+ async (args: any) => {
437
+ if (cliUrl) {
438
+ const res = await apiClient.delete(`/${args.id}`);
439
+ return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
440
+ } else {
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
+ });
483
+ }
218
484
  },
219
485
  );
220
486
 
221
487
  server.registerTool(
222
488
  "update_memory",
223
489
  {
224
- description: "Update a memory by ID",
225
- inputSchema: z.object({
226
- id: z.string(),
227
- content: z.string().optional(),
228
- tags: z.array(z.string()).optional(),
229
- }),
490
+ description: "Update memory",
491
+ inputSchema: z.object({ id: z.string(), content: z.string().optional() }),
230
492
  },
231
- async (args) => {
232
- // TODO: Implement update in SDK
233
- return {
234
- content: [
235
- {
236
- type: "text",
237
- text: `Update not yet implemented in SDK. Memory ID: ${args.id}`,
238
- },
239
- ],
240
- };
493
+ async (args: any) => {
494
+ await logActivity("update", {
495
+ method: "PATCH",
496
+ endpoint: `/memory/${args.id}`,
497
+ status: 501,
498
+ });
499
+ return { content: [{ type: "text", text: "Update not implemented" }] };
241
500
  },
242
501
  );
243
502
 
244
- // Determine transport mode
245
- const transportArg = args.find(
246
- (arg) => arg === "--stdio" || arg === "--http",
247
- );
248
- const useHttp = transportArg === "--http" || args.includes("--port");
503
+ // --- TRANSPORT ---
504
+
505
+ const useHttp = args.includes("--http") || args.includes("--port");
249
506
 
250
507
  if (useHttp) {
251
- // HTTP mode for testing/development
252
508
  const port = parseInt(getArg("--port") || "3100", 10);
253
509
  const app = express();
254
-
255
510
  app.use(cors());
256
511
  app.use(express.json());
257
512
 
258
- app.get("/health", (_req, res) => {
259
- res.json({ ok: true, version: "0.8.0", mode: "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
+ }
260
571
  });
261
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
+ },
585
+ );
586
+
587
+ if (!cliUrl && memory) {
588
+ app.post("/add", async (req: express.Request, res: express.Response) => {
589
+ try {
590
+ const contextUserId = requestContext.getStore()?.userId;
591
+ const { content, user_id, tags } = req.body;
592
+ const finalTags = addSourceTag(tags);
593
+ const result = await memory!.add(content, {
594
+ user_id: user_id || contextUserId,
595
+ tags: finalTags,
596
+ });
597
+ await logActivity("create", {
598
+ client: "rest-api",
599
+ method: "POST",
600
+ endpoint: "/add",
601
+ status: 200,
602
+ });
603
+ res.json(result);
604
+ } catch (e: any) {
605
+ await logActivity("create", {
606
+ client: "rest-api",
607
+ method: "POST",
608
+ endpoint: "/add",
609
+ status: 500,
610
+ });
611
+ res.status(500).json({ error: e.message });
612
+ }
613
+ });
614
+
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
+ );
643
+
644
+ app.get("/all", async (req: express.Request, res: express.Response) => {
645
+ try {
646
+ const contextUserId = requestContext.getStore()?.userId;
647
+ const limit = parseInt(req.query.limit as string) || 10;
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
+ });
658
+ res.json(result);
659
+ } catch (e: any) {
660
+ await logActivity("read", {
661
+ client: "rest-api",
662
+ method: "GET",
663
+ endpoint: "/all",
664
+ status: 500,
665
+ });
666
+ res.status(500).json({ error: e.message });
667
+ }
668
+ });
669
+ }
670
+
262
671
  const transport = new StreamableHTTPServerTransport({
263
672
  sessionIdGenerator: () => crypto.randomUUID(),
264
673
  });
265
674
 
266
- app.all("/mcp", async (req, res) => {
267
- await transport.handleRequest(req, res, req.body);
268
- });
675
+ app.all(
676
+ "/mcp",
677
+ async (req: express.Request, res: express.Response) =>
678
+ await transport.handleRequest(req, res, req.body),
679
+ );
269
680
 
270
- app.all("/sse", async (req, res) => {
271
- await transport.handleRequest(req, res, req.body);
272
- });
681
+ app.all(
682
+ "/sse",
683
+ async (req: express.Request, res: express.Response) =>
684
+ await transport.handleRequest(req, res, req.body),
685
+ );
273
686
 
274
687
  server.connect(transport).then(() => {
275
688
  app.listen(port, () => {
276
- console.log(
277
- `CyberMem MCP (SDK mode) running on http://localhost:${port}`,
689
+ console.error(
690
+ `CyberMem MCP (ready: ${(server as any)._memoryReady}) running on http://localhost:${port}`,
278
691
  );
279
- console.log("Health: /health | MCP: /mcp");
280
692
  });
281
693
  });
282
694
  } else {
283
- // STDIO mode (default for MCP clients)
284
695
  const transport = new StdioServerTransport();
285
- server.connect(transport).then(() => {
286
- console.error("CyberMem MCP (SDK mode) connected via STDIO");
287
- });
696
+ server
697
+ .connect(transport)
698
+ .then(() => console.error("CyberMem MCP connected via STDIO"));
288
699
  }
289
700
  }