anthropics / anthropics/claude-code

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

Abierto
#91,414 0 comentarios 0 reacciones 0 asignados Ver en GitHub
area:mcp bug has repro platform:macos
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

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.