From 223a1c973336c77ee0036ee373f32fff73a46ed5 Mon Sep 17 00:00:00 2001 From: Zelys Date: Tue, 12 May 2026 18:17:11 -0500 Subject: [PATCH 1/3] fix(test): extend cloudflare workers retry to cover full request cycle Wrangler on Node 20 accepts the initialize handshake before it can reliably handle subsequent requests. The previous retry loop only wrapped connect(), so a successful connect followed by a failed callTool() still caused the test to fail. Extend the loop to retry the full interaction (connect + callTool + assertion) and close the client on each failed attempt before retrying. --- .../test/server/cloudflareWorkers.test.ts | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/test/integration/test/server/cloudflareWorkers.test.ts b/test/integration/test/server/cloudflareWorkers.test.ts index 9c2d73a40e..1cfa9a38d2 100644 --- a/test/integration/test/server/cloudflareWorkers.test.ts +++ b/test/integration/test/server/cloudflareWorkers.test.ts @@ -150,28 +150,25 @@ export default { it('should handle MCP requests', async () => { expect(env).not.toBeNull(); - // Retry connection — wrangler may report "Ready" before it can handle requests - let client!: Client; + // Retry the full interaction — wrangler may report "Ready" before it can + // reliably handle all requests (initialize succeeds but subsequent calls + // can still get "Network connection lost." on slower runtimes like Node 20). let lastError: unknown; for (let attempt = 0; attempt < 5; attempt++) { + const client = new Client({ name: 'test-client', version: '1.0.0' }); + const transport = new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${PORT}/`)); try { - client = new Client({ name: 'test-client', version: '1.0.0' }); - const transport = new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${PORT}/`)); await client.connect(transport); - lastError = undefined; - break; + const result = await client.callTool({ name: 'greet', arguments: { name: 'World' } }); + expect(result.content).toEqual([{ type: 'text', text: 'Hello, World!' }]); + await client.close(); + return; } catch (error) { lastError = error; - await new Promise(resolve => setTimeout(resolve, 1000)); + try { await client.close(); } catch { /* ignore cleanup errors */ } + if (attempt < 4) await new Promise(resolve => setTimeout(resolve, 1000)); } } - if (lastError) { - throw lastError; - } - - const result = await client.callTool({ name: 'greet', arguments: { name: 'World' } }); - expect(result.content).toEqual([{ type: 'text', text: 'Hello, World!' }]); - - await client.close(); + throw lastError; }, 30_000); }); From 7eafbfecf5ff21b1dc580aae94186ddcf8424e09 Mon Sep 17 00:00:00 2001 From: Zelys Date: Tue, 12 May 2026 18:24:04 -0500 Subject: [PATCH 2/3] style: fix prettier formatting in cloudflare workers test --- test/integration/test/server/cloudflareWorkers.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/integration/test/server/cloudflareWorkers.test.ts b/test/integration/test/server/cloudflareWorkers.test.ts index 1cfa9a38d2..f8b07f2966 100644 --- a/test/integration/test/server/cloudflareWorkers.test.ts +++ b/test/integration/test/server/cloudflareWorkers.test.ts @@ -165,7 +165,11 @@ export default { return; } catch (error) { lastError = error; - try { await client.close(); } catch { /* ignore cleanup errors */ } + try { + await client.close(); + } catch { + /* ignore cleanup errors */ + } if (attempt < 4) await new Promise(resolve => setTimeout(resolve, 1000)); } } From de450db39deb2c254b400543b11db8a18f0d0a21 Mon Sep 17 00:00:00 2001 From: Zelys Date: Wed, 6 May 2026 14:21:59 -0500 Subject: [PATCH 3/3] test(@modelcontextprotocol/node): cover pre-read body pattern in stateless mode Add a test for the handleRequest(req, res, parsedBody) overload in stateless mode: drain the IncomingMessage body upstream (body-parser pattern), pass it as parsedBody, and verify that both an initialize request and a subsequent tools/list request succeed on the same reused transport. Found while investigating #1994. In v2 the _hasHandledRequest reuse restriction was removed, so both requests succeed. This test pins that. Co-Authored-By: Claude Sonnet 4.6 --- .../node/test/streamableHttp.test.ts | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/packages/middleware/node/test/streamableHttp.test.ts b/packages/middleware/node/test/streamableHttp.test.ts index c427aa2eea..ed7b7c3dd9 100644 --- a/packages/middleware/node/test/streamableHttp.test.ts +++ b/packages/middleware/node/test/streamableHttp.test.ts @@ -1682,6 +1682,50 @@ describe('Zod v4', () => { }); expect(stream2.status).toBe(409); // Conflict - only one stream allowed }); + + it('should handle subsequent requests when body is pre-read before handleRequest (body-parser pattern)', async () => { + // Covers the body-parser / middleware pattern where the server drains the + // IncomingMessage body before calling transport.handleRequest(req, res, parsedBody). + // Both the initialize request and subsequent non-initialize requests must succeed + // when the transport is reused across requests in stateless mode. + + // Build the server manually so the handler closure can reference transport directly + const preReadTransport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined }); + const preReadMcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }, { capabilities: { logging: {} } }); + preReadMcpServer.registerTool( + 'greet', + { description: 'A greeting tool', inputSchema: z.object({ name: z.string() }) }, + async ({ name }): Promise => ({ content: [{ type: 'text', text: `Hello, ${name}!` }] }) + ); + await preReadMcpServer.connect(preReadTransport); + + const preReadServer = createServer(async (req, res) => { + // Simulate body-parser middleware: drain the stream, then pass parsedBody + const chunks: Buffer[] = []; + for await (const chunk of req) chunks.push(Buffer.from(chunk)); + const bodyStr = Buffer.concat(chunks).toString(); + const parsedBody = bodyStr ? (JSON.parse(bodyStr) as unknown) : undefined; + try { + await preReadTransport.handleRequest(req, res, parsedBody); + } catch (error) { + console.error('Error handling request:', error); + if (!res.headersSent) res.writeHead(500).end(); + } + }); + const preReadBaseUrl = await listenOnRandomPort(preReadServer); + + try { + // Initialize — must succeed + const initResponse = await sendPostRequest(preReadBaseUrl, TEST_MESSAGES.initialize); + expect(initResponse.status).toBe(200); + + // Subsequent request on the same reused transport — must also succeed + const toolsResponse = await sendPostRequest(preReadBaseUrl, TEST_MESSAGES.toolsList); + expect(toolsResponse.status).toBe(200); + } finally { + await stopTestServer({ server: preReadServer, transport: preReadTransport }); + } + }); }); // Test SSE priming events for POST streams