@cybermem/mcp 0.9.12 → 0.13.4

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.
@@ -0,0 +1,259 @@
1
+ import { expect, test } from "@playwright/test";
2
+
3
+ const BASE_URL = process.env.MCP_URL
4
+ ? process.env.MCP_URL.replace(/\/mcp$/, "")
5
+ : "http://localhost:8626";
6
+
7
+ // Tailscale environments require auth token
8
+ const isLocalhost =
9
+ BASE_URL.includes("localhost") || BASE_URL.includes("127.0.0.1");
10
+ const CYBERMEM_TOKEN = process.env.CYBERMEM_TOKEN || "";
11
+
12
+ // Helper to build headers with optional auth
13
+ function getHeaders(clientName: string): Record<string, string> {
14
+ const headers: Record<string, string> = { "X-Client-Name": clientName };
15
+ if (!isLocalhost && CYBERMEM_TOKEN) {
16
+ headers["X-API-Key"] = CYBERMEM_TOKEN;
17
+ }
18
+ return headers;
19
+ }
20
+
21
+ // CRITICAL: MCP CRUD tests MUST run in serial order (each depends on the previous)
22
+ test.describe.configure({ mode: "serial" });
23
+
24
+ test.describe("MCP:E2E (Core CRUD)", () => {
25
+ let memoryId: string;
26
+ const crudLog: Array<{
27
+ operation: string;
28
+ endpoint: string;
29
+ payload?: object;
30
+ status: number;
31
+ response: object;
32
+ }> = [];
33
+
34
+ test.beforeAll(async ({}, testInfo) => {
35
+ console.log(`šŸ”§ Testing against: ${BASE_URL}`);
36
+
37
+ // Attach environment info
38
+ await testInfo.attach("šŸ”§ Test Environment", {
39
+ body: `Base URL: ${BASE_URL}\nTimestamp: ${new Date().toISOString()}\nClient: antigravity-client\nVersion: 0.13.0`,
40
+ contentType: "text/plain",
41
+ });
42
+ });
43
+
44
+ test("1. Create Memory (POST /add)", async ({ request }, testInfo) => {
45
+ const payload = {
46
+ content: `E2E Verification ${new Date().toISOString()}`,
47
+ tags: ["e2e", "automated"],
48
+ };
49
+
50
+ await test.step("šŸ“¤ CRUD — POST /add — Create new memory", async () => {
51
+ console.log("šŸ“¤ POST /add");
52
+ console.log(" Payload:", JSON.stringify(payload, null, 2));
53
+
54
+ const response = await request.post(`${BASE_URL}/add`, {
55
+ data: payload,
56
+ headers: {
57
+ ...getHeaders("antigravity-client"),
58
+ "X-Client-Version": "0.13.0",
59
+ },
60
+ timeout: 30000, // 30s timeout
61
+ });
62
+
63
+ // Handle non-JSON responses gracefully
64
+ const status = response.status();
65
+ let body: any;
66
+
67
+ try {
68
+ body = await response.json();
69
+ } catch {
70
+ const text = await response.text();
71
+ throw new Error(
72
+ `Non-JSON response (status ${status}): ${text.substring(0, 200)}`,
73
+ );
74
+ }
75
+
76
+ console.log(" Status:", status);
77
+ console.log(" Response:", JSON.stringify(body, null, 2));
78
+
79
+ crudLog.push({
80
+ operation: "CREATE",
81
+ endpoint: "POST /add",
82
+ payload,
83
+ status,
84
+ response: body,
85
+ });
86
+
87
+ expect(status).toBe(200);
88
+ expect(body.id).toBeTruthy();
89
+ memoryId = body.id;
90
+ console.log(` āœ… Memory ID: ${memoryId}`);
91
+ });
92
+
93
+ // Attach CRUD operation to trace
94
+ await testInfo.attach("šŸ“ CRUD — CREATE", {
95
+ body: `Endpoint: POST /add\n\nRequest:\n${JSON.stringify(payload, null, 2)}\n\nResponse:\n${JSON.stringify(crudLog[crudLog.length - 1]?.response, null, 2)}`,
96
+ contentType: "text/plain",
97
+ });
98
+ });
99
+
100
+ test("2. Read Memory (POST /query)", async ({ request }, testInfo) => {
101
+ await test.step("ā³ Wait — Vector Indexing Delay — 1 second", async () => {
102
+ await new Promise((r) => setTimeout(r, 1000));
103
+ });
104
+
105
+ const payload = { query: "Verification", k: 1 };
106
+
107
+ await test.step("šŸ“¤ CRUD — POST /query — Semantic search", async () => {
108
+ console.log("šŸ“¤ POST /query");
109
+ console.log(" Payload:", JSON.stringify(payload, null, 2));
110
+
111
+ const response = await request.post(`${BASE_URL}/query`, {
112
+ data: payload,
113
+ headers: getHeaders("antigravity-client"),
114
+ });
115
+
116
+ const body = await response.json();
117
+ console.log(" Status:", response.status());
118
+ console.log(" Response:", JSON.stringify(body, null, 2));
119
+
120
+ crudLog.push({
121
+ operation: "READ",
122
+ endpoint: "POST /query",
123
+ payload,
124
+ status: response.status(),
125
+ response: body,
126
+ });
127
+
128
+ expect(response.status()).toBe(200);
129
+ expect(Array.isArray(body)).toBe(true);
130
+ console.log(` āœ… Returned ${body.length} result(s)`);
131
+ });
132
+
133
+ await testInfo.attach("šŸ“– CRUD — READ", {
134
+ body: `Endpoint: POST /query\n\nRequest:\n${JSON.stringify(payload, null, 2)}\n\nResponse:\n${JSON.stringify(crudLog[crudLog.length - 1]?.response, null, 2)}`,
135
+ contentType: "text/plain",
136
+ });
137
+ });
138
+
139
+ test("3. Update Memory (PATCH /memory/:id)", async ({
140
+ request,
141
+ }, testInfo) => {
142
+ test.skip(!memoryId, "Skipped — memoryId was not created in Step 1");
143
+
144
+ const payload = {
145
+ content: `Updated E2E Context ${new Date().toISOString()}`,
146
+ tags: ["e2e", "updated"],
147
+ };
148
+
149
+ await test.step(`šŸ“¤ CRUD — PATCH /memory/${memoryId} — Update content`, async () => {
150
+ console.log(`šŸ“¤ PATCH /memory/${memoryId}`);
151
+ console.log(" Payload:", JSON.stringify(payload, null, 2));
152
+
153
+ const response = await request.patch(`${BASE_URL}/memory/${memoryId}`, {
154
+ data: payload,
155
+ headers: getHeaders("antigravity-client"),
156
+ });
157
+
158
+ const body = await response.json();
159
+ console.log(" Status:", response.status());
160
+ console.log(" Response:", JSON.stringify(body, null, 2));
161
+
162
+ crudLog.push({
163
+ operation: "UPDATE",
164
+ endpoint: `PATCH /memory/${memoryId}`,
165
+ payload,
166
+ status: response.status(),
167
+ response: body,
168
+ });
169
+
170
+ expect(response.status()).toBe(200);
171
+ console.log(` āœ… Memory updated successfully`);
172
+ });
173
+
174
+ await testInfo.attach("āœļø CRUD — UPDATE", {
175
+ body: `Endpoint: PATCH /memory/${memoryId}\n\nRequest:\n${JSON.stringify(payload, null, 2)}\n\nResponse:\n${JSON.stringify(crudLog[crudLog.length - 1]?.response, null, 2)}`,
176
+ contentType: "text/plain",
177
+ });
178
+ });
179
+
180
+ test("4. Reinforce Memory (POST /memory/:id/reinforce)", async ({
181
+ request,
182
+ }, testInfo) => {
183
+ test.skip(!memoryId, "Skipped — memoryId was not created in Step 1");
184
+
185
+ const payload = { boost: 0.5 };
186
+
187
+ await test.step(`šŸ“¤ CRUD — POST /memory/${memoryId}/reinforce — Boost salience`, async () => {
188
+ console.log(`šŸ“¤ POST /memory/${memoryId}/reinforce`);
189
+ console.log(" Payload:", JSON.stringify(payload, null, 2));
190
+
191
+ const response = await request.post(
192
+ `${BASE_URL}/memory/${memoryId}/reinforce`,
193
+ {
194
+ data: payload,
195
+ headers: getHeaders("antigravity-client"),
196
+ },
197
+ );
198
+
199
+ const body = await response.json();
200
+ console.log(" Status:", response.status());
201
+ console.log(" Response:", JSON.stringify(body, null, 2));
202
+
203
+ crudLog.push({
204
+ operation: "REINFORCE",
205
+ endpoint: `POST /memory/${memoryId}/reinforce`,
206
+ payload,
207
+ status: response.status(),
208
+ response: body,
209
+ });
210
+
211
+ expect(response.status()).toBe(200);
212
+ console.log(` āœ… Memory reinforced successfully`);
213
+ });
214
+
215
+ await testInfo.attach("⚔ CRUD — REINFORCE", {
216
+ body: `Endpoint: POST /memory/${memoryId}/reinforce\n\nRequest:\n${JSON.stringify(payload, null, 2)}\n\nResponse:\n${JSON.stringify(crudLog[crudLog.length - 1]?.response, null, 2)}`,
217
+ contentType: "text/plain",
218
+ });
219
+ });
220
+
221
+ test("5. Delete Memory (DELETE /memory/:id)", async ({
222
+ request,
223
+ }, testInfo) => {
224
+ test.skip(!memoryId, "Skipped — memoryId was not created in Step 1");
225
+
226
+ await test.step(`šŸ“¤ CRUD — DELETE /memory/${memoryId} — Hard delete from DB`, async () => {
227
+ console.log(`šŸ“¤ DELETE /memory/${memoryId}`);
228
+
229
+ const response = await request.delete(`${BASE_URL}/memory/${memoryId}`, {
230
+ headers: getHeaders("antigravity-client"),
231
+ });
232
+
233
+ const body = await response.json();
234
+ console.log(" Status:", response.status());
235
+ console.log(" Response:", JSON.stringify(body, null, 2));
236
+
237
+ crudLog.push({
238
+ operation: "DELETE",
239
+ endpoint: `DELETE /memory/${memoryId}`,
240
+ status: response.status(),
241
+ response: body,
242
+ });
243
+
244
+ expect(response.status()).toBe(200);
245
+ console.log(` āœ… Memory deleted successfully`);
246
+ });
247
+
248
+ await testInfo.attach("šŸ—‘ļø CRUD — DELETE", {
249
+ body: `Endpoint: DELETE /memory/${memoryId}\n\nResponse:\n${JSON.stringify(crudLog[crudLog.length - 1]?.response, null, 2)}\n\n--- FULL CRUD LOG ---\n${crudLog.map((c) => `${c.operation}: ${c.endpoint} → ${c.status}`).join("\n")}`,
250
+ contentType: "text/plain",
251
+ });
252
+
253
+ // Attach full CRUD summary at the end
254
+ await testInfo.attach("šŸ“Š CRUD Lifecycle Complete", {
255
+ body: `Memory ID: ${memoryId}\n\nOperations Performed:\n${crudLog.map((c) => `āœ… ${c.operation}: ${c.endpoint} → HTTP ${c.status}`).join("\n")}\n\nStorage State: CLEANED (memory deleted)`,
256
+ contentType: "text/plain",
257
+ });
258
+ });
259
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cybermem/mcp",
3
- "version": "0.9.12",
3
+ "version": "0.13.4",
4
4
  "description": "CyberMem MCP Server - AI Memory with openmemory-js SDK",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -9,7 +9,9 @@
9
9
  "scripts": {
10
10
  "build": "tsc",
11
11
  "start": "node dist/index.js",
12
- "dev": "ts-node src/index.ts"
12
+ "dev": "ts-node src/index.ts",
13
+ "lint": "tsc --noEmit",
14
+ "test:e2e": "playwright test"
13
15
  },
14
16
  "repository": {
15
17
  "type": "git",
@@ -39,14 +41,14 @@
39
41
  "cors": "^2.8.5",
40
42
  "dotenv": "^16.0.0",
41
43
  "express": "^5.2.1",
42
- "keytar": "^7.9.0",
43
44
  "open": "^11.0.0",
44
- "openmemory-js": "^1.3.2",
45
+ "openmemory-js": "1.3.0",
45
46
  "sqlite": "^5.1.1",
46
47
  "sqlite3": "^5.1.7",
47
48
  "zod": "^3.25.76"
48
49
  },
49
50
  "devDependencies": {
51
+ "@playwright/test": "^1.57.0",
50
52
  "@types/cors": "^2.8.19",
51
53
  "@types/express": "^5.0.6",
52
54
  "@types/node": "^18.0.0",
@@ -0,0 +1,17 @@
1
+ import { defineConfig } from "@playwright/test";
2
+
3
+ export default defineConfig({
4
+ testDir: "./e2e",
5
+ outputDir: "./test-results",
6
+ reporter: "html",
7
+ use: {
8
+ baseURL: process.env.MCP_URL || "http://localhost:8626/mcp",
9
+ trace: "on-first-retry",
10
+ },
11
+ projects: [
12
+ {
13
+ name: "api",
14
+ testMatch: "api.spec.ts",
15
+ },
16
+ ],
17
+ });
@@ -0,0 +1,13 @@
1
+ // Redirect all stdout to stderr IMMEDIATELY to protect Stdio protocol
2
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
3
+ (process.stdout as any).write = (chunk: any, encoding: any, callback: any) => {
4
+ const str = typeof chunk === "string" ? chunk : chunk.toString();
5
+ if (str.includes('"jsonrpc":')) {
6
+ return originalStdoutWrite(chunk, encoding, callback);
7
+ }
8
+ return process.stderr.write(chunk, encoding, callback);
9
+ };
10
+
11
+ // Also redirect console outputs
12
+ console.log = console.error;
13
+ console.info = console.error;
package/src/env.ts CHANGED
@@ -1,10 +1,27 @@
1
- /**
2
- * Environment Initialization for CyberMem MCP
3
- * Must be imported first to set side-effect vars.
4
- */
5
1
  import dotenv from "dotenv";
2
+ import os from "os";
3
+ import path from "path";
4
+
6
5
  dotenv.config();
7
6
 
7
+ // CLI Enforcement: Ensure CyberMem is deployed via @cybermem/cli
8
+ if (!process.env.CYBERMEM_INSTANCE) {
9
+ console.error(
10
+ "\nāŒ FATAL: CyberMem must be started via @cybermem/cli ('mcp install' or 'mcp up').",
11
+ );
12
+ console.error(
13
+ "Manual 'npm start' or 'docker-compose up' without CLI tagging is forbidden.\n",
14
+ );
15
+ process.exit(1);
16
+ }
17
+
18
+ // Normalize OM_DB_PATH early so all components (SDK, exporters) use the same file
19
+ const homedir = os.homedir();
20
+ process.env.OM_DB_PATH =
21
+ process.env.OM_DB_PATH ||
22
+ path.resolve(homedir, ".cybermem/data/openmemory.sqlite");
23
+ process.env.DB_PATH = process.env.OM_DB_PATH;
24
+
8
25
  process.env.OM_TIER = process.env.OM_TIER || "hybrid";
9
26
  process.env.OM_PORT = process.env.OM_PORT || "0";
10
27
  process.env.PORT = process.env.PORT || "0";