Error reference

MCP Server Errors: Connection Failures, Causes and Fixes

Updated July 27, 2026

In short
Most Model Context Protocol failures are transport failures, not logic failures. Over stdio, the two dominant causes are a command the host cannot execute (spawn ENOENT: wrong path, or a GUI app that does not inherit your shell PATH) and a server that writes log output to stdout, which corrupts the JSON-RPC stream and produces parse errors. Over HTTP the causes shift to auth, CORS and proxies that buffer streaming responses. Reproduce outside your client with npx @modelcontextprotocol/inspector <command>. It shows the raw protocol traffic and the server's stderr.
start here
npx @modelcontextprotocol/inspector node ./build/index.js
Diagnosis

Causes at a glance

CauseHow to recognize itFix
Command not found by the host processspawn <cmd> ENOENT in the client log, server never startsUse an absolute path to the interpreter/binary; GUI apps do not inherit shell PATH
Server writes logs to stdoutParse errors or "Unexpected token ... is not valid JSON" right after startupSend every log line to stderr; stdout carries the JSON-RPC framing only
Server crashes during initialize"Server disconnected" or "transport closed" within seconds of connectingRun the exact command in a terminal and read the stack trace on stderr
Auth failure on a remote serverHTTP 401 with a WWW-Authenticate header, or an OAuth callback that never completesRe-run discovery/consent; check redirect URI and that the token audience matches the server
Protocol or SDK version mismatchInitialize fails, or tools/list returns but tool calls error with invalid paramsAlign the SDK with a protocol revision the client supports; check the negotiated version

Two transports, two families of failure

MCP servers run in one of two shapes and the failure modes barely overlap. A stdio server is a local child process: the host spawns it and exchanges newline-delimited JSON-RPC messages over its stdin and stdout. Everything that can go wrong with launching a process (wrong path, missing runtime, missing environment variable, non-zero exit) shows up as a connection failure, and the useful diagnostics are on the child's stderr.

A remote server speaks JSON-RPC over HTTP. Current servers use Streamable HTTP, which replaced the older HTTP+SSE transport in the 2025-03-26 protocol revision; many deployed servers still expose the legacy /sse endpoint, and pointing a client at the wrong one produces 404s or 405s rather than a helpful error. Here the failures are network-shaped: TLS, CORS preflight, an expired token, a missing session header, or a reverse proxy that buffers a streaming response until the client times out.

Identify which you have before debugging. If your config has a command and args, it is stdio. If it has a url, it is HTTP.

{
  "mcpServers": {
    "local-fs": {
      "command": "/opt/homebrew/bin/npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/projects"]
    },
    "remote-api": {
      "type": "http",
      "url": "https://mcp.example.com/mcp",
      "headers": { "Authorization": "Bearer ${API_TOKEN}" }
    }
  }
}
Step 1

Step 1: reproduce outside your client with the Inspector

Debugging through a chat client is slow and hides the evidence. The MCP Inspector is the official test harness: it launches the server itself, shows the raw JSON-RPC request and response for every message, lists the tools and resources the server advertises, lets you invoke a tool with arbitrary arguments, and surfaces the server's stderr in a pane instead of discarding it.

If the Inspector connects and the tools list populates, your server is fine and the problem is in the client config: a wrong path, a missing environment variable, or a working directory difference. If the Inspector fails the same way, you have isolated the bug to the server and can iterate without restarting an application.

# launch a stdio server directly
npx @modelcontextprotocol/inspector node ./build/index.js

# Python server via uv, passing env through
npx @modelcontextprotocol/inspector \
  -e API_KEY=$API_KEY \
  uv run mcp-server-demo

# then open the printed localhost URL and hit "Connect"
Step 2

Step 2: fix spawn ENOENT and immediate disconnects

spawn npx ENOENT does not mean the server is broken. It means the operating system could not find the executable named in command. The most common reason is that desktop applications launched from a dock or start menu do not inherit the PATH your shell builds from its profile, so npx, uv, python and version-manager shims that work perfectly in a terminal are simply not visible to the host.

The fix is to write the absolute path. Resolve it in your shell with which, and use that literal value in the config. Version managers such as nvm, pyenv and mise make this worse because the real binary lives inside a versioned directory that changes on upgrade: prefer a stable system path where you can.

"Server disconnected" or "transport closed" seconds after a successful spawn is a different failure: the process started and then exited. Run the exact command from the config in a terminal and read what it prints. Typical causes are an unset environment variable the server requires at import time, a relative path resolved against a working directory that is not what you assumed, or a missing dependency. Only the variables you declare in env are passed to a stdio server. It does not inherit your shell environment.

# find the real path
which npx      # -> /opt/homebrew/bin/npx
which uv       # -> /Users/me/.local/bin/uv

# run exactly what the client runs, and read stderr
/opt/homebrew/bin/npx -y @modelcontextprotocol/server-filesystem /Users/me/projects

# pass required env explicitly; use absolute paths in args
{
  "command": "/Users/me/.local/bin/uv",
  "args": ["--directory", "/Users/me/src/my-server", "run", "my-server"],
  "env": { "DATABASE_URL": "postgres://localhost/app" }
}
Step 3

Step 3: stop the server printing to stdout

This is the classic MCP bug and it catches almost everyone once. In a stdio server, stdout *is* the protocol channel. Every byte written there must be a valid JSON-RPC message. A single print("starting up"), a console.log left in from development, a library that emits a deprecation notice, or a progress bar rendering to stdout will interleave with the framing and the client will report a parse error, often quoting the offending text back at you.

The rule is absolute: all human-readable logging goes to stderr. In Python that means configuring the root logger with a stderr stream (and never print without file=sys.stderr); in Node it means console.error rather than console.log, and checking that any logger you pull in defaults to stderr. Watch third-party libraries too: banners printed at import time are a frequent culprit, and they appear before your own code runs.

JSON-RPC error codes tell you which layer failed. -32700 is a parse error, meaning the bytes were not valid JSON at all. That is the stdout-pollution signature. -32600 is an invalid request, -32601 a method the server does not implement, -32602 invalid params (usually arguments that do not match your tool's input schema), and -32603 an unhandled exception inside the server.

# Python — logging to stderr, never stdout
import logging, sys
logging.basicConfig(stream=sys.stderr, level=logging.INFO)

# WRONG: print("server starting")
# RIGHT:
print("server starting", file=sys.stderr)

// TypeScript
// WRONG: console.log("connected");
// RIGHT:
console.error("connected");

# HTTP servers are exempt from this rule; stdout is free there.

Auth, timeouts, and version mismatches

Remote MCP servers act as OAuth 2.1 resource servers. A 401 carries a WWW-Authenticate header pointing at protected-resource metadata, from which the client discovers the authorisation server and runs an authorisation-code flow with PKCE, often using dynamic client registration so no client ID is configured by hand. When that flow breaks, the usual culprits are a redirect URI the authorisation server does not have registered, an authorisation server without dynamic registration support, or a token issued for a different audience than the MCP server: recent protocol revisions require the client to bind tokens to a specific resource precisely to stop tokens being replayed elsewhere. Clearing the stored credentials and re-running consent resolves a surprising share of these.

Tool-call timeouts are a separate class. A tool that shells out to a build, queries a slow warehouse, or waits on a human will exceed the client's default request timeout and be cancelled even though the server is healthy. The protocol answer is progress notifications: a server that reports progress keeps the call alive rather than appearing hung. Clients also expose their own knobs (Claude Code, for instance, reads MCP_TIMEOUT for server startup and MCP_TOOL_TIMEOUT for individual calls) but raising a timeout is a workaround, and streaming progress is the fix.

Version mismatches are quieter. The client proposes a protocol revision in initialize and the server answers with one it supports; revisions are dated (2024-11-05, 2025-03-26, 2025-06-18 and later), and features such as Streamable HTTP, elicitation and structured tool output arrived at specific revisions. A server built on a much newer SDK than the client understands can connect and still fail on capabilities the client never negotiated. When something lists correctly but behaves oddly, check the negotiated version in the initialize response before suspecting your tool code. On HTTP, clients also send an MCP-Protocol-Version header on subsequent requests, and servers that ignore it can misbehave across revisions.

# where clients log MCP traffic
# Claude Desktop (macOS): ~/Library/Logs/Claude/mcp*.log
# Claude Desktop (Windows): %APPDATA%\Claude\logs\mcp*.log
tail -n 100 -f ~/Library/Logs/Claude/mcp-server-*.log

# does the endpoint answer at all, and with what?
curl -i -X POST https://mcp.example.com/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}'

The part that is not a bug: an MCP server is code with tool access

Every debugging guide treats MCP servers as infrastructure. They are closer to dependencies with privileges. Installing one from a README typically means running arbitrary third-party code on your machine, handing it the environment variables and API keys you configured, and granting your model permission to call whatever tools it advertises. npx -y fetches and executes the latest published version at every launch, so the code you reviewed on Monday is not necessarily the code that runs on Friday.

The realistic risks are worth naming. Tool descriptions and tool results are text that enters the model's context, so a malicious or compromised server can attempt prompt injection through them: instructions hidden in a description the user never reads, or in the content returned by an otherwise legitimate-looking call. A server can also change its tool definitions after you approved them. Combining a server holding sensitive data with a server that can reach the internet gives an injected instruction a path to exfiltrate, even though neither server is dangerous alone. And a server that proxies credentials for several users can be tricked into acting with the wrong user's authority.

Mitigations are ordinary supply-chain hygiene applied somewhere new: pin versions instead of floating on latest, prefer servers you can read or that come from the vendor whose API they wrap, scope each server's credentials to the minimum it needs rather than reusing a broad token, run untrusted servers in a container or sandbox with no access to your source tree, and keep human approval on tools with side effects. Review what a server can reach before you review whether it connects.

# pin instead of floating on latest
{
  "command": "npx",
  "args": ["-y", "@modelcontextprotocol/server-filesystem@2025.8.21", "/Users/me/projects/sandbox"]
}

# scope the filesystem server to one directory, not $HOME
# scope tokens per server; do not reuse an org-wide admin key
Beyond the one-off fix

An MCP server is an unvetted dependency that your agents can call, holding credentials you handed it, which makes it an identity and blast-radius question, not just a connectivity one. Nexus inventories the agents and non-human identities in your environment and maps them in an identity graph, so you can see which MCP servers each agent is connected to, what credentials those servers hold, and what a compromised or newly-updated server could reach before it reaches it. Runtime hooks capture every tool call and file change the connection produces, giving you the trace that turns "the server disconnected" into a timeline; policy checks block violating calls in flight rather than reporting them afterwards; and shadow-AI detection surfaces MCP servers wired into developer machines that were never reviewed at all.

Explore Swfte Nexus

Common questions

Why does my MCP server say "server disconnected"?
The child process exited or never started. Run the exact `command` and `args` from your config in a terminal and read stderr: the usual causes are a missing environment variable the server needs at import time, a relative path resolved against an unexpected working directory, or an uncaught exception during initialize. A stdio server only receives the variables you list under `env`.
How do I fix spawn ENOENT in an MCP server config?
Replace the command with an absolute path. GUI applications do not inherit the PATH your shell builds from its profile, so `npx`, `uv` and version-manager shims are invisible to them even though they work in a terminal. Run `which npx` and paste the literal result into `command`.
Why do I get JSON parse errors from my MCP server?
Something wrote non-JSON to stdout, which is the JSON-RPC channel for stdio servers. A stray `print` or `console.log`, a library banner emitted at import time, or a progress bar will corrupt the message stream and produce a -32700 parse error. Route all logging to stderr: `console.error` in Node, a stderr stream handler in Python.
How do I debug an MCP server?
Use the MCP Inspector: `npx @modelcontextprotocol/inspector <your command>`. It launches the server, shows every JSON-RPC message in both directions, lists the advertised tools, lets you call them with arbitrary arguments, and displays the server's stderr. If the Inspector works and your client does not, the fault is in the client config, not the server.
Are MCP servers safe to install?
Treat them as dependencies with privileges rather than plugins. An MCP server runs arbitrary code with the credentials you give it and injects text into the model's context through tool descriptions and results, which makes prompt injection and post-approval tool changes real risks. Pin versions, prefer first-party or auditable servers, scope credentials narrowly, and sandbox anything you have not read.