@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 +20 -0
- package/dist/index.js +227 -136
- package/e2e/sse_transport.spec.ts +81 -0
- package/package.json +1 -1
- package/playwright.config.ts +1 -1
- package/src/index.ts +335 -198
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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
description: "
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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:
|
|
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
|
-
|
|
346
|
-
|
|
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.
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
package/playwright.config.ts
CHANGED
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:
|
|
93
|
-
let sdk_update_memory:
|
|
94
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
{
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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) =>
|
|
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
|
-
|
|
431
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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"));
|