← Blog

August 6, 2026 · 8 min read

How to Debug an MCP Server: MCP Inspector, Logs, and Common Fixes

MCP servers fail in ways that are harder to diagnose than normal HTTP APIs. When Claude Desktop says "tool failed" or Cursor stops showing your tools, there's no obvious stack trace. The error is somewhere between the client, the transport layer, the protocol negotiation, and your tool handler — and the client usually doesn't tell you which.

This guide covers the diagnostic tools and common failure patterns that cover the majority of MCP server issues.

Start with MCP Inspector

MCP Inspector is the official debugging tool for MCP servers. It's a local web UI that connects to your server, lets you call any tool manually, and shows the raw JSON-RPC messages going back and forth. If you haven't used it, start here before anything else.

# For a stdio server (local process)
npx @modelcontextprotocol/inspector node dist/server.js

# For a remote HTTP server
npx @modelcontextprotocol/inspector --url https://your-server.com/api/mcp

# For a server with environment variables
npx @modelcontextprotocol/inspector \
  --env API_KEY=your_key \
  node dist/server.js

Inspector opens at http://localhost:5173. The left panel shows your server's tools and resources. The right panel shows the raw protocol messages. When something is wrong, the error usually shows up in one of three places:

  • Server list tab — if Inspector can't connect at all, the server isn't starting or the transport is wrong
  • Tools list — if tools are missing or have wrong schemas, the registration code has an issue
  • Tool call response — if the tool appears but returns an error, the handler itself is failing

Enable protocol-level logging

The MCP SDK has a built-in debug mode that logs every JSON-RPC message. Enable it by setting the MCP_DEBUG environment variable before running your server.

MCP_DEBUG=1 node dist/server.js

For a stdio server, the debug output goes to stderr (not stdout, which is reserved for protocol messages). You'll see every message the client sends and every response your server returns. This is usually enough to identify where in the protocol the failure is happening.

For an HTTP server, add request/response logging to your route handler:

export async function POST(req: Request) {
  const body = await req.json()
  console.log('[MCP IN]', JSON.stringify(body))

  const result = await server.handleRequest(body)
  console.log('[MCP OUT]', JSON.stringify(result))

  return Response.json(result)
}

The 5 most common MCP server failures

1. Server not appearing in the client's tool list

Symptom: Claude Desktop or Cursor doesn't show the tools at all. The server appears to be connected but nothing is listed.

Cause: Most often a startup crash before the server registers its tools. The MCP client connects, the server process starts, crashes, and the client silently gives up.

Fix: Check the client's MCP log file. In Claude Desktop, it's at ~/Library/Logs/Claude/mcp-server-*.log. You'll see the server's stderr output — startup errors show up here. The most common culprits are missing environment variables and module import failures.

# Check Claude Desktop MCP logs
tail -f ~/Library/Logs/Claude/mcp-server-your-server-name.log

# Common startup errors:
# "Cannot find module '@modelcontextprotocol/sdk'"  → npm install
# "Error: API_KEY is not defined"                   → check env config
# "SyntaxError: ..."                                → TypeScript not compiled

2. Tools registered but never called correctly

Symptom: The agent can see your tools but calls them with wrong parameters, skips them entirely, or calls a different tool when yours would be more appropriate.

Cause: Tool descriptions are unclear or input schemas are too permissive. The agent is guessing based on the description.

Fix: Rewrite your tool descriptions to be more explicit about when to use the tool and what each parameter means. Use .describe() on every schema field.

// Before: vague schema
{ query: z.string() }

// After: explicit schema with descriptions
{
  query: z.string().describe(
    'The search query. Use natural language — e.g. "authentication setup" not "auth".'
  ),
  limit: z.number().int().min(1).max(20).optional().describe(
    'Max results to return. Defaults to 5. Increase only if the first results are insufficient.'
  ),
}

3. Tool call returns error with no useful message

Symptom: The tool is called, the agent reports an error, but the error message is generic: "tool failed" or "an error occurred".

Cause: Your handler is throwing an exception and the SDK is catching it before it reaches the client with any context.

Fix: Return structured errors from your handlers instead of throwing. The MCP SDK converts thrown exceptions into generic error responses. Returning an error content block preserves your message.

// ❌ Throws — agent sees "tool failed" with no context
server.tool('fetchData', '...', schema, async ({ id }) => {
  const data = await db.find(id)  // throws if not found
  return { content: [{ type: 'text', text: JSON.stringify(data) }] }
})

// ✅ Returns structured error — agent sees what went wrong
server.tool('fetchData', '...', schema, async ({ id }) => {
  try {
    const data = await db.find(id)
    return { content: [{ type: 'text', text: JSON.stringify(data) }] }
  } catch (err) {
    const message = err instanceof Error ? err.message : 'Unknown error'
    return {
      content: [{ type: 'text', text: `Failed to fetch id=${id}: ${message}` }],
      isError: true,
    }
  }
})

4. CORS errors on remote HTTP servers

Symptom: Works locally, fails when deployed. Browser-based clients (Claude.ai, WebMCP) get CORS errors. Curl to the same endpoint works fine.

Cause: The server returns correct MCP responses but doesn't include CORS headers. Browsers block the response before the client can read it.

Fix: Add CORS headers to every response and handle OPTIONS preflight requests. The 2026 spec adds Mcp-Method and Mcp-Name headers — include them in your allowed headers list.

const CORS_HEADERS = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Methods': 'POST, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Authorization, Mcp-Method, Mcp-Name',
}

export async function OPTIONS() {
  return new Response(null, { status: 204, headers: CORS_HEADERS })
}

export async function POST(req: Request) {
  const body = await req.json()
  const result = await server.handleRequest(body)
  return Response.json(result, { headers: CORS_HEADERS })
}

5. Timeout on long-running tools

Symptom: Tools that take more than 30-60 seconds fail with a timeout error. This is common for tools that crawl sites, call slow external APIs, or run analysis jobs.

Cause: Serverless platforms (Vercel, Cloudflare Workers) have function timeout limits. The MCP client also has its own timeout. If either fires first, the call fails.

Fix options: Move long-running tools off serverless to a persistent server with a longer timeout (Railway, Fly.io). Or use the MCP Tasks extension (2026-07-28 spec) for async tool calls with polling. Or split the operation: a start_job tool returns a job ID immediately, and a get_job_result tool polls for completion.

// Split pattern for long-running operations
server.tool('startCrawl', 'Begin crawling a site. Returns a jobId.',
  { url: z.string() },
  async ({ url }) => {
    const jobId = await queue.enqueue({ type: 'crawl', url })
    return { content: [{ type: 'text', text: JSON.stringify({ jobId }) }] }
  }
)

server.tool('getCrawlResult', 'Check the status of a crawl job by jobId.',
  { jobId: z.string() },
  async ({ jobId }) => {
    const job = await queue.getStatus(jobId)
    return { content: [{ type: 'text', text: JSON.stringify(job) }] }
  }
)

Validating protocol compliance with a test suite

Beyond ad-hoc debugging, it's worth running a basic protocol compliance check before deploying. These four tests cover the most common issues:

# 1. server/discover should return tools list inline
curl -s -X POST https://your-server.com/api/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"server/discover"}' | jq '.result.tools | length'

# 2. tools/list should also work (backward compat)
curl -s -X POST https://your-server.com/api/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq '.result.tools[].name'

# 3. CORS preflight should return 2xx
curl -s -o /dev/null -w "%{http_code}" -X OPTIONS https://your-server.com/api/mcp \
  -H "Origin: https://claude.ai" \
  -H "Access-Control-Request-Method: POST"

# 4. Unknown method should return JSON-RPC error, not 404 or 500
curl -s -X POST https://your-server.com/api/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"nonexistent/method"}' | jq '.error.code'
  # Should be -32601 (Method not found), not null

When the agent gives wrong answers despite correct tool calls

Sometimes the MCP plumbing is fine — the tools connect, calls succeed, responses return — but the agent still gives wrong answers. This is a different category of problem: the agent is getting the right data but synthesizing it incorrectly, or the data itself is incomplete.

The most common cause is tool responses that return too much data in an unstructured format. If your tool returns 20,000 characters of raw HTML or a deeply nested JSON object, the agent has to parse it in context — and often gets it wrong.

Structure your tool responses for agents: return flat JSON with clearly named fields, trim to the most relevant subset of the data, and include a brief explanation of what the response means when it might be ambiguous.

If your tools are working correctly but agents still give wrong answers about your product or documentation, the issue is usually the knowledge layer — the agent doesn't have enough context about your domain to interpret the tool responses. Indexed documentation solves this: the agent can retrieve context before calling tools, or look up clarifications mid-workflow.

AgentReady's MCP endpoint is a reference implementation of a production-ready stateless MCP server. Connect it to Claude or Cursor to query any indexed site — no debugging required.

Connect AgentReady →