MCP Server Works in Terminal but Not in Claude Desktop: 6 Causes and Fixes
Nikhil Tiwari
MCP Playground
π TL;DR
- Your terminal and your MCP client start the server in different worlds. The PATH, the working folder, the environment variables and even the Node or Python version can all differ.
- Read the client's log first. Claude Desktop only says "Server disconnected"; the real error is in
~/Library/Logs/Claude/(Mac) or%APPDATA%\Claude\logs\(Windows). - The six causes: PATH, working folder, environment variables, output on stdout, config mistakes, and a different runtime version.
- Almost every fix is in the config: absolute paths, an
envblock, and one program incommand. - To narrow it down, start the server outside both your terminal and the client, with exactly the config's command, args and env.
Table of Contents
- Why the terminal is a bad test
- Step 0: read the client's log
- Match your error to a cause
- 1. The client has a shorter PATH
- 2. The server starts in a different folder
- 3. Your environment variables aren't there
- 4. Something prints to stdout
- 5. The config isn't what you think
- 6. A different Node or Python runs it
- Find which one it is
- FAQ
Why "it works in my terminal" proves less than you think
When you run node build/index.js or uv run server.py in a terminal, the server inherits everything your shell set up. That includes your full PATH, the folder you're in, every variable you exported, and whichever Node or Python your version manager picked.
A stdio MCP client starts the same command from scratch. On a Mac, Claude Desktop and Cursor opened from the Dock are started by launchd, not by your shell, so none of your .zshrc or .zprofile runs. On any system, the client passes only the environment it chooses.
So the server code is the same, but the process is not. The six causes below are the ways those two processes differ.
Step 0: read the client's log
Don't guess from "Server disconnected" or a red dot. The client writes the actual error, plus whatever your server printed to stderr, to a log file.
- Claude Desktop on macOS:
~/Library/Logs/Claude/, withmcp.logfor all servers andmcp-server-NAME.logfor each one. - Claude Desktop on Windows:
%APPDATA%\Claude\logs\ - Cursor: the Output panel, with the MCP logs picked in the dropdown.
On a Mac, keep this running while you restart the client:
tail -n 50 -f ~/Library/Logs/Claude/mcp*.log
The last error before the disconnect is usually the answer. If the server crashed, its stack trace is in the same file.
Match your error to a cause
| What the log says | Likely cause |
|---|---|
spawn npx ENOENT, env: node: No such file or directory | 1. PATH |
EROFS: read-only file system, ENOENT: no such file or directory, open './data.db' | 2. Working folder |
"API key is required", 401 Unauthorized, KeyError: 'OPENAI_API_KEY' | 3. Environment variables |
Unexpected token β¦ is not valid JSON | 4. Output on stdout |
| Nothing at all, or the server isn't listed | 5. Config |
ModuleNotFoundError: No module named 'mcp', SyntaxError in a dependency | 6. Runtime version |
1. The client has a shorter PATH
This is the most common one. An app opened from the Dock gets /usr/bin:/bin:/usr/sbin:/sbin as its PATH. Homebrew, nvm, Volta, fnm, asdf and uv all live outside those folders, so npx, node, uvx or docker can't be found.
The log shows spawn npx ENOENT (or spawn uvx ENOENT). If you already put the full path to npx in the config, it changes to env: node: No such file or directory, because npx itself looks up node on PATH.
Fix: find the program with which npx, use that absolute path in command, and give the server a PATH that includes the same folder:
{
"mcpServers": {
"my-server": {
"command": "/opt/homebrew/bin/npx",
"args": ["-y", "my-mcp-server"],
"env": {
"PATH": "/opt/homebrew/bin:/usr/bin:/bin"
}
}
}
}
Windows has its own version of this: npx is npx.cmd, which needs "command": "cmd" with "/c", "npx" at the start of args. Fix "spawn npx ENOENT" covers both systems step by step.
2. The server starts in a different folder
In a terminal you usually cd into the project first. Relative paths in your code, like ./data.db, .env or logs/, then resolve inside the project.
The client doesn't cd anywhere. On a Mac, an app opened from the Dock runs with / as its working folder, and the server inherits it. A server that writes a log file or creates a database next to itself then fails with:
Error: EROFS: read-only file system, mkdir '/logs'
Reading a file gives ENOENT: no such file or directory instead. A Python server that calls load_dotenv() with no arguments quietly loads nothing, which then looks like cause 3.
Fix: don't depend on the working folder.
- In your code, build paths from the script's own location:
path.join(import.meta.dirname, 'data.db')in Node 20.11+, orPath(__file__).parent / "data.db"in Python. - In the config, use absolute paths for the script and for any folder you pass as an argument.
- For uv projects, tell uv where the project is:
"args": ["run", "--directory", "/Users/you/my-server", "server.py"].
3. Your environment variables aren't there
Exporting GITHUB_TOKEN or OPENAI_API_KEY in .zshrc only affects programs started from a shell. The client never reads that file.
Clients also don't pass their whole environment to the server. The official TypeScript SDK's stdio client, for example, passes only HOME, LOGNAME, PATH, SHELL, TERM and USER on Mac and Linux (and a similar short list on Windows), plus whatever is in the config's env. A .env file in your project only helps if the server finds it, which runs into cause 2.
The symptom is the server starting, then failing on the first tool call with "API key is required", a 401, or a KeyError.
Fix: put every variable the server needs in the config:
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_your_token_here",
"LOG_LEVEL": "debug"
}
On Windows, a variable set with setx or in System Properties only reaches apps started after the change. Quit the client fully from the system tray and open it again.
4. Something prints to stdout
A stdio server uses stdout for JSON-RPC messages and nothing else. Every line the client reads there must be a JSON-RPC message.
In a terminal, a stray console.log("Server started") or print("Loadingβ¦") looks harmless; it just appears on screen. Inside the client, that line is read as a protocol message, fails to parse, and the log shows something like:
Unexpected token 'S', "Server started" is not valid JSON
Depending on the client, the connection then drops or the bad line is skipped. Banners from libraries and progress bars from package downloads cause the same thing.
Fix: send all logging to stderr. In Node use console.error. In Python use print(..., file=sys.stderr) or the logging module, which writes to stderr by default. For npx, keep -y in args so it never stops to ask whether to install a package.
5. The config isn't what you think
If the log shows nothing for your server at all, the client probably never read your entry. Check these in order:
- Right file? Claude Desktop reads
~/Library/Application Support/Claude/claude_desktop_config.jsonon a Mac and%APPDATA%\Claude\claude_desktop_config.jsonon Windows. Settings β Developer β Edit Config opens the one it actually uses. Cursor reads~/.cursor/mcp.jsonplus.cursor/mcp.jsonin the open project. The config file guide lists every client. - Valid JSON? A trailing comma or a single backslash in a Windows path makes the whole file invalid, and every server disappears, not just the new one. Paste the file into a JSON validator.
- One program in
command?"command": "node build/index.js"fails, because the client looks for a program with that whole name. Putnodeincommandand the path inargs. - Fully restarted? Closing the window isn't enough. Quit with Cmd+Q on a Mac or from the system tray on Windows, then open the client again.
- Server built? For a TypeScript server, the config points at
build/index.js. If you changed the source but didn't run the build, the client is still running the old code.
6. A different Node or Python runs it
Your terminal picks a runtime through nvm, pyenv, a virtualenv or uv. The client finds whatever comes first on its own, shorter PATH. That can be a much older version, or a Python without your packages.
- Python:
"command": "python3"in Claude Desktop on a Mac often finds/usr/bin/python3, Apple's copy, which doesn't have your project's packages installed. The log showsModuleNotFoundError: No module named 'mcp'. - Node: an old Node in
/usr/local/binruns a server written for Node 20. Errors range fromSyntaxErrorin a dependency tofetch is not defined.
Fix: point command at the exact runtime you tested with.
- For a virtualenv:
"command": "/Users/you/my-server/.venv/bin/python". - For uv:
uv run --directory /Users/you/my-server server.py, so uv uses the project's own Python and packages. - For Node: the full path from
which node, run in the terminal where the server works.
Check what the terminal is using with node --version and which python3, then compare it with what the config points at.
Find which one it is
The terminal test hides causes 1, 2, 3 and 6, because the terminal supplies everything the client doesn't. The client test shows you the failure but not much else. What helps is a third place to run the same config, where you can change one thing at a time.
MCP Playground Desktop works well for this:
- Import your Claude Desktop, Cursor, VS Code, Windsurf or Claude Code config. Each server comes in with the same command, arguments, working folder and env vars the client uses.
- It passes environment variables like an SDK client does: only the basic system ones plus the server's own env list. If a key is missing from your config, the server fails here too, and you'll see its complaint in the Server stderr tab.
- On a Mac it loads your login shell's PATH. If the server connects here but not in Claude Desktop, PATH is your problem (cause 1). If it fails in both, look at causes 2 to 6.
- Spawn errors come with a next step. A missing program shows "Couldn't find uvx on this computer" with the install command, and a working folder that doesn't exist is reported as a missing folder, not a missing command.
- The JSON-RPC tab shows each message once it connects, so you can see whether
tools/listcame back and what it contained.
New to the app? The MCP Playground Desktop guide walks through every tab. Once the server connects, call a tool that reads and one that writes before you trust it. How to test local MCP servers has a checklist.
If the server connects everywhere but drops after a while, the problem is in the protocol, not the launch. See MCP Server Not Working? Fix Error -32000, Timeouts, and Connection Failures.
Frequently Asked Questions
Why does my MCP server work in the terminal but not in Claude Desktop?+
Where do I see why Claude Desktop says "Server disconnected"?+
Why aren't my exported environment variables reaching the MCP server?+
What does "EROFS: read-only file system" mean for an MCP server?+
Why does my MCP server log "is not valid JSON"?+
Does the same apply to Cursor, VS Code and Windsurf?+
Conclusion
"Works in my terminal" means the code is fine. It says nothing about the process the client starts. Read the client's log, match the error to one of the six causes, and fix the config with absolute paths and an env block.
Then test the server outside both the terminal and the client, with the same config, so you know which side the problem is on. Get MCP Playground Desktop for Mac and Windows β
Written by Nikhil Tiwari
15+ years in product development. AI enthusiast building developer tools that make complex technologies accessible to everyone.
Free MCP Tools (no install)
Build, compare & ship MCP agents
Connect any MCP server, run evals on it, compare 80+ models side-by-side, deploy hosted servers, and save reusable agents you can export as an API β all in your browser.