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

Open
#93,143 2 comments 0 reactions 0 assignees View on GitHub
area:mcp bug has repro platform:linux
Dominant language
Python
Stars
145k
Forks
23.1k
PR merge metrics
PR metrics pending

Description

### 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

Contributor guide

No contributing guide indexed for this repository

Research direction

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.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, node.js
Domain
api, cli
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.