Debugging MCP Servers with MCP Inspector: A Field Guide
MCP Inspector is the official debugging tool for Model Context Protocol servers, and it is the first thing to reach for when a server misbehaves: run npx @modelcontextprotocol/inspector in front of your server command and you get a browser UI that connects over stdio or HTTP, lists your tools, calls them with arguments you type by hand, and shows every JSON-RPC message on the wire. The bisection logic is simple. If your server works in MCP Inspector but not in Claude Desktop or Claude Code, the problem is client configuration; if it fails in the inspector too, the problem is your server, and the message history usually points at the exact request that went wrong.
This field guide is the distilled version of debugging a lot of MCP servers: how the inspector is put together, how to launch it against local and remote servers, the stdout corruption bug that breaks a huge share of first-time stdio servers, schema and timeout failures, auth against remote servers, CLI mode for CI, and the raw JSON-RPC tricks for the few cases the inspector cannot see.
What MCP Inspector Actually Is
MCP Inspector ships as two cooperating pieces, and knowing the split saves real confusion later. The first piece is a web UI, the part you click around in. The second is a small Node.js proxy that does the actual MCP work: it spawns your stdio server as a child process, or opens a Streamable HTTP connection to a remote one, and bridges everything back to the browser. By default the UI is served on localhost:6274 and the proxy listens on localhost:6277. The port numbers are a mnemonic: on a phone keypad, 6274 spells MCPI (MCP Inspector) and 6277 spells MCPP (MCP Proxy).
Two practical consequences fall out of that architecture.
- The proxy, not your browser, is the MCP client. Anything that only affects browsers, CORS headers on your Streamable HTTP endpoint being the classic example, will never show up in MCP Inspector. If you are building a browser-based client for your server, test CORS separately; the inspector cannot catch it by design.
- The proxy can spawn arbitrary local processes, because that is literally its job. That is why current versions generate a session token on startup and print a pre-authenticated URL that looks like
http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=<token>. Open that exact URL from the terminal output. If you open a plainlocalhost:6274instead, the UI loads but connection attempts fail with an auth error until you paste the token into the configuration panel.
You do not install the inspector. npx @modelcontextprotocol/inspector fetches and runs the current release each time. It wants a recent Node runtime (the project targets Node 22 and newer), which matters if your system Node is pinned older by nvm or a distro package.
Launching MCP Inspector Against a Local Server
The invocation pattern is always the same: inspector first, then the exact command that starts your server.
# TypeScript or JavaScript server, compiled to build/index.js
npx @modelcontextprotocol/inspector node build/index.js
# Python server run through uv
npx @modelcontextprotocol/inspector uv run python server.py
# Pass environment variables with -e, and use -- to separate
# inspector flags from your server's own flags
npx @modelcontextprotocol/inspector -e API_KEY=test-key -- node build/index.js --verboseThree details bite people here.
- Invoke the runtime directly, not a package manager script.
npm run startprints its own banner lines to stdout before your server produces a single byte, and stdout is the protocol channel (much more on this below). If you must go through npm, usenpm run --silent start. - Relative paths resolve against the directory where the proxy process is running, not against your server project. When you edit the command in the UI, use absolute paths for the entry file and anything it loads.
- The spawned server inherits the environment of the
npxprocess, nothing more. If the server needsDATABASE_URL, pass it with-eor export it in the same shell before launching.
If you already maintain an mcp.json-style config for your client, the inspector can consume it directly, which is the single best habit in this guide: debug with the same config you ship, so you never chase a phantom difference between what the inspector ran and what Claude Desktop runs.
{
"mcpServers": {
"demo": {
"command": "node",
"args": ["build/index.js"],
"env": { "API_KEY": "test-key" }
}
}
}npx @modelcontextprotocol/inspector --config ./mcp.json --server demoThe arrow points the other way too. Once a hand-entered setup finally connects, use the export buttons in the connection pane (Server Entry copies one server block, Servers File copies a full config) to paste a known-good entry straight into your client config.
Python developers get a shortcut: the official Python SDK ships a dev helper that wraps the same tool.
uv run mcp dev server.pyBoth ports are configurable for the day 6274 or 6277 collides with something else on your machine:
CLIENT_PORT=8080 SERVER_PORT=9000 npx @modelcontextprotocol/inspector node build/index.jsA Debugging Workflow That Actually Works
Random clicking wastes time. The inspector rewards a fixed sequence, because each step rules out a whole class of bugs before you look at the next one.
- Connect. If this fails, nothing else matters. The two dominant causes are a wrong command (test it standalone in a terminal first) and stdout pollution (next section).
- Read the connection pane. After a successful connect it shows the negotiated protocol version and the capability set the server declared. If you expected
toolsand the server only advertisesresources, you registered things after connecting the transport, or you are running a stale build. Rebuild, then disconnect and reconnect: the proxy respawns the process, picking up the new binary. - List before you call. Open the Tools tab and hit List Tools. An empty list is a registration or capability bug, not a call bug, and it is a different fix (covered below).
- Call with minimal arguments. Pick the simplest tool, fill in the smallest valid input, and run it. The UI renders the structured result and flags errors.
- Read the history pane. Every request and response is captured as raw JSON at the bottom of the UI. When behavior is confusing, the wire truth settles it: you can see the exact
tools/callparams the UI sent and the exact result or error object your server returned. - Watch notifications and stderr. Log messages your server emits via MCP logging notifications land in the notifications pane, and anything the process writes to stderr is surfaced by the inspector too. A server that crashes on startup usually tells you why on stderr.
If your server initiates sampling or elicitation requests, the inspector pauses them and lets you hand-write the response that a model or user would have produced. That is the only sane way to exercise those code paths without wiring up a full client.
Once everything passes in the inspector, and the server still fails in your actual client, you have localized the bug to client config: a wrong absolute path, a missing environment variable (desktop clients do not inherit your shell profile), or a Node version conflict. Check the client logs, not your server code.
The Golden Rule of stdio: Keep stdout Clean
If you learn one thing from this guide, make it this one. A stdio MCP server owns exactly two streams: stdout is reserved for protocol messages, newline-delimited JSON-RPC and nothing else, while stderr is yours for logging. A single stray print or console.log interleaves human text into the JSON stream, and the client's parser gives up. The symptom in MCP Inspector is an immediate connection failure, with a parse error along the lines of Unexpected token ... is not valid JSON in the error output.
// BAD: goes to stdout and corrupts the protocol stream
console.log("demo server starting...");
// GOOD: stderr is safe, and the inspector shows it to you
console.error("demo server starting...");Python has the same trap with print. Configure logging to stderr once, at the top of the entry file, and never print:
import logging
import sys
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
log = logging.getLogger("demo")
log.info("starting up") # safe: stderr
# print("starting up") # never in a stdio server: stdoutThe direct offenders are easy to find. The sneaky ones are third parties writing to stdout on your behalf:
- npm and friends printing script banners when the configured command is
npm run ...rather thannode .... - Env loaders that announce themselves: some dotenv releases print an injection tip line to stdout on load. Silence it via its quiet option, or load env vars yourself.
- Any dependency with a "welcome" banner, progress bar, or update notifier that defaults to stdout.
- Debugging leftovers inside request handlers, which corrupt the stream mid-session, so the server connects fine and then dies on the first tool call.
When in doubt, prove cleanliness at the byte level. A stdio server is just a process that reads JSON lines and writes JSON lines, so you can drive the handshake with a pipe and eyeball the output:
(
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0.0.1"}}}'
printf '%s\n' '{"jsonrpc":"2.0","method":"notifications/initialized"}'
printf '%s\n' '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
sleep 1
) | node build/index.jsEvery line that comes back must be parseable JSON. If the first bytes are a banner, a log line, or an emoji, you have found the bug before opening a single UI.
Empty Tool Lists, Schema Errors, and Failed Calls
The Tools tab is where most real debugging happens, and its failures sort into three buckets.
The list is empty. Either the server never declared the tools capability, or it declared it and registered nothing. With the high-level APIs (McpServer in the TypeScript SDK, FastMCP in Python) capabilities are inferred from what you register, so an empty list usually means the registration code never ran: a conditional that did not fire, an import with a side effect that got tree-shaken, or an old build. With the low-level APIs you must both declare the capability and install a tools/list handler yourself; forgetting either half produces an empty list or a Method not found error (JSON-RPC code -32601) that you will see verbatim in the history pane.
The tool is listed but the schema looks wrong. A tool's inputSchema must be a JSON Schema object with type: "object" at the top level. The SDKs generate this for you from typed definitions, which is the reliable path:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({ name: "demo", version: "1.0.0" });
server.registerTool(
"add",
{
description: "Add two integers",
inputSchema: { a: z.number().int(), b: z.number().int() },
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }],
})
);
await server.connect(new StdioServerTransport());
console.error("demo server ready");The inspector renders the parsed schema as a form. If a parameter you expected is missing from the form, your schema is not saying what you think it says, and the tools/list response in the history pane shows the JSON Schema the server actually emitted. Ten seconds of reading beats an hour of guessing.
The call fails. Distinguish two different kinds of failure, because they have different owners. A protocol-level error comes back as a JSON-RPC error object: -32602 Invalid params means the arguments failed validation before your handler ran (the SDK's validator rejected them), and an unknown tool name is caught the same way. A tool-level error comes back as a normal result with isError: true and the failure text in the content array; that means your handler ran and reported a failure, which is exactly what a model is supposed to see and react to. If your handler throws raw exceptions instead of returning isError results, the SDK converts them, but you lose control of the message. Return errors deliberately.
One more subtlety: if your server adds tools dynamically at runtime, it should emit a listChanged notification. The inspector shows the notification arriving, and re-listing picks up the new tools. If your dynamic tools never appear in a real client, this notification is the first thing to check.
Timeouts, Progress Notifications, and Long-Running Tools
MCP Inspector applies a request timeout of 10 seconds by default. A tool that crawls a site, runs a test suite, or calls a slow upstream API will blow through that and the UI reports a timeout even though your server is working fine. You have two levers, and they teach you something about real clients.
The blunt lever is configuration. The inspector's configuration dialog exposes the request timeout (MCP_SERVER_REQUEST_TIMEOUT), a toggle to reset the clock whenever progress arrives (MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS), and a hard ceiling on total request time (MCP_REQUEST_MAX_TOTAL_TIMEOUT). Cranking these up unblocks your debugging session immediately.
The correct lever is progress notifications. When a caller includes a progress token in a request, a long-running tool should stream notifications/progress messages back, both to keep timeout clocks alive and to give users a live status. With the TypeScript SDK the tool callback receives an extra context object carrying the token:
server.registerTool(
"crawl_site",
{ description: "Crawl a site", inputSchema: { url: z.string() } },
async ({ url }, extra) => {
for (let page = 1; page <= 50; page++) {
await crawlPage(url, page);
if (extra._meta?.progressToken !== undefined) {
await extra.sendNotification({
method: "notifications/progress",
params: {
progressToken: extra._meta.progressToken,
progress: page,
total: 50,
},
});
}
}
return { content: [{ type: "text", text: "crawled 50 pages" }] };
}
);The inspector displays incoming progress updates as the call runs, which makes it the ideal harness for verifying this behavior before a real client ever connects. Remember that fixing the timeout inside the inspector fixes nothing for production: every client enforces its own limits, so a tool that needs minutes should report progress, or split the work, rather than rely on generous client settings.
Debugging Remote Servers: Streamable HTTP, Auth, and Sessions
Everything so far spawns a local process, but MCP Inspector speaks the remote transports too. Switch the transport dropdown from stdio to Streamable HTTP, point it at your endpoint (for local development that is typically http://localhost:3000/mcp), and connect. For historical context: the original remote transport was HTTP plus a separate SSE channel from the 2024-11-05 protocol revision; the 2025-03-26 revision replaced it with Streamable HTTP, a single endpoint handling POSTs with optional streaming responses. The inspector still offers the legacy SSE transport because plenty of deployed servers have not migrated, and connecting with both is the quickest way to check which one a mystery server actually speaks.
The HTTP status codes in the failed-connection output map to specific mistakes:
- 404: wrong path. The MCP endpoint is a specific route, and a bare domain root is usually not it.
- 401 or 403: the server wants auth. Add a token in the Authentication section of the connection pane, which sends it as a bearer header, or run the inspector's OAuth flow if the server implements MCP authorization.
- 400 complaining about a session: Streamable HTTP servers issue a session id header (
Mcp-Session-Id) on initialize, and every later request must echo it. If your own server-side implementation forgets to return or validate it, the inspector's request history shows the headers on each message so you can see exactly where the chain broke. - 405: the client tried an HTTP method the server does not implement. The GET-based standalone stream is optional for servers, so a 405 on GET can be fine, while a 405 on POST means the endpoint is misrouted.
For servers behind OAuth, the inspector implements the authorization flow interactively, including metadata discovery, so you can debug a protected server without writing a throwaway client. Watch the flow step by step, note which stage stalls (metadata fetch, redirect, token exchange), and you have your bug report.
And repeating the architectural caveat, because it costs people afternoons: the inspector proxy is a server-side client. It will happily connect to a Streamable HTTP server that has no CORS headers at all. If your production client is a web page, missing CORS will break it while MCP Inspector reports all green, so browser-client CORS needs its own test.
MCP Inspector CLI Mode: Smoke Tests in CI
The UI is for interactive debugging; the same package also has a CLI mode that speaks the protocol, prints JSON to stdout, and exits, which turns the inspector into a scriptable smoke-test harness.
# List tools, machine-readable
npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/list
# Call a tool with arguments
npx @modelcontextprotocol/inspector --cli node build/index.js \
--method tools/call --tool-name add --tool-arg a=2 --tool-arg b=3
# Same, against a remote Streamable HTTP server
npx @modelcontextprotocol/inspector --cli https://api.example.com/mcp \
--transport http --method tools/list
# Reuse your shipped config file
npx @modelcontextprotocol/inspector --cli --config ./mcp.json --server demo --method tools/listBecause the output is JSON, jq gives you assertions, and a failed connection or call fails the pipeline. A minimal GitHub Actions job that catches the two most common regressions (server no longer starts, tool disappeared or broke) looks like this:
jobs:
mcp-smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci && npm run build
- name: Server must expose at least one tool
run: |
npx @modelcontextprotocol/inspector --cli node build/index.js \
--method tools/list | jq -e '.tools | length > 0'
- name: Tool must return a result
run: |
npx @modelcontextprotocol/inspector --cli node build/index.js \
--method tools/call --tool-name add --tool-arg a=2 --tool-arg b=3This is deliberately not a full test suite. Real coverage belongs in unit tests against your handlers and integration tests through an SDK client. The CLI smoke test earns its place because it exercises the one thing unit tests skip: the actual spawn, handshake, and wire format of the shipped artifact.
When MCP Inspector Is Not Enough
A few classes of bugs live below or beside the inspector, and it helps to know the escape hatches.
Raw pipes for startup bugs. The printf handshake shown earlier is the fastest check when the inspector itself will not connect, because it removes every moving part except your process and your terminal.
Capture stderr to a file. The inspector shows stderr, but for a crash that scrolls past or a long soak session, wrap the command so stderr also lands on disk:
npx @modelcontextprotocol/inspector sh -c 'node build/index.js 2>>/tmp/mcp-stderr.log'Read the real client's logs. When the inspector says yes and the client says no, the client's logs name the actual failure. Claude Desktop on macOS writes per-server logs you can tail live:
tail -f ~/Library/Logs/Claude/mcp-server-demo.logClaude Code reports server health with the /mcp command inside a session and claude mcp list outside one. Nine times out of ten the discrepancy is an environment variable that your shell had and the client did not, or a node binary resolved from a different location.
Check protocol version drift. The initialize response in the history pane names the protocol version your server negotiated. If your SDK is a year old and a client speaks a newer revision, most things still work thanks to version negotiation, but newer features (structured tool output, elicitation, task-style long-running requests) silently will not exist. Upgrading the SDK is usually the whole fix.
Security Notes for Running MCP Inspector
The inspector is a development tool with the power to execute arbitrary commands, and it has had a real vulnerability, so this section is short but not optional.
- Update it. Versions before 0.14.1 shipped without authentication between the browser UI and the proxy, and CVE-2025-49596 demonstrated that a malicious web page could reach
localhost:6277and achieve remote code execution on a developer machine. Current releases require the session token by default. Plainnpx @modelcontextprotocol/inspectoralways fetches a patched version; the risk is pinned old versions in scripts and lockfiles, so audit those. - Keep the token check on. The
DANGEROUSLY_OMIT_AUTHescape hatch exists and the name is honest. Do not use it because pasting a token once per session felt tedious. - Keep it on localhost. Do not port-forward 6277 off the machine, do not bind it to a public interface on a shared dev box, and treat the proxy port exactly like an open shell, because that is what it is.
- Scope credentials. The env vars you pass with
-eend up in a child process you are actively poking at with experimental code. Use test keys, not production ones.
Field Checklist
Pin this next to your terminal for the next server you build.
- Test the server command standalone in a terminal before pointing MCP Inspector at it.
- Open the tokenized URL the inspector prints, not a bare localhost address.
- stdout is for protocol only: log to stderr, silence dotenv-style banners, never wrap the command in a bare
npm run. - Empty tool list -> registration or capability bug. Failed call -> read the raw request and response in the history pane before touching code.
- Slow tools: send progress notifications, and only then raise inspector timeouts.
- Remote servers: check the status code first (404 path, 401 auth, 400 session id), and remember the inspector cannot see CORS problems.
- Wire the CLI mode into CI with a
tools/listassertion so a broken handshake never reaches users. - Works in the inspector but not in the client? Tail the client's own MCP logs and compare environments.
The inspector will not write your server for you, but it collapses the debugging loop from "edit config, restart client, wonder" to "click, read the wire, fix". That loop speed is the whole game.
FAQ
What is MCP Inspector and do I need to install it? It is the official debugging UI and CLI for Model Context Protocol servers, maintained in the modelcontextprotocol GitHub organization. There is nothing to install: npx @modelcontextprotocol/inspector <your server command> downloads and runs the current release. You need a recent Node runtime (Node 22 or newer) even when the server under test is written in Python or another language, because the inspector itself runs on Node.
How do I change the ports MCP Inspector uses? Set CLIENT_PORT for the web UI (default 6274) and SERVER_PORT for the proxy (default 6277) when launching, for example CLIENT_PORT=8080 SERVER_PORT=9000 npx @modelcontextprotocol/inspector node build/index.js.
Why does my server work in MCP Inspector but fail in Claude Desktop? Because the inspector inherited your shell environment and the desktop app did not. The usual culprits are relative paths (make every path in the client config absolute), missing environment variables (declare them in the config's env block), and a node or python binary that your shell resolves through a version manager the GUI app knows nothing about. The client's MCP log files name the exact failure.
Can MCP Inspector debug Python MCP servers? Yes. It launches any command, so npx @modelcontextprotocol/inspector uv run python server.py works the same as a Node server. The official Python SDK also bundles a shortcut, mcp dev server.py, which starts your server under the inspector in one step.
How do I run MCP Inspector headless in CI? Use CLI mode: npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/list prints JSON to stdout and exits, so you can pipe it into jq -e for assertions. It supports tools/call with --tool-name and repeated --tool-arg key=value flags, plus resource and prompt methods, and it can target remote servers with --transport http.
Why is my tools list empty in the inspector? The server either never declared the tools capability or never registered a tool before connecting the transport. With high-level SDK APIs this usually means the registration code did not execute (stale build, conditional import); with low-level APIs it means the tools/list handler or the capability declaration is missing. The negotiated capabilities shown in the connection pane tell you which case you are in.
Does MCP Inspector support Streamable HTTP and OAuth? Yes. Choose the Streamable HTTP transport and enter the endpoint URL; for protected servers you can supply a static bearer token or walk through the interactive OAuth flow the inspector provides, which is also a convenient way to debug your authorization server integration step by step. The legacy SSE transport remains available for servers that have not migrated.
Is it safe to leave MCP Inspector running? On an up-to-date version, bound to localhost, with the session token enabled: yes, for a development session. But the proxy exists to execute commands, so never expose port 6277 beyond your machine, never disable the token with DANGEROUSLY_OMIT_AUTH, and make sure nothing in your setup pins an inspector version older than 0.14.1, which predates the authentication added in response to CVE-2025-49596.
BootcampA 30-day guided bootcamp: build, harden and ship a production autonomous agent from scratch.
AI AgentsUnderstand how AI agents really work: the loop, the tools, the memory, and why most agent projects fail.