anthropics / anthropics/claude-code
[BUG] MCP HTTP: first turn blocks for MCP_TIMEOUT-5000ms awaiting subscriptions/listen after a successful server/discover
- Lenguaje dominante
- Python
- Estrellas
- 145k
- Forks
- 23.1k
- Métricas de merge de PR
- Métricas de PR pendientes
Descripción
### 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"`
Guía de contribución
No hay ninguna guía de contribución indexada para este repositorio
Línea de trabajo
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.
Escrito por el modelo de indexación a partir del texto del issue.
Evaluación
- Stack tecnológico
- node.js
- Área
- backend-api-design, networking
- Tipo de issue
- Error
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Estado de actividad
- Activo
- Claridad
- Bien especificado
- Aptitud para principiantes
- 58/100