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