anthropics / anthropics/claude-code
[BUG] MCP HTTP: first turn blocks for MCP_TIMEOUT-5000ms awaiting subscriptions/listen after a successful server/discover
- Ngôn ngữ chính
- Python
- Star
- 145k
- Fork
- 23.1k
- Chỉ số merge pull request
- Chỉ số pull request đang chờ
Mô tả
### 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](https://github.com/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`:
```bash
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.
3. 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.
2. If the wait is deliberate, give it its own short bound rather than sharing `MCP_TIMEOUT`
with tool-call deadlines.
3. 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"`
Hướng dẫn đóng góp
Chưa lập chỉ mục được hướng dẫn đóng góp cho kho mã nguồn này
Hướng nghiên cứu
Reproduce the delay with the qmd MCP HTTP server and the provided MCP_TIMEOUT values, then trace the client sequence from server/discover through subscriptions/listen, notifications/cancelled, resources/list, and tools/list. Check why connection readiness awaits the first subscriptions/listen stream and verify that an accepted stream no longer blocks the first turn or scales with MCP_TIMEOUT.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Đánh giá
- Công nghệ
- node.js
- Lĩnh vực
- backend-api-design, networking
- Loại issue
- Lỗi
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức độ hoạt động
- Sôi nổi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức phù hợp với người mới
- 58/100