@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/src/index.ts CHANGED
@@ -1,42 +1,73 @@
1
1
  #!/usr/bin/env node
2
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
- import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
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
- // Parse CLI args for remote mode
21
+ // Handle CLI auth commands first
20
22
  const args = process.argv.slice(2);
21
- const getArg = (name: string): string | undefined => {
22
- const idx = args.indexOf(name);
23
- return idx !== -1 && args[idx + 1] ? args[idx + 1] : undefined;
24
- };
25
-
26
- const cliUrl = getArg("--url");
27
- const cliApiKey = getArg("--api-key");
28
- const cliClientName = getArg("--client-name");
29
-
30
- // Use CLI args first, then env, then defaults
31
- // Default to local CyberMem backend (via Traefik on port 8626)
32
- const API_URL =
33
- cliUrl || process.env.CYBERMEM_URL || "http://localhost:8626/memory";
34
- const API_KEY = cliApiKey || process.env.OM_API_KEY || "";
35
-
36
- // Track client name per session
37
- let currentClientName = cliClientName || "cybermem-mcp";
38
- // CyberMem Agent Protocol - instructions sent to clients on handshake
39
- const CYBERMEM_INSTRUCTIONS = `CyberMem is a persistent context daemon for AI agents.
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/docs/agent-protocol`;
59
-
60
- // Short protocol reminder for tool descriptions (derived from main instructions)
61
- const PROTOCOL_REMINDER =
62
- "CyberMem Protocol: Store FULL content (no summaries), always include tags [topic, year, source:client-name]. Query 'user context profile' on session start.";
63
-
64
- const server = new Server(
65
- {
66
- name: "cybermem",
67
- version: "0.6.8",
68
- },
69
- {
70
- capabilities: {
71
- tools: {},
72
- resources: {}, // Enable resources for protocol document
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
- instructions: CYBERMEM_INSTRUCTIONS,
75
- },
76
- );
108
+ );
77
109
 
78
- // Register resources handler for protocol document
79
- server.setRequestHandler(ListResourcesRequestSchema, async () => ({
80
- resources: [
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
- throw new Error(`Unknown resource: ${request.params.uri}`);
103
- });
104
-
105
- const tools: Tool[] = [
106
- {
107
- name: "add_memory",
108
- description: `Store a new memory in CyberMem. ${PROTOCOL_REMINDER}`,
109
- inputSchema: {
110
- type: "object",
111
- properties: {
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: { type: "string" },
118
- tags: {
119
- type: "array",
120
- items: { type: "string" },
121
- description: "Always include [topic, year, source:your-client-name]",
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
- name: "list_memories",
141
- description: "List recent memories",
142
- inputSchema: {
143
- type: "object",
144
- properties: {
145
- limit: { type: "number", default: 10 },
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
- name: "delete_memory",
151
- description: "Delete a memory by ID",
152
- inputSchema: {
153
- type: "object",
154
- properties: {
155
- id: { type: "string" },
156
- },
157
- required: ["id"],
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
- name: "update_memory",
162
- description: "Update a memory by ID",
163
- inputSchema: {
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
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
177
- tools,
178
- }));
179
-
180
- // Create axios instance
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.setRequestHandler(CallToolRequestSchema, async (request) => {
226
- const { name, arguments: args } = request.params;
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
- try {
229
- switch (name) {
230
- case "add_memory": {
231
- const response = await getClient().post("/add", args);
232
- return {
233
- content: [{ type: "text", text: JSON.stringify(response.data) }],
234
- };
235
- }
236
- case "query_memory": {
237
- const response = await getClient().post("/query", args);
238
- return {
239
- content: [{ type: "text", text: JSON.stringify(response.data) }],
240
- };
241
- }
242
- case "list_memories": {
243
- const limit = args?.limit || 10;
244
- const response = await getClient().get(`/all?l=${limit}`);
245
- return {
246
- content: [{ type: "text", text: JSON.stringify(response.data) }],
247
- };
248
- }
249
- case "delete_memory": {
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
- async function run() {
273
- const isSse = process.argv.includes("--sse") || !!process.env.PORT;
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 (isSse) {
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
- let transport: SSEServerTransport | null = null;
255
+ app.use(cors());
256
+ app.use(express.json());
281
257
 
282
- app.get("/sse", async (req, res) => {
283
- // Extract client name from header
284
- const clientName = req.headers["x-client-name"] as string;
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
- transport = new SSEServerTransport("/messages", res);
290
- await server.connect(transport);
262
+ const transport = new StreamableHTTPServerTransport({
263
+ sessionIdGenerator: () => crypto.randomUUID(),
291
264
  });
292
265
 
293
- app.post("/messages", async (req, res) => {
294
- // Also check headers on messages
295
- const clientName = req.headers["x-client-name"] as string;
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
- if (transport) {
301
- await transport.handlePostMessage(req, res);
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
- app.listen(port, () => {
308
- console.error(
309
- `CyberMem MCP Server running on SSE at http://localhost:${port}`,
310
- );
311
- console.error(` - SSE endpoint: http://localhost:${port}/sse`);
312
- console.error(` - Message endpoint: http://localhost:${port}/messages`);
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
- await server.connect(transport);
317
- console.error("CyberMem MCP Server running on stdio");
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
+ }