anthropics / anthropics/claude-code
sdk-cli mode: Streamable HTTP MCP GET-stream aborts ~200-500ms after connect, tools vanish before first call
- Lingua principale
- Python
- Stelle
- 145k
- Fork
- 23.1k
- Metriche di merge delle PR
- Metriche PR in attesa
Descrizione
## Summary
In "sdk-cli" mode (`--input-format stream-json --output-format stream-json`, the mode used by SDK-driven/headless Claude Code automation), the Streamable HTTP client transport for a remote MCP server aborts its GET notification stream ~200-500ms after a successful connection, before any tool call can complete — even though the connection, authentication, and `initialize` handshake all succeed cleanly. The same server, same tools, and same OAuth tokens work perfectly in plain interactive `cli` mode. This makes the affected MCP server's tools permanently unusable from any headless/automated Claude Code session.
## Environment
- Claude Code version: 2.1.203 (confirmed via `User-Agent: claude-code/2.1.203 (sdk-cli)` in client logs)
- Platform: Linux, Node v26.3.0 (per client's own debug log)
- MCP server: a self-hosted Streamable HTTP MCP server (`@modelcontextprotocol/sdk`'s `StreamableHTTPServerTransport`), OAuth 2.0 + PKCE, reachable over the public internet via HTTPS
## Reproduction
Run Claude Code exactly as an SDK/automation harness would:
```bash
echo '{"type":"user","message":{"role":"user","content":[{"type":"text","text":"Call and return the raw result."}]}}' \
| claude --input-format stream-json --output-format stream-json --verbose --permission-mode dontAsk \
--allowedTools "mcp____" -p
```
Result: the `system/init` event reports the MCP server as `"status":"connected"`, but the very next `ToolSearch` call for that tool returns `"No matching deferred tools found"` — the tool has already vanished from the index by the time the model tries to use it, seconds into the very same turn.
The corresponding MCP client debug log (`~/.cache/claude-cli-nodejs//mcp-logs-plugin-/*.jsonl`) shows:
```
14:03:25.969 | HTTP transport options: {"url":"https:///mcp","headers":{"User-Agent":"claude-code/2.1.203 (sdk-cli)","Accept-Encoding":"identity"},"hasAuthProvider":true,"timeoutMs":60000}
14:03:25.984 | Has refresh token: true
14:03:26.196 | Successfully connected (transport: http) in 228ms
14:03:26.196 | Connection established with capabilities: {"hasTools":true, ...}
14:03:26.435 | HTTP connection dropped after 0s uptime
14:03:26.435 | Connection error: SSE stream disconnected: AbortError: The operation was aborted.
14:03:26.435 | Terminal connection error 1/3
```
~239ms after a clean, fully-authenticated connection, the GET/SSE notification stream gets an `AbortError` and the client gives up (only 1 of 3 configured reconnect attempts is logged before the tool disappears from ToolSearch for the rest of the turn).
## What we ruled out (server-side and infra)
We control the affected MCP server's source and its reverse proxy, so we were able to rule out several server-side and network hypotheses over an extended investigation:
1. **Not an auth/token bug.** Token/refresh-token handling is correct and verified (`Has refresh token: true`, `Successfully connected`, valid `hasTools:true` capabilities). A separate, now-fixed bug where invalid tokens returned bare HTTP 500 instead of 401/400 was found and fixed independently — this abort happens on a *freshly and successfully authenticated* connection, unrelated to that fix.
2. **Not HTTP/2-specific.** The server negotiates HTTP/2 via its reverse proxy (Traefik, ALPN). We forced the reverse proxy to offer only HTTP/1.1 (`tls.options.alpnProtocols: ["http/1.1"]`) and confirmed via `curl` that the negotiated protocol was indeed HTTP/1.1 for this endpoint. The abort reproduced identically:
```
Successfully connected (transport: http) in 228ms
HTTP connection dropped after 0s uptime
Connection error: SSE stream disconnected: AbortError: The operation was aborted.
```
(Same ~200-500ms timing, same error, under both HTTP/1.1 and HTTP/2.)
3. **Not a missing "Connection: keep-alive" / h2 header violation.** We already ship a server-side fix stripping the SDK's default `Connection: keep-alive` header from h2 responses (which RFC 7540 §8.1.2.2 forbids) — no change in behavior with or without it.
4. **Not "no bytes sent" / idle-stream heuristic.** The server sends an SSE comment-line keep-alive (`: ping\n\n`) immediately on stream open and every 50ms for the first 3 seconds specifically to beat any such heuristic. The abort still happens ~200-500ms in, after at least one ping should have been delivered.
5. **Server does hold the connection open correctly.** Correlating the reverse proxy's own access logs with the client's abort timestamp shows the proxy received a clean `200` and the connection was closed **from the client side**, not reset by the server or an intermediary.
6. **Not a general "sdk-cli mode can't hold a Streamable HTTP GET stream" issue.** The exact same Claude Code binary, same `-p`/`--input-format stream-json` invocation, same OS/network, connecting instead to a trivial local reference MCP server (plain HTTP/1.1, no TLS, no reverse proxy, using the same `@modelcontextprotocol/sdk`'s `StreamableHTTPServerTransport` with no custom modifications) — ToolSearch finds the tool and the call succeeds normally:
```
TOOL_USE: mcp__oauth-hypothesis-test-v2__ping_tool {}
TOOL_RESULT: [{'type': 'text', 'text': 'pong'}]
```
7. **Plain interactive `cli` mode against the SAME real server works.** A long-running interactive session (`User-Agent: ... (cli)`) held the identical connection open for 6975+ seconds with normal use before an unrelated, much-later disconnect.
## What this narrows the bug down to
The abort is specific to the **combination** of: (a) `sdk-cli` mode's Streamable HTTP client transport, and (b) something about this particular server's response — but *not* its HTTP version, its `Connection` header, or the presence/timing of keep-alive bytes, all of which we controlled for directly. It is not a general "can't hold a slow/silent HTTP stream" limitation of the sdk-cli client, since the same client mode is fine against a trivial reference server implemented with the same open-source MCP SDK transport class. We were not able to identify what specific difference in the two servers' responses triggers the client-side abort — that requires visibility into the sdk-cli transport's internal abort/watchdog logic, which is closed-source from our side.
## Impact
Any MCP server accessed exclusively through headless/SDK-driven Claude Code sessions (CI pipelines, background agents, `claude -p` automation) that happens to trigger this condition is effectively unusable from those sessions — tools connect, report `hasTools:true`, and then disappear before any call can be made — while working completely normally in interactive sessions. This is easy to misdiagnose as a server auth/config bug (which is what led to an extended, ultimately inconclusive investigation on our end) rather than a client-side transport issue.
## Ask
- Any insight into what could cause the sdk-cli transport's Streamable-HTTP GET-stream `AbortError` at ~200-500ms post-connect, independent of HTTP version, `Connection` header, and keep-alive traffic, given it does not occur against a trivial reference server using the same SDK transport class.
- Happy to provide further logs/traces or a minimal public reproduction server if useful.
Guida per i contributori
Nessuna guida per i contributori indicizzata per questo repository
Direzione di ricerca
Start with the provided sdk-cli reproduction command and compare its MCP debug log with the successful local reference-server run. Trace the Streamable HTTP GET/SSE connection around the 200–500ms AbortError, especially the transport's abort or watchdog behavior described in the report. Done means the affected server's tools remain indexed and callable in sdk-cli mode without breaking the working interactive and local-server cases.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Valutazione
- Stack tecnologico
- node.js
- Ambito
- api, cli, networking
- Tipo di issue
- Bug
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Stato di attività
- Attiva
- Chiarezza
- Abbastanza chiara
- Idoneità per principianti
- 42/100