·MCP Servers
A field manual for debugging MCP servers — how to drive a server with the MCP Inspector, where each host writes its logs, the failure modes behind most real bugs, and what changes when you move a server to production.
Debugging MCP Servers: Inspectors, Logs, and the Failure Modes That Bite
An MCP server is a long-running process that speaks JSON-RPC over a pipe or a socket — which means when something goes wrong, the failure is almost never in the place you expect. A server can start cleanly, advertise its tools, and still return nothing useful because an environment variable was empty, because stderr leaked onto the protocol stream, or because a permission prompt was declined in a window you have closed. This guide is a field manual for debugging MCP servers. It covers the MCP Inspector, where each host writes its logs, the failure modes that account for most real-world bugs, and what changes when you move a server from your laptop to production.
Start with the Inspector
The single best debugging tool for MCP is the official MCP Inspector, a browser UI that talks the protocol directly to a server with no host in the way. You launch it against any server command:
npx @modelcontextprotocol/inspector node path/to/server.js
The Inspector opens a local web page. From there you can:
- See the connection handshake and the capabilities the server advertises.
- List the server's tools, resources, and prompts with their full JSON schemas.
- Call a tool with hand-typed arguments and inspect the raw response.
- Watch the JSON-RPC traffic between the Inspector and the server, message by message.
This is the fastest possible feedback loop. If a tool works in the Inspector but not in Claude Desktop, the problem is in the host config or environment, not your server. If it fails in the Inspector too, you can iterate on the server code without waiting for a full app restart on every change.
The Inspector works with any stdio server. For HTTP servers, point it at the URL instead. It is also a great way to explore someone else's server before you commit to integrating it — you learn the tool names and argument shapes before writing any config.
Read the logs the host already writes
When a server runs inside a host, the host captures its output. The two logs worth knowing:
Claude Desktop writes one log file per server on macOS at ~/Library/Logs/Claude/mcp-server-<name>.log. On Windows the equivalent lives under %USERPROFILE%\AppData\Roaming\Claude\logs\. Anything your server writes to stdout or stderr lands here, interleaved with the host's own protocol messages. Tail the file while you reproduce the bug:
tail -n 100 -f ~/Library/Logs/Claude/mcp-server-filesystem.log
Claude Code surfaces server status interactively through the /mcp command, which lists every configured server, its connection state, and any tools it has registered. For deeper detail it writes logs alongside its other session output; the /mcp panel is usually enough to see whether a server connected at all.
A common pattern is to add structured logging to your own server during development and strip it for release. Write each log line as a single JSON object with a timestamp and a level — it makes the interleaved log far easier to scan than free-form text.
The failure modes that account for most bugs
After you rule out "the server doesn't run at all," almost every remaining MCP bug falls into one of these buckets.
The server crashes on startup
The host spawns the process, the process exits immediately, and the server shows up as failed. The cause is almost always visible in the log: an unhandled exception, a missing dependency, or a syntax error in your entry file. The fix is to run the exact command from your config in a terminal:
node /absolute/path/to/server.js
If it exits non-zero there, it will exit non-zero in the host too. Fix it standalone first.
The connection opens but no tools appear
The server is alive but the host shows zero tools. Two usual suspects: the server never registered its tools (you forgot to call server.tool(...) before server.connect(...)), or the server is speaking a protocol version the host does not understand. The Inspector will tell you instantly — if it lists the tools, the registration code is fine and the issue is version negotiation. Pin your SDK version and check the host's supported protocol version if you suspect a mismatch.
Tools appear but return nothing
The model decides to call a tool and gets an empty or error response. This is where the tool handler itself is the prime suspect. Log the incoming arguments at the top of every handler — you would be surprised how often the schema validation passes but the argument shape is not what the handler assumed. Return errors as proper MCP content rather than throwing:
server.tool("get_user", { id: z.string() }, async ({ id }) => {
const user = await db.findUser(id);
if (!user) {
return {
isError: true,
content: [{ type: "text", text: `No user with id ${id}` }],
};
}
return { content: [{ type: "text", text: JSON.stringify(user) }] };
});
Returning isError: true tells the model the call failed in a structured way, which lets it recover gracefully. Throwing an unhandled exception, by contrast, often surfaces to the model as a generic "tool failed" with no detail.
stderr leaking onto the protocol
This is the subtlest one. Over the stdio transport, stdout is the protocol channel and stderr is the log channel. If your server writes anything to console.log (which goes to stdout) instead of console.error, those bytes corrupt the JSON-RPC stream and the host sees malformed messages. The symptom is intermittent protocol errors that vanish when you remove logging. Rule of thumb: in a stdio server, send every diagnostic to console.error, never console.log. For HTTP servers this constraint does not apply.
Permissions and prompts
Some hosts surface a permission prompt the first time a tool is invoked. If you decline it, or if it fires in a context where it cannot be shown (a headless CI run, a background agent), the tool silently does nothing. In Claude Code, check the permission mode in your session and grant the tool explicitly if needed. In automated environments, pre-approve tools in config so no interactive prompt is required.
A debugging workflow that works
When a server misbehaves, work outward from the simplest possible reproduction:
- Run the server command standalone. Paste the exact
commandandargsfrom your config into a terminal. Does it start and stay alive? - Drive it with the Inspector. Connect the Inspector and call the failing tool with the same arguments the host was using. Does it return the right thing?
- Check the host log. Reproduce the failure in the host and read
mcp-server-<name>.log. What did the server print? - Verify the environment. Print
process.envfrom inside the server during startup and compare it to what you expect. Missing tokens are the most common cause of "works in terminal, fails in host." - Narrow the protocol. If you suspect a version or capability mismatch, compare what the Inspector reports against what the host sees.
This order matters. Skipping straight to reading host logs when the server does not even start is a waste of time.
Production considerations
Moving an MCP server from your laptop to a shared environment changes the debugging story.
Transport shifts from stdio to HTTP. In production you usually run the server as a remote HTTP+SSE or streamable-HTTP endpoint rather than a subprocess. That means you lose the per-process log file and gain network failures, timeouts, and authentication to worry about. Add health-check and structured logging from day one.
Concurrency. A local stdio server serves one user. A remote HTTP server may serve many. Make sure your handlers are stateless or that any shared state is protected. Database connection pools, in-memory caches, and rate limiters all need to be safe under concurrent access.
Authentication and authorization. A remote server needs to authenticate the host before trusting its requests, and the tools it exposes need to respect per-user permissions. Do not ship a remote MCP server that lets any caller run any tool — that is the production equivalent of leaving the database write user in your config.
Observability. At scale, log every tool call with the tool name, arguments (redacting secrets), latency, and outcome. This is the only way to answer "why is the agent slow today?" when the server is shared across many sessions.
Versioning. Pin your server and SDK versions explicitly. MCP is still maturing, and a transitive dependency bump can quietly change protocol behavior. Treat the @modelcontextprotocol/sdk version the same way you treat a database driver version — upgrade on purpose, not by accident.
Frequently asked questions
What is the MCP Inspector and when should I use it?
The MCP Inspector is an official browser UI that speaks the MCP protocol directly to a server, with no host in between. Use it as your first debugging step: launch it against your server command and you can list tools, call them with typed arguments, and watch the raw JSON-RPC traffic. If a tool works in the Inspector but fails in a host, the problem is in the host config or environment, not your server.
Why do my MCP tools appear but return nothing useful?
The server connected and registered its tools, but the tool handlers themselves are failing. Log the incoming arguments at the top of each handler to confirm the shape your code assumes matches what the model sends, and return errors as structured MCP content with isError: true rather than throwing. An unhandled exception usually surfaces to the model as a generic failure with no detail.
Where does Claude Desktop write MCP server logs?
On macOS, Claude Desktop writes one log file per server at ~/Library/Logs/Claude/mcp-server-<name>.log, containing everything the server writes to stdout and stderr interleaved with the host's protocol messages. On Windows the equivalent lives under %USERPROFILE%\AppData\Roaming\Claude\logs\. Tail the file while reproducing the bug.
Can I use console.log to debug a stdio MCP server?
No — over the stdio transport, stdout is the JSON-RPC protocol channel. Anything you write to console.log corrupts the protocol stream and causes intermittent malformed-message errors. Send every diagnostic to console.error (stderr) instead, which is the designated log channel. This constraint does not apply to HTTP-based servers.
Official references
- MCP Inspector (modelcontextprotocol/inspector)
- MCP TypeScript SDK
- MCP specification
- Claude Code MCP documentation
- MCP debugging guide
This article reflects publicly available information as of July 2026; relevant APIs may evolve.