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 |
|
@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.
|
@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 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