You build an MCP server, it runs fine on its own, then you wire it into a client and the tools never show up. Or a tool throws, but only when the model calls it, and there is no error message you can see. Debugging that blind is miserable.
MCP Inspector is the official debugger for the Model Context Protocol, and it turns that blind process into a tight loop. You launch your server inside it, see the exact tools it exposes, call one by hand, and read the raw JSON response. The boring bugs get caught before any client is involved.
This guide covers the whole workflow on a real Python server: the web UI, the CLI mode for scripting and CI, then the failure modes that actually bite when you connect to Claude Desktop. Every command here is one I ran and checked.
Prerequisites
- Node.js 18 or newer (the Inspector runs through npx)
- Python 3.10+ and FastMCP:
pip install fastmcp - A server to test. Any MCP server works. The one in Step 0 is copy-paste ready.
Step 0: a small server worth testing
You cannot debug an empty server, so here is one exposing two tools. server_health reads Linux process and memory stats. save_note appends one line to a file.
from __future__ import annotations
from datetime import datetime
from pathlib import Path
from fastmcp import FastMCP
mcp = FastMCP("dev-utils")
@mcp.tool()
def server_health() -> dict[str, str]:
"""Return uptime, load, and memory usage of this machine."""
now = datetime.now().isoformat(timespec="seconds")
try:
with open("/proc/uptime") as fh:
uptime_sec = float(fh.read().split()[0])
uptime = f"{int(uptime_sec // 86400)}d {int(uptime_sec % 86400 // 3600)}h"
except OSError:
uptime = "n/a"
try:
mem = {}
with open("/proc/meminfo") as fh:
for line in fh:
key, value = line.split(":", 1)
mem[key] = value.strip()
total_kb = int(mem["MemTotal"].split()[0])
avail_kb = int(mem["MemAvailable"].split()[0])
memory = f"{round(100 * (total_kb - avail_kb) / total_kb, 1)}% used"
except (OSError, KeyError, ValueError):
memory = "n/a"
return {"time": now, "uptime": uptime, "memory": memory}
@mcp.tool()
def save_note(path: str, line: str) -> str:
"""Append one line to a file."""
p = Path(path).expanduser().resolve()
with p.open("a") as fh:
fh.write(line.rstrip() + "\n")
return f"appended {len(line)} chars to {p}"
if __name__ == "__main__":
mcp.run()
Save it as server.py and run it once to confirm it starts:
python server.py
It sits there waiting on stdio, which is exactly what local clients expect. Kill it with Ctrl+C and move on to the Inspector.
Step 1: launch the Inspector
The Inspector ships as an npm package, so there is no separate install. Point it at your server's startup command:
npx -y @modelcontextprotocol/inspector \
.venv/bin/python server.py
The Inspector starts two pieces and prints a URL, usually http://localhost:6274. Open that in a browser. If you manage the server with uv, the pattern is the same, you just point at uv instead:
npx @modelcontextprotocol/inspector \
uv --directory path/to/server run server.py
Step 2: the web UI
The web UI is where most day-to-day checks happen. After it connects, the left panel lists every tool the server exposes, with the JSON schema for each. This one check catches the most common mistake there is: a tool that never registered. If a tool is missing from the list, the bug is in registration or the decorator, before any client logic is involved.
For each tool you fill in the arguments and hit the call button. The response pane shows the raw message in the exact shape your client will receive, including the isError flag. You watch save_note write a file and confirm the return value before any model ever touches it.
Calling server_health by hand returns something like:
{"time": "2026-08-28T13:03:15", "uptime": "11d 9h", "memory": "55.4% used"}
If a tool errors here, debug it directly in the server code instead of guessing from client logs. That is the loop the Inspector buys you.
Step 3: CLI mode for scripting and CI
The web UI is for humans. The CLI mode is for everything repeatable, and it saves you from building a bespoke test harness. Add --cli and a method:
npx @modelcontextprotocol/inspector --cli \
.venv/bin/python server.py --method tools/list
That prints the full tool schema list as JSON, perfect for a sanity check. To call a tool with arguments on the command line:
npx @modelcontextprotocol/inspector --cli \
.venv/bin/python server.py \
--method tools/call --tool-name save_note \
--tool-arg path=/tmp/notes.txt --tool-arg line="hello from mcp"
Structured arguments work too; pass them as JSON:
npx @modelcontextprotocol/inspector --cli \
.venv/bin/python server.py \
--method tools/call --tool-name save_note \
--tool-arg 'options={"format": "markdown"}'
Because the CLI returns clean JSON, you can drop a tools/list or tools/call check into a CI job or a pre-commit hook. A tool that drifts out of schema breaks the pipeline before it ships, instead of failing on an engineer's laptop.
Step 4: connect to Claude Desktop, then debug when it fails
The manual check passes, so now the real test: register the server in Claude Desktop and see whether the client can reach it.
On macOS the config lives at ~/Library/Application Support/Claude/claude_desktop_config.json. On Windows it is %APPDATA%\Claude\claude_desktop_config.json; on Linux ~/.config/Claude/claude_desktop_config.json. Add an mcpServers block:
{
"mcpServers": {
"dev-utils": {
"command": "/absolute/path/to/.venv/bin/python",
"args": ["/absolute/path/to/server.py"]
}
}
}
Then quit Claude Desktop completely (closing the window is not enough on macOS) and reopen.
When a server does not appear, check in this order:
- The hammer icon. A small icon appears at the bottom left of the input box when MCP servers are connected. No icon means a config error. Click it to see the connected servers and their tool lists.
- The logs. On macOS,
tail -n 20 -f ~/Library/Logs/Claude/mcp*.logshows what the server printed at startup and when tools were called. - Print to stderr, not stdout. MCP servers speak JSON-RPC over stdout. A stray
print()on stdout corrupts the protocol and the client sees garbage or nothing. Useprint(..., file=sys.stderr)or the logging module for debug output.
Step 5: the failure modes that actually bite
Three issues cause most "my server will not connect" reports, and all are config or transport, not logic:
- A trailing comma in the JSON config. JSON does not allow it, and some clients fail silently over it. Validate the file at jsonlint.com before restarting.
- A relative
commandpath. Desktop clients run with a minimal PATH, so barenpxorpythonmay not resolve. Use an absolute path. Runwhich python(orwhere pythonon Windows) to find it. - Printing to stdout. Worth repeating because it looks like a logic bug but is a transport bug.
- Not fully restarting the client. Windows and macOS both keep the app running in the background. Quit fully and relaunch, or the old config stays loaded.
When to use the Inspector vs just shipping it
Use the Inspector every time you add or change a tool. It is fastest for local, stdio servers.
For a remote server over Streamable HTTP you can still run the CLI against a URL, passing headers like an API key with -e or --header. Subscription-based MCP connections have their own auth flows though, so for those you still write integration tests and assert on what a real callback returns.
For growing server codebases, keep both: the Inspector for interactive debugging, and a test harness built on the official client SDK for assertions you want in CI. The Inspector does not replace tests. It makes the manual part fast enough that you actually do it.
Where to go next
- Read the Inspector README for the full flag list (env vars, config-file mode, header support).
- Read the official build an MCP server guide to add resources and prompts to the same server.
- See how to connect local servers and where the log files live per operating system.
The payoff is simple. A tool you tested by hand is a tool you trust. The Inspector makes that check take seconds instead of a dull session staring at a client that shows nothing.