Skip to content

Commit 10330bc

Browse files
authored
fix: connect error when sse with multiple clients (#233)
* fix: connect error when sse with multiple clients * chore: lock zod-to-json-schema version * fix: typo of connect
1 parent 0df6ba0 commit 10330bc

File tree

5 files changed

+74
-11
lines changed

5 files changed

+74
-11
lines changed

__tests__/server.spec.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ describe("MCP Server", () => {
7575
const transport = new SSEClientTransport(new URL(url), {});
7676

7777
const client = new Client(
78-
{ name: "stress-client", version: "1.0.0" },
78+
{ name: "sse-client", version: "1.0.0" },
7979
{ capabilities: {} },
8080
);
8181

@@ -159,4 +159,66 @@ describe("MCP Server", () => {
159159

160160
await killAsync(child);
161161
});
162+
163+
it("sse with multiple clients", async () => {
164+
const child = await spawnAsync("ts-node", ["./src/index.ts", "-t", "sse"]);
165+
166+
const url = "http://localhost:1122/sse";
167+
168+
const transport1 = new SSEClientTransport(new URL(url), {});
169+
const client1 = new Client(
170+
{ name: "sse-client-1", version: "1.0.0" },
171+
{ capabilities: {} },
172+
);
173+
174+
const transport2 = new SSEClientTransport(new URL(url), {});
175+
const client2 = new Client(
176+
{ name: "sse-client-2", version: "1.0.0" },
177+
{ capabilities: {} },
178+
);
179+
180+
await Promise.all([
181+
client1.connect(transport1),
182+
client2.connect(transport2),
183+
]);
184+
185+
expect((await client1.listTools()).tools.length).toBe(
186+
(await client2.listTools()).tools.length,
187+
);
188+
189+
await killAsync(child);
190+
});
191+
192+
it("streamable with multiple clients", async () => {
193+
const child = await spawnAsync("ts-node", [
194+
"./src/index.ts",
195+
"-t",
196+
"streamable",
197+
]);
198+
199+
const url = "http://localhost:1122/mcp";
200+
201+
const transport1 = new StreamableHTTPClientTransport(new URL(url), {});
202+
const client1 = new Client(
203+
{ name: "streamable-client-1", version: "1.0.0" },
204+
{ capabilities: {} },
205+
);
206+
207+
const transport2 = new StreamableHTTPClientTransport(new URL(url), {});
208+
const client2 = new Client(
209+
{ name: "streamable-client-2", version: "1.0.0" },
210+
{ capabilities: {} },
211+
);
212+
213+
await Promise.all([
214+
client1.connect(transport1),
215+
client2.connect(transport2),
216+
]);
217+
218+
expect((await client1.listTools()).tools.length).toBe(
219+
(await client2.listTools()).tools.length,
220+
);
221+
222+
await killAsync(child);
223+
});
162224
});

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"cors": "^2.8.5",
4848
"express": "^5.1.0",
4949
"zod": "^3.25.16",
50-
"zod-to-json-schema": "^3.24.5"
50+
"zod-to-json-schema": "3.24.6"
5151
},
5252
"devDependencies": {
5353
"@biomejs/biome": "1.9.4",

src/server.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,7 @@ export async function runSSEServer(
9292
port = 1122,
9393
endpoint = "/sse",
9494
): Promise<void> {
95-
const server = createServer();
96-
await startSSEMcpServer(server, endpoint, port, host);
95+
await startSSEMcpServer(createServer, endpoint, port, host);
9796
}
9897

9998
/**

src/services/sse.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,24 @@ import express, { type Request, type Response } from "express";
44
import { logger } from "../utils/logger";
55

66
export const startSSEMcpServer = async (
7-
server: Server,
7+
createServer: () => Server,
88
endpoint = "/sse",
99
port = 1122,
1010
host = "localhost",
1111
): Promise<void> => {
1212
const app = express();
1313
app.use(express.json());
1414

15-
const transports: Record<string, SSEServerTransport> = {};
15+
const connections: Record<string, SSEServerTransport> = {};
1616

1717
app.get(endpoint, async (req: Request, res: Response) => {
18+
const server = createServer();
19+
1820
const transport = new SSEServerTransport("/messages", res);
19-
transports[transport.sessionId] = transport;
21+
connections[transport.sessionId] = transport;
2022

2123
transport.onclose = () => {
22-
delete transports[transport.sessionId];
24+
delete connections[transport.sessionId];
2325
logger.info(`SSE Server disconnected: sessionId=${transport.sessionId}`);
2426
};
2527

@@ -34,7 +36,7 @@ export const startSSEMcpServer = async (
3436
return res.status(400).send("Missing sessionId parameter");
3537
}
3638

37-
const transport = transports[sessionId];
39+
const transport = connections[sessionId];
3840
if (!transport) {
3941
logger.warn(`SSE Server session not found: sessionId=${sessionId}`);
4042
return res.status(404).send("Session not found");

vitest.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ const enableCoverage = process.argv.includes("--coverage");
55
export default defineConfig({
66
resolve: {},
77
test: {
8-
testTimeout: 20_000,
9-
hookTimeout: 20_000,
8+
testTimeout: 60_000,
9+
hookTimeout: 60_000,
1010
include: ["__tests__/**/*.{test,spec}.?(c|m)[jt]s?(x)"],
1111
...(enableCoverage
1212
? {

0 commit comments

Comments
 (0)