[BUG] v2.1.111: ESC during MCP tool call kills all Python stdio MCPs (regression from v2.1.104, 30-line repro) PLEASE FIX NOW!

Resolved 💬 9 comments Opened Apr 16, 2026 by rf2f7f7sg4-dev Closed May 12, 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?

Pressing ESC to cancel an in-flight MCP tool call kills the entire MCP
subprocess. The server shows as "failed" in /mcp and all its tools become
unavailable until the user manually runs /mcp to reconnect.

The bug is specific to Python-based stdio MCPs using the FastMCP SDK.
Node-based MCPs (e.g. @playwright/mcp) survive the same ESC gesture in the
same session. Remote HTTP/SSE MCPs (claude.ai Gmail, Calendar) are unaffected
by design.

Reproduced with a 30-line bare-minimum Python MCP server — no business
logic, just await asyncio.sleep(30) in the tool. Signal handlers
(SIGINT/SIGTERM/SIGHUP/SIGPIPE set to custom no-op) do NOT prevent the
kill and never fire — pointing to SIGKILL or stdin-close from Claude Code's
side, which no Python code can intercept.

This is a regression first reported in #47724 (v2.1.105). It persists through
v2.1.106 → v2.1.111. 6+ patch releases, same bug.

What Should Happen?

Per MCP spec, ESC should deliver notifications/cancelled for the in-flight
request ID. The tool call should be marked "rejected by user" and the MCP
subprocess must stay alive, all other tools remain available, no /mcp
reconnect required.

This is exactly how v2.1.104 behaves. Same server code, same ESC gesture, same
result: tool cancelled, MCP still connected.

Error Messages/Logs

The following MCP servers have disconnected: esc-test
  The following deferred tools are no longer available (their MCP server
  disconnected)                                                                 
  1 MCP server failed · /mcp
  Nothing on the server-side stderr — no Python traceback, no KeyboardInterrupt,
   no CancelledError, no custom signal-handler log. The process is killed
  externally.

Steps to Reproduce

  1. Install the MCP SDK:

```bash
pip install "mcp[cli]>=1.0"

  1. Save as ~/esc_test.py:

"""Minimal FastMCP Python server — reproduces ESC-kill regression."""
import asyncio, os, signal, sys
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("esc-test")
print(f"[esc-test] pid={os.getpid()} booting", file=sys.stderr, flush=True)

@mcp.tool()
async def sleep_long(seconds: int = 30) -> str:
"""Sleep for N seconds. ESC mid-call should cancel but NOT kill the
MCP."""
try:
await asyncio.sleep(seconds)
return f"slept {seconds}s"
except BaseException as e:
print(f"[esc-test] cancelled with {type(e).__name__}: {e}",
file=sys.stderr, flush=True)
raise

@mcp.tool()
async def ping() -> str:
"""Health check. If this still works after ESC, the MCP survived."""
return f"alive pid={os.getpid()}"

def _diag(signum, frame):
try: name = signal.Signals(signum).name
except Exception: name = f"sig-{signum}"
print(f"[esc-test] received {name} ({signum}); ignored", file=sys.stderr,
flush=True)

if __name__ == "__main__":
for s in ("SIGINT","SIGTERM","SIGHUP","SIGPIPE"):
sig = getattr(signal, s, None)
if sig is not None:
try: signal.signal(sig, _diag)
except Exception: pass
mcp.run()

  1. Register:

claude mcp add esc-test -- python3 ~/esc_test.py

  1. Start a fresh Claude Code session and run /mcp — confirm esc-test · ✔

connected.

  1. Prompt Claude: "call the sleep_long tool from the esc-test MCP with

seconds=30".

  1. As soon as the tool call starts, press ESC once to cancel.
  2. Run /mcp — esc-test · ✘ failed.
  3. Prompt Claude: "call the ping tool from the esc-test MCP" — fails with "MCP

server has disconnected". Requires manual /mcp to recover.

Expected (v2.1.104 behavior): esc-test remains connected after ESC. ping
returns alive pid=<N> immediately with the same PID as before. No reconnect
needed.

Claude Model

Opus

Is this a regression?

Yes, this worked in a previous version

Last Working Version

2.1.104

Claude Code Version

2.1.111 (Claude Code)

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Terminal.app (macOS)

Additional Information

  Follow-up to #47724 (same bug, now reproduced cleanly on v2.1.111 with a      
  minimal 30-line server).                                                      
                                                                                
  **What I ruled out across ~8 hours of debugging**:                            
                                                                  
  1. **Signal handling** — `signal.signal(SIGINT/SIGTERM/SIGHUP/SIGPIPE, no-op)`
   installed before `mcp.run()` does NOT prevent the kill. The handler never
  fires — no stderr log. Rules out Python-level signal termination; points to   
  `SIGKILL` or stdin close.                                       

  2. **Event-loop teardown** — wrapping `mcp.run()` in `while True: try:        
  mcp.run() except BaseException: continue` does NOT save the process. The
  subprocess actually exits. The restart loop never triggers. Rules out uncaught
   exceptions / `KeyboardInterrupt`.                              

  3. **MCP scope / bucket** — all equally affected:                             
     - `--mcp-config <file>` (shown as "Built-in MCP" in `/mcp`) — dies
     - auto-loaded `.mcp.json` at cwd ("Project MCP") — dies                    
     - `claude mcp add --scope local` (".claude.json" project-scope, "Local     
  MCP") — dies                                                                  
     - `claude mcp add --scope user` ("User MCPs") — dies                       
                                                                                
  4. **Python MCP SDK version** — tested with `mcp[cli]==1.26.0` and `1.27.0`.  
  Same behavior.                                                                
                                                                                
  5. **Lifespan / tool count / imports** — bare FastMCP with just `mcp.run()` + 
  one `asyncio.sleep` tool (no lifespan, 2 tools, no imports beyond stdlib +
  `mcp`) dies identically to a production server with 40+ tools, lifespan,      
  daemon threads, and heavy imports.                              

  6. **Language comparison, same session**:                                     
     - `@playwright/mcp@latest` (Node) — survives ESC during `navigate` tool,
  stays connected.                                                              
     - Any Python FastMCP — dies on ESC, no exceptions, no logs.  


Happy to pair-debug with an Anthropic engineer on this — I have the minimal 
  ▎ repro plus 8 hours of bisection data already narrowed down.
                                                                                
  **Impact**: any Python MCP with long tool calls (mesh coordination, wait      
  loops, data pipelines, polling) is unusable on 2.1.105+ without the user
  manually `/mcp`-reconnecting after every cancellation. Workaround: pin Claude 
  Code to `2.1.104`.                                           

View original on GitHub ↗

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