@cybermem/mcp 0.6.10 → 0.8.1
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/auth.js +238 -0
- package/dist/index.js +185 -237
- package/package.json +5 -4
- package/src/auth.ts +244 -0
- package/src/index.ts +225 -260
- package/src/openmemory-js.d.ts +23 -0
package/src/index.ts
CHANGED
|
@@ -1,42 +1,73 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* CyberMem MCP Server
|
|
4
|
+
*
|
|
5
|
+
* MCP server for AI agents to interact with CyberMem memory system.
|
|
6
|
+
* Uses openmemory-js SDK directly (no HTTP, embedded SQLite).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
10
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
-
import {
|
|
6
|
-
CallToolRequestSchema,
|
|
7
|
-
ListResourcesRequestSchema,
|
|
8
|
-
ListToolsRequestSchema,
|
|
9
|
-
ReadResourceRequestSchema,
|
|
10
|
-
Tool,
|
|
11
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
12
|
-
import axios from "axios";
|
|
11
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
13
12
|
import cors from "cors";
|
|
14
13
|
import dotenv from "dotenv";
|
|
15
14
|
import express from "express";
|
|
15
|
+
import { Memory } from "openmemory-js";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import { login, logout, showStatus } from "./auth.js";
|
|
16
18
|
|
|
17
19
|
dotenv.config();
|
|
18
20
|
|
|
19
|
-
//
|
|
21
|
+
// Handle CLI auth commands first
|
|
20
22
|
const args = process.argv.slice(2);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
23
|
+
|
|
24
|
+
if (args.includes("--login")) {
|
|
25
|
+
login()
|
|
26
|
+
.then(() => process.exit(0))
|
|
27
|
+
.catch((err) => {
|
|
28
|
+
console.error("Login failed:", err.message);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
});
|
|
31
|
+
} else if (args.includes("--logout")) {
|
|
32
|
+
logout();
|
|
33
|
+
process.exit(0);
|
|
34
|
+
} else if (args.includes("--status")) {
|
|
35
|
+
showStatus();
|
|
36
|
+
process.exit(0);
|
|
37
|
+
} else {
|
|
38
|
+
// Continue with MCP server startup
|
|
39
|
+
startServer();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function startServer() {
|
|
43
|
+
// Parse CLI args
|
|
44
|
+
const getArg = (name: string): string | undefined => {
|
|
45
|
+
const idx = args.indexOf(name);
|
|
46
|
+
return idx !== -1 && args[idx + 1] ? args[idx + 1] : undefined;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const cliClientName = getArg("--client-name");
|
|
50
|
+
|
|
51
|
+
// Track client name per session (used in tags)
|
|
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();
|
|
68
|
+
|
|
69
|
+
// CyberMem Agent Protocol - instructions sent to clients on handshake
|
|
70
|
+
const CYBERMEM_INSTRUCTIONS = `CyberMem is a persistent context daemon for AI agents.
|
|
40
71
|
|
|
41
72
|
PROTOCOL:
|
|
42
73
|
1. On session start: call query_memory("user context profile") to load persona
|
|
@@ -55,41 +86,36 @@ INTEGRITY RULES:
|
|
|
55
86
|
- Sync before critical decisions
|
|
56
87
|
- Last-write-wins for conflicts
|
|
57
88
|
|
|
58
|
-
For full protocol: https://cybermem.dev/
|
|
59
|
-
|
|
60
|
-
// Short protocol reminder for tool descriptions
|
|
61
|
-
const PROTOCOL_REMINDER =
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
89
|
+
For full protocol: https://docs.cybermem.dev/agent-protocol`;
|
|
90
|
+
|
|
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
|
+
const server = new McpServer(
|
|
97
|
+
{
|
|
98
|
+
name: "cybermem",
|
|
99
|
+
version: "0.8.0",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
capabilities: {
|
|
103
|
+
tools: {},
|
|
104
|
+
resources: {},
|
|
105
|
+
},
|
|
106
|
+
instructions: CYBERMEM_INSTRUCTIONS,
|
|
73
107
|
},
|
|
74
|
-
|
|
75
|
-
},
|
|
76
|
-
);
|
|
108
|
+
);
|
|
77
109
|
|
|
78
|
-
// Register resources
|
|
79
|
-
server.
|
|
80
|
-
|
|
110
|
+
// Register resources
|
|
111
|
+
server.registerResource(
|
|
112
|
+
"CyberMem Agent Protocol",
|
|
113
|
+
"cybermem://protocol",
|
|
81
114
|
{
|
|
82
|
-
uri: "cybermem://protocol",
|
|
83
|
-
name: "CyberMem Agent Protocol",
|
|
84
115
|
description: "Instructions for AI agents using CyberMem memory system",
|
|
85
116
|
mimeType: "text/plain",
|
|
86
117
|
},
|
|
87
|
-
|
|
88
|
-
}));
|
|
89
|
-
|
|
90
|
-
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
91
|
-
if (request.params.uri === "cybermem://protocol") {
|
|
92
|
-
return {
|
|
118
|
+
async () => ({
|
|
93
119
|
contents: [
|
|
94
120
|
{
|
|
95
121
|
uri: "cybermem://protocol",
|
|
@@ -97,228 +123,167 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
97
123
|
text: CYBERMEM_INSTRUCTIONS,
|
|
98
124
|
},
|
|
99
125
|
],
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
content: {
|
|
113
|
-
type: "string",
|
|
114
|
-
description:
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// Register tools using openmemory-js SDK
|
|
130
|
+
server.registerTool(
|
|
131
|
+
"add_memory",
|
|
132
|
+
{
|
|
133
|
+
description: `Store a new memory in CyberMem. ${PROTOCOL_REMINDER}`,
|
|
134
|
+
inputSchema: z.object({
|
|
135
|
+
content: z
|
|
136
|
+
.string()
|
|
137
|
+
.describe(
|
|
115
138
|
"Full content with all details - NO truncation or summarization",
|
|
116
|
-
|
|
117
|
-
user_id:
|
|
118
|
-
tags:
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
},
|
|
124
|
-
required: ["content"],
|
|
125
|
-
},
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
name: "query_memory",
|
|
129
|
-
description: `Search for relevant memories. On session start, call query_memory("user context profile") first.`,
|
|
130
|
-
inputSchema: {
|
|
131
|
-
type: "object",
|
|
132
|
-
properties: {
|
|
133
|
-
query: { type: "string" },
|
|
134
|
-
k: { type: "number", default: 5 },
|
|
135
|
-
},
|
|
136
|
-
required: ["query"],
|
|
139
|
+
),
|
|
140
|
+
user_id: z.string().optional(),
|
|
141
|
+
tags: z
|
|
142
|
+
.array(z.string())
|
|
143
|
+
.optional()
|
|
144
|
+
.describe("Always include [topic, year, source:your-client-name]"),
|
|
145
|
+
}),
|
|
137
146
|
},
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
+
async (args) => {
|
|
148
|
+
// Add source tag automatically
|
|
149
|
+
const tags = args.tags || [];
|
|
150
|
+
if (!tags.some((t) => t.startsWith("source:"))) {
|
|
151
|
+
tags.push(`source:${currentClientName}`);
|
|
152
|
+
}
|
|
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
|
+
};
|
|
147
162
|
},
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
server.registerTool(
|
|
166
|
+
"query_memory",
|
|
167
|
+
{
|
|
168
|
+
description: `Search for relevant memories. On session start, call query_memory("user context profile") first.`,
|
|
169
|
+
inputSchema: z.object({
|
|
170
|
+
query: z.string(),
|
|
171
|
+
k: z.number().default(5),
|
|
172
|
+
}),
|
|
158
173
|
},
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
type: "object",
|
|
165
|
-
properties: {
|
|
166
|
-
id: { type: "string" },
|
|
167
|
-
content: { type: "string" },
|
|
168
|
-
tags: { type: "array", items: { type: "string" } },
|
|
169
|
-
metadata: { type: "object" },
|
|
170
|
-
},
|
|
171
|
-
required: ["id"],
|
|
174
|
+
async (args) => {
|
|
175
|
+
const results = await memory.search(args.query, { limit: args.k });
|
|
176
|
+
return {
|
|
177
|
+
content: [{ type: "text", text: JSON.stringify(results) }],
|
|
178
|
+
};
|
|
172
179
|
},
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const apiClient = axios.create({
|
|
182
|
-
baseURL: API_URL,
|
|
183
|
-
headers: {
|
|
184
|
-
Authorization: `Bearer ${API_KEY}`,
|
|
185
|
-
},
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
// Helper to get client with context
|
|
189
|
-
function getClient(customHeaders: Record<string, string> = {}) {
|
|
190
|
-
// Get client name from MCP protocol (sent during initialize) or fallback to CLI arg
|
|
191
|
-
const clientVersion = server.getClientVersion();
|
|
192
|
-
const clientName =
|
|
193
|
-
customHeaders["X-Client-Name"] || clientVersion?.name || currentClientName;
|
|
194
|
-
|
|
195
|
-
return {
|
|
196
|
-
...apiClient,
|
|
197
|
-
get: (url: string, config?: any) =>
|
|
198
|
-
apiClient.get(url, {
|
|
199
|
-
...config,
|
|
200
|
-
headers: { "X-Client-Name": clientName, ...config?.headers },
|
|
201
|
-
}),
|
|
202
|
-
post: (url: string, data?: any, config?: any) =>
|
|
203
|
-
apiClient.post(url, data, {
|
|
204
|
-
...config,
|
|
205
|
-
headers: { "X-Client-Name": clientName, ...config?.headers },
|
|
206
|
-
}),
|
|
207
|
-
put: (url: string, data?: any, config?: any) =>
|
|
208
|
-
apiClient.put(url, data, {
|
|
209
|
-
...config,
|
|
210
|
-
headers: { "X-Client-Name": clientName, ...config?.headers },
|
|
211
|
-
}),
|
|
212
|
-
patch: (url: string, data?: any, config?: any) =>
|
|
213
|
-
apiClient.patch(url, data, {
|
|
214
|
-
...config,
|
|
215
|
-
headers: { "X-Client-Name": clientName, ...config?.headers },
|
|
216
|
-
}),
|
|
217
|
-
delete: (url: string, config?: any) =>
|
|
218
|
-
apiClient.delete(url, {
|
|
219
|
-
...config,
|
|
220
|
-
headers: { "X-Client-Name": clientName, ...config?.headers },
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
server.registerTool(
|
|
183
|
+
"list_memories",
|
|
184
|
+
{
|
|
185
|
+
description: "List recent memories",
|
|
186
|
+
inputSchema: z.object({
|
|
187
|
+
limit: z.number().default(10),
|
|
221
188
|
}),
|
|
222
|
-
|
|
223
|
-
|
|
189
|
+
},
|
|
190
|
+
async (args) => {
|
|
191
|
+
// Use search with empty query to list recent
|
|
192
|
+
const results = await memory.search("", { limit: args.limit || 10 });
|
|
193
|
+
return {
|
|
194
|
+
content: [{ type: "text", text: JSON.stringify(results) }],
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
);
|
|
224
198
|
|
|
225
|
-
server.
|
|
226
|
-
|
|
199
|
+
server.registerTool(
|
|
200
|
+
"delete_memory",
|
|
201
|
+
{
|
|
202
|
+
description: "Delete a memory by ID",
|
|
203
|
+
inputSchema: z.object({
|
|
204
|
+
id: z.string(),
|
|
205
|
+
}),
|
|
206
|
+
},
|
|
207
|
+
async (args) => {
|
|
208
|
+
// openmemory-js doesn't have delete by ID, use wipe for now
|
|
209
|
+
// TODO: Implement delete_by_id in SDK or via direct DB query
|
|
210
|
+
return {
|
|
211
|
+
content: [
|
|
212
|
+
{
|
|
213
|
+
type: "text",
|
|
214
|
+
text: `Delete not yet implemented in SDK. Memory ID: ${args.id}`,
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
};
|
|
218
|
+
},
|
|
219
|
+
);
|
|
227
220
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const { id } = args as { id: string };
|
|
251
|
-
await getClient().delete(`/${id}`);
|
|
252
|
-
return { content: [{ type: "text", text: `Memory ${id} deleted` }] };
|
|
253
|
-
}
|
|
254
|
-
case "update_memory": {
|
|
255
|
-
const { id, ...updates } = args as { id: string; [key: string]: any };
|
|
256
|
-
const response = await getClient().patch(`/${id}`, updates);
|
|
257
|
-
return {
|
|
258
|
-
content: [{ type: "text", text: JSON.stringify(response.data) }],
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
default:
|
|
262
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
263
|
-
}
|
|
264
|
-
} catch (error: any) {
|
|
265
|
-
return {
|
|
266
|
-
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
267
|
-
isError: true,
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
});
|
|
221
|
+
server.registerTool(
|
|
222
|
+
"update_memory",
|
|
223
|
+
{
|
|
224
|
+
description: "Update a memory by ID",
|
|
225
|
+
inputSchema: z.object({
|
|
226
|
+
id: z.string(),
|
|
227
|
+
content: z.string().optional(),
|
|
228
|
+
tags: z.array(z.string()).optional(),
|
|
229
|
+
}),
|
|
230
|
+
},
|
|
231
|
+
async (args) => {
|
|
232
|
+
// TODO: Implement update in SDK
|
|
233
|
+
return {
|
|
234
|
+
content: [
|
|
235
|
+
{
|
|
236
|
+
type: "text",
|
|
237
|
+
text: `Update not yet implemented in SDK. Memory ID: ${args.id}`,
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
);
|
|
271
243
|
|
|
272
|
-
|
|
273
|
-
const
|
|
244
|
+
// Determine transport mode
|
|
245
|
+
const transportArg = args.find(
|
|
246
|
+
(arg) => arg === "--stdio" || arg === "--http",
|
|
247
|
+
);
|
|
248
|
+
const useHttp = transportArg === "--http" || args.includes("--port");
|
|
274
249
|
|
|
275
|
-
if (
|
|
250
|
+
if (useHttp) {
|
|
251
|
+
// HTTP mode for testing/development
|
|
252
|
+
const port = parseInt(getArg("--port") || "3100", 10);
|
|
276
253
|
const app = express();
|
|
277
|
-
app.use(cors());
|
|
278
|
-
const port = process.env.PORT || 8627;
|
|
279
254
|
|
|
280
|
-
|
|
255
|
+
app.use(cors());
|
|
256
|
+
app.use(express.json());
|
|
281
257
|
|
|
282
|
-
app.get("/
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if (clientName) {
|
|
286
|
-
currentClientName = clientName;
|
|
287
|
-
}
|
|
258
|
+
app.get("/health", (_req, res) => {
|
|
259
|
+
res.json({ ok: true, version: "0.8.0", mode: "sdk" });
|
|
260
|
+
});
|
|
288
261
|
|
|
289
|
-
|
|
290
|
-
|
|
262
|
+
const transport = new StreamableHTTPServerTransport({
|
|
263
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
291
264
|
});
|
|
292
265
|
|
|
293
|
-
app.
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
if (clientName) {
|
|
297
|
-
currentClientName = clientName;
|
|
298
|
-
}
|
|
266
|
+
app.all("/mcp", async (req, res) => {
|
|
267
|
+
await transport.handleRequest(req, res, req.body);
|
|
268
|
+
});
|
|
299
269
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
} else {
|
|
303
|
-
res.status(400).send("Session not established");
|
|
304
|
-
}
|
|
270
|
+
app.all("/sse", async (req, res) => {
|
|
271
|
+
await transport.handleRequest(req, res, req.body);
|
|
305
272
|
});
|
|
306
273
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
274
|
+
server.connect(transport).then(() => {
|
|
275
|
+
app.listen(port, () => {
|
|
276
|
+
console.log(
|
|
277
|
+
`CyberMem MCP (SDK mode) running on http://localhost:${port}`,
|
|
278
|
+
);
|
|
279
|
+
console.log("Health: /health | MCP: /mcp");
|
|
280
|
+
});
|
|
313
281
|
});
|
|
314
282
|
} else {
|
|
283
|
+
// STDIO mode (default for MCP clients)
|
|
315
284
|
const transport = new StdioServerTransport();
|
|
316
|
-
|
|
317
|
-
|
|
285
|
+
server.connect(transport).then(() => {
|
|
286
|
+
console.error("CyberMem MCP (SDK mode) connected via STDIO");
|
|
287
|
+
});
|
|
318
288
|
}
|
|
319
289
|
}
|
|
320
|
-
|
|
321
|
-
run().catch((error) => {
|
|
322
|
-
console.error("Fatal error running server:", error);
|
|
323
|
-
process.exit(1);
|
|
324
|
-
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
declare module "openmemory-js" {
|
|
2
|
+
export interface MemoryOptions {
|
|
3
|
+
user_id?: string;
|
|
4
|
+
tags?: string[];
|
|
5
|
+
[key: string]: any;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface SearchOptions {
|
|
9
|
+
user_id?: string;
|
|
10
|
+
limit?: number;
|
|
11
|
+
sectors?: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class Memory {
|
|
15
|
+
constructor(user_id?: string);
|
|
16
|
+
add(content: string, opts?: MemoryOptions): Promise<any>;
|
|
17
|
+
get(id: string): Promise<any>;
|
|
18
|
+
search(query: string, opts?: SearchOptions): Promise<any[]>;
|
|
19
|
+
delete_all(user_id?: string): Promise<void>;
|
|
20
|
+
wipe(): Promise<void>;
|
|
21
|
+
source(name: string): any;
|
|
22
|
+
}
|
|
23
|
+
}
|