Aident AI

MCP tools/list Hangs on Node? Fix the Undici HTTP/2 Deadlock
If a Node-based MCP client initializes, opens a Streamable HTTP SSE connection, and then hangs forever on tools/list, check its Undici version first. A confirmed Undici HTTP/2 bug queued a POST body behind a long-lived SSE GET on the same connection. The fix shipped in Undici 8.8.0 on July 20, 2026.
Use this order:
Confirm that the hang occurs after the SSE GET opens and before
tools/listreaches the server.Upgrade a directly managed Undici dependency to 8.8.0 or later.
If your Node runtime still bundles an affected build, inject a scoped fetch that forces HTTP/1.1.
Remove the fallback after the runtime carries the upstream fix.
Do not disable HTTP/2 across your entire service, and do not change the MCP server when traces show that the POST never arrived.
Match the Exact Failure Before Changing Transport
This fix applies to a narrow signature reported against the TypeScript MCP SDK's StreamableHTTPClientTransport:
Observation | What it suggests |
|---|---|
| The endpoint and basic protocol path work |
A standalone SSE GET returns | The long-lived receive stream is established |
The next | The client may be queueing the request locally |
Canceling the GET immediately releases the POST | The streams are incorrectly serialized |
The same sequence works over HTTP/1.1 or a non-Node client stack | The server is probably not the failure boundary |
Use a different diagnosis if the server receives tools/list and returns an error, OAuth never completes, DNS or TLS fails, or the client is negotiating the newer 2026-07-28 transport behavior. For broad connection failures, start with How to Fix Claude Code MCP Failed to Connect Errors. For callback-specific failures, use the MCP OAuth callback guide.
Prerequisites
Record the runtime and the Undici version that actually performs the request:
The first command shows Node's bundled Undici version when one is exposed. The second shows a package dependency that your application may control. These can differ.
You also need one non-production MCP endpoint that you are authorized to test. Use a server with a harmless tools/list operation. Do not test a write tool or send credentials to a public reproduction service.
Expected result: you know the Node version, bundled Undici version, direct package version, MCP SDK version, and test endpoint before changing anything.
Step 1: Prove Where the POST Stops
Capture one short client and server trace around this sequence:
This sequence describes the affected 2025-11-25-style client path. The current 2026-07-28 specification changes Streamable HTTP message flow, so confirm the negotiated protocol version rather than assuming every modern MCP timeout has the same cause.
Apply a 15-second diagnostic timeout around tools/list. If the server access log never records that POST, cancel the open GET once. A queued POST that arrives immediately after cancellation is strong evidence for the Undici scheduling bug.
Expected result: the failure is isolated below MCP message handling. If the server saw and answered the POST, stop here and investigate the response, session, proxy, or SDK instead.
Step 2: Upgrade Undici When You Own the Dependency
The upstream regression test and fix landed in Undici pull request 5538. Undici 8.8.0 is the first release whose notes include “allow stream-bodied requests to multiplex on a busy session.” Upgrade to that release or newer:
Restart the client process after the lockfile changes. Then repeat the exact sequence from Step 1 with HTTP/2 still enabled.
Expected result: tools/list reaches the server and returns while the SSE stream remains open. If your code imports Undici directly but process.versions.undici is older, verify that the MCP transport is actually using your imported fetch rather than Node's global fetch.
Step 3: Force HTTP/1.1 for Only This MCP Transport
If the application cannot yet move off an affected bundled Undici build, create one dispatcher with HTTP/2 disabled and pass a custom fetch to the MCP transport:
Keep the dispatcher alive for the transport's lifetime and close it during application shutdown. Do not set a global dispatcher unless every outgoing request in the process has been reviewed for that change.
Why this works: HTTP/1.1 places the long-lived GET and later POSTs on separate connections. It avoids the broken HTTP/2 queue condition without changing the MCP payload, server, authentication, or tool behavior.
Expected result: tools/list completes over the scoped HTTP/1.1 path. Record this as a temporary compatibility fallback, not the final fix.
Step 4: Run an A/B Verification
Test the same endpoint, credentials, and request sequence three ways:
Test path | Expected result |
|---|---|
Affected fetch with HTTP/2 | Reproduces the hang only if the old runtime is still affected |
Scoped |
|
Undici 8.8.0+ with HTTP/2 enabled |
|
Measure the time from sending tools/list to receiving its response and confirm that the server saw the POST. A client-side timeout alone is not enough because OAuth, proxies, server handlers, and session routing can produce similar symptoms.
Expected result: both the temporary HTTP/1.1 path and the upgraded HTTP/2 path work, while only the affected build reproduces the queue.
Common Failure Modes
Failure | Better response |
|---|---|
Upgrading | Confirm the MCP SDK uses the imported fetch, not Node's bundled global fetch |
The server logs | Inspect the server response and handler; this is not the client queue bug |
OAuth hangs before | Debug discovery and callback state before transport multiplexing |
HTTP/1.1 also hangs | Check proxy buffering, session headers, and server-side request handling |
The client uses the 2026-07-28 protocol path | Compare behavior with the current specification and SDK implementation |
A team disables HTTP/2 process-wide | Scope the dispatcher to this transport and document its removal condition |
A fallback remains after the runtime is upgraded | Re-enable HTTP/2, repeat the A/B test, then delete the compatibility code |
Another Streamable HTTP deadlock can look similar without sharing this cause. A July 20 report in mcp-go, for example, traced a 45-second tool-call freeze to an elicitation message sent on the wrong stream. Exact request traces matter more than the word “hang.”
Why the Deadlock Happened
Undici represented the non-empty POST body as a stream. Its HTTP/2 client then treated a busy connection as unable to accept another stream-bodied request, even though HTTP/2 is designed to multiplex independent streams. Because the in-flight request was an SSE GET that could remain open indefinitely, the POST waited indefinitely too.
The upstream patch removed that obsolete busy-session guard and added a regression test for an open SSE stream plus a fetch POST. Cloudflare reproduced the MCP startup stall, reduced it to plain Undici and a local HTTP/2 server, and verified that raw HTTP/2 completed normally. That evidence puts this failure in the client runtime rather than Cloudflare Workers, the MCP SDK, or the MCP server.
The diagnostic structure follows the repeatable lesson from Aident's Ollama network guide: name the exact symptom, isolate the boundary, apply one reversible change, state the expected result, and explain why it works. If your transport works but tool catalogs consume too much context, see How to Reduce MCP Token Usage in Claude Code and Codex. If you are choosing a deployment boundary, compare local and remote MCP servers.
Inspect Three Live Action Schemas Safely
After your MCP client is healthy, set up Aident Loadout by pasting:
Then ask:
Expected result: three current catalog records and zero provider executions. That confirms discovery and schema inspection through a real agent integration layer without turning a transport test into an external write.
Sources
Cloudflare Agents issue 1965, MCP reproduction and root-cause analysis opened July 19, 2026
Undici issue 5524, upstream SSE and HTTP/2 deadlock report
Undici pull request 5538, merged fix and regression test
Undici 8.8.0 release notes, first release containing pull request 5538, published July 20, 2026
MCP 2026-07-28 Streamable HTTP specification, accessed August 2, 2026
TypeScript SDK Streamable HTTP client, current custom-fetch contract, accessed August 2, 2026
One Request, Two Streams, independent July 20, 2026 analysis of a different Streamable HTTP deadlock class
Refresh this guide when supported Node releases bundle the fixed Undici line, the TypeScript SDK removes the affected legacy flow, or the MCP specification changes transport negotiation again.


