@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.
- package/Dockerfile +15 -18
- package/dist/console-fix.js +14 -0
- package/dist/env.js +14 -4
- package/dist/index.js +168 -326
- package/e2e/api.spec.ts +259 -0
- package/package.json +6 -4
- package/playwright.config.ts +17 -0
- package/src/console-fix.ts +13 -0
- package/src/env.ts +21 -4
- package/src/index.ts +202 -407
- package/src/openmemory-js.d.ts +25 -0
- package/requirements.txt +0 -2
- package/server.py +0 -347
- package/test_mcp.py +0 -111
package/e2e/api.spec.ts
ADDED
|
@@ -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.
|
|
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": "
|
|
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";
|