[BUG] MCP HTTP: first turn blocks for MCP_TIMEOUT-5000ms awaiting subscriptions/listen after a successful server/discover
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 asubscriptions/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 andtools/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 includingsubscriptions/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 onlisten: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
- Silent. Nothing surfaces it; it looks like the model is thinking.
MCP_TIMEOUTscales 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.
- It applies to every server that implements
subscriptions/listenper 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:
- Do not await
subscriptions/listenas 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.
- If the wait is deliberate, give it its own short bound rather than sharing
MCP_TIMEOUT
with tool-call deadlines.
- Treat a
subscriptions/listenthat 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/server2.0.0, streamable HTTP,responseMode: "json"
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗