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