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 diff --git a/test/integration/test/server/cloudflareWorkers.test.ts b/test/integration/test/server/cloudflareWorkers.test.ts index 9c2d73a40e..f8b07f2966 100644 --- a/test/integration/test/server/cloudflareWorkers.test.ts +++ b/test/integration/test/server/cloudflareWorkers.test.ts @@ -150,28 +150,29 @@ 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); });