DaddyNote

UK, Irish and EU money and travel rules, worked through step by step, with the date each figure was checked.

AI how-to notes · 2026-09-17 · last run 2026-09-17 · 한국어 원문

When an MCP server says ✘ Failed to connect — telling a bad launch path (CONNECTION_CLOSED) from stdout pollution (connection timed out)

The short version

In our reproduction (Claude Code 2.1.274, Windows), registering a file that does not exist made claude mcp list report ✘ Failed to connect — CONNECTION_CLOSED: Connection closed. Registering an executable that does not exist produced identical wording in claude mcp get. When a server wrote characters to stdout without a newline and corrupted the initialize response line, we got connection timed out after 30000ms instead. A server with a single console.log showed as ✔ Connected in both placements (at startup and inside the tool), so this listing alone never revealed it.

Symptoms

What we tested on

ItemValue
OSWindows 11 Home (10.0.26200)
ShellGit Bash (PowerShell 7 for process inspection only)
Node.jsv24.19.0
Claude Code2.1.274 (Claude Code)
Serverthe add-tool server built with @modelcontextprotocol/sdk 1.30.0 (confirmed via npm ls). Full source is in the minimal MCP server article, and it is also preserved as cat server.mjs output in this article's raw material
Project folderC:\Users\User\AppData\Local\Temp\longtail_D2\proj2, every server added with --scope project
Approvalclaude --settings '{"enableAllProjectMcpServers": true}' mcp …

The server variants used in the reproduction

Variants that add a log line to the working server.mjs, plus cases where only the registered argument or command is wrong.

Registered nameWhat changed
d2oknothing
d2missingargument points at a nonexistent file, no_such_server.mjs
d2nocmdcommand points at a nonexistent executable, C:/nope/node.exe
d2logstartconsole.log('add-server starting'); before connecting
d2logtoolconsole.log('add called', a, b); inside the tool
d2stderrthe two lines above, as console.error
d2nonlstartprocess.stdout.write('add-server starting '); before connecting (no newline)
d2nonltoolprocess.stdout.write('add called '); inside the tool (no newline)

The variant with a log inside the tool looks like this (diff server.mjs server_log_tool.mjs).

14,16c14,17
<   async ({ a, b }) => ({
<     content: [{ type: 'text', text: String(a + b) }]
<   })
---
>   async ({ a, b }) => {
>     console.log('add called', a, b);
>     return { content: [{ type: 'text', text: String(a + b) }] };
>   }

Case 1 — the launch path is wrong

The add command succeeded without checking the path.

$ claude mcp add d2missing --scope project -- node C:/Users/User/AppData/Local/Temp/longtail_D2/srv/no_such_server.mjs
Added stdio MCP server d2missing with command: node C:/Users/User/AppData/Local/Temp/longtail_D2/srv/no_such_server.mjs to project config

Turn approval on and check the status:

d2missing:
  Scope: Project config (shared via .mcp.json)
  Status: ✘ Failed to connect
  Issue: CONNECTION_CLOSED: Connection closed
  Type: stdio
  Command: node
  Args: C:/Users/User/AppData/Local/Temp/longtail_D2/srv/no_such_server.mjs

d2nocmd, where the executable itself is wrong (Command: C:/nope/node.exe), gave the same Issue: CONNECTION_CLOSED: Connection closed. That wording alone cannot tell you whether the file or the command is missing.

The cause shows up when you run the registered command yourself. Launching the nonexistent file from our test script, node wrote this to stderr and exited immediately (92ms) with code 1.

Error: Cannot find module 'C:\Users\User\AppData\Local\Temp\longtail_D2\srv\no_such_server.mjs'
    at Module._resolveFilename (node:internal/modules/cjs/loader:1517:15)
    …
  code: 'MODULE_NOT_FOUND',
  requireStack: []
}

Node.js v24.19.0
[child exit 92ms] code=1 signal=null

Connecting with the official SDK client (Client + StdioClientTransport) raised McpError: MCP error -32000: Connection closed. claude mcp get d2missing took about 4.2 seconds.

The nonexistent executable behaved differently. Typed straight into Git Bash it gave bash: line 1: C:/nope/node.exe: No such file or directory (exit code 127), and launched through node's spawn the process never started at all — an error event with ENOENT spawn C:/nope/node.exe ENOENT. Even so, Claude Code's Issue: line matched the missing-file case exactly.

Case 2 — the server writes plain text to stdout

The stdio section of the MCP specification (modelcontextprotocol.io/specification/2025-06-18/basic/transports, checked 2026-09-17) says "The server MUST NOT write anything to its stdout that is not a valid MCP message.", and that logs may go to stderr. The build-a-server doc (modelcontextprotocol.io/docs/develop/build-server, checked the same day) says in its TypeScript section "Never use console.log()", and to use console.error() instead.

We looked at what actually breaks, three ways.

Raw JSON-RPC (`rpc_test.mjs`, which flags non-JSON lines and moves on)

$ node rpc_test.mjs server_log_start.mjs
[send 12ms] initialize
[stdout 1] (NOT-JSON) add-server starting
[stdout 2] (JSON) {"result":{"protocolVersion":"2025-06-18",…},"jsonrpc":"2.0","id":1}
…
[stdout 4] (JSON) {"result":{"content":[{"type":"text","text":"5"}]},"jsonrpc":"2.0","id":3}

$ node rpc_test.mjs server_write_nonl_start.mjs
[send 11ms] initialize
[stdout 1] (NOT-JSON) add-server starting {"result":{"protocolVersion":"2025-06-18",…},"jsonrpc":"2.0","id":1}
[timeout 10021ms] id=1 no response (10s)

console.log writes a trailing newline, so the log took its own line and the JSON response behind it stayed intact. process.stdout.write without a newline left the response glued onto the end of the log, making that whole line non-JSON. This script never received a response for id=1.

The official SDK client (the installed 1.30.0)

# server_log_start.mjs
[client onerror] SyntaxError: Unexpected token 'a', "add-server starting" is not valid JSON
[connect] ok
[listTools] add
[callTool] [{"type":"text","text":"5"}]

# server_write_nonl_start.mjs
[client onerror] SyntaxError: Unexpected token 'a', "add-server"... is not valid JSON
[catch] Error: connect exceeded 10s

The SDK client reported a SyntaxError through onerror for every non-JSON line. With a single newline-terminated log, connecting and calling carried on regardless. With the response glued on, it stalled at the connect step (the 10 seconds is our own limit). server_write_nonl.mjs, which writes without a newline inside the tool, connected and got through listTools, then hit the 10-second limit at callTool.

What Claude Code's status display showed

$ claude --settings '{"enableAllProjectMcpServers": true}' mcp list
Checking MCP server health…
d2ok: node C:/Users/User/AppData/Local/Temp/longtail_D2/srv/server.mjs - ✔ Connected
d2missing: node C:/Users/User/AppData/Local/Temp/longtail_D2/srv/no_such_server.mjs - ✘ Failed to connect — CONNECTION_CLOSED: Connection closed
d2logstart: node C:/Users/User/AppData/Local/Temp/longtail_D2/srv/server_log_start.mjs - ✔ Connected
d2logtool: node C:/Users/User/AppData/Local/Temp/longtail_D2/srv/server_log_tool.mjs - ✔ Connected
d2stderr: node C:/Users/User/AppData/Local/Temp/longtail_D2/srv/server_log_stderr.mjs - ✔ Connected
d2nonlstart: node C:/Users/User/AppData/Local/Temp/longtail_D2/srv/server_write_nonl_start.mjs - ✘ Failed to connect — MCP server "d2nonlstart" connection timed out after 30000ms
d2nonltool: node C:/Users/User/AppData/Local/Temp/longtail_D2/srv/server_write_nonl.mjs - ✔ Connected

(Lines for servers in other scopes are filtered out.) Checking all seven in one command took 57.8 seconds; the single claude mcp get d2nonlstart took 33.6 seconds. The other get calls ran 4 to 6 seconds.

d2logtool and d2nonltool, which only pollute stdout during a tool call, both read ✔ Connected. We did not check how far Claude Code goes with its requests during a status check. What this run establishes is that a ✔ Connected in this listing can still be a server that breaks when a tool is called.

Does fixing it restore the connection?

We registered server_fix.mjs, whose line 19 was process.stdout.write('add-server starting ');, as d2fix. Before the fix its status was Issue: MCP server "d2fix" connection timed out after 30000ms. We changed line 19 and nothing else.

19:process.stdout.write('add-server starting ');   ← before
19:console.error('add-server starting');           ← after

After the fix, the direct calls put the log on its own [stderr] add-server starting line and all three responses came back as JSON. We ran claude mcp get d2fix immediately afterwards:

d2fix:
  Scope: Project config (shared via .mcp.json)
  Status: ✔ Connected
  Type: stdio
  Command: node
  Args: C:/Users/User/AppData/Local/Temp/longtail_D2/srv/server_fix.mjs

Results

ServerRaw JSON-RPCSDK clientclaude mcp list/get
d2okall three responses finefine✔ Connected
d2missingno response, node exit code 1 (MODULE_NOT_FOUND)MCP error -32000: Connection closed✘ Failed to connect / CONNECTION_CLOSED: Connection closed
d2nocmd(node spawn) process never started, ENOENTnot runget: ✘ Failed to connect / CONNECTION_CLOSED: Connection closed
d2logstart1 non-JSON line + responses fine1 onerror, call succeeded✔ Connected
d2logtool1 non-JSON line at call time + responses fine1 onerror, call succeeded✔ Connected
d2stderrlog went to stderr, responses finefine✔ Connected
d2nonlstartinitialize response line corrupted, id=1 never receivedconnect exceeded 10s✘ Failed to connect / connection timed out after 30000ms
d2nonltooltools/call response line corrupted, id=3 never receivedcallTool exceeded 10s✔ Connected
d2fix (after the fix)all three responses finenot run✔ Connected

Here is the distinction this reproduction supports. CONNECTION_CLOSED came up in two cases: a file that does not exist (run it directly and node exits 1 immediately) and an executable that does not exist (run it directly and the launch itself is ENOENT). connection timed out after 30000ms came from the server whose startup log ends up on the same line as the initialize response, where the direct calls also failed to read that line as JSON. So: on CONNECTION_CLOSED, type the registered command into a shell as-is; on a timeout, drive the server over JSON-RPC yourself and inspect the stdout lines. That order matches what we saw. The official docs (code.claude.com/docs/en/mcp, checked 2026-09-17) explain that on ✘ Failed to connect, claude mcp get <name> shows the failure detail on the Issue: line.

What didn't work

What we did not check

See also