@cybermem/mcp 0.14.15 → 0.16.0

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/e2e/api.spec.ts CHANGED
@@ -1,259 +1,205 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
1
2
  import { expect, test } from "@playwright/test";
3
+ import { spawn } from "child_process";
4
+ import path from "path";
2
5
 
3
- const BASE_URL = process.env.MCP_URL
4
- ? process.env.MCP_URL.replace(/\/mcp$/, "")
5
- : "http://localhost:8626";
6
+ import { FastMCPHandshakeTransport } from "./utils/FastMCPHandshakeTransport";
6
7
 
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
- }
8
+ const PORT = 3102;
9
+ const BASE_URL = `http://localhost:${PORT}`;
20
10
 
21
11
  // CRITICAL: MCP CRUD tests MUST run in serial order (each depends on the previous)
22
12
  test.describe.configure({ mode: "serial" });
23
13
 
24
- test.describe("MCP:E2E (Core CRUD)", () => {
14
+ test.describe("MCP:E2E (Protocol Tool Calls)", () => {
15
+ let client: Client;
16
+ let transport: FastMCPHandshakeTransport;
25
17
  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) => {
18
+ let serverProcess: any;
19
+
20
+ test.beforeAll(async () => {
21
+ // Spawn server
22
+ const serverPath = path.join(__dirname, "../dist/index.js");
23
+ serverProcess = spawn(
24
+ "node",
25
+ [
26
+ serverPath,
27
+ "--port",
28
+ PORT.toString(),
29
+ "--env",
30
+ "test",
31
+ "--db-path",
32
+ ":memory:",
33
+ ],
34
+ {
35
+ stdio: "pipe",
36
+ env: { ...process.env, OM_DB_PATH: ":memory:" },
37
+ },
38
+ );
39
+
40
+ await new Promise<void>((resolve, reject) => {
41
+ serverProcess.stderr?.on("data", (data: any) => {
42
+ const output = data.toString();
43
+ if (
44
+ output.includes(`CyberMem MCP running on http://localhost:${PORT}`)
45
+ ) {
46
+ resolve();
47
+ }
48
+ });
49
+ serverProcess.on("error", reject);
50
+ setTimeout(() => reject(new Error("Server start timeout")), 60000);
51
+ });
52
+
35
53
  console.log(`🔧 Testing against: ${BASE_URL}`);
36
54
 
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
- });
55
+ // Initialize MCP Client
56
+ client = new Client(
57
+ { name: "e2e-tester", version: "1.0.0" },
58
+ { capabilities: {} },
59
+ );
60
+
61
+ const sseUrl = new URL(`${BASE_URL}/mcp`);
62
+ const headers: Record<string, string> = {
63
+ "X-Client-Name": "antigravity-e2e",
64
+ };
65
+
66
+ transport = new FastMCPHandshakeTransport(sseUrl, headers);
67
+
68
+ console.log(`🔗 Connecting to MCP via Handshake: ${sseUrl.toString()}`);
69
+ await client.connect(transport);
70
+ console.log("✅ MCP Client connected");
71
+ });
72
+
73
+ test.afterAll(async () => {
74
+ if (transport) {
75
+ await transport.close();
76
+ }
77
+ if (serverProcess) {
78
+ serverProcess.kill();
79
+ }
80
+ // Clean up port 3102 just in case
81
+ spawn("sh", ["-c", `lsof -ti:${PORT} | xargs kill -9 || true`]);
42
82
  });
43
83
 
44
- test("1. Create Memory (POST /add)", async ({ request }, testInfo) => {
84
+ test("1. Create Memory (add_memory)", async ({}, testInfo) => {
45
85
  const payload = {
46
- content: `E2E Verification ${new Date().toISOString()}`,
47
- tags: ["e2e", "automated"],
86
+ content: `E2E Protocol Verification ${new Date().toISOString()}`,
87
+ tags: ["e2e", "protocol", "mcp"],
48
88
  };
49
89
 
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
- });
90
+ await test.step("📤 Tool Calladd_memory", async () => {
91
+ console.log("📤 Calling tool: add_memory");
62
92
 
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,
93
+ const result = await client.callTool({
94
+ name: "add_memory",
95
+ arguments: payload,
85
96
  });
86
97
 
87
- expect(status).toBe(200);
98
+ console.log(" Result:", JSON.stringify(result, null, 2));
99
+
100
+ expect(result.isError).toBeFalsy();
101
+ const content = result.content as any[];
102
+ const body = JSON.parse(content[0].text);
88
103
  expect(body.id).toBeTruthy();
89
104
  memoryId = body.id;
90
105
  console.log(` ✅ Memory ID: ${memoryId}`);
91
106
  });
92
107
 
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)}`,
108
+ await testInfo.attach("📝 Tool Call CREATE", {
109
+ body: `Tool: add_memory\n\nArguments:\n${JSON.stringify(
110
+ payload,
111
+ null,
112
+ 2,
113
+ )}`,
96
114
  contentType: "text/plain",
97
115
  });
98
116
  });
99
117
 
100
- test("2. Read Memory (POST /query)", async ({ request }, testInfo) => {
101
- await test.step("⏳ Wait — Vector Indexing Delay — 1 second", async () => {
118
+ test("2. Read Memory (query_memory)", async ({}, testInfo) => {
119
+ await test.step("⏳ Wait — Vector Indexing Delay", async () => {
102
120
  await new Promise((r) => setTimeout(r, 1000));
103
121
  });
104
122
 
105
- const payload = { query: "Verification", k: 1 };
123
+ const payload = { query: "Protocol Verification", k: 1 };
106
124
 
107
- await test.step("📤 CRUD POST /query Semantic search", async () => {
108
- console.log("📤 POST /query");
109
- console.log(" Payload:", JSON.stringify(payload, null, 2));
125
+ await test.step("📤 Tool Callquery_memory", async () => {
126
+ console.log("📤 Calling tool: query_memory");
110
127
 
111
- const response = await request.post(`${BASE_URL}/query`, {
112
- data: payload,
113
- headers: getHeaders("antigravity-client"),
128
+ const result = await client.callTool({
129
+ name: "query_memory",
130
+ arguments: payload,
114
131
  });
115
132
 
116
- const body = await response.json();
117
- console.log(" Status:", response.status());
118
- console.log(" Response:", JSON.stringify(body, null, 2));
133
+ console.log(" Result:", JSON.stringify(result, null, 2));
119
134
 
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);
135
+ expect(result.isError).toBeFalsy();
136
+ const content = result.content as any[];
137
+ const body = JSON.parse(content[0].text);
129
138
  expect(Array.isArray(body)).toBe(true);
139
+ expect(body.length).toBeGreaterThan(0);
130
140
  console.log(` ✅ Returned ${body.length} result(s)`);
131
141
  });
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
142
  });
138
143
 
139
- test("3. Update Memory (PATCH /memory/:id)", async ({
140
- request,
141
- }, testInfo) => {
144
+ test("3. Update Memory (update_memory)", async ({}, testInfo) => {
142
145
  test.skip(!memoryId, "Skipped — memoryId was not created in Step 1");
143
146
 
144
147
  const payload = {
145
- content: `Updated E2E Context ${new Date().toISOString()}`,
146
- tags: ["e2e", "updated"],
148
+ id: memoryId,
149
+ content: `Updated E2E Context via Protocol ${new Date().toISOString()}`,
150
+ tags: ["e2e", "protocol", "updated"],
147
151
  };
148
152
 
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));
153
+ await test.step("📤 Tool Callupdate_memory", async () => {
154
+ console.log(`📤 Calling tool: update_memory for ${memoryId}`);
152
155
 
153
- const response = await request.patch(`${BASE_URL}/memory/${memoryId}`, {
154
- data: payload,
155
- headers: getHeaders("antigravity-client"),
156
+ const result = await client.callTool({
157
+ name: "update_memory",
158
+ arguments: payload,
156
159
  });
157
160
 
158
- const body = await response.json();
159
- console.log(" Status:", response.status());
160
- console.log(" Response:", JSON.stringify(body, null, 2));
161
+ console.log(" Result:", JSON.stringify(result, null, 2));
161
162
 
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);
163
+ expect(result.isError).toBeFalsy();
171
164
  console.log(` ✅ Memory updated successfully`);
172
165
  });
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
166
  });
179
167
 
180
- test("4. Reinforce Memory (POST /memory/:id/reinforce)", async ({
181
- request,
182
- }, testInfo) => {
168
+ test("4. Reinforce Memory (reinforce_memory)", async ({}, testInfo) => {
183
169
  test.skip(!memoryId, "Skipped — memoryId was not created in Step 1");
184
170
 
185
- const payload = { boost: 0.5 };
171
+ const payload = { id: memoryId, boost: 0.5 };
186
172
 
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));
173
+ await test.step("📤 Tool Callreinforce_memory", async () => {
174
+ console.log(`📤 Calling tool: reinforce_memory for ${memoryId}`);
190
175
 
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,
176
+ const result = await client.callTool({
177
+ name: "reinforce_memory",
178
+ arguments: payload,
209
179
  });
210
180
 
211
- expect(response.status()).toBe(200);
212
- console.log(` ✅ Memory reinforced successfully`);
213
- });
181
+ console.log(" Result:", JSON.stringify(result, null, 2));
214
182
 
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",
183
+ expect(result.isError).toBeFalsy();
184
+ console.log(` ✅ Memory reinforced successfully`);
218
185
  });
219
186
  });
220
187
 
221
- test("5. Delete Memory (DELETE /memory/:id)", async ({
222
- request,
223
- }, testInfo) => {
188
+ test("5. Delete Memory (delete_memory)", async ({}, testInfo) => {
224
189
  test.skip(!memoryId, "Skipped — memoryId was not created in Step 1");
225
190
 
226
- await test.step(`📤 CRUD DELETE /memory/${memoryId} Hard delete from DB`, async () => {
227
- console.log(`📤 DELETE /memory/${memoryId}`);
191
+ await test.step("📤 Tool Calldelete_memory", async () => {
192
+ console.log(`📤 Calling tool: delete_memory for ${memoryId}`);
228
193
 
229
- const response = await request.delete(`${BASE_URL}/memory/${memoryId}`, {
230
- headers: getHeaders("antigravity-client"),
194
+ const result = await client.callTool({
195
+ name: "delete_memory",
196
+ arguments: { id: memoryId },
231
197
  });
232
198
 
233
- const body = await response.json();
234
- console.log(" Status:", response.status());
235
- console.log(" Response:", JSON.stringify(body, null, 2));
199
+ console.log(" Result:", JSON.stringify(result, null, 2));
236
200
 
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);
201
+ expect(result.isError).toBeFalsy();
245
202
  console.log(` ✅ Memory deleted successfully`);
246
203
  });
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
204
  });
259
205
  });
@@ -2,14 +2,13 @@ import { expect, test } from "@playwright/test";
2
2
  import { ChildProcess, spawn } from "child_process";
3
3
  import path from "path";
4
4
 
5
- test.describe("MCP SSE Transport", () => {
5
+ test.describe("MCP SSE Transport Handshake", () => {
6
6
  let serverProcess: ChildProcess;
7
- const PORT = 3101; // Use unique port for this test
7
+ const PORT = 3101;
8
8
 
9
9
  test.setTimeout(120000);
10
10
 
11
11
  test.beforeAll(async () => {
12
- // Start the server in http mode
13
12
  const serverPath = path.join(__dirname, "../dist/index.js");
14
13
  serverProcess = spawn(
15
14
  "node",
@@ -28,11 +27,9 @@ test.describe("MCP SSE Transport", () => {
28
27
  },
29
28
  );
30
29
 
31
- // Wait for server to start
32
30
  await new Promise<void>((resolve, reject) => {
33
31
  serverProcess.stderr?.on("data", (data) => {
34
32
  const output = data.toString();
35
- console.log("[Server]", output);
36
33
  if (
37
34
  output.includes(`CyberMem MCP running on http://localhost:${PORT}`)
38
35
  ) {
@@ -45,37 +42,80 @@ test.describe("MCP SSE Transport", () => {
45
42
  });
46
43
 
47
44
  test.afterAll(() => {
48
- serverProcess.kill();
45
+ if (serverProcess && !serverProcess.killed) {
46
+ serverProcess.kill();
47
+ }
49
48
  });
50
49
 
51
- test("should establish SSE connection without crashing", async () => {
52
- const response = await fetch(`http://localhost:${PORT}/sse`);
53
- expect(response.status).toBe(200);
54
- expect(response.headers.get("content-type")).toBe("text/event-stream");
50
+ test("should establishment session and call tool via handshake", async () => {
51
+ const CLIENT_NAME = "e2e-handshake-tester";
55
52
 
56
- // Read stream for a bit to ensure it doesn't close immediately due to error
57
- const reader = response.body?.getReader();
58
- expect(reader).toBeDefined();
53
+ // 1. Handshake (POST initialize)
54
+ const initResponse = await fetch(`http://localhost:${PORT}/mcp`, {
55
+ method: "POST",
56
+ headers: {
57
+ "Content-Type": "application/json",
58
+ Accept: "application/json, text/event-stream",
59
+ "X-Client-Name": CLIENT_NAME,
60
+ },
61
+ body: JSON.stringify({
62
+ jsonrpc: "2.0",
63
+ id: 1,
64
+ method: "initialize",
65
+ params: {
66
+ protocolVersion: "2024-11-05",
67
+ capabilities: {},
68
+ clientInfo: { name: "e2e", version: "1.0.0" },
69
+ },
70
+ }),
71
+ });
59
72
 
60
- const decoder = new TextDecoder();
61
- let endpointFound = false;
73
+ expect(initResponse.status).toBe(200);
74
+ const sessionId = initResponse.headers.get("mcp-session-id");
75
+ const initBody = await initResponse.json();
76
+ console.log(
77
+ "[Test] Init Headers:",
78
+ JSON.stringify(Object.fromEntries(initResponse.headers.entries())),
79
+ );
80
+ console.log("[Test] Init Body:", JSON.stringify(initBody));
81
+ expect(sessionId).toBeDefined();
62
82
 
63
- // Read first few chunks
64
- for (let i = 0; i < 3; i++) {
65
- const { value, done } = await reader!.read();
66
- if (done) break;
67
- const text = decoder.decode(value);
68
- if (text.includes("event: endpoint")) {
69
- endpointFound = true;
70
- }
71
- }
83
+ // 2. Open Stream (GET)
84
+ const streamResponse = await fetch(`http://localhost:${PORT}/mcp`, {
85
+ headers: {
86
+ Accept: "text/event-stream",
87
+ "mcp-session-id": sessionId!,
88
+ },
89
+ });
90
+ expect(streamResponse.status).toBe(200);
91
+ const reader = streamResponse.body?.getReader();
72
92
 
73
- expect(endpointFound).toBe(true);
93
+ // 3. Call Tool (POST)
94
+ const toolResponse = await fetch(`http://localhost:${PORT}/mcp`, {
95
+ method: "POST",
96
+ headers: {
97
+ "Content-Type": "application/json",
98
+ Accept: "application/json, text/event-stream",
99
+ "mcp-session-id": sessionId!,
100
+ "X-Client-Name": CLIENT_NAME,
101
+ },
102
+ body: JSON.stringify({
103
+ jsonrpc: "2.0",
104
+ id: 2,
105
+ method: "tools/call",
106
+ params: {
107
+ name: "add_memory",
108
+ arguments: { content: "Handshake Test Memory" },
109
+ },
110
+ }),
111
+ });
74
112
 
75
- // Cleanup connection
76
- await reader?.cancel();
113
+ expect(toolResponse.status).toBe(200);
114
+ const toolBody = await toolResponse.json();
115
+ console.log("[Test] Tool Result:", JSON.stringify(toolBody));
116
+ expect(toolBody.result).toBeDefined();
77
117
 
78
- // Check if server process is still running (didn't crash)
79
- expect(serverProcess.exitCode).toBeNull();
118
+ // Cleanup
119
+ await reader?.cancel();
80
120
  });
81
121
  });