How to Fix MCP Server Connection Errors
When an MCP server won't connect, the AI app usually logs the reason. Open that app's MCP log first, find the error, and match it to the sections below. The common causes are a command the app can't find, a config file that isn't valid JSON or TOML, output on stdout that corrupts the connection, and a server that rejects your sign-in. For Claude Desktop on macOS, the config lives at ~/Library/Application Support/Claude/claude_desktop_config.json.
Start here
Match what you see to the first place to look.
| What you see | Start with |
|---|---|
| The server isn't in the app's list at all | The config file: its location and whether it parses |
The log says spawn <command> ENOENT | The command path |
| The server starts, then drops straight away | Anything printed to stdout |
The log says ModuleNotFoundError | The Python environment the server runs in |
| The server asks you to sign in, or you get a 401 | Sign-in and the token |
| The server connects but the model never calls its tools | Tool count and descriptions |
Where the config and logs live
Each app keeps its server list and its log in a different place. The table covers the apps this guide's fixes apply to.
| App | Config | Where to look for errors |
|---|---|---|
| Claude Desktop | macOS: ~/Library/Application Support/Claude/claude_desktop_config.json. Windows: %APPDATA%\Claude\claude_desktop_config.json | macOS: ~/Library/Logs/Claude. Windows: %APPDATA%\Claude\logs. mcp.log covers connections, and mcp-server-SERVERNAME.log holds each server's stderr |
| Cursor | Global: ~/.cursor/mcp.json. Project: .cursor/mcp.json | Open the Output panel (Cmd+Shift+U) and select "MCP Logs" |
| VS Code | Project: .vscode/mcp.json. User: run "MCP: Open User Configuration" | Command Palette, "MCP: List Servers", select the server, then "Show Output" |
| Claude Code | Project: .mcp.json in the project root. User: ~/.claude.json | Run claude mcp list, or claude mcp get <name> to see the Issue: line. In a session, /mcp shows status |
| Codex | ~/.codex/config.toml, or .codex/config.toml in a trusted project | Run codex mcp list. Type /mcp in the composer to see connected servers |
Claude Desktop only reads its config when it starts. Edit the file, then quit the app completely and reopen it. Closing the window isn't enough.
To watch the Claude Desktop log while you restart, run this in a terminal:
tail -n 20 -F ~/Library/Logs/Claude/mcp*.log
On Windows, run type "%APPDATA%\Claude\logs\mcp*.log" in Command Prompt.
Anything a local (stdio) server writes to stderr goes into its per-server log. If that log is empty, the server may never have started.
Check your runtime versions first
Many failures are an old runtime, not a bug in the server. These are the minimums from each package's own metadata:
| Tool | Minimum |
|---|---|
MCP Inspector (@modelcontextprotocol/inspector 2.11.0) | Node.js 22.19.0 |
TypeScript server SDK (@modelcontextprotocol/server 2.3.1) | Node.js 20 |
Python SDK (mcp 2.3.0) | Python 3.10 |
Check your versions with node --version and python3 --version. The app may use a different Node or Python from your terminal, so the path to the command in your config matters too (see the next section).
Error messages and fixes
spawn <command> ENOENT
Node.js prints this when it can't find the program named in your config. The name after spawn is the command value. On my test machine, a command that doesn't exist gave this:
spawn uvx-not-installed ENOENT
The app couldn't find the executable with that name. A server started by an app gets only a limited set of environment variables, so a command that works in your terminal may not resolve there. Use the full path to the command. Get it with which uv or which node on macOS and Linux, or where uv on Windows, and put that path in command:
{
"mcpServers": {
"bookmarks": {
"command": "/ABSOLUTE/PATH/TO/uv",
"args": [
"--directory",
"/ABSOLUTE/PATH/TO/bookmarks",
"run",
"bookmarks.py"
]
}
}
}
The MCP docs make the same point: when a client launches a stdio server, the working directory may be undefined, so use absolute paths in command and args.
On Windows, the Claude docs also describe an ENOENT error that mentions ${APPDATA} in a path. The fix there is to add the expanded value to the server's env key, such as "APPDATA": "C:\\Users\\username\\AppData\\Roaming\\".
Invalid JSON in the config file
A trailing comma, a missing brace or a stray quote stops the file from loading, and the server never appears. Check the file before you restart the app:
python3 -m json.tool ~/Library/Application\ Support/Claude/claude_desktop_config.json
A file with a trailing comma after "command": "uv", produced this output:
Illegal trailing comma before end of object: line 4 column 22 (char 60)
Delete the comma, save the file, and run the check again. A file that parses prints the same JSON back. Cursor's mcp.json needs valid JSON too. Codex uses TOML, so check config.toml with python3 -c "import tomllib,sys;tomllib.load(open(sys.argv[1],'rb'))" config.toml instead.
ModuleNotFoundError: No module named 'mcp'
The server started, but the Python that ran it doesn't have the mcp package. Running python3 -c "import mcp" in a clean environment gives this:
ModuleNotFoundError: No module named 'mcp'
The app may start your server with a different Python from the one where you ran uv add. Launch the server through uv, so it uses the project's own environment:
{
"mcpServers": {
"bookmarks": {
"command": "/ABSOLUTE/PATH/TO/uv",
"args": [
"--directory",
"/ABSOLUTE/PATH/TO/bookmarks",
"run",
"bookmarks.py"
],
"env": {
"BOOKMARKS_FILE": "/ABSOLUTE/PATH/TO/bookmarks.json"
}
}
}
}
The --directory flag tells uv which project to use, and run makes sure its dependencies are installed.
The server connects, then disconnects straight away
On stdio, the server's standard output (stdout) carries the protocol messages, so anything else you print there gets in the way. The MCP debugging guide says local servers should not log to stdout, because it interferes with protocol operation. The app can then drop the connection, and the server looks like it died for no reason.
Find the stray output. In Python, look for print() calls, including inside libraries you import. In TypeScript, look for console.log(). Send diagnostic output to stderr instead. In Python, logging with its default handler writes to stderr, so configure it once at startup:
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.info("Server starting")
In Node, console.error() writes to stderr.
Unsupported protocol version (-32022)
A client and a server that don't share a protocol revision can't talk. The server answers with the error code -32022, and its data field lists the versions it does support. To see which versions a server supports, the client sends a server/discover request. Compare that list with the version your client uses, then update whichever side is older. The Inspector helps here: --protocol-era legacy tests the older handshake and --protocol-era modern tests the 2026-07-28 revision.
Sign-in fails, or you get a 401
A remote server returns HTTP 401 when a token is missing, invalid or expired. The spec requires the response to include a WWW-Authenticate header that points at the server's protected resource metadata. Claude Code shows this as a server that needs authentication, and you can sign in again from the app:
- Claude Code: run
/mcp, select the server, and choose Re-authenticate. To remove the stored sign-in first, choose "Clear authentication". From the shell,claude mcp login <name>runs the same sign-in flow. - Cursor: if the provider asks for a registered redirect URL, add
https://www.cursor.com/agents/mcp/oauth/callbackandhttp://localhost:8787/callbackto its OAuth app. Cursor uses the first for web and Cursor Agents, and the second for the desktop app. - Codex: run
codex mcp login <server-name>for a server that supports OAuth.
If you set a token by hand in headers, check the value itself. Claude Code warns about leading or trailing whitespace, which often comes from pasting a token with a newline on the end. In a remote server's url and headers, Claude Code also reads credential variables from your environment as empty, so a header like Bearer ${SOME_TOKEN} can reach the server with no token. Use the app's sign-in flow where the server supports it.
For OAuth connections that keep looping, see OAuth sign-in for MCP servers.
Insufficient scope (needs additional permissions)
Signing in worked, but a tool call failed because the token lacks a permission. Claude Code reports this as a needs additional permissions message that names the scope the server wants. Add that scope to the server's pinned oauth.scopes, then run /mcp and authenticate again. Claude Code requests the pinned scopes, so signing in again without the new scope gives you a token that still lacks it.
Failed to connect or disconnected · open /mcp to reconnect
Claude Code retries a failed first connection up to three times, then marks the server as failed. claude mcp list adds the failure detail to that line, and claude mcp get <name> shows it on an Issue: line. Open /mcp to reconnect one server, or run /mcp reconnect all in the interactive terminal (Claude Code v2.1.284 or later). Claude Code doesn't retry authentication errors or 404s, so fix the configuration first.
Pending approval (run claude to approve)
Claude Code asks you to approve each project-scoped server in .mcp.json before it connects. Run claude in the project, accept the workspace trust dialog, then approve the server. A cloned repository can't approve its own servers. The approval has to come from the trust dialog on your machine. To reset your earlier choices, run claude mcp reset-project-choices.
Warnings about whitespace or unset variables
Claude Code warns when a config value has hidden leading or trailing whitespace, and names the field, for example headers.Authorization. It doesn't trim the value for you, so open the file and remove the space or newline.
It also warns when a ${VAR} reference names a variable that isn't set and has no default. It still loads the server, but with the text ${VAR} unexpanded. Set the variable, or give it a fallback in the form ${VAR:-default}.
Timeouts
A server that takes too long to start or to answer gets cut off. The limits are set in config:
| App | Setting | Default | What it controls |
|---|---|---|---|
| Codex | startup_timeout_sec | 10 | Seconds to wait for the server to start |
| Codex | tool_timeout_sec | 60 | Seconds for one tool call |
| Claude Code | MCP_TIMEOUT (environment variable, milliseconds) | Startup timeout, for example MCP_TIMEOUT=10000 claude | |
| Claude Code | MCP_TOOL_TIMEOUT (environment variable, milliseconds) | About 28 hours if unset | Hard limit for one tool call |
| Claude Code | timeout in the server's entry in .mcp.json (milliseconds) | Hard limit for one tool call on that server, overriding MCP_TOOL_TIMEOUT |
For Codex, these keys sit in the server's table in config.toml. This example file parses:
[mcp_servers.bookmarks]
command = "/ABSOLUTE/PATH/TO/uv"
args = ["--directory", "/ABSOLUTE/PATH/TO/bookmarks", "run", "bookmarks.py"]
startup_timeout_sec = 20
tool_timeout_sec = 60
[mcp_servers.bookmarks.env]
BOOKMARKS_FILE = "/ABSOLUTE/PATH/TO/bookmarks.json"
Raise the startup value if the server is slow to load its dependencies, and check the server for a hang if a tool call times out.
The model never calls the tool
The server is connected, but the tool doesn't show up in the chat or the model picks another one. Claude Code loads MCP tools on demand by default, and /mcp shows how many tools each connected server exposes. It also flags a server that advertises tools but exposes none. In VS Code, run "MCP: Reset Cached Tools" after a server changes its tools.
If the server exposes a lot of tools, the model has more to choose between. How many tools is too many covers how to split or scope a server. The tool's name and description decide whether the model picks it. Tool names and descriptions covers writing them.
FAQ
Why doesn't my MCP server show up in Claude Desktop after I edit the config?
Claude Desktop reads its config only at startup. Check the file parses with python3 -m json.tool, then quit the app completely and open it again. If the server still doesn't appear, read the mcp.log file in the Claude logs folder for the reason.
Where do I find MCP logs in Cursor?
Open the Output panel with Cmd+Shift+U, then choose "MCP Logs" from the dropdown. It shows server startup, tool calls and error messages for each server.
Why does my server work in the terminal but fail in the app?
Apps start stdio servers with a limited set of environment variables, and the working directory may not be your project folder. Your shell settings don't always carry over. Use absolute paths in command and args, and set any variables the server needs in its env block.
Do I have to start a local MCP server myself?
No. When an app connects to a local (stdio) server, the app launches it as a child process. You only run the server yourself when you test it, for example with MCP Inspector.
- Claude DesktopConfig file locations, local and remote servers, and logs.
- Cursormcp.json for global and project servers, plus OAuth.
- Claude Codeclaude mcp add, scopes, and checking status with /mcp.
- Test with MCP InspectorRun a server on its own to see if the fault is in the server or the app.
- Sign in to OAuth serversWhat happens when a remote server asks you to sign in, and how to sign out.