10 Common MCP Server Bugs and How to Fix Them

Every developer who builds an MCP server hits the same handful of bugs. Most of them are not really bugs in your code — they are quirks of how Model Context Protocol works under the hood, or interactions with the host (Claude Desktop, Cursor, etc.) that the documentation does not call out.

This article is a triage manual: ten of the most common MCP server problems, what causes each, and the exact fix. Bookmark it and save yourself a few hours.

1. console.log crashes the server

Symptom: Your server connects, then immediately disconnects with no error. Or the host shows it as "failed." In the log file you might see SyntaxError: Unexpected token in JSON.

Cause: In stdio mode, stdout is the protocol channel. Anything you print to stdout gets interpreted as malformed JSON-RPC and breaks the connection.

Fix: Replace every console.log with console.error. Stderr is captured by the host's log file but is not part of the protocol stream.

// ❌ Breaks the server
console.log('Fetching user', userId);

// ✅ Safe
console.error('Fetching user', userId);

If you have a logging library, configure it to write to stderr. Pino: pino({}, pino.destination(2)). Winston: new winston.transports.Console({ stderrLevels: ['info', 'debug', 'warn', 'error'] }).

2. Relative paths in Claude Desktop config silently fail

Symptom: Your server works when launched from a terminal but does not start from Claude Desktop. Logs show Error: Cannot find module.

Cause: Claude Desktop spawns the subprocess from its own working directory, not the directory containing your config file. Relative paths resolve from Claude's working dir.

Fix: Use absolute paths in args:

{
  "mcpServers": {
    "my-server": {
      "command": "node",
      "args": ["/Users/you/code/my-server/dist/server.js"]
    }
  }
}

Cursor handles relative paths correctly for project-scoped configs, but Claude Desktop does not. When in doubt, go absolute.

3. Tools list is empty after the server connects

Symptom: The server's status indicator goes green, but no tools appear in the host's UI.

Cause: Almost always, server.connect(transport) is called before any server.tool(...) registrations. The tools must be registered first.

Fix: Register all tools, resources, and prompts before connecting the transport:

const server = new McpServer({ name: 'my-server', version: '1.0.0' });

// ✅ Register first
server.tool('first_tool', '...', {...}, async () => {...});
server.tool('second_tool', '...', {...}, async () => {...});

// ✅ Then connect last
await server.connect(new StdioServerTransport());

4. The LLM never calls a tool that should be obvious

Symptom: You built a tool for fetching the current weather. The user asks "is it raining in Mumbai?" and the LLM ignores your tool entirely.

Cause: The tool description is too vague. The LLM picks tools based on description, not name.

Fix: Write descriptions that include what the tool does, when to use it, and an example query if helpful:

// ❌ Too vague
server.tool('weather', 'weather utility', { city: z.string() }, ...);

// ✅ Tells the LLM exactly when to use it
server.tool(
  'get_weather',
  'Get current weather conditions for a specific city. Use this whenever the user asks about temperature, rain, snow, or general weather in any location.',
  { city: z.string().describe('City name like "Mumbai" or "London"') },
  ...
);

Good descriptions are easily 50% of what makes a tool reliable.

5. command not found: npx from inside the host

Symptom: Your npx-based MCP server works in the terminal but crashes when launched from Claude Desktop with Error: spawn npx ENOENT.

Cause: macOS GUI apps inherit a stripped-down PATH that does not include Homebrew (/opt/homebrew/bin) or nvm directories. The npx binary is on your shell's PATH, not on Claude Desktop's PATH.

Fix: Use absolute paths. Run which npx in your terminal to find the right one:

{
  "mcpServers": {
    "my-server": {
      "command": "/opt/homebrew/bin/npx",
      "args": ["-y", "my-server-package"]
    }
  }
}

On Windows the equivalent issue is rarer because Node installers typically add themselves to the system-wide PATH.

6. Tool arguments arrive as undefined

Symptom: Inside your tool handler, the destructured arguments are undefined even though the LLM clearly passed something.

Cause: The Zod schema passed to server.tool(...) must be a raw shape object at the top level, not a z.object(...) wrapper. The SDK auto-wraps it.

Fix:

// ❌ Double-wrapped — fields arrive as undefined
server.tool('echo', '...', z.object({ message: z.string() }), async ({ message }) => ...);

// ✅ Plain object shape
server.tool('echo', '...', { message: z.string() }, async ({ message }) => ...);

This bites people coming from raw Zod usage where z.object(...) is standard.

7. The server runs but crashes on the second tool call

Symptom: First invocation works. Second invocation kills the subprocess. Or you see Error: connection closed.

Cause: An uncaught exception in your tool handler propagates up to the protocol layer and tears down the transport.

Fix: Catch errors inside handlers and return them as MCP error responses:

server.tool('risky_op', '...', { id: z.number() }, async ({ id }) => {
  try {
    const data = await riskyOperation(id);
    return { content: [{ type: 'text', text: JSON.stringify(data) }] };
  } catch (err) {
    return {
      content: [{ type: 'text', text: `Error: ${err.message}` }],
      isError: true,
    };
  }
});

Returning isError: true is recoverable context for the LLM — throwing breaks the protocol.

8. Env vars set in config are not visible inside the server

Symptom: Your config has "env": { "API_KEY": "..." } but process.env.API_KEY is undefined inside the server.

Cause: Typo in the key name, or the env block is at the wrong nesting level in JSON.

Fix: Verify the JSON shape. env is a sibling of command and args, not nested inside args:

{
  "mcpServers": {
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_..." }
    }
  }
}

Also, double-check the variable name your server actually reads. The official GitHub MCP server expects GITHUB_PERSONAL_ACCESS_TOKEN, not GITHUB_TOKEN. Reading the wrong key name silently yields undefined.

9. Connection drops after about 30 seconds idle

Symptom: First few requests work. After a quiet minute, the next request hangs or errors with connection closed.

Cause: Some HTTP-based transports (or upstream proxies) close idle connections aggressively. Less common with stdio, more common when you deploy a remote MCP server behind a load balancer.

Fix: For Streamable HTTP, send periodic keep-alive pings, or set the upstream proxy's idle-connection timeout higher. For stdio, this almost never happens — if it does, the issue is usually that the host is killing your subprocess for being unresponsive.

10. Server works in Claude Desktop but not in Cursor (or vice versa)

Symptom: Identical config, but the server is green in one host and dead in the other.

Cause: Slight differences in how each host launches subprocesses — PATH, working directory, shell vs no-shell, env propagation rules.

Fix: Always use absolute paths for command. Set env explicitly rather than relying on shell-inherited variables. Avoid shell-builtin commands like && or pipes in args. If you need shell semantics, write a shell wrapper script and point command at that.

When debugging cross-host issues, run the exact command and args from a fresh terminal with the same env vars to confirm the server works in isolation, then narrow down what differs in the host environment.

Bonus: how to actually debug

For any of the above, three tools save you the most time:

  1. MCP Inspectornpx @modelcontextprotocol/inspector node server.js. Browser UI that connects to your server and shows every JSON-RPC message. Invaluable for protocol-level bugs.
  2. Host log files — Claude Desktop: Settings → Developer → Open Logs. Cursor: click the server entry in MCP & Integrations. Both surface your server's stderr.
  3. stderr for everything — log liberally to stderr while developing. Once stable, switch to a real logger that writes to stderr.

A checklist before declaring "it's broken"

Before reaching for the docs:

  • Did you fully quit and relaunch the host? (Claude Desktop especially)
  • Is your config valid JSON? (Paste into a validator)
  • Are all paths absolute?
  • Have you actually checked the host's log file?
  • Does the server work standalone via MCP Inspector?
  • Are all your console.log calls now console.error?
  • Are tool/resource registrations before server.connect()?
  • Do tool descriptions tell the LLM when to use the tool, not just what it does?

Nine times out of ten, walking through that list finds the problem in under five minutes.

Conclusion

Most MCP server bugs are not protocol bugs — they are environment bugs (PATH, working directory, env vars) or tiny SDK conventions (stderr-only logging, raw Zod shapes, registration order). Once you have hit each of these once, you stop hitting them.

Keep this list bookmarked. The next time something is mysteriously broken, walk through the ten items above before you start questioning the protocol or your sanity.

Try it yourself

Here is what a debugging session looks like with MCP Inspector connected — the inspector surfaces the exact problem so you do not have to guess:

YouMy MCP server connects but Claude Desktop shows no tools. What is wrong?
Claude · used read_file on your server.jsI see the issue. In server.js line 28, you call server.connect(transport) before the server.tool(...) registrations on lines 31–47. The connection handshake completes, but no tools are registered yet — so the host’s tools/list call returns an empty array.

Move the server.connect() call to the bottom of the file, after all tool registrations. That should fix it.

The LLM read your code, spotted bug #3 from this list, and pointed at the exact line — because the filesystem MCP server gave it the access. That is the meta-level value of having MCP set up correctly: even debugging MCP becomes faster with MCP.

Leave a Comment

Your email address will not be published. Required fields are marked *