Tutorials Builder
How to debug an MCP server: Inspector, logs, and a workflow that finds the fault
A repeatable workflow for debugging MCP servers: isolate with Inspector, read client logs, and climb the failure ladder from spawn errors to tool bugs.
“My server doesn’t work” is four different problems wearing the same trench coat. The server won’t spawn. It spawns but the handshake fails. The handshake works but no tools show up. The tools show up but calls error out. Each rung has different causes and different evidence — and the fastest way to find your rung is to stop staring at the client and isolate the server first.
This tutorial teaches the workflow, not just the tool. You’ll use the MCP Inspector to test the server on its own, then read each client’s logs when the server is fine but the connection isn’t, and finish with a catalogue of the exact error strings you’ll meet along the way.
Step 1 — Isolate the server with the Inspector
The single most useful move in MCP debugging: take the client out of the picture. The MCP Inspector is an official test harness that spawns your server exactly the way a client would, performs the handshake, and gives you a UI to poke every tool, resource, and prompt.
No install needed — npx fetches it on demand. Point it at your server’s launch command:
# TypeScript server (compiled output)
npx @modelcontextprotocol/inspector node build/index.js
# Python server managed with uv
npx @modelcontextprotocol/inspector uv --directory /absolute/path/to/project run server.py
The Inspector starts a local web UI (it prints the URL, typically on port 6274). Open it and click Connect. Now work through this sequence:
- Does it connect at all? If the connection fails here, the fault is in your server or its launch command — no client config will fix it. Skip to Step 4, rung 1.
- List tools. Open the Tools tab and hit List Tools. Every tool you registered should appear with its description and input schema. Missing tools here means a registration bug, not a client bug.
- Call a tool. Fill in arguments and run it. Read the raw result — including the
isErrorflag and any error text. - Check the Notifications pane. Anything your server writes to stderr shows up here. This is your server-side print debugging channel.
If everything works in the Inspector but fails in your client, you’ve just cut the search space in half: the server is fine, and the problem is the client’s config, environment, or cache. That’s Step 2 and Step 3 territory.
Inspector CLI mode for fast iteration
The UI is great for exploring; the CLI mode is better for tight edit-test loops and scripting:
# List tools, print JSON, exit
npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/list
# Call a tool with arguments
npx @modelcontextprotocol/inspector --cli node build/index.js \
--method tools/call --tool-name get_forecast --tool-arg city=London
Wire that into your terminal history and you can re-test a tool in two seconds after every code change — no browser, no client restart.
Step 2 — Read the client logs
When the Inspector says the server is healthy, the evidence you need lives in the client’s logs. Each client hides them somewhere different.
Claude Desktop writes two kinds of log file:
# macOS
~/Library/Logs/Claude/mcp.log # connection-level events, all servers
~/Library/Logs/Claude/mcp-server-<name>.log # stderr from your server, per server
# Windows
%AppData%\Claude\logs\
Tail them live while you restart the app:
tail -n 50 -f ~/Library/Logs/Claude/mcp*.log
mcp.log tells you whether Claude Desktop even tried to spawn your server and what happened to the handshake. mcp-server-<name>.log captures everything your server printed to stderr — which is why the stderr logging rule in Step 3 matters so much.
Claude Code gives you three angles:
# In-session: connection status per server, with an option to view errors
/mcp
# Health check from the shell
claude mcp list
# Full debug output, including MCP handshake details
claude --debug
/mcp is the quickest check — it shows each configured server as connected or failed. When a server shows as failed, claude --debug prints the spawn command, the environment, and the raw error, which is usually enough to spot a wrong path or missing binary.
Cursor shows server status with a green/red indicator under Settings → MCP, and logs to the Output panel: View → Output, then pick MCP Logs from the dropdown.
Whichever client you use, the reading order is the same: did it spawn? did the handshake complete? did tools/list return what you expected?
Step 3 — The stdio logging rule: stderr only, always
This one rule prevents the single most common self-inflicted MCP bug.
Over the stdio transport, stdout is the wire. The client and server exchange newline-delimited JSON-RPC messages on it. The moment your code prints anything else to stdout — a debug line, a startup banner, a stray print() — the client tries to parse it as JSON-RPC and the connection breaks, usually with a JSON parse error or a dropped connection.
stderr, by contrast, is completely free. Clients capture it as log output (that’s exactly what mcp-server-<name>.log is).
# Python — never print() in a stdio server
import sys
print("debug info", file=sys.stderr) # safe
print("debug info") # breaks the protocol
// TypeScript — console.log writes to stdout
console.error("debug info"); // safe
console.log("debug info"); // breaks the protocol
Also audit your dependencies: a library that prints a progress bar or a version banner to stdout will corrupt the stream just as thoroughly as your own code. If you can’t silence it, redirect stdout around the offending call.
graph TD
A[Rung 1: server will not spawn] --> B[Rung 2: spawns, handshake fails]
B --> C[Rung 3: handshake OK, tools missing]
C --> D[Rung 4: tools listed, calls error]
Step 4 — Climb the failure ladder
Every broken MCP setup sits on one of four rungs. Identify the rung, and the cause list shrinks to a handful of suspects.
Rung 1 — the server won’t spawn. Symptoms: spawn npx ENOENT, spawn uv ENOENT, or the server appearing and instantly disappearing from the client. The client couldn’t start your process at all. Causes: the command isn’t on the PATH the client sees (GUI apps on macOS and Windows get a much smaller PATH than your terminal), a relative path in the config, or Windows needing cmd /c to run .cmd shims like npx. Fix: absolute paths for everything — the command and the script. Test the exact command from your config in a bare terminal.
Rung 2 — it spawns, but the handshake fails. Symptoms: MCP error -32000: Connection closed, or JSON parse errors in the client log. The process started, then died or spoke garbage before completing initialize. Causes: a crash on startup (missing dependency, import error, unreadable config file), or stdout pollution — see Step 3. Fix: run the launch command yourself in a terminal; the traceback that flashes past inside the client is sitting right there in your face. Then check the per-server log for stderr output.
Rung 3 — handshake OK, but tools are missing. Symptoms: the server shows as connected, but the model says it has no such tool, or the tool list is empty. Causes: tools registered after the server connected, a conditional registration path that didn’t run, or a client caching an old tool list. Fix: confirm with the Inspector’s List Tools first. If the Inspector sees the tools but the client doesn’t, fully restart the client — Claude Desktop in particular needs a real quit (Cmd+Q / File → Exit), not just a closed window.
Rung 4 — tools listed, but calls error. Symptoms: the model calls the tool and gets an error result, or arguments arrive malformed. Causes: schema mismatches (the model sent arguments your schema rejected — -32602 Invalid params), unhandled exceptions inside the tool, or upstream API failures leaking raw stack traces. Fix: reproduce the exact call in the Inspector with the same arguments. This rung is about your code, and it’s the healthiest rung to be on — you’ve got a working MCP server with an ordinary bug in it.
Work the ladder bottom-up in evidence and top-down in checks: Inspector first (rules out rungs 1–3 server-side in a minute), then client logs, then a full client restart, then the tool code itself. How you design those tools — and the other primitives — is the subject of tools vs resources vs prompts.
Troubleshooting
The client couldn’t find spawn npx ENOENTnpx at all — classic on Windows, where npx is a .cmd shim that can’t be spawned directly, and on macOS where GUI apps don’t inherit your shell’s PATH. Fix: use the absolute path to the binary (which npx / where npx), or on Windows route through the shell. Remember JSON needs escaped backslashes:
{
"mcpServers": {
"my-server": {
"command": "cmd",
"args": ["/c", "npx", "-y", "my-mcp-package"]
}
}
}
The server process started and then exited before (or during) the handshake. Nine times out of ten it crashed on startup: a missing dependency, an import error, or a wrong path in MCP error -32000: Connection closedargs. Run the exact command from your config in a plain terminal and read the traceback. If the command works in your terminal but not in the client, compare environments — the client won’t have your virtualenv activated or your shell exports, so use absolute paths and pass secrets via the config’s env block.
Something wrote non-protocol text to stdout, and the client choked trying to parse it as JSON-RPC. The quoted fragment tells you exactly what was printed — hunt down that Unexpected token 'H', "Hello worl"... is not valid JSONprint() or console.log and move it to stderr (print(..., file=sys.stderr) in Python, console.error in TypeScript). If the fragment comes from a library banner or progress bar, silence it or redirect stdout around the call. See Step 3.
“No such tool available”
The model tried to call a tool the client doesn’t have in its current list. Common causes: you renamed a tool and the client is holding a stale list; the conversation started before the server connected; or the tool name in the model’s call doesn’t match your registered name exactly (names are case-sensitive). Verify the live list with the Inspector (--method tools/list), then fully restart the client and start a fresh conversation.
Server shows connected but tools don’t appear
Rung 3. First check the Inspector — if List Tools comes back empty there too, your registration code never ran: in TypeScript, make sure every server.registerTool(...) executes before await server.connect(transport); in Python, make sure the decorated functions are actually imported by server.py. If the Inspector sees the tools, the client is stale: quit Claude Desktop completely (Cmd+Q, not the window close button) and reopen it.
Works in the Inspector, fails in the client
The difference is always environment. The Inspector inherits your terminal’s PATH, working directory, and exported variables; the client has none of them. Check for: relative paths (make them absolute), env vars your server reads (put them in the config’s env block), and version managers (nvm, pyenv) whose shims aren’t on the client’s PATH — point command at the real binary instead.
Next step
Now that you can find any fault in minutes, it’s worth making fewer of them by design — next up: what actually belongs in a tool versus a resource versus a prompt, with code for both SDKs.
Next in this learning path MCP tools vs resources vs prompts: when to use each (with code)Was this guide useful?
Thanks — noted. It shapes what gets written next.
newsletter
One practical MCP guide in your inbox. No news, no hype.
Tutorials and decision frameworks as they ship. Unsubscribe anytime.