How do I see errors from my MCP server in production?
Wrap each tool handler, report the exception with the tool name, and return an error result to the client. In a stdio server, never write to stdout: it carries the protocol, so stray output breaks the connection. Runheed's SDK sends errors over HTTPS and writes nothing to stdout unless you turn on debug mode.
Why MCP server errors go missing
- A tool that fails should return a result with isError set, so the model can see the failure and try something else. That is correct, but it means the exception never reaches a process-level handler or a crash log.
- The user sees "the tool failed" in Claude or Cursor. You see nothing unless the server reports it.
- A local stdio server runs on the user's machine, and its stderr goes to the client's log files (Claude Desktop writes them to ~/Library/Logs/Claude/ on macOS), not to you.
TypeScript (official MCP SDK)
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import runheed from 'runheed';
runheed.init({ dsn: process.env.RUNHEED_DSN!, release: 'orders-mcp@1.2.0' });
type ToolResult = { content: Array<{ type: 'text'; text: string }>; isError?: boolean };
function tracked<A>(tool: string, fn: (args: A) => Promise<ToolResult>) {
return async (args: A): Promise<ToolResult> => {
runheed.setAgent({ name: 'orders-mcp', version: '1.2.0', toolName: tool, action: 'tools/call' });
try {
return await fn(args);
} catch (err) {
runheed.captureException(err as Error, { tool });
return { content: [{ type: 'text', text: `${tool} failed: ${(err as Error).message}` }], isError: true };
}
};
}
const server = new McpServer({ name: 'orders', version: '1.2.0' });
server.registerTool(
'get_order',
{ description: 'Look up an order by id', inputSchema: { id: z.string() } },
tracked('get_order', async ({ id }: { id: string }) => {
const order = await db.orders.find(id); // your code
return { content: [{ type: 'text', text: JSON.stringify(order) }] };
}),
);
await server.connect(new StdioServerTransport());Use console.error, not console.log, for your own logging in a stdio server. The SDK records console.error calls as breadcrumbs, so they arrive with the next error.
Python (FastMCP)
import os, runheed
from mcp.server.fastmcp import FastMCP
runheed.init(dsn=os.environ["RUNHEED_DSN"], release="orders-mcp@1.2.0")
mcp = FastMCP("orders")
@mcp.tool()
def get_order(id: str) -> str:
"""Look up an order by id."""
runheed.set_agent(name="orders-mcp", version="1.2.0", tool_name="get_order", action="tools/call")
try:
return lookup_order(id) # your code
except Exception as e:
runheed.capture_exception(e, extra={"tool": "get_order"})
raise # FastMCP turns the exception into an error result for the client
if __name__ == "__main__":
mcp.run()Remote servers and published packages
A remote (HTTP) MCP server takes the same wrapper. The request details you attach help when one client sends arguments the others do not.
If you publish a local server for other people to install, the project key ships with it, so anyone with the package can read it and send events. Give that server its own project, strip arguments that could hold your users' data with beforeSend, and remember every event counts toward your plan's monthly limit.
Then let your coding agent fix it
With Runheed connected to Claude Code or Cursor, ask "which tool in the orders MCP server failed most this week?" The tool name is on every error, so the agent goes straight to the handler. See having Claude Code fix and resolve production errors for the full loop.
Questions
- Can I use console.log in an MCP server?
- Not in a stdio server: stdout carries the JSON-RPC messages, and any other output corrupts them. Write logs to stderr with console.error (Node) or logging (Python, which writes to stderr by default).
- Where do MCP server logs go in Claude Desktop?
- Claude Desktop writes each local server's stderr to a log file, under ~/Library/Logs/Claude/ on macOS. Those files stay on the user's machine, which is why reporting errors from the server itself matters.
- Should a failing tool throw or return isError?
- Return an error result with isError so the model can recover, and report the exception to your error tracker in the same place. The wrapper above does both.
- Is Runheed itself an MCP server?
- Yes. Runheed has its own MCP server, which is how coding agents read the errors your MCP server reports. Remote at https://runheed.com/api/mcp, or local with npx.