Test and debug an MCP server with MCP Inspector

Debug an MCP server before you connect it to ChatGPT: MCP Inspector web UI and CLI, tunnels, logs, schema and annotation checks, and OAuth debugging.

Divy ChaurasiaIntermediate 20 minFor builders

When an MCP server misbehaves in ChatGPT, the chat window tells you very little. Test it outside ChatGPT first: confirm it initializes, lists valid tools, returns the right results and handles auth. Then add a tunnel and ChatGPT, one layer at a time, so you know which layer broke.

The main tool for this is MCP Inspector, the official MCP debugging client. It comes in one package with a web UI, a CLI and a terminal UI.

Before you start#

  • Node.js 22.19 or newer for the current Inspector (v2). The older v1 line is still available as npx @modelcontextprotocol/inspector@v1-latest, but its CLI flags differ from the examples here.
  • Your server running locally, for example on http://localhost:8787/mcp. If you don't have one, follow Build your first ChatGPT app.
  • jq, if you want to script assertions.

Tip: To check that your client setup works before blaming your server, point the Inspector at a known-good server. The reference "Everything" server implements every MCP feature, and DeepWiki is a public remote server with no auth (https://mcp.deepwiki.com/mcp).

1. Inspect it in the web UI#

bash
npx @modelcontextprotocol/inspector@latest

This opens the Inspector in your browser. Choose Streamable HTTP, enter your server URL, and connect. Then work through this list:

  1. Initialization succeeds. Note the protocol version and capabilities the server reports.
  2. Server instructions read well. ChatGPT and Codex use the MCP instructions field as server-wide guidance, and Codex recommends keeping the first 512 characters self-contained.
  3. The tool list is what you expect, with no leftovers from earlier experiments.
  4. Every tool works with normal inputs, edge cases, missing IDs and empty results.
  5. Errors come back as readable messages, not stack traces.
  6. Auth is enforced for private data and write actions.

For a stdio server, the Inspector can start it for you. Pass the command after the package name:

bash
npx @modelcontextprotocol/inspector node build/index.js

Note: The Inspector stores OAuth tokens, client secrets and stdio environment values in your OS keychain when one is available, and otherwise in ~/.mcp-inspector/secrets.json. Keep that in mind on shared machines.

2. Script checks with the Inspector CLI#

The CLI runs one MCP call per command and exits with a status code, which makes it good for repeatable checks and CI.

bash
# Connect-only probe: handshake, print server info, disconnect
npx @modelcontextprotocol/inspector --cli --transport http \
  --server-url http://localhost:8787/mcp --method initialize

# List tools as JSON
npx @modelcontextprotocol/inspector --cli --transport http \
  --server-url http://localhost:8787/mcp --method tools/list --format json

# Call one tool with typed arguments
npx @modelcontextprotocol/inspector --cli --transport http \
  --server-url http://localhost:8787/mcp \
  --method tools/call --tool-name roll_dice \
  --tool-args-json '{"count":2,"sides":6}' --format json

Prefer --tool-args-json over --tool-arg key=value. The key=value form parses values as JSON when it can, so an ID like 10001 arrives as a number, and a schema expecting a string will reject it.

Turn checks into assertions with jq -e, which fails when the result is null or false:

bash
npx @modelcontextprotocol/inspector --cli --transport http \
  --server-url http://localhost:8787/mcp --method tools/list --format json \
  | jq -e '.result.tools | map(.name) | index("roll_dice")' > /dev/null

Two more flags worth knowing:

  • --app-info reports whether a tool has an MCP Apps UI and which ui:// resource it points to, without calling the tool. It exits 2 when there is no UI.
  • --strict on tools/list adds schemaFindings for schema portability problems.

In CI, pin an exact Inspector version (npx --yes @modelcontextprotocol/inspector@<version> --cli ...) so results don't change under you.

3. Check tool schemas and annotations#

Most "the model won't call my tool" problems are metadata problems. For each tool, check:

  • Name: specific and verb-first (get_order_status), unique within the server.
  • Description: says what it does, when to use it ("Use this when…"), and its limits.
  • Input schema: every parameter has a type and a description; use enums for fixed choices; mark required fields. Ask only for what the tool needs.
  • Output schema: if you declare outputSchema, structuredContent must match it on every call, including error cases.
  • Annotations: set readOnlyHint, destructiveHint and openWorldHint explicitly. ChatGPT treats a tool with no readOnlyHint as a write and asks for confirmation every time. OpenAI's plugin review rejects annotations that don't match behavior.
  • UI link: for app tools, _meta.ui.resourceUri points at a resource that exists, served with MIME type text/html;profile=mcp-app.

4. Watch the raw traffic and logs#

When the Inspector and your expectations disagree, go one level lower. A Streamable HTTP server is just HTTP, so you can call it with curl:

bash
curl -s -X POST http://localhost:8787/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'

A healthy server returns a JSON-RPC result with serverInfo and capabilities. A 406 Not Acceptable from a server built on the official TypeScript SDK means the Accept header doesn't list both application/json and text/event-stream.

On the server side:

  • Log every incoming method name, tool name and duration, plus the full error for failures.
  • For stdio servers, never write logs to stdout. Stdout carries the protocol; a stray console.log corrupts it. Log to stderr instead.
  • Don't log access tokens or sensitive tool results.

5. Test through a tunnel, then ChatGPT#

Once it works locally, expose it and repeat the same checks against the public URL:

bash
ngrok http 8787
# or
cloudflared tunnel --url http://localhost:8787
bash
npx @modelcontextprotocol/inspector --cli --transport http \
  --server-url https://abc123.ngrok.app/mcp --method tools/list --format json

If it passes here but fails in ChatGPT, the problem is in the ChatGPT setup (URL, auth mode, stale metadata), not the server. Connect it with developer mode, and remember to select Refresh on the connection after every server change.

For raw request and response logs from OpenAI's side, use the API Playground: Tools > Add > MCP Server, enter the HTTPS endpoint, and run test prompts.

6. Debug OAuth#

ChatGPT follows the MCP authorization spec. Check each link in the chain:

  1. Protected resource metadata. Your server publishes it at /.well-known/oauth-protected-resource, or points to it in a WWW-Authenticate header on 401 responses:

    bash
    curl -i https://mcp.example.com/.well-known/oauth-protected-resource
    curl -i -X POST https://mcp.example.com/mcp -H 'Content-Type: application/json' -d '{}'

    The second call should return 401 with WWW-Authenticate: Bearer resource_metadata="...".

  2. Authorization server metadata is reachable at /.well-known/oauth-authorization-server or /.well-known/openid-configuration on your identity provider.

  3. Client registration works: CIMD (client_id_metadata_document_supported: true), DCR, or static credentials entered in ChatGPT.

  4. Redirect URI matches exactly. Copy it from the connection's page in ChatGPT; it is either https://chatgpt.com/connector/oauth/{callback_id} or the stable https://chatgpt.com/connector_platform_oauth_redirect, depending on whether your authorization server supports issuer identification.

  5. Tokens are checked for issuer, audience, expiry and scopes. Reject bad tokens with a 401 and the WWW-Authenticate challenge, so the client re-authorizes.

To test with the Inspector, register its callback URL with your provider (http://localhost:6274/oauth/callback for the web UI, http://127.0.0.1:6276/oauth/callback for the CLI). Then:

bash
# skip OAuth and pass a token you already have
npx @modelcontextprotocol/inspector --cli --transport http \
  --server-url https://mcp.example.com/mcp \
  --header "Authorization: Bearer $TOKEN" --method tools/list

# throw away stored tokens and log in again
npx @modelcontextprotocol/inspector --cli --transport http \
  --server-url https://mcp.example.com/mcp --relogin --method tools/list

Common protocol mistakes#

  • The URL in ChatGPT is missing the /mcp path.
  • OAuth discovery routes return an HTML error page or 500 on a no-auth server. Return 404.
  • CORS preflight (OPTIONS) isn't handled, or Mcp-Session-Id isn't exposed.
  • A stateful server loses sessions behind a load balancer. Use stateless mode or sticky sessions.
  • Tool results include only content when the widget needs structuredContent.
  • Tool results leak internal IDs, timestamps or debug payloads.
  • Tool names or schemas change between deploys, breaking chats that already loaded the old list.

Next steps#

Keep reading