Cursor-agent CLI: stdio MCP server fails with "Connection closed" on Windows, works in the IDE

Where does the bug appear (feature/product)?

Cursor CLI

Describe the Bug

Summary
cursor-agent reports MCP error -32000: Connection closed for a local stdio MCP server about 2 seconds after it starts the server. The same server connects normally in the Cursor IDE, using the same mcp.json entry. A staff reply in a related thread (Docker MCP servers) says the CLI uses a more aggressive connection timeout than the GUI and that this is a known issue. This report adds Windows details, the observed timing, and the child-process differences.

Environment
OS: Windows 11 Pro 10.0.26200
cursor-agent: 2026.10.19-6bbf2c4
Cursor IDE: 3.26.16
PowerShell: 7.6.6
Docker Desktop: 29.8.2
Node.js: v24.21.0
Config (~/.cursor/mcp.json, redacted)
“mcp-atlassian-cli”: {
“type”: “stdio”,
“command”: “pwsh”,
“args”: [“-NoProfile”, “-File”, “/.cursor/mcp-docker-wrapper.ps1”, “-ImageName”, “/:latest”],
“envFile”: “/.cursor/mcp.env”,
“env”: { “…”: “…” },
“startupTimeoutSec”: 60
}
The wrapper script is required by our organization. It starts a shared, already-running HTTP MCP server on 127.0.0.1:9000, bridges it to stdio, and answers MCP initialize in about 1.4 seconds when run by hand.

Steps to Reproduce

Configure a stdio MCP server as above (any server with a multi-second startup works; the one above is a PowerShell wrapper).
Run agent mcp list-tools in PowerShell.

Expected Behavior

The server connects and its tools are listed, as it does in the IDE. startupTimeoutSec: 60 should give the server up to 60 seconds to start.
Actual
Failed to list tools: Failed to load MCP ‘’: MCP error -32000: Connection closed
The CLI log (%TEMP%\cursor-agent-logs-\session-*.log) shows:

[…07:54:31.881Z] Starting local MCP server “” without MCP sandboxing
[…07:54:33.922Z] MCP OAuth provider initialized (second attempt)
[…07:38:09.441Z] Failed to load MCP server … “errorMessage”:“MCP error -32000: Connection closed”
The failure is logged about 2 seconds after the spawn. The startupTimeoutSec value doesn’t change this.

Observations
The wrapper script answers initialize in about 1.4 seconds when started from a normal PowerShell session with a piped stdin. The IDE’s MCP log for the same server shows Successfully connected to stdio server in 3 to 6 seconds.
Replaying the CLI’s child environment (the values from the CLI’s own debug log) against the wrapper also works. So the environment variables are not the cause.
Inside the CLI-launched process chain, the wrapper’s child Node process (a small stdio bridge) reports process.stdin.isTTY === true and fstat(0) as a character device, not a pipe. The same bridge launched from a normal shell gets a pipe. The Node process then exits within about 1 second without logging an exit, so the MCP handshake never completes.
Adding --approve-mcps --trust makes no difference.
Using command: “C:/Program Files/PowerShell/7/pwsh.exe” instead of “pwsh” makes the CLI report MCP server ‘’ is blocked by team policy. The policy seems to check the command string, so the “pwsh” command is the only one it accepts.

Operating System

Windows 10/11

Version Information

Cursor IDE: 3.26.16
Node.js: v24.21.0

Additional Information

Related
Docker MCP servers: staff confirm the CLI has a more aggressive timeout than the GUI
Cursor-agent MCP load error: same error, macOS, cursor-agent 2025.10.02
CRITICAL BUG: cursor-agent CLI MCP STDIO Transport Failure: CLI hangs on structured content, same family of stdio issues

Does this stop you from using Cursor

Yes - Cursor is unusable

Hey @CarlosBarrera-IAG,

“MCP error -32000: Connection closed” means the server process closed its stdio before answering initialize, not that the CLI stopped waiting. A startup timeout shows up as a different “timed out after …ms” error, and your CLI version does honor startupTimeoutSec (up to 300 seconds), so raising it won’t change this.

The CLI and the IDE launch stdio servers the same way: the process they start (your pwsh wrapper) gets stdin and stdout as pipes. One difference we can see on Windows is that the CLI’s server process shares your terminal’s console, while the IDE starts it with a hidden console of its own. That lines up with what you found: the Node bridge your wrapper starts ends up with the console as stdin instead of the wrapper’s pipe, so it never sees the handshake and exits.

A couple of things to try in the meantime:

  1. Since the HTTP MCP server is already running on 127.0.0.1:9000, point the CLI at it directly with a url entry in mcp.json instead of the stdio wrapper, if your admin’s MCP allowlist permits that URL.
  2. Make sure the wrapper hands its own stdin to the bridge explicitly rather than letting the bridge inherit a handle. Could you share the line in the wrapper that starts the bridge (for example & node ... or Start-Process ...)? That tells us exactly which handle it picks up.

On the team policy message: the MCP allowlist compares the exact command string from mcp.json (the command plus its arguments) against your admin’s entries, with no path normalization. So C:/Program Files/PowerShell/7/pwsh.exe -NoProfile -File ... won’t match an entry written for pwsh .... Your admin can add an entry for the full-path form, or a wildcard pattern such as *pwsh.exe -NoProfile -File *, if you need it.

Thanks for the detailed reply. We tested your points and the shared HTTP server and the stdio wrapper both work when driven directly. cursor-agent still fails on the stdio entry, and our bridge never receives a complete initialize from it, so the problem appears to be on the client side of the stdio handshake.

Environment: Windows 11, cursor-agent 2026.10.19-6bbf2c4, PowerShell 7, bundled Node 24.5, Docker Desktop.

What we verified

  • The shared server on 127.0.0.1:9000 answers initialize in about 180 ms and tools/list in about 15 ms.
  • The wrapper (pwsh -NoProfile -File mcp-docker-wrapper.ps1 -ImageName ...) returns initialize in about 0.8–1.3 s. The full initialize → notifications/initialized → tools/list sequence completes when driven from Node or bash over pipes.
  • agent mcp list reports Connection failed, and agent mcp list-tools mcp-atlassian-cli reports MCP error -32000: Connection closed. Disabling and re-enabling the server doesn’t change either result.
  • With the bridge instrumented to log stdin, the bridge started during an agent mcp list run received no data before the client reported failure. In a agent -p --approve-mcps run, tools/list requests timed out after 10 seconds.
  • A agent -p --approve-mcps session afterwards reported no mcp-atlassian-cli tools.
  • The allowlist isn’t blocking the stdio entry; it loads with the exact command string from mcp.json.

Questions

  1. Does cursor-agent send stdio MCP messages as newline-delimited JSON? Our bridge splits on \n. If you use another framing, the bridge would never parse a request.
  2. What stdin handle does cursor-agent give a stdio child on Windows: a pipe, or a console/NUL? We haven’t captured this in the failing run, so we can’t tell which.
  3. When “Connection failed” happens, does cursor-agent log the child’s exit code or stderr anywhere? Our wrapper writes nothing to stderr in our tests.
  4. Does agent mcp list ever spawn stdio servers? In our runs it didn’t; the spawns only happened in -p sessions.

If you can answer 1 and 2, we can narrow this down on our side.

Hey @CarlosBarrera-IAG,

Quick answers to your questions:

  1. Yes, messages are newline-delimited JSON (one object per \n, no Content-Length headers). Several can arrive in one read, so buffer and split on every newline.
  2. The CLI gives your pwsh wrapper pipes for stdin, stdout and stderr. The Node bridge’s handles depend on how the wrapper starts it, so check that it passes the wrapper’s stdin through.
  3. The exit code isn’t logged. Anything the server writes to stderr is appended to the error as ; stderr: .... Use agent mcp list-tools mcp-atlassian-cli to see it, because agent mcp list shortens it to “Connection failed”.
  4. Yes, agent mcp list starts every enabled server.

We also found that the CLI currently doesn’t load envFile, so the variables in mcp.env never reach the wrapper. The IDE does load them. We’ve passed this on to the team.

As a test, copy those variables into the entry’s env block and run list-tools again. Let us know what happens!

Thanks for the answers. Our results after your suggestions:

Stderr: agent mcp list-tools mcp-atlassian-cli returns MCP error -32000: Connection closed with no ; stderr: suffix, so the wrapper writes nothing to stderr before the connection closes.

envFile: we copied JIRA_API_TOKEN and CONFLUENCE_API_TOKEN from mcp.env into the entry’s env block. list-tools still returns Connection closed, and agent mcp list still shows Connection failed. The variables were already in the shell that launched cursor-agent, so the envFile gap doesn’t appear to be the cause. We’ve restored mcp.json to its pre-test state.

Still open: a agent -p --approve-mcps session reports no mcp-atlassian-cli tools, and we haven’t captured the stdin handle the bridge gets in a failing run. Can you confirm whether cursor-agent gives the wrapper’s stdin as a pipe on Windows in every launch path (mcp list, -p sessions, and the IDE)? A console or NUL handle in any path would explain the missing initialize.


Hey @CarlosBarrera-IAG,

Yes. In every launch path (agent mcp list, agent mcp list-tools, agent -p and interactive sessions) the CLI starts your pwsh wrapper with stdin, stdout and stderr as pipes, never a console or NUL handle. The IDE does the same, so the difference is in what surrounds that process. Three things differ on Windows, most likely first:

  1. Environment. The CLI passes the wrapper only a short base set of variables (PATH, TEMP, APPDATA, LOCALAPPDATA, USERPROFILE, SYSTEMROOT, PROGRAMFILES and a few others) plus the entry’s env block. Nothing else from the shell you run agent in reaches it, including ComSpec, ProgramData, proxy settings or any DOCKER_ variables. The IDE passes its full environment. Quick test: add the ones the wrapper, Node or Docker might rely on to the env block, for example "ComSpec": "${env:ComSpec}" and "ProgramData": "${env:ProgramData}". The CLI fills them in from your shell.

  2. Working directory. The CLI starts the server in the folder you ran agent from, and the IDE uses your home folder. Any relative path or $PWD in the wrapper or bridge (the bridge script, a compose file, a volume mount) resolves differently. Quick test: cd ~ and run agent mcp list-tools mcp-atlassian-cli again. To fix it for good, use $PSScriptRoot for paths in the wrapper, or set "cwd" to the full path of your home folder in the entry.

  3. Console. The IDE gives pwsh a hidden console, and the CLI leaves it attached to your terminal. If the wrapper starts the bridge directly (& node ...), the bridge should inherit the wrapper’s pipe either way, so this alone probably isn’t it (if your Node-over-pipes test ran from a normal terminal, it already covered this case). It would matter if the bridge is started with Start-Process, cmd /c start or a job, which don’t pass the pipe on. Quick test: log [Console]::IsInputRedirected in the wrapper and process.stdin.isTTY in the bridge during a failing list-tools run, and tell us how the wrapper starts the bridge.

Let us know if one changes the result!

Thanks, the three hypotheses were useful. We instrumented the wrapper and bridge for one run each, then reverted them.

Pipes (confirmed for the wrapper): inside the CLI-launched wrapper, [Console]::IsInputRedirected and IsOutputRedirected are both True. So stdin and stdout are pipes, as you said.

Environment (partly confirmed): the wrapper doesn’t see ComSpec, ProgramData, DOCKER_HOST or HTTPS_PROXY, though the shell that launched agent has them set. The base set is as you described.

Path resolution (new): inside that wrapper run, Get-Command node returns nothing even though PATH contains a nodejs entry. The wrapper’s own check of 127.0.0.1:9000 also fails. The same checks succeed from our shell, and the shared server answers there.

Consequence: the wrapper takes the fallback path and never starts the Node bridge. It exits without writing anything to stderr, so the CLI reports only -32000: Connection closed.

Working directory: running mcp list-tools from the home folder gives the same error, so the working directory isn’t the cause.

Sandbox: agent --sandbox disabled mcp list-tools mcp-atlassian-cli gives the same error and the same wrapper results.

Questions:

  1. Does the CLI pass a different PATH to MCP child processes than the one it inherits, or strip entries that contain nodejs or Program Files?
  2. Does the CLI run stdio children with loopback networking restricted (for example, in a restricted token or sandbox), which would explain the failed connect to 127.0.0.1:9000?
  3. Can you confirm the exact environment the CLI passes to stdio children, including PATH, so we can compare it with our shell?

Hey @CarlosBarrera-IAG,

That narrows it down. The missing PATHEXT explains why Get-Command node comes back empty!

  1. PATH is passed through exactly as the CLI has it, with nothing stripped. What’s missing is everything else: the server only gets APPDATA, HOMEDRIVE, HOMEPATH, LOCALAPPDATA, PATH, PROCESSOR_ARCHITECTURE, SYSTEMDRIVE, SYSTEMROOT, TEMP, USERNAME, USERPROFILE and PROGRAMFILES, plus your entry’s env block. So there’s no PATHEXT, ComSpec, windir, ProgramData, TMP, proxy or DOCKER_* variables. That also answers your question 3.
  2. No. Stdio servers start as plain child processes, with no restricted token, sandbox or network restriction. The “Starting local MCP server … without MCP sandboxing” line in your log confirms that path, and --sandbox only applies to the agent’s own commands, not to MCP servers.

Without PATHEXT, PowerShell has no executable extensions to try, so node only matches node.ps1 or a file literally named node, never node.exe. If your 127.0.0.1:9000 check runs a tool by name (curl, node, docker), it likely fails the same way, not because of the network.

Two ways to fix it:

  • Pass the variables through in the env block. The CLI fills in ${env:NAME} from the shell you start agent in:
"env": {
  "PATHEXT": "${env:PATHEXT}",
  "ComSpec": "${env:ComSpec}",
  "windir": "${env:windir}",
  "ProgramData": "${env:ProgramData}"
}

Only add variables that are set in that shell (add DOCKER_HOST or HTTPS_PROXY too if the wrapper needs them). A variable that isn’t set is passed as the literal text ${env:NAME}.

  • Or, in the wrapper itself, set $env:PATHEXT = '.COM;.EXE;.BAT;.CMD' at the top when it’s empty, or call node.exe explicitly.

Post 8 looks right on the cause. Two things that might help:

Since the fix is already going into the env block, the same gap likely hits npm/npx too if the bridge or wrapper ever calls them bare - those resolve as .cmd shims through cmd.exe, so they need ComSpec for the same reason node needs PATHEXT.

If you’d rather not depend on the parent’s PATHEXT/ComSpec at all: have the wrapper call the bridge with a full path ((Get-Command node.exe).Source) instead of the bare node name. Nothing then depends on extension resolution in the child, and it sidesteps the fallback path entirely.

(Tangential to this bug, but since the thread is about spawning MCP servers from Windows children with filtered environments - I maintain an open-source MCP agent that does the same kind of stdio bridging on Windows, in case the approach is a useful reference: Open-source edition — Local Commander)