anthropics / anthropics/claude-code
[BUG] Streamable HTTP MCP tool call still times out ("The operation timed out") at ~6min despite per-server timeout, CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT=0, and a requestTimeout=0 server
- Lingua principale
- Python
- Stelle
- 145k
- Fork
- 23.1k
- Metriche di merge delle PR
- Metriche PR in attesa
Descrizione
### Preflight Checklist
- [x] I have searched [existing issues](https://github.com/anthropics/claude-code/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug) and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
### What's Wrong?
A Streamable HTTP MCP server's `tools/call` genuinely needs to stay open for several minutes (in our case, waiting on a human to answer a question rendered from the tool call) errors out with:
```
is_error: true
content: "The operation timed out."
```
at a consistently narrow ~352-363 second window (measured 3 separate times: 352.x, 363.1, 362.5 -- an 11-second spread), even though every documented mechanism to raise or disable that timeout was applied at the same time:
1. The server's own entry `timeout` field in `--mcp-config`'s `mcpServers` set to `86400000` (24h).
2. `CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT=0` set as an environment variable on the `claude` process (confirmed via a debug print that the child actually receives `0`, not empty/unset).
3. (Ruled out, not the cause) the connecting server's own `http.Server.requestTimeout` set to `0`, in case a local server-side default was the real culprit instead of the CLI -- it wasn't; the timing didn't change.
The MCP server itself stays healthy and reachable the entire time (verified independently -- our production bridge never errors, closes, or resets the connection; a companion reproduction below confirms it in isolation too), so this is the `claude` client giving up on its own `tools/call`, not a transport failure.
### What Should Happen?
With the per-server `timeout` set to 24h and/or `CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT=0` set, the `tools/call` should not abort at ~6 minutes -- it should either honor the 24h ceiling, or (per the `=0` docs, "disables the check entirely") not idle-timeout at all, for as long as the server stays connected and doesn't itself return an error.
### Error Messages/Logs
```shell
Captured from the real (non-minimal) case, verbatim tool_result content:
is_error= true "The operation timed out."
And from the minimal reproduction's stream-json output, the equivalent tool_result block:
{"type":"tool_result","is_error":true,"content":[{"type":"text","text":"The operation timed out."}]}
```
### Steps to Reproduce
Minimal, self-contained reproduction (no dependency on any specific project -- a bare Streamable HTTP MCP server whose one tool deliberately never responds):
```javascript
import { createServer } from "node:http";
import { spawn } from "node:child_process";
const TOOL = "wait_forever";
const server = createServer((req, res) => {
if (req.method !== "POST") return res.writeHead(405).end();
let body = "";
req.on("data", (c) => (body += c));
req.on("end", () => {
const msg = JSON.parse(body);
if (msg.method === "initialize") {
res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({
jsonrpc: "2.0", id: msg.id,
result: { protocolVersion: "2025-06-18", capabilities: { tools: {} }, serverInfo: { name: "repro", version: "1.0.0" } },
}));
} else if (msg.method === "tools/list") {
res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({
jsonrpc: "2.0", id: msg.id,
result: { tools: [{ name: TOOL, description: "Never returns", inputSchema: { type: "object", properties: {} } }] },
}));
} else if (msg.method === "tools/call") {
// Deliberately never respond -- simulates a tool genuinely waiting (e.g. on a human).
} else if (msg.id === undefined) {
res.writeHead(202).end();
} else {
res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({ jsonrpc: "2.0", id: msg.id, result: {} }));
}
});
});
server.requestTimeout = 0; // rule out the *reproduction's own* server as the cause
server.listen(0, "127.0.0.1", () => {
const port = server.address().port;
const mcpConfig = JSON.stringify({
mcpServers: { repro: { type: "http", url: `http://127.0.0.1:${port}/mcp`, timeout: 86400000 } },
});
const env = { ...process.env, CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT: "0" };
const start = Date.now();
const child = spawn("claude", [
"-p", `Call the ${TOOL} tool right now, then report exactly what tool_result you get back, verbatim.`,
"--output-format", "stream-json", "--verbose",
"--mcp-config", mcpConfig,
"--allowedTools", `mcp__repro__${TOOL}`,
"--strict-mcp-config",
], { env });
let buf = "";
child.stdout.on("data", (d) => (buf += d));
child.on("close", () => {
console.log("elapsed seconds:", (Date.now() - start) / 1000);
console.log(buf);
server.close();
});
});
```
1. Save the script above as `repro.mjs`.
2. Run `node repro.mjs`.
3. Wait about 6 minutes.
4. Observe stdout: `elapsed seconds: ~352-365`, and the captured `stream-json` output contains a `tool_result` block with `is_error: true` and content `"The operation timed out."`.
Real-world context this was found in: an open-source Claude Code relay (`ultron`, https://github.com/wilmacedo/ultron) that exposes an MCP tool (`present_choice`) to let the model ask the human a closed multiple-choice question and block on the answer. Any question a human takes longer than ~6 minutes to notice/answer degrades the picker into a timeout error -- the model recovers gracefully by falling back to plain text, but the intended UI never gets a chance to work for a slow response.
### Claude Model
None
### Is this a regression?
I don't know
### Last Working Version
_No response_
### Claude Code Version
2.1.266 (Claude Code)
### Platform
Anthropic API
### Operating System
Ubuntu/Debian Linux
### Terminal/Shell
Non-interactive/CI environment
### Additional Information
- Three separate live measurements, each a fresh process: 352.x s (real production case, "The operation timed out" from a genuine multi-minute human wait), 363.1 s (isolated repro with the per-server `timeout` field set to 24h), 362.5 s (isolated repro with `CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT=0` added on top), 357.8 s (the minimal standalone reproduction included above, run independently). All four within a 11-second band -- reads like one fixed, undocumented internal timeout, not jitter.
- Related but NOT a duplicate: #50289 ("`.mcp.json` per-server `timeout` field no longer honored for HTTP MCP tool calls since 2.1.113"), closed/completed. Same general area (per-server HTTP timeout config being silently ignored), but that report's observed ceiling was ~60s, ours is ~360s, and it doesn't cover `CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT` (which post-dates that report) failing to disable the check.
- `#16837` ("Claude code does not obey values of MCP_TIMEOUT longer than 60 seconds") is a long-open issue in the same recurring theme (documented MCP timeout config being ignored) but a different specific variable than either of the two we tested.
- Transport is `"type": "http"` (Streamable HTTP, single POST per JSON-RPC call), not SSE -- worth noting since some prior reports in this area are SSE-specific.
- The CLI itself emits `tool_progress` heartbeat events for the pending call at `elapsed_time_seconds: 300` and `330` (visible in `--output-format stream-json`) shortly before the timeout fires -- so the call is not being treated as silent/idle by the CLI's own instrumentation, yet it still aborts around 350-365s. That reads more like a fixed internal wall-clock ceiling than a true idle-detection timeout, which would be consistent with `CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT=0` having no effect
Guida per i contributori
Nessuna guida per i contributori indicizzata per questo repository
Direzione di ricerca
Start with the self-contained repro.mjs and run node repro.mjs, then inspect the Streamable HTTP tools/call timeout path and the handling of timeout: 86400000 and CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT=0. Compare the tool_progress events with the final tool_result; done means the call remains open beyond the observed ~6-minute ceiling while the server stays connected.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Valutazione
- Stack tecnologico
- javascript, node.js
- Ambito
- api, cli
- Tipo di issue
- Bug
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Stato di attività
- Attiva
- Chiarezza
- Abbastanza chiara
- Idoneità per principianti
- 52/100