@cybermem/mcp 0.14.6 → 0.14.10

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,25 @@
1
1
  # @cybermem/mcp
2
2
 
3
+ ## 0.14.9
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
+
10
+ ## 0.14.8
11
+
12
+ ### Patch Changes
13
+
14
+ - Automated patch version bump.
15
+
16
+ ## 0.14.7
17
+
18
+ ### Patch Changes
19
+
20
+ - [#114](https://github.com/mikhailkogan17/cybermem/pull/114) [`7871ba9`](https://github.com/mikhailkogan17/cybermem/commit/7871ba96c9008a8188a84bc379e9687e716ed9e9) Thanks [@mikhailkogan17-antigravity](https://github.com/mikhailkogan17-antigravity)! - fix(mcp): switch to SSEServerTransport for multi-client support
21
+ fix(dashboard): update mcp-config API to support SSE and --allow-http
22
+
3
23
  ## 0.14.6
4
24
 
5
25
  ### Patch Changes
package/dist/index.js CHANGED
@@ -6,17 +6,29 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  require("./console-fix.js");
7
7
  require("./env.js");
8
8
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
9
+ const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js");
9
10
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
10
- const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
11
11
  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() {
@@ -29,45 +41,6 @@ async function startServer() {
29
41
  console.error("[MCP] Running in Staging environment");
30
42
  process.env.CYBERMEM_ENV = "staging";
31
43
  }
32
- let stdioClientName = undefined;
33
- // Protocol Instructions
34
- const CYBERMEM_INSTRUCTIONS = `CyberMem is a persistent context daemon for AI agents.
35
- PROTOCOL:
36
- 1. On session start: call query_memory("user context profile")
37
- 2. Store new insights immediately with add_memory (STABLE data)
38
- 3. For corrections: use update_memory (STRUCTURAL mutation, high cost)
39
- 4. To prevent decay: use reinforce_memory (METABOLIC boost, low cost)
40
- 5. Always include tags: [topic, year, source:your-client-name]
41
- For full protocol: https://docs.cybermem.dev/agent-protocol`;
42
- const server = new mcp_js_1.McpServer({ name: "cybermem", version: "0.12.4" }, {
43
- instructions: CYBERMEM_INSTRUCTIONS,
44
- });
45
- server.registerResource("CyberMem Agent Protocol", "cybermem://protocol", { description: "Instructions for AI agents", mimeType: "text/plain" }, async () => ({
46
- contents: [
47
- {
48
- uri: "cybermem://protocol",
49
- mimeType: "text/plain",
50
- text: CYBERMEM_INSTRUCTIONS,
51
- },
52
- ],
53
- }));
54
- // Capture client info from handshake
55
- // @ts-ignore - access underlying server
56
- server.server.setRequestHandler(types_js_1.InitializeRequestSchema, async (request) => {
57
- stdioClientName = request.params.clientInfo.name;
58
- console.error(`[MCP] Client identified via handshake: ${stdioClientName}`);
59
- return {
60
- protocolVersion: "2024-11-05",
61
- capabilities: {
62
- tools: { listChanged: true },
63
- resources: { subscribe: true },
64
- },
65
- serverInfo: {
66
- name: "cybermem",
67
- version: "0.12.4",
68
- },
69
- };
70
- });
71
44
  // --- IMPLEMENTATION LOGIC ---
72
45
  let memory = null;
73
46
  let sdk_update_memory = null;
@@ -88,7 +61,6 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
88
61
  sdk_update_memory = hsg.update_memory;
89
62
  sdk_reinforce_memory = hsg.reinforce_memory;
90
63
  memory = new Memory();
91
- server._memoryReady = true;
92
64
  // Initialize Tables
93
65
  const sqlite3 = await import("sqlite3");
94
66
  const db = new sqlite3.default.Database(dbPath);
@@ -122,24 +94,40 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
122
94
  });
123
95
  });
124
96
  };
97
+ let stdioClientName = undefined;
98
+ // Protocol Instructions
99
+ const CYBERMEM_INSTRUCTIONS = `CyberMem is a persistent context daemon for AI agents.
100
+ PROTOCOL:
101
+ 1. On session start: call query_memory("user context profile")
102
+ 2. Store new insights immediately with add_memory (STABLE data)
103
+ 3. For corrections: use update_memory (STRUCTURAL mutation, high cost)
104
+ 4. To prevent decay: use reinforce_memory (METABOLIC boost, low cost)
105
+ 5. Always include tags: [topic, year, source:your-client-name]
106
+ For full protocol: https://docs.cybermem.dev/agent-protocol`;
125
107
  const logActivity = async (operation, opts = {}) => {
126
- if (!memory)
127
- return;
128
- const { client: providedClient, method = "POST", endpoint = "/mcp", status = 200, } = opts;
108
+ // Determine client name (priority: specific > store > default)
109
+ let client;
129
110
  const ctx = requestContext.getStore();
130
- const client = providedClient ||
131
- ctx?.clientName ||
132
- stdioClientName ||
133
- "antigravity-client";
111
+ if (opts.sessionId) {
112
+ // For SSE sessions, prefer the real client name from the request context when available
113
+ client = ctx?.clientName || "sse-client";
114
+ }
115
+ else if (ctx) {
116
+ client = ctx.clientName || stdioClientName || "unknown";
117
+ }
118
+ else {
119
+ client = stdioClientName || "unknown";
120
+ }
121
+ const { method = "POST", endpoint = "/mcp", status = 200 } = opts;
134
122
  try {
135
- const db = (await initLoggingDb());
123
+ const db = await initLoggingDb();
136
124
  const ts = Date.now();
137
125
  const is_error = status >= 400 ? 1 : 0;
138
126
  db.serialize(() => {
139
127
  db.run("INSERT INTO cybermem_access_log (timestamp, client_name, client_version, method, endpoint, operation, status, is_error) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
140
128
  ts,
141
129
  client,
142
- "0.12.4",
130
+ PACKAGE_VERSION,
143
131
  method,
144
132
  endpoint,
145
133
  operation,
@@ -151,92 +139,143 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
151
139
  }
152
140
  catch { }
153
141
  };
154
- // TOOLS
155
- server.registerTool("add_memory", {
156
- description: "Store a new memory. Use for high-quality, stable data. " +
157
- CYBERMEM_INSTRUCTIONS,
158
- inputSchema: zod_1.z.object({
159
- content: zod_1.z.string(),
160
- tags: zod_1.z.array(zod_1.z.string()).optional(),
161
- }),
162
- }, async (args) => {
163
- const res = await memory.add(args.content, { tags: args.tags });
164
- await logActivity("create", {
165
- method: "POST",
166
- endpoint: "/memory/add",
167
- status: 200,
142
+ // Factory to create configured McpServer instance
143
+ const createConfiguredServer = (onClientConnected) => {
144
+ const server = new mcp_js_1.McpServer({ name: "cybermem", version: PACKAGE_VERSION }, {
145
+ instructions: CYBERMEM_INSTRUCTIONS,
168
146
  });
169
- return { content: [{ type: "text", text: JSON.stringify(res) }] };
170
- });
171
- server.registerTool("query_memory", {
172
- description: "Search memories.",
173
- inputSchema: zod_1.z.object({ query: zod_1.z.string(), k: zod_1.z.number().default(5) }),
174
- }, async (args) => {
175
- const res = await memory.search(args.query, { limit: args.k });
176
- await logActivity("read", {
177
- method: "POST",
178
- endpoint: "/memory/query",
179
- status: 200,
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;
150
+ server.registerResource("CyberMem Agent Protocol", "cybermem://protocol", { description: "Instructions for AI agents", mimeType: "text/plain" }, async () => ({
151
+ contents: [
152
+ {
153
+ uri: "cybermem://protocol",
154
+ mimeType: "text/plain",
155
+ text: CYBERMEM_INSTRUCTIONS,
156
+ },
157
+ ],
158
+ }));
159
+ // access underlying server
160
+ server.server.setRequestHandler(types_js_1.InitializeRequestSchema, async (request) => {
161
+ // For SSE multiple clients, stdioClientName global is less useful,
162
+ // but we can set it for context if running in single-user mode.
163
+ // For multi-user, rely on requestContext.
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}`);
173
+ return {
174
+ protocolVersion: "2024-11-05",
175
+ capabilities: {
176
+ tools: { listChanged: true },
177
+ resources: { subscribe: true },
178
+ },
179
+ serverInfo: {
180
+ name: "cybermem",
181
+ version: PACKAGE_VERSION,
182
+ },
183
+ };
180
184
  });
181
- return { content: [{ type: "text", text: JSON.stringify(res) }] };
182
- });
183
- server.registerTool("update_memory", {
184
- description: "Mutate existing memory (content/tags). HIGH COST: re-embeds and re-links. Use for corrections.",
185
- inputSchema: zod_1.z.object({
186
- id: zod_1.z.string(),
187
- content: zod_1.z.string().optional(),
188
- tags: zod_1.z.array(zod_1.z.string()).optional(),
189
- }),
190
- }, async (args) => {
191
- if (!sdk_update_memory)
192
- throw new Error("Update not available in SDK");
193
- const res = await sdk_update_memory(args.id, args.content, args.tags);
194
- await logActivity("update", {
195
- method: "PATCH",
196
- endpoint: `/memory/${args.id}`,
197
- status: 200,
185
+ // TOOLS
186
+ server.registerTool("add_memory", {
187
+ description: "Store a new memory. Use for high-quality, stable data. " +
188
+ CYBERMEM_INSTRUCTIONS,
189
+ inputSchema: zod_1.z.object({
190
+ content: zod_1.z.string(),
191
+ tags: zod_1.z.array(zod_1.z.string()).optional(),
192
+ }),
193
+ }, async (args) => {
194
+ const res = await memory.add(args.content, { tags: args.tags });
195
+ await logActivity("create", {
196
+ method: "POST",
197
+ endpoint: "/memory/add",
198
+ status: 200,
199
+ });
200
+ return { content: [{ type: "text", text: JSON.stringify(res) }] };
198
201
  });
199
- return { content: [{ type: "text", text: JSON.stringify(res) }] };
200
- });
201
- server.registerTool("reinforce_memory", {
202
- description: "Metabolic boost (salience). LOW COST: prevents decay without mutation. Use for active topics.",
203
- inputSchema: zod_1.z.object({ id: zod_1.z.string(), boost: zod_1.z.number().default(0.1) }),
204
- }, async (args) => {
205
- if (!sdk_reinforce_memory)
206
- throw new Error("Reinforce not available in SDK");
207
- await sdk_reinforce_memory(args.id, args.boost);
208
- await logActivity("update", {
209
- method: "POST",
210
- endpoint: `/memory/${args.id}/reinforce`,
211
- status: 200,
202
+ server.registerTool("query_memory", {
203
+ description: "Search memories.",
204
+ inputSchema: zod_1.z.object({ query: zod_1.z.string(), k: zod_1.z.number().default(5) }),
205
+ }, async (args) => {
206
+ const res = await memory.search(args.query, { limit: args.k });
207
+ await logActivity("read", {
208
+ method: "POST",
209
+ endpoint: "/memory/query",
210
+ status: 200,
211
+ });
212
+ return { content: [{ type: "text", text: JSON.stringify(res) }] };
212
213
  });
213
- return { content: [{ type: "text", text: "Reinforced" }] };
214
- });
215
- server.registerTool("delete_memory", {
216
- description: "Delete memory",
217
- inputSchema: zod_1.z.object({ id: zod_1.z.string() }),
218
- }, async (args) => {
219
- const dbPath = process.env.OM_DB_PATH;
220
- const sqlite3 = await import("sqlite3");
221
- const db = new sqlite3.default.Database(dbPath);
222
- return new Promise((resolve, reject) => {
223
- db.serialize(() => {
224
- db.run("DELETE FROM memories WHERE id = ?", [args.id]);
225
- db.run("DELETE FROM vectors WHERE id = ?", [args.id], async (err) => {
226
- db.close();
227
- await logActivity("delete", {
228
- method: "DELETE",
229
- endpoint: `/memory/${args.id}`,
230
- status: err ? 500 : 200,
214
+ server.registerTool("update_memory", {
215
+ description: "Mutate existing memory (content/tags). HIGH COST: re-embeds and re-links. Use for corrections.",
216
+ inputSchema: zod_1.z.object({
217
+ id: zod_1.z.string(),
218
+ content: zod_1.z.string().optional(),
219
+ tags: zod_1.z.array(zod_1.z.string()).optional(),
220
+ }),
221
+ }, async (args) => {
222
+ if (!sdk_update_memory)
223
+ throw new Error("Update not available in SDK");
224
+ if (args.content === undefined && args.tags === undefined) {
225
+ throw new Error("At least one of 'content' or 'tags' must be provided to update_memory");
226
+ }
227
+ const res = await sdk_update_memory(args.id, args.content, args.tags);
228
+ await logActivity("update", {
229
+ method: "PATCH",
230
+ endpoint: `/memory/${args.id}`,
231
+ status: 200,
232
+ });
233
+ return { content: [{ type: "text", text: JSON.stringify(res) }] };
234
+ });
235
+ server.registerTool("reinforce_memory", {
236
+ description: "Metabolic boost (salience). LOW COST: prevents decay without mutation. Use for active topics.",
237
+ inputSchema: zod_1.z.object({
238
+ id: zod_1.z.string(),
239
+ boost: zod_1.z.number().default(0.1),
240
+ }),
241
+ }, async (args) => {
242
+ if (!sdk_reinforce_memory)
243
+ throw new Error("Reinforce not available in SDK");
244
+ const res = await sdk_reinforce_memory(args.id, args.boost);
245
+ await logActivity("update", {
246
+ method: "POST",
247
+ endpoint: `/memory/${args.id}/reinforce`,
248
+ status: 200,
249
+ });
250
+ return { content: [{ type: "text", text: JSON.stringify(res) }] };
251
+ });
252
+ server.registerTool("delete_memory", {
253
+ description: "Delete memory",
254
+ inputSchema: zod_1.z.object({ id: zod_1.z.string() }),
255
+ }, async (args) => {
256
+ const dbPath = process.env.OM_DB_PATH;
257
+ const sqlite3 = await import("sqlite3");
258
+ const db = new sqlite3.default.Database(dbPath);
259
+ return new Promise((resolve, reject) => {
260
+ db.serialize(() => {
261
+ db.run("DELETE FROM memories WHERE id = ?", [args.id]);
262
+ db.run("DELETE FROM vectors WHERE id = ?", [args.id], async (err) => {
263
+ db.close();
264
+ await logActivity("delete", {
265
+ method: "DELETE",
266
+ endpoint: `/memory/${args.id}`,
267
+ status: err ? 500 : 200,
268
+ });
269
+ if (err)
270
+ reject(err);
271
+ else
272
+ resolve({ content: [{ type: "text", text: "Deleted" }] });
231
273
  });
232
- if (err)
233
- reject(err);
234
- else
235
- resolve({ content: [{ type: "text", text: "Deleted" }] });
236
274
  });
237
275
  });
238
276
  });
239
- });
277
+ return server;
278
+ };
240
279
  // EXPRESS SERVER
241
280
  // HTTP server mode for Docker/Traefik deployment
242
281
  const useHttp = args.includes("--http") || args.includes("--port");
@@ -245,11 +284,10 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
245
284
  const app = (0, express_1.default)();
246
285
  app.use((0, cors_1.default)());
247
286
  app.use(express_1.default.json());
248
- 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 }));
249
288
  app.use((req, res, next) => {
250
289
  const clientName = req.headers["x-client-name"] || "antigravity-client";
251
290
  requestContext.run({ clientName }, next);
252
- // next(); // DELETED! Correctly handled by requestContext.run
253
291
  });
254
292
  if (memory) {
255
293
  app.post("/add", async (req, res) => {
@@ -342,17 +380,70 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
342
380
  });
343
381
  });
344
382
  }
345
- const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
346
- sessionIdGenerator: () => crypto.randomUUID(),
383
+ // MULTI-SESSION SSE SUPPORT
384
+ const sessions = new Map();
385
+ // Legacy MCP endpoint - 410 Gone
386
+ app.all("/mcp", (req, res) => {
387
+ res
388
+ .status(410)
389
+ .send("Endpoint /mcp is deprecated. Please update your client configuration to use /sse for Server-Sent Events.");
347
390
  });
348
- app.all("/mcp", async (req, res) => await transport.handleRequest(req, res, req.body));
349
- app.all("/sse", async (req, res) => await transport.handleRequest(req, res, req.body));
350
- server.connect(transport).then(() => {
351
- app.listen(port, () => console.error(`CyberMem MCP running on http://localhost:${port}`));
391
+ app.get("/sse", async (req, res) => {
392
+ console.error("[MCP] Attempting SSE Connection...");
393
+ const transport = new sse_js_1.SSEServerTransport("/message", res);
394
+ const newServer = createConfiguredServer((name) => {
395
+ const session = sessions.get(transport.sessionId);
396
+ if (session)
397
+ session.clientName = name;
398
+ });
399
+ try {
400
+ await newServer.connect(transport);
401
+ sessions.set(transport.sessionId, { server: newServer, transport });
402
+ transport.onclose = () => {
403
+ console.error(`[MCP] SSE Connection Closed: ${transport.sessionId}`);
404
+ sessions.delete(transport.sessionId);
405
+ };
406
+ transport.onerror = (err) => {
407
+ console.error(`[MCP] SSE Connection Error: ${transport.sessionId}`, err);
408
+ sessions.delete(transport.sessionId);
409
+ };
410
+ // await transport.start(); // FIXED: connect() starts it automatically
411
+ }
412
+ catch (err) {
413
+ console.error("[MCP] Failed to start SSE transport:", err);
414
+ sessions.delete(transport.sessionId);
415
+ // If headers haven't been sent, send 500
416
+ if (!res.headersSent) {
417
+ res.status(500).send("Internal Server Error during SSE handshake");
418
+ }
419
+ }
420
+ });
421
+ app.post("/message", async (req, res) => {
422
+ const sessionId = req.query.sessionId;
423
+ const session = sessions.get(sessionId);
424
+ if (!session) {
425
+ res.status(404).send("Session not found");
426
+ return;
427
+ }
428
+ try {
429
+ const clientName = session.clientName || "sse-client";
430
+ await requestContext.run({ clientName }, async () => {
431
+ await session.transport.handlePostMessage(req, res);
432
+ });
433
+ }
434
+ catch (err) {
435
+ console.error(`[MCP] Error handling message for session ${sessionId}:`, err);
436
+ if (!res.headersSent) {
437
+ res.status(500).send("Internal Server Error processing message");
438
+ }
439
+ }
352
440
  });
441
+ app.listen(port, () => console.error(`CyberMem MCP running on http://localhost:${port}`));
353
442
  }
354
443
  else {
444
+ // STDIO
355
445
  const transport = new stdio_js_1.StdioServerTransport();
446
+ const server = createConfiguredServer();
356
447
  server
357
448
  .connect(transport)
358
449
  .then(() => console.error("CyberMem MCP connected via STDIO"));
@@ -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.6",
3
+ "version": "0.14.10",
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
@@ -2,14 +2,36 @@ import "./console-fix.js";
2
2
  import "./env.js";
3
3
 
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
5
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
7
7
  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
 
15
+ // Type definition for OpenMemory Memory class
16
+ interface IMemory {
17
+ add(
18
+ content: string,
19
+ opts?: { tags?: string[]; user_id?: string; [key: string]: unknown },
20
+ ): Promise<unknown>;
21
+ search(
22
+ query: string,
23
+ opts?: {
24
+ limit?: number;
25
+ user_id?: string;
26
+ sectors?: unknown;
27
+ [key: string]: unknown;
28
+ },
29
+ ): Promise<unknown>;
30
+ get(id: string): Promise<unknown>;
31
+ delete_all(user_id: string): Promise<unknown>;
32
+ wipe(): Promise<unknown>;
33
+ }
34
+
13
35
  // Async Storage for Request Context (User ID and Client Name)
14
36
  const requestContext = new AsyncLocalStorage<{
15
37
  userId?: string;
@@ -19,6 +41,16 @@ const requestContext = new AsyncLocalStorage<{
19
41
  // CLI args processing
20
42
  const args = process.argv.slice(2);
21
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
+
22
54
  // Start the server
23
55
  startServer();
24
56
 
@@ -35,63 +67,20 @@ async function startServer() {
35
67
  process.env.CYBERMEM_ENV = "staging";
36
68
  }
37
69
 
38
- let stdioClientName: string | undefined = undefined;
39
-
40
- // Protocol Instructions
41
- const CYBERMEM_INSTRUCTIONS = `CyberMem is a persistent context daemon for AI agents.
42
- PROTOCOL:
43
- 1. On session start: call query_memory("user context profile")
44
- 2. Store new insights immediately with add_memory (STABLE data)
45
- 3. For corrections: use update_memory (STRUCTURAL mutation, high cost)
46
- 4. To prevent decay: use reinforce_memory (METABOLIC boost, low cost)
47
- 5. Always include tags: [topic, year, source:your-client-name]
48
- For full protocol: https://docs.cybermem.dev/agent-protocol`;
49
-
50
- const server = new McpServer(
51
- { name: "cybermem", version: "0.12.4" },
52
- {
53
- instructions: CYBERMEM_INSTRUCTIONS,
54
- },
55
- );
56
-
57
- server.registerResource(
58
- "CyberMem Agent Protocol",
59
- "cybermem://protocol",
60
- { description: "Instructions for AI agents", mimeType: "text/plain" },
61
- async () => ({
62
- contents: [
63
- {
64
- uri: "cybermem://protocol",
65
- mimeType: "text/plain",
66
- text: CYBERMEM_INSTRUCTIONS,
67
- },
68
- ],
69
- }),
70
- );
71
-
72
- // Capture client info from handshake
73
- // @ts-ignore - access underlying server
74
- server.server.setRequestHandler(InitializeRequestSchema, async (request) => {
75
- stdioClientName = request.params.clientInfo.name;
76
- console.error(`[MCP] Client identified via handshake: ${stdioClientName}`);
77
- return {
78
- protocolVersion: "2024-11-05",
79
- capabilities: {
80
- tools: { listChanged: true },
81
- resources: { subscribe: true },
82
- },
83
- serverInfo: {
84
- name: "cybermem",
85
- version: "0.12.4",
86
- },
87
- };
88
- });
89
-
90
70
  // --- IMPLEMENTATION LOGIC ---
91
71
 
92
- let memory: any = null;
93
- let sdk_update_memory: any = null;
94
- let sdk_reinforce_memory: any = null;
72
+ let memory: IMemory | null = null;
73
+ let sdk_update_memory:
74
+ | ((
75
+ id: string,
76
+ content?: string,
77
+ tags?: string[],
78
+ metadata?: Record<string, unknown>,
79
+ ) => Promise<unknown>)
80
+ | null = null;
81
+ let sdk_reinforce_memory:
82
+ | ((id: string, boost?: number) => Promise<unknown>)
83
+ | null = null;
95
84
 
96
85
  // LOCAL SDK MODE
97
86
  const dbPath = process.env.OM_DB_PATH!;
@@ -107,8 +96,7 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
107
96
  const hsg = await import("openmemory-js/dist/memory/hsg.js");
108
97
  sdk_update_memory = hsg.update_memory;
109
98
  sdk_reinforce_memory = hsg.reinforce_memory;
110
- memory = new Memory();
111
- (server as any)._memoryReady = true;
99
+ memory = new Memory() as IMemory;
112
100
 
113
101
  // Initialize Tables
114
102
  const sqlite3 = await import("sqlite3");
@@ -151,30 +139,43 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
151
139
  });
152
140
  };
153
141
 
142
+ let stdioClientName: string | undefined = undefined;
143
+
144
+ // Protocol Instructions
145
+ const CYBERMEM_INSTRUCTIONS = `CyberMem is a persistent context daemon for AI agents.
146
+ PROTOCOL:
147
+ 1. On session start: call query_memory("user context profile")
148
+ 2. Store new insights immediately with add_memory (STABLE data)
149
+ 3. For corrections: use update_memory (STRUCTURAL mutation, high cost)
150
+ 4. To prevent decay: use reinforce_memory (METABOLIC boost, low cost)
151
+ 5. Always include tags: [topic, year, source:your-client-name]
152
+ For full protocol: https://docs.cybermem.dev/agent-protocol`;
153
+
154
154
  const logActivity = async (
155
155
  operation: string,
156
156
  opts: {
157
- client?: string;
158
157
  method?: string;
159
158
  endpoint?: string;
160
159
  status?: number;
160
+ sessionId?: string;
161
161
  } = {},
162
162
  ) => {
163
- if (!memory) return;
164
- const {
165
- client: providedClient,
166
- method = "POST",
167
- endpoint = "/mcp",
168
- status = 200,
169
- } = opts;
163
+ // Determine client name (priority: specific > store > default)
164
+ let client: string;
170
165
  const ctx = requestContext.getStore();
171
- const client =
172
- providedClient ||
173
- ctx?.clientName ||
174
- stdioClientName ||
175
- "antigravity-client";
166
+
167
+ if (opts.sessionId) {
168
+ // For SSE sessions, prefer the real client name from the request context when available
169
+ client = ctx?.clientName || "sse-client";
170
+ } else if (ctx) {
171
+ client = ctx.clientName || stdioClientName || "unknown";
172
+ } else {
173
+ client = stdioClientName || "unknown";
174
+ }
175
+
176
+ const { method = "POST", endpoint = "/mcp", status = 200 } = opts;
176
177
  try {
177
- const db = (await initLoggingDb()) as any;
178
+ const db = await initLoggingDb();
178
179
  const ts = Date.now();
179
180
  const is_error = status >= 400 ? 1 : 0;
180
181
  db.serialize(() => {
@@ -183,7 +184,7 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
183
184
  [
184
185
  ts,
185
186
  client,
186
- "0.12.4",
187
+ PACKAGE_VERSION,
187
188
  method,
188
189
  endpoint,
189
190
  operation,
@@ -199,120 +200,190 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
199
200
  } catch {}
200
201
  };
201
202
 
202
- // TOOLS
203
- server.registerTool(
204
- "add_memory",
205
- {
206
- description:
207
- "Store a new memory. Use for high-quality, stable data. " +
208
- CYBERMEM_INSTRUCTIONS,
209
- inputSchema: z.object({
210
- content: z.string(),
211
- tags: z.array(z.string()).optional(),
212
- }),
213
- },
214
- async (args: any) => {
215
- const res = await memory!.add(args.content, { tags: args.tags });
216
- await logActivity("create", {
217
- method: "POST",
218
- endpoint: "/memory/add",
219
- status: 200,
220
- });
221
- return { content: [{ type: "text", text: JSON.stringify(res) }] };
222
- },
223
- );
203
+ // Factory to create configured McpServer instance
204
+ const createConfiguredServer = (
205
+ onClientConnected?: (name: string) => void,
206
+ ) => {
207
+ const server = new McpServer(
208
+ { name: "cybermem", version: PACKAGE_VERSION },
209
+ {
210
+ instructions: CYBERMEM_INSTRUCTIONS,
211
+ },
212
+ );
224
213
 
225
- server.registerTool(
226
- "query_memory",
227
- {
228
- description: "Search memories.",
229
- inputSchema: z.object({ query: z.string(), k: z.number().default(5) }),
230
- },
231
- async (args: any) => {
232
- const res = await memory!.search(args.query, { limit: args.k });
233
- await logActivity("read", {
234
- method: "POST",
235
- endpoint: "/memory/query",
236
- status: 200,
237
- });
238
- return { content: [{ type: "text", text: JSON.stringify(res) }] };
239
- },
240
- );
241
-
242
- server.registerTool(
243
- "update_memory",
244
- {
245
- description:
246
- "Mutate existing memory (content/tags). HIGH COST: re-embeds and re-links. Use for corrections.",
247
- inputSchema: z.object({
248
- id: z.string(),
249
- content: z.string().optional(),
250
- tags: z.array(z.string()).optional(),
214
+ // access underlying server to set internal state for direct memory access
215
+ // Casting to unknown first to allow adding private property
216
+ (server as unknown as { _memoryReady: boolean })._memoryReady = true;
217
+
218
+ server.registerResource(
219
+ "CyberMem Agent Protocol",
220
+ "cybermem://protocol",
221
+ { description: "Instructions for AI agents", mimeType: "text/plain" },
222
+ async () => ({
223
+ contents: [
224
+ {
225
+ uri: "cybermem://protocol",
226
+ mimeType: "text/plain",
227
+ text: CYBERMEM_INSTRUCTIONS,
228
+ },
229
+ ],
251
230
  }),
252
- },
253
- async (args: any) => {
254
- if (!sdk_update_memory) throw new Error("Update not available in SDK");
255
- const res = await sdk_update_memory(args.id, args.content, args.tags);
256
- await logActivity("update", {
257
- method: "PATCH",
258
- endpoint: `/memory/${args.id}`,
259
- status: 200,
260
- });
261
- return { content: [{ type: "text", text: JSON.stringify(res) }] };
262
- },
263
- );
264
-
265
- server.registerTool(
266
- "reinforce_memory",
267
- {
268
- description:
269
- "Metabolic boost (salience). LOW COST: prevents decay without mutation. Use for active topics.",
270
- inputSchema: z.object({ id: z.string(), boost: z.number().default(0.1) }),
271
- },
272
- async (args: any) => {
273
- if (!sdk_reinforce_memory)
274
- throw new Error("Reinforce not available in SDK");
275
- await sdk_reinforce_memory(args.id, args.boost);
276
- await logActivity("update", {
277
- method: "POST",
278
- endpoint: `/memory/${args.id}/reinforce`,
279
- status: 200,
280
- });
281
- return { content: [{ type: "text", text: "Reinforced" }] };
282
- },
283
- );
231
+ );
284
232
 
285
- server.registerTool(
286
- "delete_memory",
287
- {
288
- description: "Delete memory",
289
- inputSchema: z.object({ id: z.string() }),
290
- },
291
- async (args: any) => {
292
- const dbPath = process.env.OM_DB_PATH!;
293
- const sqlite3 = await import("sqlite3");
294
- const db = new sqlite3.default.Database(dbPath);
295
- return new Promise((resolve, reject) => {
296
- db.serialize(() => {
297
- db.run("DELETE FROM memories WHERE id = ?", [args.id]);
298
- db.run(
299
- "DELETE FROM vectors WHERE id = ?",
300
- [args.id],
301
- async (err: any) => {
302
- db.close();
303
- await logActivity("delete", {
304
- method: "DELETE",
305
- endpoint: `/memory/${args.id}`,
306
- status: err ? 500 : 200,
307
- });
308
- if (err) reject(err);
309
- else resolve({ content: [{ type: "text", text: "Deleted" }] });
310
- },
233
+ // access underlying server
234
+ server.server.setRequestHandler(
235
+ InitializeRequestSchema,
236
+ async (request) => {
237
+ // For SSE multiple clients, stdioClientName global is less useful,
238
+ // but we can set it for context if running in single-user mode.
239
+ // For multi-user, rely on requestContext.
240
+ // For SSE multiple clients, rely on per-request context instead of a global.
241
+ const clientName = request.params.clientInfo.name;
242
+ if (onClientConnected) {
243
+ onClientConnected(clientName);
244
+ } else {
245
+ stdioClientName = clientName;
246
+ }
247
+ console.error(`[MCP] Client identified via handshake: ${clientName}`);
248
+ return {
249
+ protocolVersion: "2024-11-05",
250
+ capabilities: {
251
+ tools: { listChanged: true },
252
+ resources: { subscribe: true },
253
+ },
254
+ serverInfo: {
255
+ name: "cybermem",
256
+ version: PACKAGE_VERSION,
257
+ },
258
+ };
259
+ },
260
+ );
261
+
262
+ // TOOLS
263
+ server.registerTool(
264
+ "add_memory",
265
+ {
266
+ description:
267
+ "Store a new memory. Use for high-quality, stable data. " +
268
+ CYBERMEM_INSTRUCTIONS,
269
+ inputSchema: z.object({
270
+ content: z.string(),
271
+ tags: z.array(z.string()).optional(),
272
+ }),
273
+ },
274
+ async (args: { content: string; tags?: string[] }) => {
275
+ const res = await memory!.add(args.content, { tags: args.tags });
276
+ await logActivity("create", {
277
+ method: "POST",
278
+ endpoint: "/memory/add",
279
+ status: 200,
280
+ });
281
+ return { content: [{ type: "text", text: JSON.stringify(res) }] };
282
+ },
283
+ );
284
+
285
+ server.registerTool(
286
+ "query_memory",
287
+ {
288
+ description: "Search memories.",
289
+ inputSchema: z.object({ query: z.string(), k: z.number().default(5) }),
290
+ },
291
+ async (args: { query: string; k?: number }) => {
292
+ const res = await memory!.search(args.query, { limit: args.k });
293
+ await logActivity("read", {
294
+ method: "POST",
295
+ endpoint: "/memory/query",
296
+ status: 200,
297
+ });
298
+ return { content: [{ type: "text", text: JSON.stringify(res) }] };
299
+ },
300
+ );
301
+
302
+ server.registerTool(
303
+ "update_memory",
304
+ {
305
+ description:
306
+ "Mutate existing memory (content/tags). HIGH COST: re-embeds and re-links. Use for corrections.",
307
+ inputSchema: z.object({
308
+ id: z.string(),
309
+ content: z.string().optional(),
310
+ tags: z.array(z.string()).optional(),
311
+ }),
312
+ },
313
+ async (args: { id: string; content?: string; tags?: string[] }) => {
314
+ if (!sdk_update_memory) throw new Error("Update not available in SDK");
315
+ if (args.content === undefined && args.tags === undefined) {
316
+ throw new Error(
317
+ "At least one of 'content' or 'tags' must be provided to update_memory",
311
318
  );
319
+ }
320
+ const res = await sdk_update_memory(args.id, args.content, args.tags);
321
+ await logActivity("update", {
322
+ method: "PATCH",
323
+ endpoint: `/memory/${args.id}`,
324
+ status: 200,
312
325
  });
313
- });
314
- },
315
- );
326
+ return { content: [{ type: "text", text: JSON.stringify(res) }] };
327
+ },
328
+ );
329
+
330
+ server.registerTool(
331
+ "reinforce_memory",
332
+ {
333
+ description:
334
+ "Metabolic boost (salience). LOW COST: prevents decay without mutation. Use for active topics.",
335
+ inputSchema: z.object({
336
+ id: z.string(),
337
+ boost: z.number().default(0.1),
338
+ }),
339
+ },
340
+ async (args: { id: string; boost?: number }) => {
341
+ if (!sdk_reinforce_memory)
342
+ throw new Error("Reinforce not available in SDK");
343
+ const res = await sdk_reinforce_memory(args.id, args.boost);
344
+ await logActivity("update", {
345
+ method: "POST",
346
+ endpoint: `/memory/${args.id}/reinforce`,
347
+ status: 200,
348
+ });
349
+ return { content: [{ type: "text", text: JSON.stringify(res) }] };
350
+ },
351
+ );
352
+
353
+ server.registerTool(
354
+ "delete_memory",
355
+ {
356
+ description: "Delete memory",
357
+ inputSchema: z.object({ id: z.string() }),
358
+ },
359
+ async (args: { id: string }) => {
360
+ const dbPath = process.env.OM_DB_PATH!;
361
+ const sqlite3 = await import("sqlite3");
362
+ const db = new sqlite3.default.Database(dbPath);
363
+ return new Promise((resolve, reject) => {
364
+ db.serialize(() => {
365
+ db.run("DELETE FROM memories WHERE id = ?", [args.id]);
366
+ db.run(
367
+ "DELETE FROM vectors WHERE id = ?",
368
+ [args.id],
369
+ async (err: Error | null) => {
370
+ db.close();
371
+ await logActivity("delete", {
372
+ method: "DELETE",
373
+ endpoint: `/memory/${args.id}`,
374
+ status: err ? 500 : 200,
375
+ });
376
+ if (err) reject(err);
377
+ else resolve({ content: [{ type: "text", text: "Deleted" }] });
378
+ },
379
+ );
380
+ });
381
+ });
382
+ },
383
+ );
384
+
385
+ return server;
386
+ };
316
387
 
317
388
  // EXPRESS SERVER
318
389
  // HTTP server mode for Docker/Traefik deployment
@@ -322,13 +393,14 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
322
393
  const app = express();
323
394
  app.use(cors());
324
395
  app.use(express.json());
325
- 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
+ );
326
399
 
327
400
  app.use((req, res, next) => {
328
401
  const clientName =
329
402
  (req.headers["x-client-name"] as string) || "antigravity-client";
330
403
  requestContext.run({ clientName }, next);
331
- // next(); // DELETED! Correctly handled by requestContext.run
332
404
  });
333
405
 
334
406
  if (memory) {
@@ -427,25 +499,90 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
427
499
  });
428
500
  }
429
501
 
430
- const transport = new StreamableHTTPServerTransport({
431
- sessionIdGenerator: () => crypto.randomUUID(),
502
+ // MULTI-SESSION SSE SUPPORT
503
+ const sessions = new Map<
504
+ string,
505
+ {
506
+ server: McpServer;
507
+ transport: SSEServerTransport;
508
+ clientName?: string;
509
+ }
510
+ >();
511
+
512
+ // Legacy MCP endpoint - 410 Gone
513
+ app.all("/mcp", (req, res) => {
514
+ res
515
+ .status(410)
516
+ .send(
517
+ "Endpoint /mcp is deprecated. Please update your client configuration to use /sse for Server-Sent Events.",
518
+ );
432
519
  });
433
- app.all(
434
- "/mcp",
435
- async (req, res) => await transport.handleRequest(req, res, req.body),
436
- );
437
- app.all(
438
- "/sse",
439
- async (req, res) => await transport.handleRequest(req, res, req.body),
440
- );
441
520
 
442
- server.connect(transport).then(() => {
443
- app.listen(port, () =>
444
- console.error(`CyberMem MCP running on http://localhost:${port}`),
445
- );
521
+ app.get("/sse", async (req, res) => {
522
+ console.error("[MCP] Attempting SSE Connection...");
523
+ const transport = new SSEServerTransport("/message", res);
524
+ const newServer = createConfiguredServer((name) => {
525
+ const session = sessions.get(transport.sessionId);
526
+ if (session) session.clientName = name;
527
+ });
528
+
529
+ try {
530
+ await newServer.connect(transport);
531
+ sessions.set(transport.sessionId, { server: newServer, transport });
532
+
533
+ transport.onclose = () => {
534
+ console.error(`[MCP] SSE Connection Closed: ${transport.sessionId}`);
535
+ sessions.delete(transport.sessionId);
536
+ };
537
+ transport.onerror = (err: Error) => {
538
+ console.error(
539
+ `[MCP] SSE Connection Error: ${transport.sessionId}`,
540
+ err,
541
+ );
542
+ sessions.delete(transport.sessionId);
543
+ };
544
+
545
+ // await transport.start(); // FIXED: connect() starts it automatically
546
+ } catch (err) {
547
+ console.error("[MCP] Failed to start SSE transport:", err);
548
+ sessions.delete(transport.sessionId);
549
+ // If headers haven't been sent, send 500
550
+ if (!res.headersSent) {
551
+ res.status(500).send("Internal Server Error during SSE handshake");
552
+ }
553
+ }
446
554
  });
555
+
556
+ app.post("/message", async (req, res) => {
557
+ const sessionId = req.query.sessionId as string;
558
+ const session = sessions.get(sessionId);
559
+ if (!session) {
560
+ res.status(404).send("Session not found");
561
+ return;
562
+ }
563
+ try {
564
+ const clientName = session.clientName || "sse-client";
565
+ await requestContext.run({ clientName }, async () => {
566
+ await session.transport.handlePostMessage(req, res);
567
+ });
568
+ } catch (err) {
569
+ console.error(
570
+ `[MCP] Error handling message for session ${sessionId}:`,
571
+ err,
572
+ );
573
+ if (!res.headersSent) {
574
+ res.status(500).send("Internal Server Error processing message");
575
+ }
576
+ }
577
+ });
578
+
579
+ app.listen(port, () =>
580
+ console.error(`CyberMem MCP running on http://localhost:${port}`),
581
+ );
447
582
  } else {
583
+ // STDIO
448
584
  const transport = new StdioServerTransport();
585
+ const server = createConfiguredServer();
449
586
  server
450
587
  .connect(transport)
451
588
  .then(() => console.error("CyberMem MCP connected via STDIO"));