You add an MCP server, restart the client, and get ✘ Failed to connect, a red dot, or a server that is listed but has no tools. The error text is short and the fix is usually one line of config, but guessing at it can eat an afternoon. This guide gives a fixed order to check things in for Claude Code, Claude Desktop and Cursor, with the real output Claude Code printed when we broke a test server on purpose in each of the common ways. We ran only Claude Code; the Claude Desktop and Cursor details come from their official docs.
What you need
- The client you are fixing (Claude Code, Claude Desktop or Cursor) and access to its config file.
- A terminal.
- The server's own README, for its exact command, URL and required variables. Never guess a package name.
- For the hands-on part: Node.js and Claude Code. We used Claude Code 2.1.303 and Node v24.13.0 on macOS.
A test server you can break
To get real error messages without touching a third-party server, we wrote a tiny stdio MCP server. It reads newline-delimited JSON-RPC on stdin, answers initialize and tools/list, and offers one echo tool. It writes its own messages to stderr only, because stdout carries the protocol.
// echo-server.mjs: minimal stdio MCP server for testing.
// Logs go to stderr only; stdout is reserved for protocol messages.
import { createInterface } from "node:readline";
if (process.env.REQUIRE_TOKEN === "1" && !process.env.ECHO_TOKEN) {
console.error("echo-server: ECHO_TOKEN is not set, exiting");
process.exit(1);
}
if (process.env.ECHO_TOKEN?.startsWith("${")) {
console.error("echo-server: ECHO_TOKEN is an unexpanded ${...} reference");
}
const send = (msg) => process.stdout.write(JSON.stringify(msg) + "\n");
const rl = createInterface({ input: process.stdin });
rl.on("line", (line) => {
let req;
try { req = JSON.parse(line); } catch { return; }
if (req.id === undefined) return; // notifications need no reply
if (req.method === "initialize") {
send({ jsonrpc: "2.0", id: req.id, result: {
protocolVersion: req.params?.protocolVersion ?? "2025-06-18",
capabilities: { tools: {} },
serverInfo: { name: "echo-server", version: "0.0.1" },
}});
} else if (req.method === "tools/list") {
send({ jsonrpc: "2.0", id: req.id, result: { tools: [{
name: "echo",
description: "Returns the text you send",
inputSchema: { type: "object", properties: { text: { type: "string" } }, required: ["text"] },
}]}});
} else if (req.method === "tools/call") {
send({ jsonrpc: "2.0", id: req.id, result: {
content: [{ type: "text", text: String(req.params?.arguments?.text ?? "") }],
}});
} else {
send({ jsonrpc: "2.0", id: req.id, error: { code: -32601, message: "Method not found" } });
}
});
We added it at project scope, which writes .mcp.json in the current folder. Put -- before the server's own command. We used the absolute path to node; use whatever which node prints on your machine.
claude mcp add --scope project echo -- /usr/local/bin/node "$PWD/echo-server.mjs"
claude mcp list
In the outputs below, <dir> stands for our temp folder.
Checking MCP server health…
echo: /usr/local/bin/node <dir>/echo-server.mjs - ⏸ Pending approval (run `claude` to approve)
That is the first lesson. Project servers from .mcp.json need your approval before Claude Code connects to them, and claude mcp list reports ⏸ Pending approval without starting the process. We also put an approval in the folder's .claude/settings.local.json. The status stayed pending. The Claude Code docs explain why: in a folder you have never trusted, Claude Code waits for the workspace trust dialog before it applies that file's approvals.
Normally you just run claude in the folder and approve the server. For a non-interactive test, we pointed CLAUDE_CONFIG_DIR at a throwaway folder and approved there. First we listed the server by name in enabledMcpjsonServers. For the later tests we used enableAllProjectMcpServers. Do not put the second setting in your real user settings. It approves every project server in every repository you clone. After approval:
echo: /usr/local/bin/node <dir>/echo-server.mjs - ✔ Connected
The triage order
Work from the outside in, and change one thing at a time.
- Does the client see the server at all? If it is missing from the list, the config was not read: wrong file, wrong location, invalid JSON, or (Claude Code) not yet approved.
-
Does the process start by itself? Copy the
commandandargsfrom the config and run them in a terminal. A healthy stdio server starts and waits silently. An error here has nothing to do with your AI client. -
Does the app see the same PATH and environment as your terminal? The MCP debugging docs say stdio servers inherit only a limited, platform-dependent set of environment variables, and the working directory may be undefined (like
/on macOS). Use absolute paths for the command and any file arguments, and set variables in the server'senvblock. -
Are your variables actually reaching the server? An unset
${VAR}does not stop the server from loading in Claude Code (see test D). -
Auth. For remote servers,
! Needs authenticationmeans the server is reachable but wants a browser sign-in or a token you have not configured. A 401 or 403 means the server is up: a 401 usually means no valid credential was sent, a 403 that the credential lacks permission. - Transport and endpoint. A local server needs a command. A remote server needs a URL and a type. Copy the URL exactly from the publisher's docs.
-
Timeouts. A first
npxrun can be slow while it downloads the package. - Logs. If the steps above do not reveal it, the client's log usually names the failing step.
Real errors and what they mean
All of these came from claude mcp list and claude mcp get <name> on Claude Code 2.1.303.
A. Wrong command name
claude mcp add --scope project typo -- nodejs-missing "$PWD/echo-server.mjs"
typo: nodejs-missing <dir>/echo-server.mjs - ✘ Failed to connect — ENOENT: Executable not found in $PATH: "nodejs-missing"
ENOENT means "no such file". Claude Code looked for the program on its PATH and did not find it. The fix is the right name or an absolute path. It is not a permissions problem, so sudo will not help.
B. A server that exits at startup
We set REQUIRE_TOKEN=1 but no ECHO_TOKEN, so the server exits right away.
needs-token: /usr/local/bin/node <dir>/echo-server.mjs - ✘ Failed to connect — CONNECTION_CLOSED: Connection closed
claude mcp get needs-token showed the same Issue: CONNECTION_CLOSED: Connection closed. The real reason was not in either output. Two ways to find it:
# 1. Run the command by hand with the same environment
REQUIRE_TOKEN=1 /usr/local/bin/node ./echo-server.mjs < /dev/null
# echo-server: ECHO_TOKEN is not set, exiting
# 2. Ask Claude Code for a debug file
claude --debug-file ./debug.log mcp list
The debug file had the server's stderr:
[ERROR] MCP server "needs-token" Server stderr: echo-server: ECHO_TOKEN is not set, exiting
[ERROR] MCP server "needs-token" Connection failed (CONNECTION_CLOSED): Connection closed
Some lines for this server showed [REDACTED] in place of ordinary words, probably because its name contains "token". Treat debug files as sensitive anyway, and delete them when you are done.
C. Invalid JSON in .mcp.json
We added a trailing comma after the last server:
No MCP servers configured. Use `claude mcp add` to add a server.
MCP config diagnostics ✘
[Failed to parse] Project config (shared via .mcp.json)
Location: <dir>/.mcp.json
└ [Error] MCP config is not a valid JSON
One stray comma dropped every server in the file. claude mcp get echo then answered No MCP server named "echo". A single bad entry behaves differently. When we added a remote entry with a url but no type, Claude Code skipped only that entry and kept the others:
└ [Warning] [remote-no-type] mcpServers.remote-no-type: Skipped — MCP server "remote-no-type" has a "url" but no "type"; add "type": "http" (or "sse" / "ws") to this entry
D. ${VAR} unset vs set
We added an entry with a variable reference. The single quotes stop the shell from expanding it:
claude mcp add-json --scope project with-token \
'{"type":"stdio","command":"/usr/local/bin/node","args":["<dir>/echo-server.mjs"],"env":{"REQUIRE_TOKEN":"1","ECHO_TOKEN":"${ECHO_TOKEN}"}}'
With ECHO_TOKEN unset:
with-token: /usr/local/bin/node <dir>/echo-server.mjs - ✔ Connected
[Contains warnings] Project config (shared via .mcp.json)
└ [Warning] [with-token] mcpServers.with-token: Missing environment variables: ECHO_TOKEN
The status says Connected. The debug file showed what the server actually received:
[ERROR] MCP server "with-token" Server stderr: echo-server: ECHO_TOKEN is an unexpanded ${...} reference
This matches the Claude Code docs: an unset variable with no default leaves the config loaded and passes the ${VAR} text through as-is. A real API would receive that literal string as its credential. Connected does not mean authenticated, so read the diagnostics block under the list. With ECHO_TOKEN=FAKE_TOKEN_FOR_DOCS set in the same shell, the warning and the stderr line both went away.
E. A server that never answers
claude mcp add --scope project silent -- /usr/local/bin/node -e "setTimeout(() => {}, 60000)"
MCP_TIMEOUT=3000 claude mcp list
silent: /usr/local/bin/node -e setTimeout(() => {}, 60000) - ✘ Failed to connect — MCP server "silent" connection timed out after 3000ms
Without MCP_TIMEOUT, our debug log showed Starting connection with timeout of 30000ms. For a slow first download, raise it (MCP_TIMEOUT=60000 claude) or run the package once by hand so it is cached.
F. Forgetting --
claude mcp add --scope project noflag /usr/local/bin/node ./echo-server.mjs --port 3000
# error: unknown option '--port'
Claude Code read --port as its own flag. With -- in front of the server command, the same line was added and connected.
G. A banner printed to stdout
The MCP docs warn that local servers should not log to stdout. We wrapped our server in sh -c "echo 'Server starting on stdio...'; exec node ...". This version of Claude Code still connected. Do not take that as permission. It was one line, before startup, in one client. Send logs to stderr.
Where each client keeps config and logs
Claude Code rows marked as tested were run by us. Claude Desktop and Cursor rows are from the official docs; not run by us.
| Claude Code | Claude Desktop | Cursor | |
|---|---|---|---|
| Config |
.mcp.json in the project root (project scope, tested); ~/.claude.json (local and user scope) |
macOS ~/Library/Application Support/Claude/claude_desktop_config.json; Windows %APPDATA%\Claude\claude_desktop_config.json. Settings → Developer → Edit Config opens it |
.cursor/mcp.json in the project; ~/.cursor/mcp.json for all projects |
| Variables |
${VAR}, ${VAR:-default} (tested ${VAR}) |
Not documented; the docs' examples put literal values in env
|
${env:NAME}, ${userHome}, ${workspaceFolder}; envFile for stdio only |
| Apply changes |
.mcp.json is read at session start; start a new session |
Fully quit and reopen; closing the window is not enough | Restart Cursor after editing mcp.json; toggles under Customize → MCPs |
| Logs |
claude --debug-file <path> (tested) |
macOS ~/Library/Logs/Claude, Windows %APPDATA%\Claude\logs: mcp.log and mcp-server-<name>.log (the server's stderr) |
Output panel (Cmd+Shift+U) → "MCP Logs" |
| Check |
claude mcp list, claude mcp get <name> (tested); /mcp in a session |
"Add files, connectors, and more" → Connectors → Manage connectors | Toggle under Customize; a failed server shows an error in chat; MCP Logs |
Two more items from the docs (not run by us):
- Claude Code does not read
~/.claude/mcp.jsonor~/.claude/.mcp.json. Useclaude mcp add --scope userinstead. - On Windows, the Claude Desktop docs cover an ENOENT error that mentions
${APPDATA}. The fix is to add the expandedAPPDATApath to the server'senv, and npm should be installed globally. Wrappingnpxas"command": "cmd", "args": ["/c", "npx", ...]is a community-reported fix that we did not find in the official docs.
On GUI apps and your shell profile: whether a Dock- or Start-menu-launched app sees the variables from your ~/.zshrc depends on the app and the OS. Cursor's MCP help page only says to make sure shell-profile variables are available to Cursor and to restart it after changing the profile. Do not rely on it. Put absolute paths in the config and the variables in the client's own variable mechanism, then confirm in the logs.
Secrets: references, not values
-
Shared files hold references only.
.mcp.jsonand.cursor/mcp.jsonare meant to be committed. Use${VAR}(Claude Code) or${env:VAR}(Cursor), and let each person's environment supply the value. -
Watch shell expansion.
claude mcp add --env API_TOKEN="$API_TOKEN"lets your shell fill in the value, and Claude Code saves that value in the private~/.claude.json. That is fine for local or user scope. For a shared.mcp.json, write the reference in single quotes, as in test D, so the file keeps${ECHO_TOKEN}and not the value. -
Claude Code will not send its own credentials to remote servers. The docs say credential variables such as
ANTHROPIC_API_KEYread as empty in a remote server'surlandheaders. If you reused one, expect an auth failure. -
Claude Desktop: the docs show literal values in
env. Type the secret into the file yourself in your editor. -
Never paste a token into an agent chat. Give the agent the variable name, not the value. Once a value is in the conversation, it can end up in an answer or a file. In our own evaluation of an MCP setup skill, all 3 answers without the skill repeated a password the user had pasted, inside an
exportcommand. If you have pasted one, treat it as exposed: revoke it and create a new one.
Verify, then prove it
-
claude mcp listshows✔ Connectedand no warnings in the diagnostics block. Test D shows why both matter. -
claude mcp get <name>shows the scope, command and environment that won. If the command differs from what you typed, you probably left out--. - Inside a session,
/mcplists servers and their tools and runs OAuth sign-in (from the docs; we did not run an interactive session). - Ask for one task that needs exactly one read-only tool, and check that real data comes back. A green status proves the connection. A tool result proves the server works.
Common mistakes
-
sudofor ENOENT. The error says the client could not find the executable. Running the client or server as root does not change that and adds risk. In our own eval runs on a Claude Desktopspawn npx ENOENTtask, two of three answers without the skill suggestedsudo ln -sfto symlinknodeinto/usr/local/bin. An absolute path in the config fixes the same lookup without root. -
Editing the wrong file, such as
~/.claude/mcp.json. -
A trailing comma that drops every server in
.mcp.json(test C). -
A remote entry without
"type"(test C). -
Treating Connected as done while a
${VAR}warning is showing (test D). - Closing the Claude Desktop window instead of quitting the app.
- Auto-approving all project servers in your real user settings to make a prompt go away.
- Changing three things at once, then not knowing which one fixed it.
If you would rather have an agent walk through this order for you without ever handling the secret values, we publish an open skill, MCP Mechanic, that encodes these checks. Everything above works without it.
Limits of this guide
- We ran only Claude Code 2.1.303 on macOS, with a local stdio server we wrote ourselves. We did not test remote HTTP servers, OAuth, Claude Desktop, Cursor, Windows or an interactive
/mcpsession. - Our server supports only the earlier handshake, and Claude Code connected in its "legacy" mode. The newer 2026-07-28 protocol revision adds required per-request fields and new error codes, so servers that use it can fail in ways not shown here.
- Client behaviour changes often. The Claude Code docs list several version-specific changes to these messages. Check your version's docs if your output looks different.
- We did not run the MCP Inspector. The MCP debugging docs call it the first stop for testing a server outside your client, and it is worth trying (from the official docs; not run by us).
References
- Claude Code: Connect to MCP servers (quickstart and troubleshooting) and MCP reference
- MCP: Debugging, Connect to local MCP servers (Claude Desktop), MCP Inspector
- Cursor: Model Context Protocol and MCP help
This article was drafted with AI assistance (Claude Code) from our project's code and test records, and reviewed and edited by Tim S before publishing.
Top comments (0)