claude-code - 💡(How to fix) Fix [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! [2 comments, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
anthropics/claude-code#49479Fetched 2026-04-17 08:40:00
View on GitHub
Comments
2
Participants
1
Timeline
11
Reactions
0
Participants
Timeline (top)
labeled ×6commented ×2cross-referenced ×1renamed ×1

Error Message

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.

Fix Action

Fix / Workaround

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.

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.

Code Example

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.

---

pip install "mcp[cli]>=1.0"                                                
                                                                  
  2. 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()                                                                 
  3. Register:                                                    
  claude mcp add esc-test -- python3 ~/esc_test.py
  4. Start a fresh Claude Code session and run /mcp — confirm esc-test · ✔      
  connected.                                                                    
  5. Prompt Claude: "call the sleep_long tool from the esc-test MCP with        
  seconds=30".                                                                  
  6. As soon as the tool call starts, press ESC once to cancel.   
  7. Run /mcp — esc-test · ✘ failed.                                            
  8. 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
RAW_BUFFERClick to expand / collapse

Preflight Checklist

  • I have searched existing issues and this hasn't been reported yet
  • This is a single bug report (please file separate reports for different bugs)
  • 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:
    pip install "mcp[cli]>=1.0"
  2. 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()
3. Register:
claude mcp add esc-test -- python3 ~/esc_test.py 4. Start a fresh Claude Code session and run /mcp — confirm esc-test · ✔
connected.
5. Prompt Claude: "call the sleep_long tool from the esc-test MCP with
seconds=30".
6. As soon as the tool call starts, press ESC once to cancel.
7. Run /mcp — esc-test · ✘ failed.
8. 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`.

extent analysis

TL;DR

The issue can be temporarily worked around by pinning Claude Code to version 2.1.104, where the ESC gesture correctly cancels the tool call without killing the MCP subprocess.

Guidance

  • Verify that the issue is indeed a regression by confirming that version 2.1.104 behaves as expected, where the MCP subprocess remains alive after the ESC gesture.
  • Test the minimal reproducible example provided in the issue to ensure it demonstrates the problem consistently.
  • Consider pairing with an Anthropic engineer to debug the issue further, utilizing the provided 30-line minimal server and the 8 hours of bisection data.
  • Evaluate the impact of this issue on your workflow, especially if you rely on Python MCPs with long tool calls, and plan accordingly, potentially by using the workaround until a fix is available.

Example

No specific code changes are suggested at this point, as the issue seems to be related to how Claude Code handles the ESC gesture in relation to Python-based MCPs. However, reviewing the provided esc_test.py example can help in understanding the minimal conditions required to reproduce the issue.

Notes

The root cause of the issue appears to be related to how Claude Code handles the ESC gesture for Python-based MCPs, potentially sending a SIGKILL or closing stdin, which cannot be intercepted by Python signal handlers. This is a regression introduced after version 2.1.104.

Recommendation

Apply the workaround by pinning Claude Code to version 2.1.104 until a fixed version is released, as this version is known to handle the ESC gesture correctly for Python MCPs.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING