Cursor 3.4.20 kills stdio MCP servers ~1.5s after successful initialize (SIGKILL, V2 FSM race)

Where does the bug appear (feature/product)?

Cursor IDE

Describe the Bug

All user-defined stdio MCP servers configured in ~/.cursor/mcp.json fail to stay connected on macOS Cursor 3.4.20. They reach the “connected” state after a successful initialize + tools/list handshake, then are killed by Cursor itself ~1.5–2 seconds later with SIGKILL (exit 137) — not a graceful stdin close. The state machine transitions are none → initializing → connected → error. Tools never become callable. The built-in cursor-ide-browser MCP is unaffected because it does not go through the stdio child-process path.

The same servers work perfectly when launched standalone in a terminal and fed JSON-RPC over stdin.

Steps to Reproduce

  1. Use this minimal ~/.cursor/mcp.json (or any other stdio MCP):
{
  "mcpServers": {
    "postgres": {
      "command": "npx",
      "args": ["-y", "mcp-postgres@latest"],
      "env": { "DATABASE_URL": "postgresql://USER:PASS@HOST:5432/postgres" }
    }
  }
}
  1. Launch Cursor with any workspace open.
  2. Open ~/Library/Application Support/Cursor/logs/<session>/window1/exthost/anysphere.cursor-mcp/MCP user-postgres.log.
  3. Observe the server reaches Successfully connected to stdio server and then transport_closed ~1.5s later.
  4. Verify the same mcp-postgres@latest package works fine when started manually from a terminal with the same JSON-RPC initialize + tools/list fed over stdin — it responds correctly and stays alive.

Expected Behavior

After a successful initialize + tools/list, the stdio MCP server should stay alive until the user (or Cursor on a clean shutdown) explicitly disables/removes it. Tools should remain callable for the entire session, just like the built-in MCPs.

Operating System

MacOS

Version Information

Cursor 3.4.20 (Electron 39.8.1)
Darwin 24.3.0 (macOS)
Node.js (system): v23.11.0 (/usr/local/bin/node)

Same behavior was observed on 3.3.27 before the auto-upgrade.

Additional Information

Direct evidence that Cursor sends SIGKILL

With a thin bash wrapper that records signals instead of execing immediately, Cursor’s kill -9 becomes visible:

/Users/me/.cursor/mcp-launcher-debug.sh: line 30:
  12685 Killed: 9               /usr/local/bin/npx -y "$@"

Race condition in V2 FSM

workbench.mcp.allowlist.log + per-server log shows CreateClient followed by DeleteClient only 15 ms later, then another CreateClient. The earlier process is then killed mid-startup:

11:42:33.683 createClient: identifier="user-postgres"
11:42:33.688 [V2] Handling CreateClient action
11:42:33.703 [V2] Handling DeleteClient action, reason: config_server_modified  ← 15ms later
11:42:35.372 [error] Connected to database: ... (still starting)
11:42:37.076 createClient: identifier="user-postgres"
11:42:37.079 [V2] Handling CreateClient action
11:42:38.806 [error] Connected to database: ...
11:42:39.683 [error] line 30: 12685 Killed: 9
11:42:39.691 Connection failed: MCP error -32000: Connection closed

The reason: config_server_modified fires even when mcp.json content is unchanged, suggesting an overly eager file-watcher trigger or duplicated CreateClient calls.

Separate but related: environment pollution leaks to MCP children

Cursor injects /Applications/Cursor.app/Contents/Resources/app/resources/helpers/ at the head of PATH when spawning the MCP child process. That directory contains a 226 MB binary named node, which is an Electron-as-Node helper, and Cursor also sets ELECTRON_RUN_AS_NODE=1 in the child environment.

which node inside the MCP wrapper returns:

/Applications/Cursor.app/Contents/Resources/app/resources/helpers/node
/usr/local/bin/npx

As a result, npx ends up executing the MCP server under the Electron-as-Node interpreter, which corrupts the stdio protocol enough that some servers cannot complete initialize at all (firebase-tools mcp, @mobilenext/mobile-mcp — both work standalone, both fail under Cursor unless ELECTRON_RUN_AS_NODE is unset and PATH is repaired).

Workaround for the env pollution (NOT for the SIGKILL bug):

#!/bin/bash
unset ELECTRON_RUN_AS_NODE
export PATH="/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin"
exec /usr/local/bin/npx -y "$@"

With this wrapper, initialize succeeds — but the SIGKILL bug still kills the server ~1.5 s later.

Impact

All user-defined stdio MCP servers (postgres / gitlab / apifox / firebase / mobile-mcp in my case) are unusable. Only the built-in cursor-ide-browser works.

Suggested investigation areas

  1. The duplicate CreateClientDeleteClient race in the V2 FSM (reason: config_server_modified firing without config changes).
  2. Use SIGTERM + graceful stdin close before SIGKILL; respect a settle window after initialize.
  3. Stop injecting helpers/ into child PATH and stop leaking ELECTRON_RUN_AS_NODE=1 to MCP child processes.
  4. Make the initialize timeout configurable per-server for heavy bootstraps like firebase-tools mcp.

A log bundle (Cursor MCP logs + wrapper-captured stdio I/O + the SIGKILL stderr) is attached.

Does this stop you from using Cursor

No - Cursor works, but with this issue

Here is a complete log bundle reproducing the issue on my machine.

Contents:

  • `cursor-logs/` — raw Cursor MCP logs for this session (per-server `.log` files + `workbench.mcp.allowlist.log` + `workbench.mcp.oauth.log` showing the `connected → error` transitions and the `reason: config_server_modified` DeleteClient race).
  • `wrapper-debug/` — the bash wrapper I used to capture stdin/stdout/stderr/exit codes, including the line that shows `Killed: 9` being delivered by Cursor to the npx child.
  • `README.md` — same content as this report, in self-contained Markdown.

Happy to provide additional traces, run other diagnostics, or test a patched build.

cursor-mcp-bug-bundle.zip (12.3 KB). 补充信息

Thanks for the incredibly detailed report and log bundle. This is a known bug with the MCP server lifecycle on startup. Our engineering team is actively working on MCP reliability improvements, and this race condition is on their radar.

For the environment pollution issue (ELECTRON_RUN_AS_NODE + PATH injection), the bash wrapper approach you found is the right workaround for now:

#!/bin/bash
unset ELECTRON_RUN_AS_NODE
export PATH="/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin"
exec /usr/local/bin/npx -y "$@"

For the SIGKILL/race condition itself, there’s unfortunately no client-side workaround at the moment.

We’ll be tracking this thread as we work on the fix. If you’re able to test a future update and report back whether the behavior changes, that would be very helpful.