@cybermem/mcp 0.8.1 → 0.8.3
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/index.js +163 -139
- package/package.json +2 -1
- package/src/index.ts +172 -156
package/.dockerignore
ADDED
package/Dockerfile
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
FROM node:18-alpine AS builder
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
# Install build dependencies for native modules (better-sqlite3)
|
|
5
|
+
RUN apk add --no-cache python3 make g++
|
|
6
|
+
|
|
7
|
+
# Copy package files
|
|
8
|
+
COPY package.json ./
|
|
9
|
+
# Install ALL dependencies (including devDeps for build if TS)
|
|
10
|
+
RUN npm install
|
|
11
|
+
|
|
12
|
+
# Copy source
|
|
13
|
+
COPY . .
|
|
14
|
+
|
|
15
|
+
# Build TypeScript
|
|
16
|
+
RUN npm run build
|
|
17
|
+
|
|
18
|
+
# Prune dev dependencies
|
|
19
|
+
RUN npm prune --production
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
FROM node:18-alpine AS runner
|
|
23
|
+
|
|
24
|
+
WORKDIR /app
|
|
25
|
+
# Install runtime dependencies for native modules if needed (usually just glibc/musl compatibility, but better-sqlite3 bundles binaries or needs rebuild.
|
|
26
|
+
# We copy node_modules from builder so native addons should work if arc matches.
|
|
27
|
+
# For cross-platform (building amd64 on arm64 or vice versa), we might need QEMU or specific handling.
|
|
28
|
+
# GitHub Actions runner is likely amd64, RPi is arm64.
|
|
29
|
+
# We rely on docker buildx (multi-arch) in CI.
|
|
30
|
+
|
|
31
|
+
# Copy built artifacts and deps
|
|
32
|
+
COPY --from=builder /app/dist ./dist
|
|
33
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
34
|
+
COPY --from=builder /app/package.json ./package.json
|
|
35
|
+
|
|
36
|
+
# Environment defaults
|
|
37
|
+
ENV NODE_ENV=production
|
|
38
|
+
ENV PORT=8080
|
|
39
|
+
|
|
40
|
+
EXPOSE 8080
|
|
41
|
+
|
|
42
|
+
# Start server in HTTP mode
|
|
43
|
+
CMD ["node", "dist/index.js", "--http", "--port", "8080"]
|
package/dist/index.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* CyberMem MCP Server
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* 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.
|
|
8
9
|
*/
|
|
9
10
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
10
11
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
@@ -13,6 +14,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
13
14
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
14
15
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
15
16
|
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
17
|
+
const axios_1 = __importDefault(require("axios"));
|
|
16
18
|
const cors_1 = __importDefault(require("cors"));
|
|
17
19
|
const dotenv_1 = __importDefault(require("dotenv"));
|
|
18
20
|
const express_1 = __importDefault(require("express"));
|
|
@@ -39,70 +41,27 @@ else if (args.includes("--status")) {
|
|
|
39
41
|
process.exit(0);
|
|
40
42
|
}
|
|
41
43
|
else {
|
|
42
|
-
// Continue with MCP server startup
|
|
43
44
|
startServer();
|
|
44
45
|
}
|
|
45
46
|
async function startServer() {
|
|
46
|
-
// Parse CLI args
|
|
47
47
|
const getArg = (name) => {
|
|
48
48
|
const idx = args.indexOf(name);
|
|
49
49
|
return idx !== -1 && args[idx + 1] ? args[idx + 1] : undefined;
|
|
50
50
|
};
|
|
51
|
-
const cliClientName = getArg("--client-name");
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
//
|
|
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
|
-
// Ensure data directory exists
|
|
60
|
-
const fs = require("fs");
|
|
61
|
-
try {
|
|
62
|
-
fs.mkdirSync(dataDir, { recursive: true });
|
|
63
|
-
}
|
|
64
|
-
catch { }
|
|
65
|
-
// Initialize openmemory-js SDK (embedded SQLite)
|
|
66
|
-
const memory = new openmemory_js_1.Memory();
|
|
67
|
-
// CyberMem Agent Protocol - instructions sent to clients on handshake
|
|
51
|
+
const cliClientName = getArg("--client-name") || "cybermem-mcp";
|
|
52
|
+
const cliUrl = getArg("--url");
|
|
53
|
+
const cliApiKey = getArg("--api-key");
|
|
54
|
+
// Protocol Instructions
|
|
68
55
|
const CYBERMEM_INSTRUCTIONS = `CyberMem is a persistent context daemon for AI agents.
|
|
69
|
-
|
|
70
56
|
PROTOCOL:
|
|
71
|
-
1. On session start: call query_memory("user context profile")
|
|
72
|
-
2. Store new insights immediately with add_memory
|
|
73
|
-
3.
|
|
74
|
-
4. Always include tags: [topic, year, source:your-client-name]
|
|
75
|
-
5. Priority: CyberMem context > session context > training data
|
|
76
|
-
|
|
77
|
-
MEMORY FORMAT:
|
|
78
|
-
- content: Full text with all details, metrics, dates. NO truncation.
|
|
79
|
-
- tags: Always include topic category + year + source:client-name
|
|
80
|
-
|
|
81
|
-
INTEGRITY RULES:
|
|
82
|
-
- Never overwrite without reading first
|
|
83
|
-
- Always include metadata (tags, source)
|
|
84
|
-
- Sync before critical decisions
|
|
85
|
-
- Last-write-wins for conflicts
|
|
86
|
-
|
|
57
|
+
1. On session start: call query_memory("user context profile")
|
|
58
|
+
2. Store new insights immediately with add_memory (FULL content)
|
|
59
|
+
3. Always include tags: [topic, year, source:your-client-name]
|
|
87
60
|
For full protocol: https://docs.cybermem.dev/agent-protocol`;
|
|
88
|
-
|
|
89
|
-
const PROTOCOL_REMINDER = "CyberMem Protocol: Store FULL content (no summaries), always include tags [topic, year, source:client-name]. Query 'user context profile' on session start.";
|
|
90
|
-
// Create McpServer instance
|
|
91
|
-
const server = new mcp_js_1.McpServer({
|
|
92
|
-
name: "cybermem",
|
|
93
|
-
version: "0.8.0",
|
|
94
|
-
}, {
|
|
95
|
-
capabilities: {
|
|
96
|
-
tools: {},
|
|
97
|
-
resources: {},
|
|
98
|
-
},
|
|
61
|
+
const server = new mcp_js_1.McpServer({ name: "cybermem", version: "0.8.2" }, {
|
|
99
62
|
instructions: CYBERMEM_INSTRUCTIONS,
|
|
100
63
|
});
|
|
101
|
-
|
|
102
|
-
server.registerResource("CyberMem Agent Protocol", "cybermem://protocol", {
|
|
103
|
-
description: "Instructions for AI agents using CyberMem memory system",
|
|
104
|
-
mimeType: "text/plain",
|
|
105
|
-
}, async () => ({
|
|
64
|
+
server.registerResource("CyberMem Agent Protocol", "cybermem://protocol", { description: "Instructions for AI agents", mimeType: "text/plain" }, async () => ({
|
|
106
65
|
contents: [
|
|
107
66
|
{
|
|
108
67
|
uri: "cybermem://protocol",
|
|
@@ -111,125 +70,190 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
|
|
|
111
70
|
},
|
|
112
71
|
],
|
|
113
72
|
}));
|
|
114
|
-
//
|
|
73
|
+
// --- IMPLEMENTATION LOGIC ---
|
|
74
|
+
let memory = null;
|
|
75
|
+
let apiClient = null;
|
|
76
|
+
if (cliUrl) {
|
|
77
|
+
// REMOTE CLIENT MODE
|
|
78
|
+
console.error(`Connecting to remote CyberMem at ${cliUrl}`);
|
|
79
|
+
apiClient = axios_1.default.create({
|
|
80
|
+
baseURL: cliUrl,
|
|
81
|
+
headers: {
|
|
82
|
+
Authorization: `Bearer ${cliApiKey}`,
|
|
83
|
+
"X-Client-Name": cliClientName,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// LOCAL SDK MODE
|
|
89
|
+
const homedir = process.env.HOME || process.env.USERPROFILE || "";
|
|
90
|
+
// Default to ~/.cybermem/data if OM_DB_PATH not set
|
|
91
|
+
if (!process.env.OM_DB_PATH) {
|
|
92
|
+
process.env.OM_DB_PATH = `${homedir}/.cybermem/data/openmemory.sqlite`;
|
|
93
|
+
}
|
|
94
|
+
// Ensure directory exists
|
|
95
|
+
const fs = require("fs");
|
|
96
|
+
try {
|
|
97
|
+
const dbPath = process.env.OM_DB_PATH;
|
|
98
|
+
const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
|
|
99
|
+
if (dir)
|
|
100
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
101
|
+
}
|
|
102
|
+
catch { }
|
|
103
|
+
memory = new openmemory_js_1.Memory();
|
|
104
|
+
}
|
|
105
|
+
// Helper to add source tag
|
|
106
|
+
const addSourceTag = (tags = []) => {
|
|
107
|
+
if (!tags.some((t) => t.startsWith("source:")))
|
|
108
|
+
tags.push(`source:${cliClientName}`);
|
|
109
|
+
return tags;
|
|
110
|
+
};
|
|
111
|
+
// --- TOOLS ---
|
|
115
112
|
server.registerTool("add_memory", {
|
|
116
|
-
description:
|
|
113
|
+
description: "Store a new memory. " + CYBERMEM_INSTRUCTIONS,
|
|
117
114
|
inputSchema: zod_1.z.object({
|
|
118
|
-
content: zod_1.z
|
|
119
|
-
.string()
|
|
120
|
-
.describe("Full content with all details - NO truncation or summarization"),
|
|
115
|
+
content: zod_1.z.string(),
|
|
121
116
|
user_id: zod_1.z.string().optional(),
|
|
122
|
-
tags: zod_1.z
|
|
123
|
-
.array(zod_1.z.string())
|
|
124
|
-
.optional()
|
|
125
|
-
.describe("Always include [topic, year, source:your-client-name]"),
|
|
117
|
+
tags: zod_1.z.array(zod_1.z.string()).optional(),
|
|
126
118
|
}),
|
|
127
119
|
}, async (args) => {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
120
|
+
const tags = addSourceTag(args.tags);
|
|
121
|
+
if (cliUrl) {
|
|
122
|
+
const res = await apiClient.post("/add", { ...args, tags });
|
|
123
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
const res = await memory.add(args.content, {
|
|
127
|
+
user_id: args.user_id,
|
|
128
|
+
tags,
|
|
129
|
+
});
|
|
130
|
+
return { content: [{ type: "text", text: JSON.stringify(res) }] };
|
|
132
131
|
}
|
|
133
|
-
const result = await memory.add(args.content, {
|
|
134
|
-
user_id: args.user_id,
|
|
135
|
-
tags,
|
|
136
|
-
});
|
|
137
|
-
return {
|
|
138
|
-
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
139
|
-
};
|
|
140
132
|
});
|
|
141
133
|
server.registerTool("query_memory", {
|
|
142
|
-
description:
|
|
143
|
-
inputSchema: zod_1.z.object({
|
|
144
|
-
query: zod_1.z.string(),
|
|
145
|
-
k: zod_1.z.number().default(5),
|
|
146
|
-
}),
|
|
134
|
+
description: "Search memories.",
|
|
135
|
+
inputSchema: zod_1.z.object({ query: zod_1.z.string(), k: zod_1.z.number().default(5) }),
|
|
147
136
|
}, async (args) => {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
151
|
-
}
|
|
137
|
+
if (cliUrl) {
|
|
138
|
+
const res = await apiClient.post("/query", args);
|
|
139
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
const res = await memory.search(args.query, { limit: args.k });
|
|
143
|
+
return { content: [{ type: "text", text: JSON.stringify(res) }] };
|
|
144
|
+
}
|
|
152
145
|
});
|
|
153
146
|
server.registerTool("list_memories", {
|
|
154
147
|
description: "List recent memories",
|
|
155
|
-
inputSchema: zod_1.z.object({
|
|
156
|
-
limit: zod_1.z.number().default(10),
|
|
157
|
-
}),
|
|
148
|
+
inputSchema: zod_1.z.object({ limit: zod_1.z.number().default(10) }),
|
|
158
149
|
}, async (args) => {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
150
|
+
if (cliUrl) {
|
|
151
|
+
// Fallback to /query with empty string if /list not available, or use /all
|
|
152
|
+
// Old API had /all
|
|
153
|
+
try {
|
|
154
|
+
const res = await apiClient.get(`/all?limit=${args.limit}`);
|
|
155
|
+
return {
|
|
156
|
+
content: [{ type: "text", text: JSON.stringify(res.data) }],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
const res = await apiClient.post("/query", {
|
|
161
|
+
query: "",
|
|
162
|
+
k: args.limit,
|
|
163
|
+
});
|
|
164
|
+
return {
|
|
165
|
+
content: [{ type: "text", text: JSON.stringify(res.data) }],
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const res = await memory.search("", { limit: args.limit });
|
|
171
|
+
return { content: [{ type: "text", text: JSON.stringify(res) }] };
|
|
172
|
+
}
|
|
164
173
|
});
|
|
165
174
|
server.registerTool("delete_memory", {
|
|
166
|
-
description: "Delete
|
|
167
|
-
inputSchema: zod_1.z.object({
|
|
168
|
-
id: zod_1.z.string(),
|
|
169
|
-
}),
|
|
175
|
+
description: "Delete memory by ID",
|
|
176
|
+
inputSchema: zod_1.z.object({ id: zod_1.z.string() }),
|
|
170
177
|
}, async (args) => {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
178
|
+
if (cliUrl) {
|
|
179
|
+
const res = await apiClient.delete(`/${args.id}`);
|
|
180
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
return {
|
|
184
|
+
content: [
|
|
185
|
+
{ type: "text", text: "Delete not implemented in SDK yet" },
|
|
186
|
+
],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
181
189
|
});
|
|
182
190
|
server.registerTool("update_memory", {
|
|
183
|
-
description: "Update
|
|
184
|
-
inputSchema: zod_1.z.object({
|
|
185
|
-
id: zod_1.z.string(),
|
|
186
|
-
content: zod_1.z.string().optional(),
|
|
187
|
-
tags: zod_1.z.array(zod_1.z.string()).optional(),
|
|
188
|
-
}),
|
|
191
|
+
description: "Update memory",
|
|
192
|
+
inputSchema: zod_1.z.object({ id: zod_1.z.string(), content: zod_1.z.string().optional() }),
|
|
189
193
|
}, async (args) => {
|
|
190
|
-
|
|
191
|
-
return {
|
|
192
|
-
content: [
|
|
193
|
-
{
|
|
194
|
-
type: "text",
|
|
195
|
-
text: `Update not yet implemented in SDK. Memory ID: ${args.id}`,
|
|
196
|
-
},
|
|
197
|
-
],
|
|
198
|
-
};
|
|
194
|
+
return { content: [{ type: "text", text: "Update not implemented" }] };
|
|
199
195
|
});
|
|
200
|
-
//
|
|
201
|
-
const
|
|
202
|
-
const useHttp = transportArg === "--http" || args.includes("--port");
|
|
196
|
+
// --- TRANSPORT ---
|
|
197
|
+
const useHttp = args.includes("--http") || args.includes("--port");
|
|
203
198
|
if (useHttp) {
|
|
204
|
-
// HTTP mode for testing/development
|
|
205
199
|
const port = parseInt(getArg("--port") || "3100", 10);
|
|
206
200
|
const app = (0, express_1.default)();
|
|
207
201
|
app.use((0, cors_1.default)());
|
|
208
202
|
app.use(express_1.default.json());
|
|
209
|
-
app.get("/health", (
|
|
210
|
-
|
|
211
|
-
|
|
203
|
+
app.get("/health", (req, res) => res.json({ ok: true, version: "0.8.2", mode: cliUrl ? "proxy" : "sdk" }));
|
|
204
|
+
// REST API Compatibility (for Remote Clients)
|
|
205
|
+
// Only enable if in SDK mode (Server)
|
|
206
|
+
if (!cliUrl && memory) {
|
|
207
|
+
app.post("/add", async (req, res) => {
|
|
208
|
+
try {
|
|
209
|
+
const { content, user_id, tags } = req.body;
|
|
210
|
+
const finalTags = addSourceTag(tags);
|
|
211
|
+
const result = await memory.add(content, {
|
|
212
|
+
user_id,
|
|
213
|
+
tags: finalTags,
|
|
214
|
+
});
|
|
215
|
+
res.json(result);
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
res.status(500).json({ error: e.message });
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
app.post("/query", async (req, res) => {
|
|
222
|
+
try {
|
|
223
|
+
const { query, k } = req.body;
|
|
224
|
+
const result = await memory.search(query || "", { limit: k || 5 });
|
|
225
|
+
res.json(result);
|
|
226
|
+
}
|
|
227
|
+
catch (e) {
|
|
228
|
+
res.status(500).json({ error: e.message });
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
app.get("/all", async (req, res) => {
|
|
232
|
+
try {
|
|
233
|
+
const limit = parseInt(req.query.limit) || 10;
|
|
234
|
+
const result = await memory.search("", { limit });
|
|
235
|
+
res.json(result);
|
|
236
|
+
}
|
|
237
|
+
catch (e) {
|
|
238
|
+
res.status(500).json({ error: e.message });
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
212
242
|
const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
|
213
243
|
sessionIdGenerator: () => crypto.randomUUID(),
|
|
214
244
|
});
|
|
215
|
-
app.all("/mcp", async (req, res) =>
|
|
216
|
-
|
|
217
|
-
});
|
|
218
|
-
app.all("/sse", async (req, res) => {
|
|
219
|
-
await transport.handleRequest(req, res, req.body);
|
|
220
|
-
});
|
|
245
|
+
app.all("/mcp", async (req, res) => await transport.handleRequest(req, res, req.body));
|
|
246
|
+
app.all("/sse", async (req, res) => await transport.handleRequest(req, res, req.body));
|
|
221
247
|
server.connect(transport).then(() => {
|
|
222
248
|
app.listen(port, () => {
|
|
223
|
-
console.log(`CyberMem MCP
|
|
224
|
-
console.log("Health: /health | MCP: /mcp");
|
|
249
|
+
console.log(`CyberMem MCP running on http://localhost:${port}`);
|
|
225
250
|
});
|
|
226
251
|
});
|
|
227
252
|
}
|
|
228
253
|
else {
|
|
229
|
-
// STDIO mode (default for MCP clients)
|
|
230
254
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
231
|
-
server
|
|
232
|
-
|
|
233
|
-
|
|
255
|
+
server
|
|
256
|
+
.connect(transport)
|
|
257
|
+
.then(() => console.error("CyberMem MCP connected via STDIO"));
|
|
234
258
|
}
|
|
235
259
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cybermem/mcp",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"description": "CyberMem MCP Server - AI Memory with openmemory-js SDK",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
39
|
+
"axios": "^1.13.2",
|
|
39
40
|
"cors": "^2.8.5",
|
|
40
41
|
"dotenv": "^16.0.0",
|
|
41
42
|
"express": "^5.2.1",
|
package/src/index.ts
CHANGED
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* CyberMem MCP Server
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* Uses openmemory-js SDK directly
|
|
5
|
+
* Supports two modes:
|
|
6
|
+
* 1. Local/Server Mode (default): Uses openmemory-js SDK directly.
|
|
7
|
+
* 2. Remote Client Mode (with --url): Proxies requests to a remote CyberMem server via HTTP.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
11
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
12
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
13
|
+
import axios from "axios";
|
|
12
14
|
import cors from "cors";
|
|
13
15
|
import dotenv from "dotenv";
|
|
14
16
|
import express from "express";
|
|
@@ -35,86 +37,38 @@ if (args.includes("--login")) {
|
|
|
35
37
|
showStatus();
|
|
36
38
|
process.exit(0);
|
|
37
39
|
} else {
|
|
38
|
-
// Continue with MCP server startup
|
|
39
40
|
startServer();
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
async function startServer() {
|
|
43
|
-
|
|
44
|
-
const getArg = (name: string): string | undefined => {
|
|
44
|
+
const getArg = (name: string) => {
|
|
45
45
|
const idx = args.indexOf(name);
|
|
46
46
|
return idx !== -1 && args[idx + 1] ? args[idx + 1] : undefined;
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
-
const cliClientName = getArg("--client-name");
|
|
49
|
+
const cliClientName = getArg("--client-name") || "cybermem-mcp";
|
|
50
|
+
const cliUrl = getArg("--url");
|
|
51
|
+
const cliApiKey = getArg("--api-key");
|
|
50
52
|
|
|
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();
|
|
68
|
-
|
|
69
|
-
// CyberMem Agent Protocol - instructions sent to clients on handshake
|
|
53
|
+
// Protocol Instructions
|
|
70
54
|
const CYBERMEM_INSTRUCTIONS = `CyberMem is a persistent context daemon for AI agents.
|
|
71
|
-
|
|
72
55
|
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
|
-
|
|
56
|
+
1. On session start: call query_memory("user context profile")
|
|
57
|
+
2. Store new insights immediately with add_memory (FULL content)
|
|
58
|
+
3. Always include tags: [topic, year, source:your-client-name]
|
|
89
59
|
For full protocol: https://docs.cybermem.dev/agent-protocol`;
|
|
90
60
|
|
|
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
61
|
const server = new McpServer(
|
|
62
|
+
{ name: "cybermem", version: "0.8.2" },
|
|
97
63
|
{
|
|
98
|
-
name: "cybermem",
|
|
99
|
-
version: "0.8.0",
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
capabilities: {
|
|
103
|
-
tools: {},
|
|
104
|
-
resources: {},
|
|
105
|
-
},
|
|
106
64
|
instructions: CYBERMEM_INSTRUCTIONS,
|
|
107
65
|
},
|
|
108
66
|
);
|
|
109
67
|
|
|
110
|
-
// Register resources
|
|
111
68
|
server.registerResource(
|
|
112
69
|
"CyberMem Agent Protocol",
|
|
113
70
|
"cybermem://protocol",
|
|
114
|
-
{
|
|
115
|
-
description: "Instructions for AI agents using CyberMem memory system",
|
|
116
|
-
mimeType: "text/plain",
|
|
117
|
-
},
|
|
71
|
+
{ description: "Instructions for AI agents", mimeType: "text/plain" },
|
|
118
72
|
async () => ({
|
|
119
73
|
contents: [
|
|
120
74
|
{
|
|
@@ -126,56 +80,88 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
|
|
|
126
80
|
}),
|
|
127
81
|
);
|
|
128
82
|
|
|
129
|
-
//
|
|
83
|
+
// --- IMPLEMENTATION LOGIC ---
|
|
84
|
+
|
|
85
|
+
let memory: Memory | null = null;
|
|
86
|
+
let apiClient: any = null;
|
|
87
|
+
|
|
88
|
+
if (cliUrl) {
|
|
89
|
+
// REMOTE CLIENT MODE
|
|
90
|
+
console.error(`Connecting to remote CyberMem at ${cliUrl}`);
|
|
91
|
+
apiClient = axios.create({
|
|
92
|
+
baseURL: cliUrl,
|
|
93
|
+
headers: {
|
|
94
|
+
Authorization: `Bearer ${cliApiKey}`,
|
|
95
|
+
"X-Client-Name": cliClientName,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
} else {
|
|
99
|
+
// LOCAL SDK MODE
|
|
100
|
+
const homedir = process.env.HOME || process.env.USERPROFILE || "";
|
|
101
|
+
// Default to ~/.cybermem/data if OM_DB_PATH not set
|
|
102
|
+
if (!process.env.OM_DB_PATH) {
|
|
103
|
+
process.env.OM_DB_PATH = `${homedir}/.cybermem/data/openmemory.sqlite`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Ensure directory exists
|
|
107
|
+
const fs = require("fs");
|
|
108
|
+
try {
|
|
109
|
+
const dbPath = process.env.OM_DB_PATH;
|
|
110
|
+
const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
|
|
111
|
+
if (dir) fs.mkdirSync(dir, { recursive: true });
|
|
112
|
+
} catch {}
|
|
113
|
+
|
|
114
|
+
memory = new Memory();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Helper to add source tag
|
|
118
|
+
const addSourceTag = (tags: string[] = []) => {
|
|
119
|
+
if (!tags.some((t) => t.startsWith("source:")))
|
|
120
|
+
tags.push(`source:${cliClientName}`);
|
|
121
|
+
return tags;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// --- TOOLS ---
|
|
125
|
+
|
|
130
126
|
server.registerTool(
|
|
131
127
|
"add_memory",
|
|
132
128
|
{
|
|
133
|
-
description:
|
|
129
|
+
description: "Store a new memory. " + CYBERMEM_INSTRUCTIONS,
|
|
134
130
|
inputSchema: z.object({
|
|
135
|
-
content: z
|
|
136
|
-
.string()
|
|
137
|
-
.describe(
|
|
138
|
-
"Full content with all details - NO truncation or summarization",
|
|
139
|
-
),
|
|
131
|
+
content: z.string(),
|
|
140
132
|
user_id: z.string().optional(),
|
|
141
|
-
tags: z
|
|
142
|
-
.array(z.string())
|
|
143
|
-
.optional()
|
|
144
|
-
.describe("Always include [topic, year, source:your-client-name]"),
|
|
133
|
+
tags: z.array(z.string()).optional(),
|
|
145
134
|
}),
|
|
146
135
|
},
|
|
147
136
|
async (args) => {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
137
|
+
const tags = addSourceTag(args.tags);
|
|
138
|
+
if (cliUrl) {
|
|
139
|
+
const res = await apiClient.post("/add", { ...args, tags });
|
|
140
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
|
|
141
|
+
} else {
|
|
142
|
+
const res = await memory!.add(args.content, {
|
|
143
|
+
user_id: args.user_id,
|
|
144
|
+
tags,
|
|
145
|
+
});
|
|
146
|
+
return { content: [{ type: "text", text: JSON.stringify(res) }] };
|
|
152
147
|
}
|
|
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
148
|
},
|
|
163
149
|
);
|
|
164
150
|
|
|
165
151
|
server.registerTool(
|
|
166
152
|
"query_memory",
|
|
167
153
|
{
|
|
168
|
-
description:
|
|
169
|
-
inputSchema: z.object({
|
|
170
|
-
query: z.string(),
|
|
171
|
-
k: z.number().default(5),
|
|
172
|
-
}),
|
|
154
|
+
description: "Search memories.",
|
|
155
|
+
inputSchema: z.object({ query: z.string(), k: z.number().default(5) }),
|
|
173
156
|
},
|
|
174
157
|
async (args) => {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
178
|
-
}
|
|
158
|
+
if (cliUrl) {
|
|
159
|
+
const res = await apiClient.post("/query", args);
|
|
160
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
|
|
161
|
+
} else {
|
|
162
|
+
const res = await memory!.search(args.query, { limit: args.k });
|
|
163
|
+
return { content: [{ type: "text", text: JSON.stringify(res) }] };
|
|
164
|
+
}
|
|
179
165
|
},
|
|
180
166
|
);
|
|
181
167
|
|
|
@@ -183,107 +169,137 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
|
|
|
183
169
|
"list_memories",
|
|
184
170
|
{
|
|
185
171
|
description: "List recent memories",
|
|
186
|
-
inputSchema: z.object({
|
|
187
|
-
limit: z.number().default(10),
|
|
188
|
-
}),
|
|
172
|
+
inputSchema: z.object({ limit: z.number().default(10) }),
|
|
189
173
|
},
|
|
190
174
|
async (args) => {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
175
|
+
if (cliUrl) {
|
|
176
|
+
// Fallback to /query with empty string if /list not available, or use /all
|
|
177
|
+
// Old API had /all
|
|
178
|
+
try {
|
|
179
|
+
const res = await apiClient.get(`/all?limit=${args.limit}`);
|
|
180
|
+
return {
|
|
181
|
+
content: [{ type: "text", text: JSON.stringify(res.data) }],
|
|
182
|
+
};
|
|
183
|
+
} catch {
|
|
184
|
+
const res = await apiClient.post("/query", {
|
|
185
|
+
query: "",
|
|
186
|
+
k: args.limit,
|
|
187
|
+
});
|
|
188
|
+
return {
|
|
189
|
+
content: [{ type: "text", text: JSON.stringify(res.data) }],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
const res = await memory!.search("", { limit: args.limit });
|
|
194
|
+
return { content: [{ type: "text", text: JSON.stringify(res) }] };
|
|
195
|
+
}
|
|
196
196
|
},
|
|
197
197
|
);
|
|
198
198
|
|
|
199
199
|
server.registerTool(
|
|
200
200
|
"delete_memory",
|
|
201
201
|
{
|
|
202
|
-
description: "Delete
|
|
203
|
-
inputSchema: z.object({
|
|
204
|
-
id: z.string(),
|
|
205
|
-
}),
|
|
202
|
+
description: "Delete memory by ID",
|
|
203
|
+
inputSchema: z.object({ id: z.string() }),
|
|
206
204
|
},
|
|
207
205
|
async (args) => {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
text:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
206
|
+
if (cliUrl) {
|
|
207
|
+
const res = await apiClient.delete(`/${args.id}`);
|
|
208
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
|
|
209
|
+
} else {
|
|
210
|
+
return {
|
|
211
|
+
content: [
|
|
212
|
+
{ type: "text", text: "Delete not implemented in SDK yet" },
|
|
213
|
+
],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
218
216
|
},
|
|
219
217
|
);
|
|
220
218
|
|
|
221
219
|
server.registerTool(
|
|
222
220
|
"update_memory",
|
|
223
221
|
{
|
|
224
|
-
description: "Update
|
|
225
|
-
inputSchema: z.object({
|
|
226
|
-
id: z.string(),
|
|
227
|
-
content: z.string().optional(),
|
|
228
|
-
tags: z.array(z.string()).optional(),
|
|
229
|
-
}),
|
|
222
|
+
description: "Update memory",
|
|
223
|
+
inputSchema: z.object({ id: z.string(), content: z.string().optional() }),
|
|
230
224
|
},
|
|
231
225
|
async (args) => {
|
|
232
|
-
|
|
233
|
-
return {
|
|
234
|
-
content: [
|
|
235
|
-
{
|
|
236
|
-
type: "text",
|
|
237
|
-
text: `Update not yet implemented in SDK. Memory ID: ${args.id}`,
|
|
238
|
-
},
|
|
239
|
-
],
|
|
240
|
-
};
|
|
226
|
+
return { content: [{ type: "text", text: "Update not implemented" }] };
|
|
241
227
|
},
|
|
242
228
|
);
|
|
243
229
|
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
);
|
|
248
|
-
const useHttp = transportArg === "--http" || args.includes("--port");
|
|
230
|
+
// --- TRANSPORT ---
|
|
231
|
+
|
|
232
|
+
const useHttp = args.includes("--http") || args.includes("--port");
|
|
249
233
|
|
|
250
234
|
if (useHttp) {
|
|
251
|
-
// HTTP mode for testing/development
|
|
252
235
|
const port = parseInt(getArg("--port") || "3100", 10);
|
|
253
236
|
const app = express();
|
|
254
|
-
|
|
255
237
|
app.use(cors());
|
|
256
238
|
app.use(express.json());
|
|
257
239
|
|
|
258
|
-
app.get("/health", (
|
|
259
|
-
res.json({ ok: true, version: "0.8.
|
|
260
|
-
|
|
240
|
+
app.get("/health", (req, res) =>
|
|
241
|
+
res.json({ ok: true, version: "0.8.2", mode: cliUrl ? "proxy" : "sdk" }),
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// REST API Compatibility (for Remote Clients)
|
|
245
|
+
// Only enable if in SDK mode (Server)
|
|
246
|
+
if (!cliUrl && memory) {
|
|
247
|
+
app.post("/add", async (req, res) => {
|
|
248
|
+
try {
|
|
249
|
+
const { content, user_id, tags } = req.body;
|
|
250
|
+
const finalTags = addSourceTag(tags);
|
|
251
|
+
const result = await memory!.add(content, {
|
|
252
|
+
user_id,
|
|
253
|
+
tags: finalTags,
|
|
254
|
+
});
|
|
255
|
+
res.json(result);
|
|
256
|
+
} catch (e: any) {
|
|
257
|
+
res.status(500).json({ error: e.message });
|
|
258
|
+
}
|
|
259
|
+
});
|
|
261
260
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
261
|
+
app.post("/query", async (req, res) => {
|
|
262
|
+
try {
|
|
263
|
+
const { query, k } = req.body;
|
|
264
|
+
const result = await memory!.search(query || "", { limit: k || 5 });
|
|
265
|
+
res.json(result);
|
|
266
|
+
} catch (e: any) {
|
|
267
|
+
res.status(500).json({ error: e.message });
|
|
268
|
+
}
|
|
269
|
+
});
|
|
265
270
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
271
|
+
app.get("/all", async (req, res) => {
|
|
272
|
+
try {
|
|
273
|
+
const limit = parseInt(req.query.limit as string) || 10;
|
|
274
|
+
const result = await memory!.search("", { limit });
|
|
275
|
+
res.json(result);
|
|
276
|
+
} catch (e: any) {
|
|
277
|
+
res.status(500).json({ error: e.message });
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
269
281
|
|
|
270
|
-
|
|
271
|
-
|
|
282
|
+
const transport = new StreamableHTTPServerTransport({
|
|
283
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
272
284
|
});
|
|
285
|
+
app.all(
|
|
286
|
+
"/mcp",
|
|
287
|
+
async (req, res) => await transport.handleRequest(req, res, req.body),
|
|
288
|
+
);
|
|
289
|
+
app.all(
|
|
290
|
+
"/sse",
|
|
291
|
+
async (req, res) => await transport.handleRequest(req, res, req.body),
|
|
292
|
+
);
|
|
273
293
|
|
|
274
294
|
server.connect(transport).then(() => {
|
|
275
295
|
app.listen(port, () => {
|
|
276
|
-
console.log(
|
|
277
|
-
`CyberMem MCP (SDK mode) running on http://localhost:${port}`,
|
|
278
|
-
);
|
|
279
|
-
console.log("Health: /health | MCP: /mcp");
|
|
296
|
+
console.log(`CyberMem MCP running on http://localhost:${port}`);
|
|
280
297
|
});
|
|
281
298
|
});
|
|
282
299
|
} else {
|
|
283
|
-
// STDIO mode (default for MCP clients)
|
|
284
300
|
const transport = new StdioServerTransport();
|
|
285
|
-
server
|
|
286
|
-
|
|
287
|
-
|
|
301
|
+
server
|
|
302
|
+
.connect(transport)
|
|
303
|
+
.then(() => console.error("CyberMem MCP connected via STDIO"));
|
|
288
304
|
}
|
|
289
305
|
}
|