The MCP Inspector is the official tool for talking to an MCP server without a chat client in the way. You point it at a server, it runs the handshake, and it shows you exactly what the server exposes and returns. It is the first thing to reach for when a tool does not show up in Claude or a call fails with an error nobody can read.
It also changed a lot this year, and most of the guides that rank for it describe the old one. Version 2 is a rewrite. As of 28 September 2026 the npm latest tag is 2.8.0, the legacy line is parked on v1-latest (1.0.2), and the architecture those guides explain, a web UI on port 6274 talking to a proxy on port 6277, no longer exists. If you followed a tutorial and something did not match, that is probably why.
This guide covers the current Inspector. We ran every CLI command below against Inspector 2.8.0 on Node 22.23, using a two-tool Python server built with the official SDK (mcp 2.2.0) and DialMCP's own OAuth-protected remote endpoint. Where the output matters, it is the output we got.
What changed from v1 to v2
The package name and the npx entry point stayed the same. Almost everything behind them moved. From the project's v1 to v2 migration guide and the CLI README:
| v1 | v2 | |
|---|---|---|
| Node | 22.7.5 or newer | 22.19.0 or newer |
| Packages | 4 (inspector, -client, -server, -cli) | 1 (@modelcontextprotocol/inspector) |
| Ports | UI on 6274 plus MCP proxy on 6277 | One web server on 6274, plus an MCP Apps sandbox on 6275 |
| Modes | web, --cli | web, --cli, --tui |
| Server list | per-browser localStorage | a catalog file, ~/.mcp-inspector/mcp.json |
| Auth token variable | MCP_PROXY_AUTH_TOKEN | MCP_INSPECTOR_API_TOKEN |
| CLI exit codes | 0 or 1 | 0 to 8, plus a JSON error line on stderr |
Three of these break existing setups more than the rest. --config now means a read-only file, and a new --catalog flag is the writable one. A failing tool call now exits non-zero, where v1 exited 0. And SERVER_PORT, which used to move the proxy, now only affects the Apps sandbox. If you need the old behavior back, npx @modelcontextprotocol/inspector@v1-latest still works, but v1 only gets security fixes.
Check node -v before anything else. npm only warns about an engine mismatch, so an older Node installs fine and then fails later in ways that do not mention the version.
Three ways to run it
One binary, three front ends. The mode flag has to come first, right after the package name:
npx @modelcontextprotocol/inspector # web UI (default)
npx @modelcontextprotocol/inspector --cli # scriptable CLI
npx @modelcontextprotocol/inspector --tui # terminal UI
The web UI is the one to use when you are exploring. It has a Tools tab with a form built from each tool's input schema, plus Resources, Prompts, Skills, and an Apps tab for servers that ship MCP App interfaces. When it starts, it prints something like this:
MCP Inspector Web is up and running at:
http://127.0.0.1:6274?MCP_INSPECTOR_API_TOKEN=cb5b4d…
Sandbox (MCP Apps): http://127.0.0.1:6275/sandbox
That token is generated per launch and guards the backend's API. The page gets a copy injected when it loads, so you rarely handle it by hand. What matters is that anything else on the machine that wants to drive the backend needs it too.
The CLI is the one you will end up using most once a server works, because it gives one request, one JSON result, and an exit code. The TUI sits in between, for when you are on a remote box over SSH and do not want to forward the web UI's ports. OAuth sign-in still needs a browser that can reach the callback on 127.0.0.1:6276.
By default all three share the same catalog file and the same OAuth token store, so a server you add in the web UI is visible to the CLI. (The web backend ignores MCP_INSPECTOR_OAUTH_STATE_PATH, which matters for the CI setup below.)
Test a local stdio server
For a stdio server, you give the Inspector the command that starts it. It spawns the process and speaks JSON-RPC over the process's stdin and stdout.
npx @modelcontextprotocol/inspector node build/index.js
npx @modelcontextprotocol/inspector .venv/bin/python server.py
With the CLI, --method initialize is the cheapest check there is. It completes the handshake, prints what the server says about itself, and disconnects:
npx @modelcontextprotocol/inspector --cli .venv/bin/python server.py \
--method initialize --format json
{"result":{"serverInfo":{"name":"demo","version":""},"protocolVersion":"2025-11-25","capabilities":{"experimental":{}, ..., "tools":{"listChanged":false}}}}
Then --method tools/list shows each tool's name, description, and the JSON Schema the model will see. That schema is worth reading closely. It is generated from your type hints or Zod definitions, and along with the name and description, it is all the model has to go on.
The ordering trap that looks like a broken server
Our first attempt failed, and the failure is worth showing because it looks exactly like a server crash:
npx @modelcontextprotocol/inspector --cli uv run --with mcp==2.2.0 python server.py \
--method initialize
# {"error":{"code":"error","message":"Connection closed"}} exit 1
The server was fine. In v2, the CLI treats only the leading run of tokens without a dash as the server command. --with has a dash, so the target became uv run, which exited immediately with nothing to run. When the server command takes flags of its own, put -- after it. Under --cli, everything before -- is the server and everything after belongs to the Inspector:
npx @modelcontextprotocol/inspector --cli uv run --with mcp==2.2.0 python server.py \
-- --method initialize
# exit 0
This is the reverse of the web and TUI clients, where arguments after -- go to the server. The related mistake is quieter. If you put the Inspector's flags first (--cli --method tools/list node build/index.js), the target is dropped and the Inspector falls back to your catalog. On a clean machine we got No servers found in config file and exit 1, an error that says nothing about your server. On a machine with a populated catalog, it would have run against some other server.
Test a remote Streamable HTTP server
For a remote server, pass a URL instead of a command:
npx @modelcontextprotocol/inspector --cli \
--server-url https://example.com/mcp --method tools/list
When you don't pass --transport, v2 picks the transport from the path suffix and nothing else. A path ending in /mcp means Streamable HTTP, one ending in /sse means the legacy SSE transport, and anything else is an error. That includes a trailing slash, which is an easy way to lose ten minutes:
npx @modelcontextprotocol/inspector --cli \
--server-url https://mcp.dialmcp.com/mcp/ --method initialize
# Transport type not specified and could not be determined from URL: https://mcp.dialmcp.com/mcp/.
v1 quietly fell back to SSE for unfamiliar paths. v2 refuses to guess, so pass --transport http when your endpoint lives somewhere like /api. If you are serving the endpoint yourself from a container, the Docker notes cover the Host allowlist and the 421 you get when it is wrong.
OAuth, and what the Inspector is doing during it
Most public remote servers sit behind OAuth, and this is where the Inspector earns its keep, because it shows you the flow a real client runs. Against DialMCP's endpoint, an unauthenticated request gets a 401 with a WWW-Authenticate header pointing at the protected-resource metadata. The Inspector follows that to the authorization server, registers itself as a client through dynamic client registration, and builds a PKCE authorization URL. Here is what the CLI printed for us, with the random values removed:
Please navigate to: https://mcp.dialmcp.com/authorize?response_type=code
&client_id=<issued-by-registration>&code_challenge_method=S256
&redirect_uri=http%3A%2F%2F127.0.0.1%3A6276%2Foauth%2Fcallback
&resource=https%3A%2F%2Fmcp.dialmcp.com%2Fmcp
Every part of that is something your own server has to get right: a registration endpoint (or a client ID the user supplies), S256 PKCE, and the resource parameter that binds the token to one server. If your client connection fails somewhere in that sequence, running the Inspector against the same URL tells you which step broke.
Two details trip people up. The CLI and TUI listen for the callback on http://127.0.0.1:6276/oauth/callback, while the web client uses http://localhost:6274/oauth/callback. If your authorization server needs redirect URIs registered in advance, register the one for the client you are using. And in the CLI, interactive OAuth waits up to 15 minutes on the loopback callback. Without a TTY it usually fails fast instead, but do not rely on that in CI.
For more on how the flow fits together, see how remote MCP servers differ from local ones and DialMCP's OAuth and phone verification page.
Turn it into a smoke test
The v2 CLI was built for scripts. Pass --format json and stdout becomes one JSON object with no banners. On failure, stderr ends with a one-line error object, and the exit code tells you what kind of failure it was:
| Exit | Meaning | What we hit it with |
|---|---|---|
| 0 | Success | initialize against the local server |
| 1 | Usage or unexpected error | the trailing-slash URL above |
| 3 | Server requires authentication | DialMCP with no stored token |
| 4 | Server unreachable | a host that does not resolve |
| 5 | Tool error: isError: true, or no such tool | a validation failure, and --tool-name nope |
| 6 | --strict found a schema portability error | not triggered |
(Exit 2 is reserved for the MCP Apps probe, --app-info. Codes 7 and 8 come from --verify checks on skills/list and skills/get.)
Code 5 is the one that changes CI behavior. In v1, a tool call that came back with isError: true still exited 0, because the request itself succeeded. A ... && deploy chain would sail past a broken tool. Now it stops.
Pass typed arguments as JSON
The CLI has two ways to send tool arguments, and they differ in a way that matters. --tool-arg key=value tries to parse each value as JSON first. Our test server has a lookup_zip(zip: str) tool:
... --method tools/call --tool-name lookup_zip --tool-arg zip=01234 # "ZIP 01234", exit 0
... --method tools/call --tool-name lookup_zip --tool-arg zip=10001 # exit 5
01234 is not valid JSON, so it went through as a string. 10001 is, so it arrived as the integer 10001, and the server's validation rejected it: Input should be a valid string. Anything that is all digits but is really a string, such as a ZIP code, an order number, or a phone number, breaks this way. Use --tool-args-json for anything typed, because it passes the object through unchanged:
... --method tools/call --tool-name lookup_zip --tool-args-json '{"zip":"10001"}' # exit 0
Keep OAuth out of CI
Add --stored-auth-only to every CI run against a protected server. It never opens a browser. If there is no usable token in the store, it fails straight away with exit 3 instead of hanging:
export MCP_STORAGE_DIR="$(mktemp -d)"
export MCP_INSPECTOR_OAUTH_STATE_PATH="$MCP_STORAGE_DIR/oauth.json"
npx --yes @modelcontextprotocol/inspector@2.8.0 --cli \
--server-url https://mcp.dialmcp.com/mcp --method tools/list \
--stored-auth-only --connect-timeout 10000 --format json
# {"error":{"code":"auth_required","message":"...Missing or invalid access token"}} exit 3
The two exports isolate the token store so a job cannot read or rotate a developer's real tokens. Set both: the Inspector checks MCP_INSPECTOR_OAUTH_STATE_PATH first, so an inherited value would override the scratch directory. --list-stored-auth prints the path it actually resolved, which is a useful sanity check. Note what this combination proves: an isolated store starts empty, so against an OAuth server it exits 3 every time. That is a useful check that auth is enforced, and nothing more. (--stored-auth-only does nothing against a server that never challenges.) For a server that genuinely needs a credential in CI, the project's smoke-testing guide recommends a static --header "Authorization: Bearer ..." from your secret store over OAuth.
Pin an exact version in CI (@2.8.0, not @2). A range lets npx pick up a newer release, so the same commit can run against a different Inspector on a different day.
What a clean Inspector run does not prove
Passing in the Inspector is necessary. It is not the same as working in every host, and we found two places where the gap shows.
It is forgiving about stdout. On stdio, stdout is the protocol channel, and the usual advice is that a stray print() corrupts it. We added print() calls to our server, once at import time and once inside a tool. Inspector 2.8.0 returned correct results both times, with exit 0. When we tested a print() inside a tool for our Python server guide last week, the client's call failed with Connection closed. So a clean Inspector run does not mean a stricter client will accept the output. Log to stderr anyway.
It defaults to the older protocol revision. Unless a server entry or --protocol-era says otherwise, the Inspector negotiates the legacy era, in all three clients. Our server answered initialize with protocolVersion: 2025-11-25 by default, and with 2026-07-28 under --protocol-era auto or modern, along with a different capabilities block. If you are checking behavior against the current spec revision, pass the flag explicitly.
Security: the Inspector runs code
The Inspector's backend can start processes on your machine. That is how it runs stdio servers. It deserves the same care as any other local service that can do that.
In June 2025, CVE-2025-49596 was published against Inspector versions before 0.14.1, rated critical (CVSS 4.0 score 9.4). The proxy did not authenticate requests from the client, so an unauthenticated request could make it launch commands over stdio, which is remote code execution. Version 0.14.1 added that authentication, and v2 goes further. It binds to 127.0.0.1 by default and refuses to bind every interface (HOST=0.0.0.0) unless you also set DANGEROUSLY_BIND_ALL_INTERFACES=true. Leave those defaults alone. Treat DANGEROUSLY_OMIT_AUTH as exactly what the name says.
Two more things from the project's own warnings:
- Secrets can land in a plaintext file. On machines without an OS keychain, such as headless Linux, SSH sessions with no D-Bus session, and containers with a mounted volume on the secrets directory, OAuth client secrets and stdio
envvalues go to~/.mcp-inspector/secrets.jsonunencrypted unless you supply a key. Our test machine's launch banner reportedSecrets: OS keychain. The banner states which store you got, so check that line on any server or container. - In Docker, publish on loopback. Use
-p 127.0.0.1:6274:6274, not-p 6274:6274. The image binds all interfaces inside the container, and without the prefix Docker publishes the port on every host interface too.
Using the Inspector with DialMCP
DialMCP is a hosted remote MCP server that lets an agent place real phone calls from the user's own verified number. Its endpoint is https://mcp.dialmcp.com/mcp and it exposes four tools: place_call, get_call, end_call, and list_calls (reference on the tools page). Connecting the Inspector to it is a quick way to see what your agent will see before you wire it into Claude Code or another client.
Run the web UI, add the URL as a Streamable HTTP server, and connect. The browser opens DialMCP's sign-in, where you verify a US or Canadian mobile number by SMS. Once you are back, the Tools tab should list the four tools with their schemas. (We verified the tool list and the sign-in redirect from the CLI; we did not complete a sign-in in the web UI for this post.)
For a smoke test, call list_calls. It is read-only and safe to run on every commit. Do not put place_call in a CI job: it dials a real phone and speaks to a real person. Server-side limits, calling hours, and opt-outs apply to every call, but a test suite is still the wrong caller.
The endpoint details are on the hosted MCP endpoint page, and the raw JSON-RPC behind the Tools tab is on MCP server commands.
Where to go next
If you are still building the server you want to inspect, start with how to build an MCP server, or the stdio to remote tutorial, which uses the Inspector at each step. For Python specifically, creating an MCP server with SDK v2 covers in-memory tests, which catch different problems than the Inspector does.
Further reading
- Create an MCP server in Python
- How to build an MCP server
- MCP server tutorial
- Remote MCP servers explained
- DialMCP OAuth flow
- DialMCP tool reference
- modelcontextprotocol/inspector on GitHub
- Inspector v1 to v2 migration guide
- Inspector CLI smoke-testing guide
Want a real tool to point the Inspector at? DialMCP is a hosted, OAuth-protected MCP server that places phone calls. Connect https://mcp.dialmcp.com/mcp, sign in with your mobile number, and run list_calls as your smoke test.
FAQ
What is the MCP Inspector?
The official developer tool for testing MCP servers, published as @modelcontextprotocol/inspector. It connects to a server over stdio, Streamable HTTP, or SSE and lets you list and call tools, read resources, and run prompts through a web UI, a terminal UI, or a scriptable CLI.
How do I run the MCP Inspector?
Run npx @modelcontextprotocol/inspector with Node 22.19.0 or newer. Add a server command after it for a local stdio server, or use --server-url for a remote one. Add --cli or --tui right after the package name for the other modes.
Why does the Inspector say "Connection closed" when my server works?
Under --cli, often because the server command contains a flag, so the Inspector cut the command short. Put -- after the full server command and the Inspector's options after that. It can also mean the server really did exit, so check its stderr.
Which port does the MCP Inspector use?
v2 serves the web UI and its API on 6274, the MCP Apps sandbox on 6275, and the CLI and TUI OAuth callback on 6276. Port 6278 is used only for MCP Apps that declare their own origin (_meta.ui.domain). Port 6277 was the v1 proxy and is no longer used.
Can the MCP Inspector test a remote server with OAuth?
Yes. It discovers the authorization server from the 401 response, registers itself if the server supports dynamic client registration, and runs a PKCE flow in your browser. In CI, use --stored-auth-only or a static bearer header so it never waits on a browser.
How do I use the MCP Inspector in CI?
Use --cli --format json, pin an exact version, pass --stored-auth-only for protected servers, and branch on the exit code: 3 for auth, 4 for unreachable, 5 for a tool error.