[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

Status Open
Reported on v2.1.266
Maintainer reply None cached
Activity 1 comment · opened Sep 9, 2026

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:

  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

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();
  });
});
  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

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗