@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 ADDED
@@ -0,0 +1,8 @@
1
+ node_modules
2
+ dist
3
+ .git
4
+ .DS_Store
5
+ *.log
6
+ .env
7
+ .venv
8
+ __pycache__
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
- * MCP server for AI agents to interact with CyberMem memory system.
7
- * Uses openmemory-js SDK directly (no HTTP, embedded SQLite).
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
- // Track client name per session (used in tags)
53
- const currentClientName = cliClientName || "cybermem-mcp";
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
- // 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") to load persona
72
- 2. Store new insights immediately with add_memory - include FULL content, not summaries
73
- 3. Refresh context: 6h for active topics, 24h for projects, 7d for insights
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
- // Short protocol reminder for tool descriptions
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
- // Register resources
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
- // Register tools using openmemory-js SDK
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: `Store a new memory in CyberMem. ${PROTOCOL_REMINDER}`,
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
- // Add source tag automatically
129
- const tags = args.tags || [];
130
- if (!tags.some((t) => t.startsWith("source:"))) {
131
- tags.push(`source:${currentClientName}`);
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: `Search for relevant memories. On session start, call query_memory("user context profile") first.`,
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
- const results = await memory.search(args.query, { limit: args.k });
149
- return {
150
- content: [{ type: "text", text: JSON.stringify(results) }],
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
- // Use search with empty query to list recent
160
- const results = await memory.search("", { limit: args.limit || 10 });
161
- return {
162
- content: [{ type: "text", text: JSON.stringify(results) }],
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 a memory by ID",
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
- // openmemory-js doesn't have delete by ID, use wipe for now
172
- // TODO: Implement delete_by_id in SDK or via direct DB query
173
- return {
174
- content: [
175
- {
176
- type: "text",
177
- text: `Delete not yet implemented in SDK. Memory ID: ${args.id}`,
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 a memory by ID",
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
- // TODO: Implement update in SDK
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
- // Determine transport mode
201
- const transportArg = args.find((arg) => arg === "--stdio" || arg === "--http");
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", (_req, res) => {
210
- res.json({ ok: true, version: "0.8.0", mode: "sdk" });
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
- await transport.handleRequest(req, res, req.body);
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 (SDK mode) running on http://localhost:${port}`);
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.connect(transport).then(() => {
232
- console.error("CyberMem MCP (SDK mode) connected via STDIO");
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.1",
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
- * MCP server for AI agents to interact with CyberMem memory system.
6
- * Uses openmemory-js SDK directly (no HTTP, embedded SQLite).
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
- // Parse CLI args
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
- // 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
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") to load persona
74
- 2. Store new insights immediately with add_memory - include FULL content, not summaries
75
- 3. Refresh context: 6h for active topics, 24h for projects, 7d for insights
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
- // Register tools using openmemory-js SDK
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: `Store a new memory in CyberMem. ${PROTOCOL_REMINDER}`,
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
- // Add source tag automatically
149
- const tags = args.tags || [];
150
- if (!tags.some((t) => t.startsWith("source:"))) {
151
- tags.push(`source:${currentClientName}`);
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: `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
- }),
154
+ description: "Search memories.",
155
+ inputSchema: z.object({ query: z.string(), k: z.number().default(5) }),
173
156
  },
174
157
  async (args) => {
175
- const results = await memory.search(args.query, { limit: args.k });
176
- return {
177
- content: [{ type: "text", text: JSON.stringify(results) }],
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
- // 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
- };
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 a memory by ID",
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
- // 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
- };
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 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
- }),
222
+ description: "Update memory",
223
+ inputSchema: z.object({ id: z.string(), content: z.string().optional() }),
230
224
  },
231
225
  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
- };
226
+ return { content: [{ type: "text", text: "Update not implemented" }] };
241
227
  },
242
228
  );
243
229
 
244
- // Determine transport mode
245
- const transportArg = args.find(
246
- (arg) => arg === "--stdio" || arg === "--http",
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", (_req, res) => {
259
- res.json({ ok: true, version: "0.8.0", mode: "sdk" });
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
- const transport = new StreamableHTTPServerTransport({
263
- sessionIdGenerator: () => crypto.randomUUID(),
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
- app.all("/mcp", async (req, res) => {
267
- await transport.handleRequest(req, res, req.body);
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
- app.all("/sse", async (req, res) => {
271
- await transport.handleRequest(req, res, req.body);
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.connect(transport).then(() => {
286
- console.error("CyberMem MCP (SDK mode) connected via STDIO");
287
- });
301
+ server
302
+ .connect(transport)
303
+ .then(() => console.error("CyberMem MCP connected via STDIO"));
288
304
  }
289
305
  }