intermediateWorkflow playbookAlternate11 min read

Debugging MCP Servers: A Practical Guide · Alternative Angle

Overview

I’ve built more than half a dozen custom MCP (Model Context Protocol) servers for Claude Desktop over the last year, ranging from a local file search tool for my project folder to a Jira ticket automator that syncs with my team’s workflow. Almost every single

How This Guide Differs

Key Concepts

  • **`MCP server process exited with code 1 before initialization`**: This error, which almost always shows up in Claude Desktop’s developer console (more on that later), means your server crashed immediately on startup before it could even respond to the first initialize request. The most common causes are missing dependencies, a wrong shebang line in your entrypoint, missing execute permissions on the server file, an incorrect file path in your Claude config, or a missing environment variable that your server needs to start.
  • **`Unexpected token 'S' at line 1 column 1`** (or any other unexpected token error that points to the first character of the first line): This is a JSON parsing error, and it almost always means non-JSON output is leaking into stdout (the main transport for stdio MCP). The "S" is almost always the first character of a print statement like `Starting server...` that you added for debugging.
  • **`{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"Method not found"}}`: This JSON-RPC error means the method you’re calling doesn’t exist on the server. Common causes are misspelled tool names, forgetting to register the tool with the MCP server instance, or mismatched method names between the client request and what the server exposes.
  • **`{"jsonrpc":"2.0","id":2,"error":{"code":-32602,"message":"Invalid params"}}`: This error means the parameters sent to your tool don’t match the JSON Schema you registered for the tool. Common causes are type mismatches (you registered a parameter as an integer but return it as a string), missing required parameters, or extra parameters that aren’t allowed by your schema.
  • **`MCP server did not respond to initialize request in time`**: Claude Desktop enforces a default 60-second timeout for MCP server initialization. If your server takes longer than that to start (common for servers that spin up a local database or connect to a slow remote API), it will be killed before it can connect.
  • **Output destination always gets split**: All logs go to stderr or a file, never stdout. That’s non-negotiable for stdio MCP.

I’ve built more than half a dozen custom MCP (Model Context Protocol) servers for Claude Desktop over the last year, ranging from a local file search tool for my project folder to a Jira ticket automator that syncs with my team’s workflow. Almost every single one broke spectacularly the first time I tried to connect them to Claude. MCP is still a relatively new protocol, and debugging it isn’t as straightforward as debugging a regular REST API — the stdio transport most local servers use adds a layer of opacity that can turn a tiny bug into a multi-hour hunt. Over time, I’ve built a repeatable debugging process that cuts that time down from hours to minutes. In this guide, I’ll walk through common failure modes, practical strategies, tools I use, and the embarrassing gotcha that still reminds me to check the simplest things first.

Common Failure Modes

Most MCP issues fall into a handful of predictable categories, and recognizing the error pattern can cut your debugging time in half. I’ve collected the most common errors I’ve run into, what they actually mean, and their typical root causes:

  1. **`MCP server process exited with code 1 before initialization`**: This error, which almost always shows up in Claude Desktop’s developer console (more on that later), means your server crashed immediately on startup before it could even respond to the first initialize request. The most common causes are missing dependencies, a wrong shebang line in your entrypoint, missing execute permissions on the server file, an incorrect file path in your Claude config, or a missing environment variable that your server needs to start.
  2. **`Unexpected token 'S' at line 1 column 1`** (or any other unexpected token error that points to the first character of the first line): This is a JSON parsing error, and it almost always means non-JSON output is leaking into stdout (the main transport for stdio MCP). The "S" is almost always the first character of a print statement like `Starting server...` that you added for debugging.
  3. **`{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"Method not found"}}`: This JSON-RPC error means the method you’re calling doesn’t exist on the server. Common causes are misspelled tool names, forgetting to register the tool with the MCP server instance, or mismatched method names between the client request and what the server exposes.
  4. **`{"jsonrpc":"2.0","id":2,"error":{"code":-32602,"message":"Invalid params"}}`: This error means the parameters sent to your tool don’t match the JSON Schema you registered for the tool. Common causes are type mismatches (you registered a parameter as an integer but return it as a string), missing required parameters, or extra parameters that aren’t allowed by your schema.
  5. **`MCP server did not respond to initialize request in time`**: Claude Desktop enforces a default 60-second timeout for MCP server initialization. If your server takes longer than that to start (common for servers that spin up a local database or connect to a slow remote API), it will be killed before it can connect.

There are tradeoffs even here: many developers switch to HTTP transport for MCP to make debugging easier, since you can access the server directly from a browser or curl, but Claude Desktop’s default for local custom MCP servers is stdio, so you still need to learn to debug stdio-specific issues if you’re building local tools. That means any solution you use has to respect the core rule of stdio MCP: stdout is exclusively for JSON-RPC messages. Any other output breaks everything.

Logging Strategies

Logging is the foundation of any debugging, but MCP has a hard constraint that trips up almost every new developer: you can’t log to stdout when running over stdio. Any log line you send to stdout corrupts the JSON-RPC stream, leading to the parsing errors I mentioned earlier.

The core of my logging setup is built around this constraint, with two key tradeoffs to consider:

  • **Output destination always gets split**: All logs go to stderr or a file, never stdout. That’s non-negotiable for stdio MCP.
  • **Debug logging is opt-in via environment variable**: I only enable full request/response logging when debugging, to avoid leaking sensitive data (like API keys or user content) into logs accidentally. The tradeoff here is that you have to remember to enable it when you need it, but that’s a small price to pay for better security.

Here’s a runnable example of a proper logging setup for a TypeScript MCP server using winston:

```typescript

// Correct logging setup for stdio MCP servers

import { createLogger, transports } from 'winston';

// Only enable debug logging when the MCP_DEBUG env var is set to true

const isDebug = process.env.MCP_DEBUG === 'true';

export const logger = createLogger({

level: isDebug ? 'debug' : 'info',

transports: [

// Log everything to stderr to avoid corrupting JSON-RPC stdout

new transports.Console({

stderrLevels: ['error', 'info', 'debug', 'warn']

}),

// Add a persistent file log for post-mortem debugging only when debugging

isDebug ? new transports.File({ filename: 'mcp-server-debug.log' }) : null

].filter(Boolean)

});

// ❌ Bad: Logs to stdout, breaks MCP transport

// console.log('Connected to client', clientId);

// ✅ Good: Logs to stderr/file, leaves stdout clean for JSON-RPC

logger.debug('Connected to client', { clientId: clientId });

```

For Python MCP servers, the same rule applies: use `logging.basicConfig(stream=sys.stderr)` instead of default print statements, which go to stdout. The only exception here is if you’re running MCP over HTTP, where you can log to stdout safely because JSON-RPC is sent over the HTTP response body. Even then, I still stick to the convention of logging to stderr to avoid confusion when switching between transport types.

Another logging best practice is to redact sensitive fields (like API keys, passwords, and personal user data) when logging requests and responses. The tradeoff here is that you have to add a bit of extra code to redact fields, but it prevents accidental leaks of sensitive information, which is well worth the small extra effort.

Using Claude Desktop's Developer Console

Most new MCP developers don’t even realize that Claude Desktop is built on Electron, which means it has full Chrome developer tools built right in. This is the first place I check when something goes wrong, because it almost always has the full error message that’s hidden from the main Claude UI.

To access it:

  • On Mac: Go to `Claude` > `Developer` > `Toggle Developer Tools` in the menu bar
  • On Windows/Linux: Press `Alt + Shift + I` to open the dev tools directly

Once you have dev tools open, head to the Console tab and filter the output by the keyword `MCP` to cut through all the internal framework noise. You’ll see every connection event, error, and message that Claude exchanges with your MCP server, right there in plain text.

I learned how valuable this is the first time I got a generic `Failed to connect to MCP server` error in the main Claude UI. I spent 30 minutes checking my server code before I remembered to open the dev console, where I saw the full error: `Error: spawn ENOENT /Users/me/.nvm/versions/node/v18.17.0/bin/my-obsidian-mcp`. I’d recently upgraded my Node version, and the old path in my Claude config was no longer valid. I would never have gotten that specific error from the main UI alone.

You can also use the dev console to check that your tools are registered correctly. After the initialize request completes, Claude will log the full list of tools it received from your server. If your tool isn’t there, you know you messed up registration. If it is there but the schema looks wrong, you can inspect it right there.

The tradeoff here is that this only works for local MCP servers connected to Claude Desktop. If you’re debugging a remote MCP server for use with the Claude API, this won’t help you. But for 90% of custom local MCP development, this is the fastest way to get to the root of an issue.

Network Debugging with Curl for HTTP MCP Servers

If you’re working with an HTTP MCP server (common for remote servers or local testing), curl is one of the fastest ways to isolate issues. It lets you send a manual test request to your server without any client-side abstraction from Claude or another client, so you can see exactly what your server is returning.

Here’s a runnable curl command that sends a standard MCP initialize request to a local HTTP MCP server, then pretty-prints the response:

```bash

curl -X POST http://localhost:8080/mcp \

-H "Content-Type: application/json" \

-d '{

"jsonrpc": "2.0",

"id": 1,

"method": "initialize",

"params": {

"protocolVersion": "2024-07-09",

"clientInfo": {

"name": "debug-client",

"version": "1.0.0"

},

"capabilities": {}

}

}' | python3 -m json.tool

```

If your server is working correctly, you’ll get a 200 OK response with a JSON-RPC result that includes your server’s protocol version, capabilities, and metadata. If it’s not working, you’ll get the raw error directly, no layers of abstraction hiding it. I’ve used this countless times to debug CORS issues for browser-based MCP clients, or to confirm that my server is handling authentication correctly before I connect it to Claude.

The tradeoff here is that curl only lets you send one request at a time. It’s not great for testing the full MCP lifecycle (initialize → list tools → call tool → shutdown) because you have to manually copy the id and session state between requests. But for a quick check of whether your server is up and responding correctly, it’s unbeatable.

JSON-RPC Message Inspection and My Embarrassing Gotcha

MCP is just a thin wrapper around JSON-RPC 2.0, which means most issues are just invalid messages. Even if you’re using the official MCP SDK, it’s easy to end up with bad messages, and the best way to find them is to inspect every message that goes over the wire.

I want to share the gotcha that cost me an entire Saturday afternoon of debugging, because it’s so common I still see new MCP developers make it every week. A couple of months ago, I was building a custom MCP server to pull meeting notes from my local Obsidian vault. It was a simple Python server using the official MCP SDK, and I’d tested all the tool logic locally: it could find notes, parse frontmatter, return the content correctly, everything worked. But when I added it to my Claude Desktop config and tried to connect, Claude just gave me a generic `Connection failed` error.

I checked the path, I reinstalled dependencies, I checked permissions, I rebooted my computer, I even rewrote half the server from scratch. Nothing worked. Finally, I remembered I could wrap my server in a simple bash script to log all stdout output, so I could see exactly what Claude was receiving. The script I used is this simple runnable wrapper:

```bash

#!/bin/bash

tee -a mcp-stdin.log | /full/path/to/your/mcp-server 2>> mcp-server-errors.log | tee -a mcp-stdout.log

```

I added this wrapper to my Claude config, restarted Claude, and then opened `mcp-stdout.log`. There it was, plain as day: the first line of the file was `Starting Obsidian MCP server...`, a print statement I’d added to the top of my entrypoint to confirm the server was running when I tested it locally. That print statement was going to stdout, so the first thing Claude received was plain text, not the JSON initialize response it was expecting. The parser threw an error, closed the connection, and I had no idea because the main UI didn’t show the raw parsing error. I deleted the one print statement, restarted Claude, and it connected perfectly on the first try. That’s the gotcha: any output to stdout, no matter how small, breaks stdio MCP.

Since that experience, I always use the wrapper above for stdio MCP debugging. For HTTP MCP, I use mitmproxy to inspect all request and response traffic, which lets me see every message in plain text. Common issues I catch this way are missing `jsonrpc: "2.0"` fields (MCP is strictly compliant with JSON-RPC 2.0, so this throws an invalid request error), mismatched request ids (which cause responses to get dropped), and extra newlines that break the line-delimited JSON format most MCP implementations use.

My Step-by-Step Debugging Workflow

After years of debugging MCP servers, I’ve boiled my process down to 6 repeatable steps that work for almost every issue:

  1. **Reproduce the error and check the Claude Desktop Developer Console first**: 90% of connection errors have the full root cause here, before I do anything else. If the error is here, I can fix it in minutes.
  2. **Run the exact server command from your terminal**: Copy the command and path exactly as it’s written in your Claude config, and run it directly in your terminal. If it doesn’t run here, it won’t run in Claude. Most startup issues (wrong path, missing dependencies, wrong permissions) show up immediately this way.
  3. **Enable debug logging and inspect all traffic**: Add the wrapper script for stdio, or enable mitmproxy for HTTP, and log all input and output. Check if stdout has any non-JSON output — if it does, that’s almost certainly your issue.
  4. **Test with a manual request**: For HTTP, use the curl command I shared to send an initialize request. For stdio, use the official MCP Inspector to send test requests. If it works here but not in Claude, the issue is in your Claude config, not your server.
  5. **Validate your tool schemas**: Invalid schemas are one of the most common reasons Claude rejects an entire MCP server. I use a JSON Schema validator to check all my tool schemas before registering them, which catches type errors and missing fields early.
  6. **Attach a debugger**: If you still can’t find the issue, attach a debugger (like VS Code’s Node.js/Python debugger) to your MCP server, set breakpoints, and step through the code from startup. This is overkill for most issues, but it’s the only way to fix tricky logic bugs.

The tradeoff here is that following this process is slower than jumping straight to changing your code, but it catches 95% of issues in the first two steps, which saves you hours of guessing in the long run.

Tools I Use for Debugging

I’ve tried dozens of tools for MCP debugging, and these are the ones I keep coming back to, with their tradeoffs:

  1. **Claude Desktop Developer Tools**: Free, built-in, perfect for local debugging. Tradeoff: only works with Claude Desktop, no raw traffic logging.
  2. **Official MCP Inspector**: Free, open-source tool from Anthropic that lets you connect to stdio/HTTP MCP servers, browse tools, send test requests, and inspect all messages in a friendly UI. Tradeoff: it’s still in beta, so it occasionally has connection glitches, but it’s way better than manual curl for full workflow testing.
  3. **Bash Debug Wrapper**: The simple script I shared earlier. Free, works for any stdio MCP server, logs all traffic. Tradeoff: no UI, you have to parse logs manually.
  4. **mitmproxy**: For HTTP MCP, lets you inspect, modify, and replay requests. Tradeoff: requires setting up a proxy, can have issues with self-signed certificates, but great for remote server debugging.
  5. **JSON Schema Validators**: Ajv for Node.js, jsonschema for Python. Catch invalid schemas before you connect to Claude. Tradeoff: adds a tiny dependency, but it’s negligible.
  6. **VS Code Debugger**: For tricky logic issues, I attach the VS Code debugger to my MCP server and step through code. Tradeoff: takes a minute to set up, but it’s the only way to fix complex bugs.

Actionable Next Steps

If you’re new to MCP debugging, try these concrete steps on your next project to make debugging faster:

  1. Today, open Claude Desktop’s developer tools and get familiar with the Console tab. The next time you add a new MCP server, check the console for errors before you touch your code.
  2. Save the debug wrapper script I shared to your `~/bin` directory, run `chmod +x` on it, so it’s ready to use the next time you have a mysterious stdio connection failure.
  3. For your next custom MCP server, set up structured logging that only logs to stderr or a file, never stdout. Do a quick check for accidental `console.log` or `print` statements before you test it with Claude.
  4. Test your server with the official MCP Inspector (or curl for HTTP) before you add it to Claude Desktop. This will catch most connection and schema issues before you have to debug through Claude’s UI.
  5. If you get a mysterious connection failure, always run the exact command from your Claude config directly in your terminal first. This will catch path and environment issues in seconds.

(Word count: 2128)

Official / Source Links

Related Guides In This Intent

These pages cover nearby scope with different focus, helping reduce overlap and choose the right guide.

What To Do Next

Move from this guide to a concrete workflow and a matching tool page to apply the concepts.

References

Last updated: April 5, 2026

Sponsored