Uh oh!
There was an error while loading. Please reload this page.
fix(mcp): use HTTP/1.1 for the streamable-HTTP transport (drop allowH2) - #5797
Conversation
The MCP transport was the only place in the codebase opting undici into HTTP/2 (`allowH2: true`). undici's h2 support is experimental and has a documented cluster of stalls where response headers arrive but the body DATA frames never do, on POST bodies over reused/coalesced sessions (nodejs/undici #2311, #3433, #4143). Behind a shared egress IP fronted by a CDN, that is exactly what hung the streamable-HTTP `initialize` after OAuth: 200 + Mcp-Session-Id, then an empty body until the SDK's 30s timeout — reproducible only from the deployed egress, never from a fresh IP or in isolation. h2 buys the MCP transport nothing (one POST per JSON-RPC message plus a single long-lived SSE stream — no concurrency to multiplex), and both official MCP SDKs run the transport on HTTP/1.1: the TypeScript SDK's StreamableHTTPClientTransport calls global fetch (undici h1.1, no allowH2), and the Python SDK builds its httpx client with no http2=True. Dropping allowH2 aligns with them and steps off the h2 stall surface. CDN fronts serve h1.1 anyway; SSRF IP-pinning and Agent teardown are unchanged.
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview
Removes optional Reviewed by Cursor Bugbot for commit 71a7259. Configure here. |
Greptile SummaryThis PR moves the streamable HTTP transport back to HTTP/1.1. The main changes are:
Confidence Score: 5/5This looks safe to merge.
Important Files Changed
Reviews (3): Last reviewed commit: "chore(credentials): drop help text on th..." | Re-trigger Greptile |
waleedlatif1
commented
Jul 21, 2026
waleedlatif1
commented
Jul 21, 2026
@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 c1f8195. Configure here.
waleedlatif1
commented
Jul 21, 2026
waleedlatif1
commented
Jul 21, 2026
@cursor review |
Uh oh!
There was an error while loading. Please reload this page.
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 71a7259. Configure here.
…hangs (#5798) * fix(mcp): pin outbound connections to IPv4 to avoid unreachable-IPv6 hangs Root cause of the MCP 'connecting forever' / tool-discovery timeouts in production: SSRF pinning forces each outbound connection to a single resolved IP, which strips Happy Eyeballs' IPv4 fallback. dns.lookup(verbatim) returns the IPv6 address first for Cloudflare-fronted dual-stack hosts (Gauge, api.exa.ai), so the connection was pinned to IPv6 — but the production app subnets have no IPv6 egress (AWS NAT gateways are IPv4-only), so the pinned IPv6 connection connects into a void and hangs until the 30s timeout. Intermittent because the resolver rotates A/AAAA order; works on retry when it happens to pick IPv4. This is why h1.1 (#5797) did not fix it (protocol-independent) and why it never reproduced locally (dev machines have IPv6 egress). Empirically verified: pinning only the IPv6 address times out; preferring the IPv4 address connects in ~600ms. Fix: at both pinned-resolution sites (validateMcpServerSsrf and validateUrlWithDNS) resolve all addresses and prefer an IPv4 one; IPv6-only hosts still pin their sole address. No signature/threading changes; SSRF validation of the pinned IP is unchanged. * chore(mcp): trim inline comments on the IPv4-preference fix
Summary
allowH2: trueincreatePinnedMcpFetch). Everything else — including the OAuth legs that now work — uses h1.1.initializeafter OAuth succeeded:200+Mcp-Session-Id, then an empty body until the SDK's 30s timeout — reproducible only from the deployed egress, never from a fresh IP or in isolation (the same authenticated initialize returns the full result in ~400-600ms over h2 or h1.1 from a dev machine).allowH2→ the transport runs HTTP/1.1.Why this is the correct, best-practice config (verified against the reference clients)
StreamableHTTPClientTransport: issues requests via(this._fetch ?? fetch)(...)— global fetch = undici on HTTP/1.1 — and never setsallowH2/ importsnode:http2.httpx.AsyncClientwith nohttp2=True→ HTTP/1.1.mcp-remoteand SDK-based clients (Continue, LibreChat, …) consume that transport as-is (h1.1).What does NOT change
connect.lookup) is orthogonal to ALPN and is untouched.close()) still tears down pooled sockets (incl. the SSE connection).Type of Change
Testing
lib/mcpsuite green (372); tsc clean; biome clean.allowH2(stays on h1.1) and still tears down onclose().Checklist