fix(mcp): stream guarded transport via undici.request so SSE responses work under Bun#5897
Conversation
…s work under Bun undici's fetch exposes its response body as a WHATWG ReadableStream whose bridge is broken under the Bun runtime the standalone server runs on: headers arrive but response.body never yields data, hanging every MCP streamable-HTTP (text/event-stream) tools/list read to its 30s timeout. undici's lower-level request() returns a Node Readable, which Bun streams natively. createSsrfGuardedFetchWithDispatcher (the MCP transport builder) now routes through undiciRequestAsResponse: same guarded Agent + connect.lookup (SSRF unchanged), request() instead of fetch(), and a hand-rolled Node->Web body bridge (not Readable.toWeb, which throws ERR_INVALID_STATE on the redirect body.cancel()). Buffered reads, followRedirectsGuarded, maxResponseSize, and abort all preserved and verified on both Node and Bun.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview The new path adds fetch parity that
Reviewed by Cursor Bugbot for commit 0c3cf59. Configure here. |
Greptile SummaryThis PR replaces the MCP guarded transport’s
Confidence Score: 5/5The PR appears safe to merge because no blocking failure related to an eligible previous Greptile thread remains. No blocking failure remains. Important Files Changed
Sequence DiagramsequenceDiagram
participant SDK as MCP SDK
participant Guard as SSRF-guarded fetch
participant Redirect as Redirect validator
participant Undici as undici.request
participant Server as MCP server
SDK->>Guard: Fetch MCP request
Guard->>Redirect: Validate initial URL
Redirect->>Undici: Request with guarded Agent
Undici->>Server: Connect using guarded DNS lookup
Server-->>Undici: Headers and Node Readable body
Undici-->>Redirect: Response with Web ReadableStream bridge
alt Redirect response
Redirect->>Redirect: Cancel body and validate next target
Redirect->>Undici: Request validated redirect target
else Final response
Redirect-->>SDK: Streaming-compatible Response
end
Reviews (5): Last reviewed commit: "fix(mcp): guard decoder errors and null-..." | Re-trigger Greptile |
|
@cursor review |
- toUndiciRequestBody serializes a URLSearchParams body (the MCP SDK's OAuth token/refresh exchange sends one); undici.request rejects it otherwise. - Default content-type application/x-www-form-urlencoded when the caller didn't set one (fetch parity). - nodeReadableToWebStream enqueues a copy (new Uint8Array(chunk)) not a view, so undici recycling the pooled source buffer can't corrupt queued chunks. - Tests for both.
|
@cursor review |
…tch parity) undici.request returns raw bytes; fetch auto-decompresses. Restore that: pipe a gzip/deflate/br response body through the matching zlib decoder before the WHATWG bridge and strip content-encoding/content-length. maxResponseSize still caps the compressed wire bytes; errors forward into the decoder and the source is torn down when the decoded stream ends or is cancelled. Adds a gzip decode test.
|
@cursor review |
…crashes - Attach the stream bridge's error listener before piping into the zlib decoder, so a synchronous zlib error (server mislabeling a non-gzip body as gzip) rejects the reader instead of crashing the process. - Attach an error listener before draining a null-body response, and wrap Response construction in try/catch that destroys the source (no socket leak) on an out-of-range status. Adds an invalid-gzip regression test.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 0c3cf59. Configure here.
Summary
Root cause precedent
Confirmed against community reports: Bun #5964 (fetch body reader behaves as closed on streaming HTTP), Bun #30381 (keep-alive body only ends on TCP FIN → hangs). The hand-rolled bridge is the pattern nodejs/node#54205 explicitly recommends over `toWeb`.
Type of Change
Testing
Checklist