@cybermem/mcp 0.14.13 → 0.14.15
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/CHANGELOG.md +12 -0
- package/dist/index.js +2 -1
- package/e2e/sse_transport_multi.spec.ts +184 -0
- package/package.json +1 -1
- package/src/index.ts +2 -1
package/CHANGELOG.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -285,7 +285,8 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
|
|
|
285
285
|
app.use((0, cors_1.default)());
|
|
286
286
|
app.use((req, res, next) => {
|
|
287
287
|
// Skip JSON parsing for SSE message endpoint - it needs raw body stream
|
|
288
|
-
|
|
288
|
+
// Use req.url to handle query params like /message?sessionId=...
|
|
289
|
+
if (req.url.startsWith("/message")) {
|
|
289
290
|
return next();
|
|
290
291
|
}
|
|
291
292
|
express_1.default.json()(req, res, next);
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { ChildProcess, spawn } from "child_process";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* MCP SSE Transport Multi-Session Test
|
|
7
|
+
*
|
|
8
|
+
* This test validates:
|
|
9
|
+
* 1. Multiple concurrent SSE connections
|
|
10
|
+
* 2. Handling of missing X-Client-Name headers
|
|
11
|
+
* 3. Rapid connection establishment and teardown behavior
|
|
12
|
+
* 4. Graceful handling of malformed SSE requests and overall server health
|
|
13
|
+
*
|
|
14
|
+
* Purpose: Prevent SSE transport regressions identified in 0.12-0.14 releases
|
|
15
|
+
*/
|
|
16
|
+
test.describe("MCP SSE Transport - Multi-Session", () => {
|
|
17
|
+
let serverProcess: ChildProcess;
|
|
18
|
+
const PORT = 3102; // Unique port to avoid conflicts
|
|
19
|
+
|
|
20
|
+
test.setTimeout(120000);
|
|
21
|
+
|
|
22
|
+
test.beforeAll(async () => {
|
|
23
|
+
// Start the server in http mode with in-memory DB
|
|
24
|
+
const serverPath = path.join(__dirname, "../dist/index.js");
|
|
25
|
+
serverProcess = spawn(
|
|
26
|
+
"node",
|
|
27
|
+
[
|
|
28
|
+
serverPath,
|
|
29
|
+
"--port",
|
|
30
|
+
PORT.toString(),
|
|
31
|
+
"--env",
|
|
32
|
+
"test",
|
|
33
|
+
"--db-path",
|
|
34
|
+
":memory:",
|
|
35
|
+
],
|
|
36
|
+
{
|
|
37
|
+
stdio: "pipe",
|
|
38
|
+
env: { ...process.env, OM_DB_PATH: ":memory:" },
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Wait for server to start
|
|
43
|
+
await new Promise<void>((resolve, reject) => {
|
|
44
|
+
let output = "";
|
|
45
|
+
const timeout = setTimeout(() => {
|
|
46
|
+
reject(new Error(`Server start timeout. Output: ${output}`));
|
|
47
|
+
}, 60000);
|
|
48
|
+
|
|
49
|
+
serverProcess.stderr?.on("data", (data) => {
|
|
50
|
+
const text = data.toString();
|
|
51
|
+
output += text;
|
|
52
|
+
console.log("[Server]", text);
|
|
53
|
+
if (text.includes(`CyberMem MCP running on http://localhost:${PORT}`)) {
|
|
54
|
+
clearTimeout(timeout);
|
|
55
|
+
resolve();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
serverProcess.on("error", (err) => {
|
|
60
|
+
clearTimeout(timeout);
|
|
61
|
+
reject(err);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test.afterAll(() => {
|
|
67
|
+
if (serverProcess && !serverProcess.killed) {
|
|
68
|
+
serverProcess.kill();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("should handle multiple concurrent SSE connections", async () => {
|
|
73
|
+
const connections: Array<{
|
|
74
|
+
response: Response;
|
|
75
|
+
reader: ReadableStreamDefaultReader<Uint8Array>;
|
|
76
|
+
}> = [];
|
|
77
|
+
|
|
78
|
+
// Open 3 concurrent SSE connections
|
|
79
|
+
for (let i = 0; i < 3; i++) {
|
|
80
|
+
const response = await fetch(`http://localhost:${PORT}/sse`, {
|
|
81
|
+
headers: { "X-Client-Name": `test-client-${i}` },
|
|
82
|
+
});
|
|
83
|
+
expect(response.status).toBe(200);
|
|
84
|
+
expect(response.headers.get("content-type")).toBe("text/event-stream");
|
|
85
|
+
|
|
86
|
+
const reader = response.body!.getReader();
|
|
87
|
+
connections.push({ response, reader });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Verify all connections receive endpoint events
|
|
91
|
+
const decoder = new TextDecoder();
|
|
92
|
+
for (let i = 0; i < connections.length; i++) {
|
|
93
|
+
let endpointFound = false;
|
|
94
|
+
const { reader } = connections[i];
|
|
95
|
+
|
|
96
|
+
for (let j = 0; j < 3; j++) {
|
|
97
|
+
const { value, done } = await reader.read();
|
|
98
|
+
if (done) break;
|
|
99
|
+
const text = decoder.decode(value);
|
|
100
|
+
console.log(`[Connection ${i}]`, text);
|
|
101
|
+
if (text.includes("event: endpoint")) {
|
|
102
|
+
endpointFound = true;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
expect(endpointFound).toBe(true);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Cleanup all connections
|
|
111
|
+
for (const { reader } of connections) {
|
|
112
|
+
await reader.cancel();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Server should still be running
|
|
116
|
+
expect(serverProcess.exitCode).toBeNull();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("should handle connection with missing X-Client-Name header", async () => {
|
|
120
|
+
// Connection should still work but may not have proper client identification
|
|
121
|
+
const response = await fetch(`http://localhost:${PORT}/sse`);
|
|
122
|
+
expect(response.status).toBe(200);
|
|
123
|
+
expect(response.headers.get("content-type")).toBe("text/event-stream");
|
|
124
|
+
|
|
125
|
+
const reader = response.body!.getReader();
|
|
126
|
+
const decoder = new TextDecoder();
|
|
127
|
+
let endpointFound = false;
|
|
128
|
+
|
|
129
|
+
for (let i = 0; i < 3; i++) {
|
|
130
|
+
const { value, done } = await reader.read();
|
|
131
|
+
if (done) break;
|
|
132
|
+
const text = decoder.decode(value);
|
|
133
|
+
if (text.includes("event: endpoint")) {
|
|
134
|
+
endpointFound = true;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
expect(endpointFound).toBe(true);
|
|
140
|
+
await reader.cancel();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("should handle rapid connection establishment and teardown", async () => {
|
|
144
|
+
// Simulate client reconnection scenarios
|
|
145
|
+
for (let i = 0; i < 5; i++) {
|
|
146
|
+
const response = await fetch(`http://localhost:${PORT}/sse`, {
|
|
147
|
+
headers: { "X-Client-Name": `rapid-test-${i}` },
|
|
148
|
+
});
|
|
149
|
+
expect(response.status).toBe(200);
|
|
150
|
+
|
|
151
|
+
const reader = response.body!.getReader();
|
|
152
|
+
|
|
153
|
+
// Read one chunk then immediately disconnect
|
|
154
|
+
await reader.read();
|
|
155
|
+
await reader.cancel();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Server should still be healthy
|
|
159
|
+
const healthResponse = await fetch(`http://localhost:${PORT}/health`);
|
|
160
|
+
expect(healthResponse.status).toBe(200);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("should handle malformed SSE requests gracefully", async () => {
|
|
164
|
+
// POST to /sse should not crash the server
|
|
165
|
+
const postResponse = await fetch(`http://localhost:${PORT}/sse`, {
|
|
166
|
+
method: "POST",
|
|
167
|
+
});
|
|
168
|
+
// POST may return 404/405/400 depending on SDK version — just verify it's not 200
|
|
169
|
+
expect(postResponse.status).not.toBe(200);
|
|
170
|
+
|
|
171
|
+
// GET with suspicious headers should still work
|
|
172
|
+
const getResponse = await fetch(`http://localhost:${PORT}/sse`, {
|
|
173
|
+
method: "GET",
|
|
174
|
+
headers: { "X-Forwarded-For": "attacker.com" },
|
|
175
|
+
});
|
|
176
|
+
expect(getResponse.status).toBe(200);
|
|
177
|
+
const reader = getResponse.body!.getReader();
|
|
178
|
+
await reader.cancel();
|
|
179
|
+
|
|
180
|
+
// Server should still be healthy after both requests
|
|
181
|
+
const healthResponse = await fetch(`http://localhost:${PORT}/health`);
|
|
182
|
+
expect(healthResponse.status).toBe(200);
|
|
183
|
+
});
|
|
184
|
+
});
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -394,7 +394,8 @@ For full protocol: https://docs.cybermem.dev/agent-protocol`;
|
|
|
394
394
|
app.use(cors());
|
|
395
395
|
app.use((req, res, next) => {
|
|
396
396
|
// Skip JSON parsing for SSE message endpoint - it needs raw body stream
|
|
397
|
-
|
|
397
|
+
// Use req.url to handle query params like /message?sessionId=...
|
|
398
|
+
if (req.url.startsWith("/message")) {
|
|
398
399
|
return next();
|
|
399
400
|
}
|
|
400
401
|
express.json()(req, res, next);
|