Skip to content

MCP client for `X` failed to start: handshaking with MCP server failed: connection closed: initialize response

One configured MCP server did not come up, and the CLI is telling you which one and how far it got. This is a local startup failure between the CLI and a server process it launched — nothing was sent to a model provider, so it is not an API key problem, not a rate limit, not a quota, and not a model failure. The session itself usually continues; what you lose is every tool that server was going to contribute.

The message names the phase, and that is worth a lot

initialize is the first exchange of the MCP handshake: the client opens the transport, sends an initialize request, and waits for the server to answer with its protocol version and capabilities. This message says the connection closed while that answer was outstanding.

So the server never declared a single tool, and nothing about tools can possibly be the cause. That rules out more than it looks like: a malformed tool schema, a tool name that is too long or duplicated, a permission declaration, an argument your prompt would have passed. None of that has been exchanged yet. People arrive at this error and start reading the server’s tool definitions; the handshake failed before the server had a chance to send any.

It also rules out the far end being merely slow in a recoverable way. A server that is alive and thinking does not close the connection. Something either exited, or was never really speaking the protocol on that channel in the first place.

Compare this with the bare -32000: Connection closed form, which carries no phase at all and can mean a server died twenty minutes into a healthy session. Same underlying event, much worse message. Here the phase is handed to you: the server was never usable, and nothing you did in the session contributed to it.

stdout is a wire, not a console

This is the cause people find last and should check first.

A stdio MCP server speaks JSON-RPC over its standard output. That stream is the protocol channel, not a place to print things. Logs, banners, progress bars, and diagnostics belong on stderr. If anything non-protocol reaches stdout before or during the handshake, the client cannot parse the frame it is waiting for, and what you see is a failed handshake — not a parse error naming the offending text.

The ways that happens are mundane and none of them look like a bug:

  • A package runner bootstrapping the server on first use and printing an install or version notice.
  • A console.log or print() left in the server’s own startup path, including one inside a dependency.
  • A wrapper script that echoes something — a shell profile sourced by a login shell, a version manager announcing which runtime it selected, a “welcome” line from a container entrypoint.
  • The server writing its logs to stdout by default, which is normal behavior for a service and fatal for a stdio MCP server.

The test takes one command: run exactly the command from your configuration, with stderr sent elsewhere, and look at what lands on stdout. If a single character appears that is not part of a JSON frame, you have found it. A healthy stdio server prints nothing on stdout until it is spoken to. The fix is on the server or wrapper side — silence stdout, or route the server’s logging to stderr — not in the client configuration, which is where people spend the afternoon.

Is retrying useful?

No. The same command will spawn the same process and fail the same way.

This is a deterministic startup path: a command, its arguments, its environment. Nothing about it varies between attempts, which is exactly why the failure is reproducible from a shell and why reproducing it there is the fastest move available to you.

There is one narrow exception, and it is worth naming because it produces a “fixed itself” story that confuses everybody later. If the failing launch was also the first launch of a server fetched on demand by a package runner, the first run may spend its startup budget downloading rather than answering the handshake, while the second run starts from a warm cache and succeeds. So: try once more if and only if that was a cold first install. If the second attempt fails identically and immediately, you are on the deterministic branch and no further attempt will differ.

Do not respond to this error by raising a startup timeout as a reflex. A timeout increase helps exactly one shape of failure — a server that is genuinely slow to initialize — and that shape is easy to rule out: a server that is slow keeps the connection open while it works. This message says the connection closed.

The name in backticks is a config key

X is the name you gave the server in your configuration, not the package name, not the binary, and not anything the server chose for itself. That makes it greppable: search your Codex configuration for that key and you are looking at the entry that failed, including which configuration layer it came from. Checking that is not busywork — a stale entry in a global config that you forgot about looks exactly like a broken entry in the project config you are editing, and you can spend a long time fixing a file that is not being read.

The CLI’s own configuration format and flags are documented in OpenAI’s Codex manual; the API error documentation does not cover the CLI’s local behavior, so do not reason about this failure from API error tables.

Fix by what the manual run shows

  • The command is not found or exits immediately with a stack trace — the server’s own startup is broken, or the runtime it needs is not on the PATH that the CLI inherits. A CLI started from a desktop launcher often has a different PATH than your interactive shell; an absolute path to the interpreter removes the ambiguity in one edit.
  • The command runs and prints something human-readable on stdout — that is the handshake corruption described above. Silence it or move it to stderr.
  • The command runs and prints nothing, but the handshake still fails — check that the server actually supports stdio. A server designed to be reached over HTTP will sit there doing nothing on a pipe, and a URL-based server configured as a local command is a category error, not a bug.
  • The server needs a credential and exits without one — supply it through the environment rather than inline. MCP servers run with your privileges and frequently hold long-lived tokens; keep values out of any config file that is committed or shared, and mask them when you paste output into an issue (sk-****). “It failed to start” is a bad reason to move a secret somewhere more convenient and less safe.
  • It works by hand and fails from the CLI — the difference is environment, working directory, or shell. Run your manual test with the same working directory the CLI uses before concluding the CLI is at fault.

Confirming the server is actually usable

A clean start is not the same as a working server. After the change, start a fresh session, confirm the server no longer reports a startup failure, and then invoke one of its tools and get a real result back. A server can complete the handshake and still fail on its first call, and that failure looks nothing like this one.

Repeat this in a second new session before you consider it closed, particularly if your fix involved a package runner or a cache. A single success right after a cold install can be the install, not the fix.

If a server starts cleanly and then disappears mid-session, the message changes shape and so does the diagnosis — that is the connection-closed form of the same family, and there the interesting question is when it died rather than why it never started. If every server starts but requests begin failing before the model runs, look at the assembled tool list instead: duplicate tool names are rejected at validation while every server reports healthy. And for remote servers reached over HTTP, a startup failure that names a status rather than a closed connection is a routing problem, not a dead server. If you are not sure which layer produced the string in front of you, the AI coding error triage tool starts from that question rather than from the vendor.