[Regression] Claude Code 2.1.x silently exits with code 1 during HTTP MCP init — loop-protection swallows the underlying exception stacks

Resolved 💬 4 comments Opened May 6, 2026 by Xiaoyuan-Liu Closed May 8, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code

What's Wrong?

Claude Code CLI 2.1.x introduced an "uncaught exception loop" self-protection
mechanism. With ≥3 HTTP transport MCP servers configured, this creates a
diagnostic dead-end:

  1. During MCP server initialization, multiple unhandled promise rejections

are emitted (apparent path: transport _onclose rejecting awaited
Promises with no .catch()).

  1. The self-protection fires once 10 uncaught exceptions occur within 5000ms.
  2. The original stack traces are swallowed by the handler, which logs

only: [ERROR] Uncaught exception loop detected (10 in 5000ms) — forcing shutdown.

  1. The process exits with code 1. SDK consumers (Claude Agent SDK) receive

only Error: Claude Code process exited with code 1 with no actionable
information.

This is an observability regression in the 2.1.x line. The same family
of unhandled MCP rejections in 2.0.76 surfaced as visible stack traces (see
#3184, #2263); the 2.1.x self-protection turns that into a silent exit.

Users have no escape hatch for self-diagnosis: claude ships as a
Bun-compiled binary, so NODE_OPTIONS=--trace-uncaught and Node-level patches
do not apply. The only signal available is the single "loop detected" line.

Steps to Reproduce

  1. Configure mcpServers with three reachable Streamable-HTTP MCP servers.

Each should complete the initialize handshake successfully:

``json
{
"mcpServers": {
"server-a": { "type": "http", "url": "https://example.com/a/mcp",
"headers": { "Authorization": "Bearer ..." } },
"server-b": { "type": "http", "url": "https://example.com/b/mcp",
"headers": { "Authorization": "Bearer ..." } },
"server-c": { "type": "http", "url": "https://example.com/c/mcp",
"headers": { "Authorization": "Bearer ..." } }
}
}
``

  1. Invoke the CLI:

``bash
claude --debug-to-stderr -p "hi" --mcp-config "$(cat config.json)"
``

  1. Observe exit 1 and the single Uncaught exception loop detected line.

No JS stack is printed.

In my own environment, the three HTTP MCP servers used to reproduce this
were web-search-prime and web-reader (both https://open.bigmodel.cn)
and WebSearch (https://dashscope.aliyuncs.com). Bisection suggests the
failure is correlated with the number of concurrently initialized HTTP
transports rather than one specific server, but I have not verified with
HTTP servers outside my configuration.

Downstream SDK impact

The SDK is not required to reproduce the underlying failure, but this is how
the same CLI shutdown surfaces to SDK consumers:

import { query } from "@anthropic-ai/claude-agent-sdk";

for await (const message of query({
  prompt: "hi",
  options: {
    mcpServers: {
      "server-a": { type: "http", url: "...", headers: { Authorization: "Bearer ..." } },
      "server-b": { type: "http", url: "...", headers: { Authorization: "Bearer ..." } },
      "server-c": { type: "http", url: "...", headers: { Authorization: "Bearer ..." } },
    },
  },
})) {
  console.log(message);
}
// → throws: Error: Claude Code process exited with code 1

Expected Behavior

Claude Code should handle both cases gracefully:

  • The transport's connection-close path should attach a .catch() or wrap

awaits in try/catch, so quickly closed transports do not surface as
uncaughtException events.

  • If the loop-detection handler still needs to force shutdown, it should log

the first 1–3 exception stacks before exiting, so users and SDK consumers
can see the underlying McpError: -32000 or other root cause.

Actual Behavior

Bisection matrix (each row is one full CLI startup; "plugin" = any single
--plugin-dir <path>; baseline stdio MCP servers held constant):

| HTTP MCP servers | Plugin | Result |
| ---------------- | :----: | :----: |
| 0 | yes | exit 0 |
| 1 | yes | exit 0 |
| 2 | no | exit 0 |
| 2 | yes | exit 1, loop detected |
| 3 | no | exit 1, loop detected |
| 3 | yes | exit 1, loop detected |

Stdio MCPs alone never trigger the failure regardless of count.

Concrete debug-log fingerprint (HTTP MCPs reach Connection established,
then within ~1s the loop fires):

[DEBUG] MCP server "WebSearch": Successfully connected (transport: http) in 546ms
[DEBUG] MCP server "WebSearch": Connection established with capabilities: {...}
[DEBUG] MCP server "web-search-prime": Successfully connected (transport: http) in 703ms
[DEBUG] MCP server "web-reader": Successfully connected (transport: http) in 928ms
[DEBUG] MCP server "bb-browser": UNKNOWN connection closed after 0s (cleanly)
[ERROR] MCP server "bb-browser" Failed to fetch tools: MCP error -32000: Connection closed
[ERROR] Uncaught exception loop detected (10 in 5000ms) — forcing shutdown
[DEBUG] MCP server "WebSearch": HTTP connection closed after 3s (cleanly)
[DEBUG] MCP server "web-search-prime": HTTP connection closed after 3s (cleanly)
[DEBUG] MCP server "web-reader": HTTP connection closed after 3s (cleanly)
[DEBUG] SessionEnd:other [...] completed with status 0
Note: bb-browser is a stdio MCP server (bb-browser --mcp) that emits -32000 Connection closed immediately after a successful initialize handshake in my setup. It does not trigger the loop by itself, but appears to contribute rejection events when HTTP transports are also present.

Suspected Root Cause

This looks closely related to #3184 and #2263, where MCP transport
close handling surfaced as unhandled promise rejections through stack frames
including _transport.oncloseclient.transport.onclose
l9.onCloseHandler. In the 2.1.x line, the global
process.on('uncaughtException') handler counts events and self-kills at
10/5000ms. Three concurrent HTTP transports each appear to emit ~3–4
unhandled rejections during their tools/list race against early connection
close, comfortably crossing the threshold. The "Uncaught exception loop
detected" string is not present in 2.0.76, which suggests the new shutdown
path / observability regression was introduced in the 2.1.x line.

Proposed Fix

  1. **Attach .catch() (or wrap in try/await) on every transport

_onclose-triggered Promise** in the MCP transport layer. This is the
underlying root-cause fix — same area as #3184 / #2263.

  1. **Log the first 1–3 exception stacks before the loop-detection handler

forces shutdown.** Even without (1), this single change would let users
and SDK consumers diagnose the actual failure instead of seeing only
exit 1.

Related Issues

  • #3184 — MCP error -32000: Connection closed - Node.js uncaught exception

(closed stale, multiple +1s)

  • #2263 — Unhandled Promise Rejection: MCP Connection Closed Error

(closed stale, same family)

  • #55992 — Streamable HTTP MCP: in-flight tool call Promise orphaned forever

on connection drop (open; same transport-layer Promise hygiene weakness,
different lifecycle phase)

  • #56456 — streamable-http MCP sessions never terminated on client shutdown
  • anthropics/claude-agent-sdk-typescript#308 — Silent process exit (code 1)

when calling query() (open; SDK-side perspective on the same opacity)

Environment

  • Platform: darwin (macOS arm64)
  • Claude Code: 2.1.129
  • Claude Agent SDK: 0.2.128 (consumer side; reproducible via the CLI alone

with --mcp-config, so the SDK is not required to trigger)

  • Runtime packaging: Claude Code is a Bun-compiled binary; NODE_OPTIONS=--trace-uncaught does not expose the swallowed stacks

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗