🤖 AI Tools
· 6 min read
Last updated on

How to Debug MCP Servers — Common Issues and Fixes


MCP servers can fail silently. Here’s how to debug the most common issues, set up proper logging, and use the available debugging tools.

Problem 1: Server not connecting

Symptom: Host doesn’t show your tools. No error message visible.

Fix: Check the server command works standalone:

# Test that the server starts without errors
node /path/to/server.js

# Check for missing dependencies
cd /path/to/server && npm ls

# Verify Node version matches what the server expects
node --version

Common causes:

  • Wrong path in your MCP config (use absolute paths)
  • Missing node_modules — run npm install in the server directory
  • Node version mismatch — some servers require Node 18+
  • Permission issues — the server binary isn’t executable
  • The command field in config points to the wrong binary

Verifying your config

Check your MCP host configuration file. For Claude Desktop on macOS:

// ~/Library/Application Support/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "my-server": {
      "command": "node",
      "args": ["/absolute/path/to/server.js"],
      "env": {
        "API_KEY": "your-key"
      }
    }
  }
}

The most common mistake is using relative paths. Always use absolute paths in MCP configurations.

Problem 2: Tools not appearing

Symptom: Server connects (no error) but tools don’t show up in the host.

Fix: Verify tools are registered before server.connect():

// Tools MUST be registered before connecting
server.tool('my_tool', { input: z.string() }, handler);
server.tool('another_tool', { query: z.string() }, handler2);

// Connect AFTER all tools are registered
await server.connect(transport);

Other causes:

  • Tool name conflicts with built-in tools
  • Schema validation errors in your tool definitions (check Zod schemas)
  • The server is connecting but crashing immediately after — check stderr output

Testing tool registration

Add a startup log to confirm tools are registered:

const tools = server.getRegisteredTools();
console.error(`Server started with ${tools.length} tools: ${tools.map(t => t.name).join(', ')}`);
await server.connect(transport);

Problem 3: Tool calls failing

Symptom: Tool appears but returns errors when called.

Fix: Add comprehensive logging to your handler:

server.tool('my_tool', { query: z.string() }, async (params) => {
  console.error(`[${new Date().toISOString()}] Tool called with:`, JSON.stringify(params));
  try {
    const result = await doWork(params);
    console.error(`[${new Date().toISOString()}] Success:`, result.substring(0, 100));
    return { content: [{ type: 'text', text: result }] };
  } catch (e) {
    console.error(`[${new Date().toISOString()}] Error:`, e.message, e.stack);
    return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
  }
});

stderr output goes to the host’s logs — check the terminal for Claude Code or the output panel in Cursor.

Problem 4: Environment variables missing

Symptom: Auth failures, API errors, “undefined” values in requests.

Fix: Pass env vars explicitly in the host config:

{
  "mcpServers": {
    "my-server": {
      "command": "node",
      "args": ["server.js"],
      "env": {
        "API_KEY": "sk-...",
        "DB_URL": "postgresql://...",
        "NODE_ENV": "production"
      }
    }
  }
}

Important: MCP servers do NOT inherit your shell environment by default. Every variable the server needs must be explicitly listed in the config. This is a security feature but catches many developers off guard.

Debugging env vars inside the server

// Add at the top of your server to verify env vars are present
const required = ['API_KEY', 'DB_URL'];
for (const key of required) {
  if (!process.env[key]) {
    console.error(`FATAL: Missing required env var: ${key}`);
    process.exit(1);
  }
}
console.error('All required env vars present');

Problem 5: Timeout on long operations

Symptom: Tool call hangs then fails after 30-60 seconds.

Fix: Add timeouts and progress reporting:

server.tool('long_operation', { task: z.string() }, async (params, { reportProgress }) => {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 120000); // 2 min max

  try {
    await reportProgress({ progress: 0, total: 100 });
    const result = await doLongWork(params, {
      signal: controller.signal,
      onProgress: (pct) => reportProgress({ progress: pct, total: 100 }),
    });
    await reportProgress({ progress: 100, total: 100 });
    return { content: [{ type: 'text', text: result }] };
  } finally {
    clearTimeout(timeout);
  }
});

If your operation genuinely takes a long time, break it into smaller steps or return partial results.

Problem 6: Transport errors (SSE/HTTP)

Symptom: Connection drops, “stream ended unexpectedly”, CORS errors.

Fix for CORS:

app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-api-key');
  res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
  if (req.method === 'OPTIONS') return res.sendStatus(204);
  next();
});

Fix for dropped connections: Ensure your SSE endpoint sends keepalive pings:

// Send a comment every 30s to keep the connection alive
setInterval(() => {
  res.write(': keepalive\n\n');
}, 30000);

Debugging tools

MCP Inspector

The official debugging tool. Run it against your server to test tools interactively:

npx @modelcontextprotocol/inspector node /path/to/server.js

This opens a web UI where you can:

  • See all registered tools and their schemas
  • Call tools manually with custom inputs
  • View raw JSON-RPC messages
  • Check server capabilities

Logging to a file

For persistent debugging, log all MCP traffic to a file:

import { appendFileSync } from 'fs';

function log(direction: 'IN' | 'OUT', message: any) {
  const entry = `[${new Date().toISOString()}] ${direction}: ${JSON.stringify(message)}\n`;
  appendFileSync('/tmp/mcp-debug.log', entry);
  console.error(entry.trim());
}

// Wrap your transport to intercept messages
const originalSend = transport.send.bind(transport);
transport.send = (message) => {
  log('OUT', message);
  return originalSend(message);
};

Using Claude Code’s verbose mode

# See all MCP communication in real-time
claude --mcp-debug

This prints every JSON-RPC message exchanged between Claude Code and your MCP servers.

Debugging checklist

When something isn’t working, go through this list in order:

  1. Can the server start standalone? Run the command from your config directly in a terminal
  2. Are paths absolute? Relative paths are the #1 config mistake
  3. Are env vars set? MCP servers don’t inherit your shell environment
  4. Is the tool registered before connect? Order matters
  5. Does stderr show errors? Check your host’s log output
  6. Does MCP Inspector work? If yes, the problem is in the host config, not the server
  7. Is it a timeout? Add progress reporting or increase timeout limits
  8. Is it auth-related? See our MCP authentication guide

General tips

  1. Always log to stderr (stdout is reserved for MCP JSON-RPC protocol messages)
  2. Test your server with MCP Inspector before connecting to a host
  3. Start simple — one tool, verify it works, then add more
  4. Use TypeScript for better error messages during development
  5. Check our MCP Security Checklist if auth-related
  6. Read the raw JSON-RPC messages when something is unclear — they’re just JSON

FAQ

How do I debug MCP servers?

The best approach is layered: first, verify the server starts standalone by running its command directly in a terminal. Then use the MCP Inspector (npx @modelcontextprotocol/inspector node server.js) to test tools interactively in a web UI. Add console.error() logging throughout your handlers — stderr output appears in your host’s logs. For persistent debugging, log all JSON-RPC traffic to a file. If using Claude Code, the --mcp-debug flag shows all MCP communication in real-time.

Why is my MCP server not connecting?

The most common causes are: wrong path in your config (always use absolute paths), missing dependencies (run npm install in the server directory), Node version mismatch, or missing environment variables. MCP servers don’t inherit your shell environment — every variable must be explicitly listed in the config’s env field. Test by running the exact command from your config in a terminal. If it works there but not in the host, the issue is likely path resolution or env vars.

How do I log MCP requests?

Use console.error() for all logging — stdout is reserved for the JSON-RPC protocol. Log at the start of each tool handler with the received parameters, and log errors with full stack traces. For comprehensive traffic logging, wrap your transport’s send method to capture all outgoing messages, and log incoming messages in your handler. Write logs to /tmp/mcp-debug.log for persistent analysis. The MCP Inspector also shows all raw messages in its web UI.

Can I test MCP servers locally?

Yes, and you should always test locally before deploying. The MCP Inspector (npx @modelcontextprotocol/inspector) is the official tool — it connects to your server and provides a web interface to call tools, view schemas, and inspect raw messages. You can also test by running your server directly and piping JSON-RPC messages to stdin. For TypeScript servers, write unit tests for your tool handlers independently of the MCP transport layer.

Related: MCP Complete Guide · Build MCP Server (TypeScript) · Build MCP Server (Python)