MCP Server Disconnected? Here's the Actual Fix (2026)
Your MCP server shows disconnected in Claude Desktop, Claude Code, or Cursor. Here's exactly why, with fixes for stdout pollution, paths, and env vars.
On this page
Your MCP server was working five minutes ago. Now Claude Desktop, Claude Code, or Cursor just shows "disconnected" and gives you nothing else to go on. No stack trace, no line number, just a status dot that turned red. Here's what is actually happening and how to fix it.
MCP servers run as a subprocess of your AI client, not as some remote service you can curl. That single fact explains almost every disconnect you'll ever see. When you see "disconnected," one of four things happened: the process never started, it started and then crashed, its stdout got polluted with something that isn't valid JSON-RPC, or there's a mismatch between the transport your client expects and the one your server actually speaks. That's the whole list. Once you know which of the four you're dealing with, the fix is usually five minutes, not an afternoon of guessing.
What "Disconnected" Actually Means
The Model Context Protocol defines two transports that matter for local development: stdio and Streamable HTTP (which replaced the older HTTP+SSE transport). Almost every MCP server you run locally, whether it's one you wrote or one you installed from npm or PyPI, uses stdio. That means your AI client spawns your server as a child process and talks to it by writing JSON-RPC messages to the child's standard input and reading JSON-RPC messages back from its standard output.
That's the entire mechanism. There's no socket, no port, no HTTP handshake to inspect in your browser's network tab. Which is exactly why "disconnected" feels like a black box: the failure is happening at the process and stream level, somewhere between your operating system spawning a subprocess and two programs agreeing on a message format. Every real cause of an MCP disconnect traces back to one of those two mechanics breaking down.
- <strong>Spawn failure.</strong> The client tried to launch your server and the operating system couldn't find the command, couldn't find the script, or the process exited immediately with a non-zero code.
- <strong>Runtime crash.</strong> The process started fine, ran for a few seconds or longer, then threw an unhandled exception and exited.
- <strong>Corrupted stream.</strong> The process is still alive, but something wrote non JSON-RPC bytes to stdout, so the client can no longer parse the messages coming back and gives up.
- <strong>Protocol mismatch.</strong> The process is alive and behaving correctly, but it's speaking a transport (stdio vs. Streamable HTTP) the client isn't configured to expect.
Check This First: Run It Manually
Before you touch any client config, try running your server's exact start command directly in a terminal, completely outside of Claude Desktop, Claude Code, or Cursor. This single step eliminates half of all MCP disconnect reports, because it immediately tells you whether the bug is in your server or in how the client is invoking it.
# Whatever command is in your mcpServers config, run it as-is
node /Users/you/projects/my-server/index.js
# or for a Python server
python /Users/you/projects/my-server/server.pyIf it throws an error or a traceback right there in your terminal, you have your answer, and it has nothing to do with MCP as a protocol. Fix that bug first, then move on. If it starts cleanly and just sits there waiting (that's normal for a stdio server with no client attached), the bug is more likely in how your client is spawning or talking to the process, which is what the rest of this guide covers.
Cause 1: The Process Never Started
This shows up as an immediate disconnect, often within a second of the client trying to connect. Usually the command in your config is wrong in a way your shell would have silently forgiven but a GUI application launching a subprocess will not.
A relative path like ./server.py or server.js works fine when you run it from your project folder in a terminal, because your shell's current working directory is that folder. Your AI client is not launching from that folder. It's launching from wherever its own process happens to run from, usually your home directory or the application bundle's own location, so a relative path resolves to a completely different (and nonexistent) file.
The same problem hits interpreters and package runners. npx, python, and uv being available in your terminal doesn't mean the GUI app sees them. On macOS in particular, GUI applications launched from Finder or Spotlight do not inherit your shell's PATH the way a Terminal.app session does, because they never load your .zshrc or .bash_profile. A command that resolves perfectly in your terminal can produce a plain "command not found" style spawn failure when the same string is handed to posix_spawn by a desktop app.
{
"mcpServers": {
"my-server": {
"command": "/usr/local/bin/python3",
"args": ["/Users/you/projects/my-server/server.py"]
}
}
}Use full, absolute paths for both the command and the script argument. If you're not sure where an interpreter actually lives, ask your shell directly rather than assuming.
which python3
which node
which npxCause 2: Stdout Pollution
This is the cause that gets almost everyone at least once, and it's the least intuitive if you haven't hit it before. With stdio transport, stdout is reserved entirely for JSON-RPC messages between your server and the client. Nothing else is allowed to touch it. Any print() statement, any library that logs to stdout by default, any startup banner your framework prints for you, corrupts the stream. The client either disconnects immediately or behaves in ways that look completely unrelated to the actual cause, because from its perspective it just received garbage where it expected a JSON object.
The frustrating part is that this bug is invisible when you test your server in isolation with a simple script, and only shows up once a client is actually parsing the stream character by character.
# This breaks your server
print("Server starting up...")
# This is fine
import sys
print("Server starting up...", file=sys.stderr)// Breaks the server
console.log("Connected to database");
// Fine
console.error("Connected to database");In Node, console.log writes to stdout and console.error writes to stderr. In Python, print() defaults to stdout unless you redirect it. If you're using a logging library instead of raw print statements, check its default output stream before assuming it's safe. Python's logging module defaults to stderr when you call basicConfig() with no handler, which is safe, but plenty of third-party loggers and ORMs default to stdout for their query logs.
This also catches people indirectly, when a dependency you didn't write logs to stdout on its own initialization. If your server works fine when you test it manually but disconnects only when the AI client runs it, and you've already ruled out path and environment issues, a dependency writing to stdout is one of the first things worth grepping your node_modules or site-packages for.
Cause 3: Path and Environment Problems
Related to cause 1, but specifically about environment variables rather than the command path itself. If your server reads an API key, a database URL, or any other secret from an environment variable, remember that the process spawning your server does not automatically inherit your shell's full environment the way a terminal session does. GUI applications on macOS and Windows launch with a minimal environment set by the OS, not the one built up by your .zshrc, .bashrc, or direnv configuration.
You almost always need to declare those variables explicitly inside the server's config block, even if the exact same variable is already exported in your shell profile.
{
"mcpServers": {
"my-server": {
"command": "/usr/local/bin/python3",
"args": ["/Users/you/projects/my-server/server.py"],
"env": {
"DATABASE_URL": "postgres://localhost/mydb",
"API_KEY": "your-key-here"
}
}
}
}The failure mode here is subtly different from a spawn failure. The process actually starts (you'll often see the client briefly show "connecting" before it flips to "disconnected"), then your server code hits a missing environment variable, throws, and exits. If your server silently fails a second or two after startup, with no obvious spawn error, a missing environment variable it expected to just be there is the first thing to check.
Working directory assumptions cause a similar failure. If your server reads a config file, a .env file, or a SQLite database using a relative path like ./data.db, that path resolves relative to whatever working directory the client happened to launch the process from, which is often not your project folder at all. Use absolute paths for any file your server reads at startup, not just for the command itself.
Cause 4: Transport Mismatch
Less common than the first three, but worth ruling out, especially if you're connecting to a remote or self-hosted MCP server rather than a local one. Your config might specify a url field, which tells the client to expect a Streamable HTTP (or the older SSE) server, when your actual server is only implemented for stdio, or the reverse: a command and args pair when the server you're pointing at only listens on a port.
Double check which transport your server actually implements against what your config is asking for. Mixing these up produces a disconnect that looks identical to every other cause on the surface, no useful error message, just a red status dot, so it's easy to burn time debugging path issues or stdout pollution on a server that was never going to connect over stdio in the first place.
How to Actually Debug This Instead of Guessing
Once you've ruled out the obvious (the manual run test from earlier in this guide), the next step isn't to keep tweaking your client config and reconnecting. It's to isolate the server from the client entirely using a tool built for exactly this.
- 1
Confirm the Server Runs Standalone
Run the exact command from your config directly in a terminal, as covered above. If this fails, you're debugging your server, not MCP. Only move to the next step once this succeeds.
- 2
Test It in the Official MCP Inspector
The MCP Inspector is a browser-based tool maintained by the Model Context Protocol team, built specifically to connect to a server the same way a client would, without any of Claude Desktop, Claude Code, or Cursor's own config layered on top.
bash โ Launch the Inspector against your servernpx @modelcontextprotocol/inspector node my-server.js # or for a Python server npx @modelcontextprotocol/inspector python my_server.pyThis opens a local web UI showing the exact JSON-RPC messages going back and forth, every tool your server exposes, and lets you call each one individually with test input. If your server misbehaves here too, you've confirmed the bug is in your server, independent of any AI client.
- 3
If the Inspector Works, the Problem Is Your Client Config
If your server behaves correctly inside the Inspector but still disconnects inside Claude Desktop or Cursor, you've narrowed the problem down to your client configuration specifically: the path, the environment variables it's passing (or not passing), or the working directory it launches from. That's a much smaller problem than "my server is broken."
This is also where a config-specific checker earns its keep. MCPConfigCheck parses your
mcp.jsonorclaude_desktop_config.jsonentirely in your browser and flags common config-level mistakes and known risky patterns before you spend more time on manual log diving.
Client-Specific Quirks: Claude Desktop, Claude Code, and Cursor
The four root causes above are the same regardless of which AI client you're using, but where you go to look for evidence differs quite a bit between them.
| Client | Where config lives | How to see what went wrong |
|---|---|---|
| Claude Desktop | <code>claude_desktop_config.json</code>, opened via Settings > Developer > Edit Config | Check the app's MCP log files (Settings > Developer shows the log directory) rather than trusting the in-app status alone; startup errors are written there even when the UI just says disconnected |
| Claude Code | Project-level <code>.mcp.json</code> or user-level config, editable via <code>claude mcp add</code> | Run <code>/mcp</code> inside a session to see connection status per server, and <code>/doctor</code> to catch config validation issues (it won't diagnose deeper stdio problems on its own) |
| Cursor | <code>.cursor/mcp.json</code> per project, or a global config in Cursor Settings | The MCP settings panel shows connection status per server and is usually the fastest place to spot which specific server is failing when you have several configured |
One Windows-specific wrinkle worth knowing about if you support users on it: Claude Desktop's config file location on Windows moved after a recent MSIX packaging update. If the Edit Config button in the app doesn't seem to match what you're actually editing, check the real file path directly rather than trusting the shortcut, and if you're specifically debugging under WSL2, treat that as a separate troubleshooting pass since it introduces its own networking and process-spawning failure modes on top of everything covered here.
Quick Reference: All Four Causes at a Glance
| Cause | Typical symptom | Fix |
|---|---|---|
| Process never started | Disconnects almost instantly, no delay | Use absolute paths for both <code>command</code> and <code>args</code>; verify the interpreter path with <code>which</code> |
| Stdout pollution | Works when run manually, disconnects only through the client | Move all logging to stderr (<code>console.error</code>, <code>print(..., file=sys.stderr)</code>); check dependencies too |
| Path/environment problems | Briefly connects, then disconnects a second or two later | Declare needed variables explicitly in the config's <code>env</code> block; use absolute paths for any file the server reads |
| Transport mismatch | Disconnects with no distinguishing symptom at all | Confirm the server's actual transport (stdio vs. Streamable HTTP) matches what the config specifies |
Run MCPConfigCheck against your config before working through this list manually. It catches a number of the most common config-level mistakes automatically, in your browser, before you go digging through client logs.
Frequently Asked Questions
Why does my MCP server work when I run it manually but not through Claude Desktop?
Almost always an environment or path difference. GUI apps on macOS and Windows do not inherit your shell's PATH or environment variables the way a terminal does, so a command that works fine in your terminal can fail silently when launched by the AI client.
- <strong>Use absolute paths</strong> for both the command and any script arguments in your config
- <strong>Declare env vars explicitly</strong> in the config's
envblock, even if they're already set in your shell profile - <strong>Use absolute paths for data files</strong> your server reads, since the working directory it launches from may not be your project folder
What does "MCP server disconnected" actually mean at a technical level?
The client launched your server as a subprocess over stdio, and either the subprocess exited unexpectedly, or something wrote non JSON-RPC content to stdout and corrupted the message stream between client and server. There's no socket or handshake to inspect the way there would be with an HTTP-based integration; it's purely a process and stream-level failure.
Is the MCP Inspector official, and is it safe to use?
Yes. It's maintained by the Model Context Protocol team and is the standard first tool to reach for when a server isn't behaving as expected, instead of guessing through client logs alone. It runs locally via npx and connects directly to your server the same way a real client would, without any AI client's own config layered on top.
Can a dependency cause this even if my own code is fine?
Yes. Any library your server imports that logs to stdout by default, an ORM printing query logs, a startup banner from a web framework, will corrupt the JSON-RPC stream the same way a stray print() statement would. This is one of the harder versions of the bug to spot, because your own code looks completely correct.
# Search for likely stdout writers in your dependencies
grep -rn "print(" node_modules/some-dependency/
grep -rn "console.log" node_modules/some-dependency/Does this apply the same way on Windows?
The same four causes apply, but Windows adds extra failure modes on top, mainly around WSL2 networking and the way spawn() crosses the Windows and Linux boundary. Claude Desktop's config file location on Windows also moved after a recent MSIX packaging update, so verify the actual file path rather than trusting the in-app shortcut.
Why does restarting Claude Desktop or Cursor temporarily fix a disconnected server?
A restart clears the client's in-memory process handle and forces it to respawn your server fresh. If the underlying cause was a one-off crash (a transient network error inside your server, a race condition on startup), a fresh process can genuinely succeed and the disconnect won't recur. But if the cause is structural, an absolute path issue, a missing environment variable, or stdout pollution baked into your server's normal startup sequence, the restart will just reproduce the exact same disconnect on the next launch.
Treat a restart as a way to confirm whether the failure is intermittent or deterministic, not as an actual fix. If it happens again after a restart, go through the four causes above rather than restarting a second time.
Related Articles
Claude Code Cheatsheet: Commands, Hooks & Subagents
The complete Claude Code reference: every slash command, keyboard shortcut, hook, subagent, and CLAUDE.md tip, with real examples for developers.
Multi-Agent AI Coding Workflow: Step-by-Step (2026)
Build a 3-agent AI coding workflow with CrewAI and Python. One agent writes, one reviews, one writes tests. Full code included.