@cybermem/mcp 0.14.8 → 0.14.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @cybermem/mcp
2
2
 
3
+ ## 0.14.11
4
+
5
+ ### Patch Changes
6
+
7
+ - [#120](https://github.com/mikhailkogan17/cybermem/pull/120) [`2319994`](https://github.com/mikhailkogan17/cybermem/commit/2319994f096e4063e2ca4bc4ca02eb8b33f192ce) Thanks [@mikhailkogan17-antigravity](https://github.com/mikhailkogan17-antigravity)! - fix(mcp): remove redundant transport.start() call causing SSE crash loop; switch to SSEServerTransport for multi-client support
8
+ fix(dashboard): update mcp-config API to support SSE and --allow-http
9
+
3
10
  ## 0.14.8
4
11
 
5
12
  ### Patch Changes
package/dist/index.js CHANGED
@@ -12,11 +12,23 @@ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
12
12
  const async_hooks_1 = require("async_hooks");
13
13
  const cors_1 = __importDefault(require("cors"));
14
14
  const express_1 = __importDefault(require("express"));
15
+ const fs_1 = require("fs");
16
+ const path_1 = require("path");
15
17
  const zod_1 = require("zod");
16
18
  // Async Storage for Request Context (User ID and Client Name)
17
19
  const requestContext = new async_hooks_1.AsyncLocalStorage();
18
20
  // CLI args processing
19
21
  const args = process.argv.slice(2);
22
+ // Read version from package.json
23
+ let PACKAGE_VERSION = "0.0.0";
24
+ try {
25
+ const packageJsonPath = (0, path_1.join)(__dirname, "../package.json");
26
+ const packageJson = JSON.parse((0, fs_1.readFileSync)(packageJsonPath, "utf-8"));
27
+ PACKAGE_VERSION = packageJson.version;
28
+ }
29
+ catch (error) {
30
+ console.error("[MCP] Failed to read package.json version", error);
31
+ }
20
32
  // Start the server
21
33
  startServer();
22
34
  async function startServer() {
@@ -97,7 +109,8 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
97
109
  let client;
98
110
  const ctx = requestContext.getStore();
99
111
  if (opts.sessionId) {
100
- client = "sse-client"; // TODO: Extract real client name from session state
112
+ // For SSE sessions, prefer the real client name from the request context when available
113
+ client = ctx?.clientName || "sse-client";
101
114
  }
102
115
  else if (ctx) {
103
116
  client = ctx.clientName || stdioClientName || "unknown";
@@ -114,7 +127,7 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
114
127
  db.run("INSERT INTO cybermem_access_log (timestamp, client_name, client_version, method, endpoint, operation, status, is_error) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
115
128
  ts,
116
129
  client,
117
- "0.12.4",
130
+ PACKAGE_VERSION,
118
131
  method,
119
132
  endpoint,
120
133
  operation,
@@ -127,13 +140,13 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
127
140
  catch { }
128
141
  };
129
142
  // Factory to create configured McpServer instance
130
- const createConfiguredServer = () => {
131
- const server = new mcp_js_1.McpServer({ name: "cybermem", version: "0.12.4" }, {
143
+ const createConfiguredServer = (onClientConnected) => {
144
+ const server = new mcp_js_1.McpServer({ name: "cybermem", version: PACKAGE_VERSION }, {
132
145
  instructions: CYBERMEM_INSTRUCTIONS,
133
146
  });
134
- // access underlying server
135
- server._memoryReady =
136
- true;
147
+ // access underlying server to set internal state for direct memory access
148
+ // Casting to unknown first to allow adding private property
149
+ server._memoryReady = true;
137
150
  server.registerResource("CyberMem Agent Protocol", "cybermem://protocol", { description: "Instructions for AI agents", mimeType: "text/plain" }, async () => ({
138
151
  contents: [
139
152
  {
@@ -148,8 +161,15 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
148
161
  // For SSE multiple clients, stdioClientName global is less useful,
149
162
  // but we can set it for context if running in single-user mode.
150
163
  // For multi-user, rely on requestContext.
151
- stdioClientName = request.params.clientInfo.name;
152
- console.error(`[MCP] Client identified via handshake: ${request.params.clientInfo.name}`);
164
+ // For SSE multiple clients, rely on per-request context instead of a global.
165
+ const clientName = request.params.clientInfo.name;
166
+ if (onClientConnected) {
167
+ onClientConnected(clientName);
168
+ }
169
+ else {
170
+ stdioClientName = clientName;
171
+ }
172
+ console.error(`[MCP] Client identified via handshake: ${clientName}`);
153
173
  return {
154
174
  protocolVersion: "2024-11-05",
155
175
  capabilities: {
@@ -158,7 +178,7 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
158
178
  },
159
179
  serverInfo: {
160
180
  name: "cybermem",
161
- version: "0.12.4",
181
+ version: PACKAGE_VERSION,
162
182
  },
163
183
  };
164
184
  });
@@ -181,7 +201,7 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
181
201
  });
182
202
  server.registerTool("query_memory", {
183
203
  description: "Search memories.",
184
- inputSchema: zod_1.z.object({ query: zod_1.z.string(), k: zod_1.z.number().default(50) }),
204
+ inputSchema: zod_1.z.object({ query: zod_1.z.string(), k: zod_1.z.number().default(5) }),
185
205
  }, async (args) => {
186
206
  const res = await memory.search(args.query, { limit: args.k });
187
207
  await logActivity("read", {
@@ -264,7 +284,7 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
264
284
  const app = (0, express_1.default)();
265
285
  app.use((0, cors_1.default)());
266
286
  app.use(express_1.default.json());
267
- app.get("/health", (req, res) => res.json({ ok: true, version: "0.12.4" }));
287
+ app.get("/health", (req, res) => res.json({ ok: true, version: PACKAGE_VERSION }));
268
288
  app.use((req, res, next) => {
269
289
  const clientName = req.headers["x-client-name"] || "antigravity-client";
270
290
  requestContext.run({ clientName }, next);
@@ -371,7 +391,11 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
371
391
  app.get("/sse", async (req, res) => {
372
392
  console.error("[MCP] Attempting SSE Connection...");
373
393
  const transport = new sse_js_1.SSEServerTransport("/message", res);
374
- const newServer = createConfiguredServer();
394
+ const newServer = createConfiguredServer((name) => {
395
+ const session = sessions.get(transport.sessionId);
396
+ if (session)
397
+ session.clientName = name;
398
+ });
375
399
  try {
376
400
  await newServer.connect(transport);
377
401
  sessions.set(transport.sessionId, { server: newServer, transport });
@@ -383,7 +407,7 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
383
407
  console.error(`[MCP] SSE Connection Error: ${transport.sessionId}`, err);
384
408
  sessions.delete(transport.sessionId);
385
409
  };
386
- await transport.start();
410
+ // await transport.start(); // FIXED: connect() starts it automatically
387
411
  }
388
412
  catch (err) {
389
413
  console.error("[MCP] Failed to start SSE transport:", err);
@@ -402,7 +426,10 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
402
426
  return;
403
427
  }
404
428
  try {
405
- await session.transport.handlePostMessage(req, res);
429
+ const clientName = session.clientName || "sse-client";
430
+ await requestContext.run({ clientName }, async () => {
431
+ await session.transport.handlePostMessage(req, res);
432
+ });
406
433
  }
407
434
  catch (err) {
408
435
  console.error(`[MCP] Error handling message for session ${sessionId}:`, err);
@@ -0,0 +1,81 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { ChildProcess, spawn } from "child_process";
3
+ import path from "path";
4
+
5
+ test.describe("MCP SSE Transport", () => {
6
+ let serverProcess: ChildProcess;
7
+ const PORT = 3101; // Use unique port for this test
8
+
9
+ test.setTimeout(120000);
10
+
11
+ test.beforeAll(async () => {
12
+ // Start the server in http mode
13
+ const serverPath = path.join(__dirname, "../dist/index.js");
14
+ serverProcess = spawn(
15
+ "node",
16
+ [
17
+ serverPath,
18
+ "--port",
19
+ PORT.toString(),
20
+ "--env",
21
+ "test",
22
+ "--db-path",
23
+ ":memory:",
24
+ ],
25
+ {
26
+ stdio: "pipe",
27
+ env: { ...process.env, OM_DB_PATH: ":memory:" },
28
+ },
29
+ );
30
+
31
+ // Wait for server to start
32
+ await new Promise<void>((resolve, reject) => {
33
+ serverProcess.stderr?.on("data", (data) => {
34
+ const output = data.toString();
35
+ console.log("[Server]", output);
36
+ if (
37
+ output.includes(`CyberMem MCP running on http://localhost:${PORT}`)
38
+ ) {
39
+ resolve();
40
+ }
41
+ });
42
+ serverProcess.on("error", reject);
43
+ setTimeout(() => reject(new Error("Server start timeout")), 60000);
44
+ });
45
+ });
46
+
47
+ test.afterAll(() => {
48
+ serverProcess.kill();
49
+ });
50
+
51
+ test("should establish SSE connection without crashing", async () => {
52
+ const response = await fetch(`http://localhost:${PORT}/sse`);
53
+ expect(response.status).toBe(200);
54
+ expect(response.headers.get("content-type")).toBe("text/event-stream");
55
+
56
+ // Read stream for a bit to ensure it doesn't close immediately due to error
57
+ const reader = response.body?.getReader();
58
+ expect(reader).toBeDefined();
59
+
60
+ const decoder = new TextDecoder();
61
+ let endpointFound = false;
62
+
63
+ // Read first few chunks
64
+ for (let i = 0; i < 3; i++) {
65
+ const { value, done } = await reader!.read();
66
+ if (done) break;
67
+ const text = decoder.decode(value);
68
+ if (text.includes("event: endpoint")) {
69
+ endpointFound = true;
70
+ }
71
+ }
72
+
73
+ expect(endpointFound).toBe(true);
74
+
75
+ // Cleanup connection
76
+ await reader?.cancel();
77
+
78
+ // Check if server process is still running (didn't crash)
79
+ expect(serverProcess.exitCode).toBeNull();
80
+ });
81
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cybermem/mcp",
3
- "version": "0.14.8",
3
+ "version": "0.14.11",
4
4
  "description": "CyberMem MCP Server - AI Memory with openmemory-js SDK",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -11,7 +11,7 @@ export default defineConfig({
11
11
  projects: [
12
12
  {
13
13
  name: "api",
14
- testMatch: "api.spec.ts",
14
+ testMatch: "*.spec.ts",
15
15
  },
16
16
  ],
17
17
  });
package/src/index.ts CHANGED
@@ -8,12 +8,25 @@ import { InitializeRequestSchema } from "@modelcontextprotocol/sdk/types.js";
8
8
  import { AsyncLocalStorage } from "async_hooks";
9
9
  import cors from "cors";
10
10
  import express from "express";
11
+ import { readFileSync } from "fs";
12
+ import { join } from "path";
11
13
  import { z } from "zod";
12
14
 
13
15
  // Type definition for OpenMemory Memory class
14
16
  interface IMemory {
15
- add(content: string, opts?: { tags?: string[]; user_id?: string; [key: string]: unknown }): Promise<unknown>;
16
- search(query: string, opts?: { limit?: number; user_id?: string; sectors?: unknown; [key: string]: unknown }): Promise<unknown>;
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>;
17
30
  get(id: string): Promise<unknown>;
18
31
  delete_all(user_id: string): Promise<unknown>;
19
32
  wipe(): Promise<unknown>;
@@ -28,6 +41,16 @@ const requestContext = new AsyncLocalStorage<{
28
41
  // CLI args processing
29
42
  const args = process.argv.slice(2);
30
43
 
44
+ // Read version from package.json
45
+ let PACKAGE_VERSION = "0.0.0";
46
+ try {
47
+ const packageJsonPath = join(__dirname, "../package.json");
48
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
49
+ PACKAGE_VERSION = packageJson.version;
50
+ } catch (error) {
51
+ console.error("[MCP] Failed to read package.json version", error);
52
+ }
53
+
31
54
  // Start the server
32
55
  startServer();
33
56
 
@@ -47,8 +70,17 @@ async function startServer() {
47
70
  // --- IMPLEMENTATION LOGIC ---
48
71
 
49
72
  let memory: IMemory | null = null;
50
- let sdk_update_memory: ((id: string, content?: string, tags?: string[], metadata?: Record<string, unknown>) => Promise<unknown>) | null = null;
51
- let sdk_reinforce_memory: ((id: string, boost?: number) => Promise<unknown>) | 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;
52
84
 
53
85
  // LOCAL SDK MODE
54
86
  const dbPath = process.env.OM_DB_PATH!;
@@ -122,15 +154,10 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
122
154
  const logActivity = async (
123
155
  operation: string,
124
156
  opts: {
125
- details?: unknown;
126
- query?: string;
127
- memoryId?: string;
128
- delta?: string;
129
- tags?: string[];
130
- sessionId?: string;
131
157
  method?: string;
132
158
  endpoint?: string;
133
159
  status?: number;
160
+ sessionId?: string;
134
161
  } = {},
135
162
  ) => {
136
163
  // Determine client name (priority: specific > store > default)
@@ -138,7 +165,8 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
138
165
  const ctx = requestContext.getStore();
139
166
 
140
167
  if (opts.sessionId) {
141
- client = "sse-client"; // TODO: Extract real client name from session state
168
+ // For SSE sessions, prefer the real client name from the request context when available
169
+ client = ctx?.clientName || "sse-client";
142
170
  } else if (ctx) {
143
171
  client = ctx.clientName || stdioClientName || "unknown";
144
172
  } else {
@@ -156,7 +184,7 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
156
184
  [
157
185
  ts,
158
186
  client,
159
- "0.12.4",
187
+ PACKAGE_VERSION,
160
188
  method,
161
189
  endpoint,
162
190
  operation,
@@ -173,17 +201,19 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
173
201
  };
174
202
 
175
203
  // Factory to create configured McpServer instance
176
- const createConfiguredServer = () => {
204
+ const createConfiguredServer = (
205
+ onClientConnected?: (name: string) => void,
206
+ ) => {
177
207
  const server = new McpServer(
178
- { name: "cybermem", version: "0.12.4" },
208
+ { name: "cybermem", version: PACKAGE_VERSION },
179
209
  {
180
210
  instructions: CYBERMEM_INSTRUCTIONS,
181
211
  },
182
212
  );
183
213
 
184
- // access underlying server
185
- (server as unknown as McpServer & { _memoryReady: boolean })._memoryReady =
186
- true;
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;
187
217
 
188
218
  server.registerResource(
189
219
  "CyberMem Agent Protocol",
@@ -207,10 +237,14 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
207
237
  // For SSE multiple clients, stdioClientName global is less useful,
208
238
  // but we can set it for context if running in single-user mode.
209
239
  // For multi-user, rely on requestContext.
210
- stdioClientName = request.params.clientInfo.name;
211
- console.error(
212
- `[MCP] Client identified via handshake: ${request.params.clientInfo.name}`,
213
- );
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}`);
214
248
  return {
215
249
  protocolVersion: "2024-11-05",
216
250
  capabilities: {
@@ -219,7 +253,7 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
219
253
  },
220
254
  serverInfo: {
221
255
  name: "cybermem",
222
- version: "0.12.4",
256
+ version: PACKAGE_VERSION,
223
257
  },
224
258
  };
225
259
  },
@@ -252,7 +286,7 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
252
286
  "query_memory",
253
287
  {
254
288
  description: "Search memories.",
255
- inputSchema: z.object({ query: z.string(), k: z.number().default(50) }),
289
+ inputSchema: z.object({ query: z.string(), k: z.number().default(5) }),
256
290
  },
257
291
  async (args: { query: string; k?: number }) => {
258
292
  const res = await memory!.search(args.query, { limit: args.k });
@@ -279,7 +313,9 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
279
313
  async (args: { id: string; content?: string; tags?: string[] }) => {
280
314
  if (!sdk_update_memory) throw new Error("Update not available in SDK");
281
315
  if (args.content === undefined && args.tags === undefined) {
282
- throw new Error("At least one of 'content' or 'tags' must be provided to update_memory");
316
+ throw new Error(
317
+ "At least one of 'content' or 'tags' must be provided to update_memory",
318
+ );
283
319
  }
284
320
  const res = await sdk_update_memory(args.id, args.content, args.tags);
285
321
  await logActivity("update", {
@@ -357,7 +393,9 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
357
393
  const app = express();
358
394
  app.use(cors());
359
395
  app.use(express.json());
360
- app.get("/health", (req, res) => res.json({ ok: true, version: "0.12.4" }));
396
+ app.get("/health", (req, res) =>
397
+ res.json({ ok: true, version: PACKAGE_VERSION }),
398
+ );
361
399
 
362
400
  app.use((req, res, next) => {
363
401
  const clientName =
@@ -467,6 +505,7 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
467
505
  {
468
506
  server: McpServer;
469
507
  transport: SSEServerTransport;
508
+ clientName?: string;
470
509
  }
471
510
  >();
472
511
 
@@ -482,7 +521,10 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
482
521
  app.get("/sse", async (req, res) => {
483
522
  console.error("[MCP] Attempting SSE Connection...");
484
523
  const transport = new SSEServerTransport("/message", res);
485
- const newServer = createConfiguredServer();
524
+ const newServer = createConfiguredServer((name) => {
525
+ const session = sessions.get(transport.sessionId);
526
+ if (session) session.clientName = name;
527
+ });
486
528
 
487
529
  try {
488
530
  await newServer.connect(transport);
@@ -500,7 +542,7 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
500
542
  sessions.delete(transport.sessionId);
501
543
  };
502
544
 
503
- await transport.start();
545
+ // await transport.start(); // FIXED: connect() starts it automatically
504
546
  } catch (err) {
505
547
  console.error("[MCP] Failed to start SSE transport:", err);
506
548
  sessions.delete(transport.sessionId);
@@ -519,7 +561,10 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
519
561
  return;
520
562
  }
521
563
  try {
522
- await session.transport.handlePostMessage(req, res);
564
+ const clientName = session.clientName || "sse-client";
565
+ await requestContext.run({ clientName }, async () => {
566
+ await session.transport.handlePostMessage(req, res);
567
+ });
523
568
  } catch (err) {
524
569
  console.error(
525
570
  `[MCP] Error handling message for session ${sessionId}:`,