MCP tools/list Hangs on Node? Fix the Undici HTTP/2 Deadlock

MCP tools/list Hangs on Node? Fix the Undici HTTP/2 Deadlock

Aident AI

A coral data capsule moves through glass channels after a silver junction separates a continuous cyan stream into independent paths.

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:

  1. Confirm that the hang occurs after the SSE GET opens and before tools/list reaches the server.

  2. Upgrade a directly managed Undici dependency to 8.8.0 or later.

  3. If your Node runtime still bundles an affected build, inject a scoped fetch that forces HTTP/1.1.

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

initialize returns normally

The endpoint and basic protocol path work

A standalone SSE GET returns 200 and remains open

The long-lived receive stream is established

The next tools/list POST never reaches the server

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:

node -p 'JSON.stringify({ node: process.version, undici: process.versions.undici })'
pnpm why undici

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:

POST initialize               -> response received
POST notifications/initialized -> accepted
GET  <mcp-endpoint>           -> 200 text/event-stream, stays open
POST tools/list               -> client waits; server sees nothing

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:

pnpm add undici@^8.8.0
pnpm why undici

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:

import { Agent, fetch as undiciFetch } from "undici";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const dispatcher = new Agent({ allowH2: false });

const transport = new StreamableHTTPClientTransport(
  new URL(process.env.MCP_URL),
  {
    fetch(input, init) {
      return undiciFetch(input, { ...init, dispatcher });
    },
  },
);

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 allowH2: false fetch

tools/list completes

Undici 8.8.0+ with HTTP/2 enabled

tools/list completes while the SSE stream remains open

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 undici changes nothing

Confirm the MCP SDK uses the imported fetch, not Node's bundled global fetch

The server logs tools/list before the timeout

Inspect the server response and handler; this is not the client queue bug

OAuth hangs before initialize

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:

Follow https://aident.ai/SETUP.md

Then ask:

Check my Aident Loadout account and Vault status.
Search the staging capability catalog for GitHub, Hacker News, and PostHog.
For each family, return exactly one current Action's canonical name,
input schema summary, required connection, operation type, and risk level.
Do not execute a provider Action and do not change any connection

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

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.

Home

Home

Home

Integrations

Integrations

Integrations

Vault

Vault

Vault

Audit

Audit

Audit

Arana Grande

Arana Grande

Arana Grande

Free

Free

Free

30-day audit summary

30-day audit summary

30-day audit summary

Daily action-call volume and the latest receipts from the Loadout audit trail.

Daily action-call volume and the latest receipts from the Loadout audit trail.

Daily action-call volume and the latest receipts from the Loadout audit trail.

View Audit

View Audit

View Audit

Loadout usage

Loadout usage

Loadout usage

617 action calls in the last 30 days

617 action calls in the last 30 days

617 action calls in the last 30 days

May 19 - Jun 17

May 19 - Jun 17

May 19 - Jun 17

10 active days

10 active days

10 active days

Less

Less

Less

More

More

More

Recent activity

Recent activity

Recent activity

Latest action-call receipts from connected agents

Latest action-call receipts from connected agents

Latest action-call receipts from connected agents

Apr 23, 09:23 AM

Apr 23, 09:23 AM

Apr 23, 09:23 AM

Shopify

Shopify

Shopify

Creates Or Updates An Asset For A Theme

Creates Or Updates An Asset For A Theme

Creates Or Updates An Asset For A Theme

Success

Success

Success

Apr 23, 09:21 AM

Apr 23, 09:21 AM

Apr 23, 09:21 AM

Shopify

Shopify

Shopify

Update Products Param Product Id

Update Products Param Product Id

Update Products Param Product Id

Success

Success

Success

Apr 23, 08:53 AM

Apr 23, 08:53 AM

Apr 23, 08:53 AM

Shopify

Shopify

Shopify

Update Products Param Product Id

Update Products Param Product Id

Update Products Param Product Id

Failed

Failed

Failed

Apr 22, 22:13 PM

Apr 22, 22:13 PM

Apr 22, 22:13 PM

Shopify

Shopify

Shopify

Create Product Image

Create Product Image

Create Product Image

Success

Success

Success

Apr 22, 22:12 PM

Apr 22, 22:12 PM

Apr 22, 22:12 PM

Shopify

Shopify

Shopify

Create Product Image

Create Product Image

Create Product Image

Success

Success

Success

Connected integration coverage

Connected integration coverage

Connected integration coverage

162

162

162

of 753 accessible connected

of 753 accessible connected

of 753 accessible connected

Callable actions

Callable actions

Callable actions

1,126

1,126

1,126

Vault credentials

Vault credentials

Vault credentials

8

8

8

Explore what's possible

Explore what's possible

Explore what's possible

See all Integrations

See all Integrations

See all Integrations

Google Ads

Google Ads

Google Ads

All available Goolge Ads tools via...

All available Goolge Ads tools via...

All available Goolge Ads tools via...

X (twitter)

X (twitter)

X (twitter)

All available X tools via...

All available X tools via...

All available X tools via...

Github

Github

Github

All available Github tools via...

All available Github tools via...

All available Github tools via...

Notion

Notion

Notion

All available Notion tools via...

All available Notion tools via...

All available Notion tools via...

Slack

Slack

Slack

All available Slack tools via...

All available Slack tools via...

All available Slack tools via...

Firecrawl

Firecrawl

Firecrawl

All available Firecrawl tools via...

All available Firecrawl tools via...

All available Firecrawl tools via...

753 integrations are available for loadouts.

753 integrations are available for loadouts.

753 integrations are available for loadouts.

Plug your entire stack into your AI agents.

Plug your entire stack into your AI agents.

Plug your entire stack into your AI agents.

Skip the integration headache. Plug 750+ tools into Claude Code, Codex, and OpenClaw in one go, and let your agents execute today.

Skip the integration headache. Plug 750+ tools into Claude Code, Codex, and OpenClaw in one go, and let your agents execute today.