[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
Preflight Checklist
- [x] I have searched existing issues 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:
- The server's own entry
timeoutfield in--mcp-config'smcpServersset to86400000(24h). CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT=0set as an environment variable on theclaudeprocess (confirmed via a debug print that the child actually receives0, not empty/unset).- (Ruled out, not the cause) the connecting server's own
http.Server.requestTimeoutset to0, 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
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):
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();
});
});
- Save the script above as
repro.mjs. - Run
node repro.mjs. - Wait about 6 minutes.
- Observe stdout:
elapsed seconds: ~352-365, and the capturedstream-jsonoutput contains atool_resultblock withis_error: trueand 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
timeoutfield set to 24h), 362.5 s (isolated repro withCLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT=0added 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.jsonper-servertimeoutfield 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 coverCLAUDE_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_progressheartbeat events for the pending call atelapsed_time_seconds: 300and330(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 withCLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT=0having no effect
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗