[BUG] MCP HTTP: first turn blocks for MCP_TIMEOUT-5000ms awaiting subscriptions/listen after a successful server/discover

Status Open
Reported on v2.1.258
Maintainer reply None cached
Activity 1 comment · opened Sep 2, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code

What's Wrong?

After a successful server/discover handshake, Claude Code opens a
subscriptions/listen long-poll and then waits for it to return before dispatching the
first turn
. subscriptions/listen is a server-to-client notification stream: a correct
server holds it open and sends nothing until it has something to send. So the client always
waits out its full budget — measured as exactly MCP_TIMEOUT - 5000 ms — then cancels the
listen, re-opens a fresh one, and proceeds normally.

The result is a silent delay before the first token of the first turn. No error, no log
line, no MCP status change. The process is alive and emitting nothing.

This is not a slow or broken server: the server answers server/discover in ~170 ms and
tools/list in ~600 ms. It is the client awaiting a stream that is not supposed to answer.

Reproduction

qmd (tobi/qmd) 2.8.3 serves MCP over streamable HTTP on
@modelcontextprotocol/server 2.0.0 and implements the 2026-07-28 protocol including
subscriptions/listen:

qmd mcp --http --host 127.0.0.1 --port 8181
echo '{"mcpServers":{"qmd":{"type":"http","url":"http://127.0.0.1:8181/mcp"}}}' > mcp.json
MCP_TIMEOUT=20000 claude -p "reply with the single word: ok" --strict-mcp-config --mcp-config mcp.json

Frame trace through a logging proxy (-> client to server, <- server to client,
seconds since proxy start):

   5.629 -> server/discover        id=server-discover-probe-1  hdr=2026-07-28
   5.800 <- server/discover        200 1784b                    <-- succeeds in 171 ms
   5.806 -> subscriptions/listen   id=listen:0
  20.812 -> notifications/cancelled requestId=listen:0          <-- 15.006 s later
  20.818 -> resources/list
  20.819 -> tools/list
  21.815 -> subscriptions/listen   id=listen:1
  21.962 <- resources/list         200 187b
  22.585 <- tools/list             200 6695b

Nothing is wrong with the discovery handshake. The 15 s gap is the client waiting on
listen:0, which the server is correctly holding open.

The delay tracks MCP_TIMEOUT exactly:

| MCP_TIMEOUT | listen opened | cancelled | block | MCP_TIMEOUT - 5000 |
|---|---|---|---|---|
| 12000 | 6.203 | 13.205 | 7.002 s | 7.000 s |
| 20000 | 5.806 | 20.812 | 15.006 s | 15.000 s |

End-to-end claude -p wall clock at MCP_TIMEOUT=20000: 6.0 s with no MCP server,
22.6 s with this one.

A second observation, from the same runs

At MCP_TIMEOUT=40000 the server happened to answer server/discover in 5.4 s (cold model
load). The client abandoned the probe at exactly 5.009 s and fell back to initialize
(hdr=NONE, negotiated 2025-11-25) — and then did not stall at all, because the legacy
path never opens subscriptions/listen:

   5.318 -> server/discover   hdr=2026-07-28
  10.327 -> initialize        hdr=NONE          <-- probe abandoned at 5.009 s
  10.717 <- server/discover   200 1784b         <-- answer arrives late, ignored
  13.855 <- initialize        200 1684b

So a server that is slower to answer discovery gets a faster startup than one that is
fast. That inversion is a useful signal about where the cost sits.

Impact

  1. Silent. Nothing surfaces it; it looks like the model is thinking.
  2. MCP_TIMEOUT scales it. The same variable governs the MCP connect deadline and

this first-turn wait. A host that raises MCP_TIMEOUT for long-running blocking tool
calls — a legitimate use, e.g. a tool that waits on human input — buys a proportionally
longer silent startup freeze. At MCP_TIMEOUT=7200000 (2 h) this is a 1 h 59 m 55 s
freeze per session, indistinguishable from a hung agent.

  1. It applies to every server that implements subscriptions/listen per spec.

At the stock 30 s default it is a ~25 s startup penalty paid silently on every session.

Expected Behaviour

Any of these fixes it independently:

  1. Do not await subscriptions/listen as part of connection readiness. It is a

fire-and-forget stream; the client already re-opens listen:1 after cancelling
listen:0, so nothing depends on the first one completing.

  1. If the wait is deliberate, give it its own short bound rather than sharing MCP_TIMEOUT

with tool-call deadlines.

  1. Treat a subscriptions/listen that has been accepted (HTTP 200, stream open) as

connected, rather than pending.

Environment

  • Claude Code 2.1.258
  • macOS 27.0, arm64, Node v22.22.0
  • Server: qmd 2.8.3 on @modelcontextprotocol/server 2.0.0, streamable HTTP, responseMode: "json"

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗