Testing with MCP Inspector

TL;DRnpx @modelcontextprotocol/inspector http://localhost:8080/mcp runs the full OAuth flow against your server in the browser, exercising PRM discovery, RFC 8414 metadata, DCR, PKCE, authorize, consent, and tool calls with a real bearer token. If Inspector works, your server is spec-compliant. Every scenario Inspector exercises is also covered by AuthPlane’s automated E2E suite (e2e/scenarios/mcp_inspector_test.go), so failures usually indicate operator misconfiguration, not spec drift.

Why Inspector first

MCP Inspector is the reference test client. It exercises every part of the spec MCP servers implement — the same wire moves Claude Desktop, Cursor, and VS Code Copilot Chat make — but with a debug UI that shows you every request, response, and token claim as they happen.

For AuthPlane specifically, Inspector is the fastest way to answer “is my SDK adapter wired correctly?” because:

  • It runs the full OAuth flow against your PRM + AuthPlane in the browser.
  • It shows the discovered PRM document and AS metadata inline.
  • It shows the decoded access token so you can verify aud, scope, cnf.jkt.
  • Every failing step surfaces the raw WWW-Authenticate challenge and error body.

Install and run

npx @modelcontextprotocol/inspector http://localhost:8080/mcp
  • URL must include the /mcp path (or whatever endpoint you configured — some SDKs default to something else). http://localhost:8080 alone points at the server root, and Inspector reports “no tools”.
  • Uses npx — no install step, always pulls the latest.
  • Opens http://localhost:6274 (Inspector’s own UI) in your default browser.

For a remote server:

npx @modelcontextprotocol/inspector https://mcp.example.com/mcp

What Inspector does — step by step

Inspector runs the same seven-step OAuth flow every MCP client does. Watching each step in Inspector’s UI is the fastest way to spot where things break.

1. Trigger the challenge → fetch PRM

Inspector first hits your MCP endpoint unauthenticated, gets 401 WWW-Authenticate with a resource_metadata URL, then follows that URL:

POST https://mcp.example.com/mcp                    # no Authorization → 401 challenge
GET  https://mcp.example.com/.well-known/oauth-protected-resource/mcp    # path-scoped (RFC 9728 §3)

Inspector shows the response inline. Verify:

  • resource matches the URL you gave Inspector, byte-for-byte
  • authorization_servers contains a URL for your AuthPlane instance
  • scopes_supported lists your resource’s declared scopes

If the initial POST /mcp returns a bare 401 without WWW-Authenticate, your SDK’s auth middleware is bypassed — the SDK generates that challenge automatically. If PRM is 404 at the path-scoped URL, your SDK didn’t mount the PRM handler (Go only — Python/TS auto-mount). Fix: http.Handle(adapter.WellKnownPRMPath(), adapter.ProtectedResourceMetadataHandler()).

2. Fetch AS metadata

GET https://auth.example.com/.well-known/oauth-authorization-server

Inspector uses authorization_servers[0] from the PRM. Verify:

  • issuer matches the URL Inspector is calling (no mismatch — one character off = every future token rejected)
  • token_endpoint, authorization_endpoint, registration_endpoint, jwks_uri all present
  • grant_types_supported includes authorization_code
  • code_challenge_methods_supported: ["S256"] (no plain — AuthPlane rejects it)

3. DCR — register itself as a client

POST https://auth.example.com/oauth/register
{
  "redirect_uris": ["http://localhost:6274/oauth/callback"],
  "token_endpoint_auth_method": "none",
  ...
}

Inspector uses token_endpoint_auth_method: none (public client + PKCE). AuthPlane returns a client_id.

If DCR fails with 403 access_denied: dcr.mode is admin_only. Either bump to approved_redirects with http://localhost:* in the list, or open for local dev.

4. Authorize + PKCE

Inspector redirects your browser to:

GET https://auth.example.com/oauth/authorize?response_type=code
    &client_id=…
    &redirect_uri=http://localhost:6274/oauth/callback
    &code_challenge=…&code_challenge_method=S256
    &resource=https://mcp.example.com/mcp
    &scope=<discovered from PRM>
    &state=…

You land on the AuthPlane login page → log in → consent screen → approve.

AuthPlane shows the scopes the client is requesting with their descriptions (pulled from the resource’s scopes config). Approve.

6. Token exchange

POST https://auth.example.com/oauth/token
grant_type=authorization_code&code=…&code_verifier=…

Inspector receives access_token, refresh_token, token_type. Its UI decodes and displays the JWT payload:

  • sub — your user
  • aud — should equal your resource URI
  • scope — should include what you approved on the consent screen
  • iss — should equal AuthPlane’s issuer
  • exp — 15 minutes ahead by default
  • cnf.jkt — only if DPoP is enabled AND Inspector sent a DPoP header (Inspector doesn’t do DPoP as of 0.14; token stays as bearer)

7. Tool calls

POST https://mcp.example.com/mcp
Authorization: Bearer <token>
{ "method": "tools/call", "params": { "name": "read", ... } }

Inspector’s UI lets you invoke tools and shows the response. This is where scope enforcement, resource binding, and your handler logic all run for real.

AuthPlane’s coverage — Inspector is under E2E

Every scenario above is exercised by e2e/scenarios/mcp_inspector_test.go in the authserver repo. The E2E suite spins up a real AuthPlane instance, runs Inspector programmatically, and asserts on the full wire flow.

ScenarioE2E test
PRM + AS metadata discoveryTestMCPInspector_MetadataDiscovery
Dynamic Client RegistrationTestMCPInspector_DCR
PKCE (S256; plain rejected)TestMCPInspector_PKCEFlow
Token exchange (auth-code → tokens)TestMCPInspector_TokenExchange
Refresh token rotationTestMCPInspector_TokenRefresh
Tool call with Bearer tokenTestMCPInspector_ListTools

If Inspector works against your AuthPlane instance, you’re passing the same conformance tests AuthPlane’s CI runs on every PR.

Debugging with Inspector’s request panel

Inspector shows every request/response in a side panel. Useful patterns:

  • PRM 404 → look at the .well-known/oauth-protected-resource request. Response body will be your server’s 404 page.
  • Discovery works but authorize 500s → response body has the error. Common: session.secret not set in production config.
  • Consent approves but /oauth/token returns invalid_grantcode_verifier mismatch (client bug — unlikely for Inspector) OR the auth code expired (AuthCodeTTL = 10 minutes — if you really took your time on consent, or your client clock is skewed).
  • Tool call returns 401 → response WWW-Authenticate header tells you why. error="invalid_token" = signature/aud/exp. error="insufficient_scope" = scope missing.
  • Tool call returns “unknown tool” despite the tool existing → PrefectHQ FastMCP filters unauthorized tools OUT of tools/list — check the token’s scope claim against what require_scope demands.

Using Inspector’s CLI mode

Non-interactive form for scripting:

npx @modelcontextprotocol/inspector --cli https://mcp.example.com/mcp \
    --bearer-token <pre-obtained-token>

Skips the OAuth flow — you provide a token you got another way (e.g., from curl). Useful for CI smoke tests where the OAuth flow can’t run interactively.

Common false alarms

Not every “failure” is a real bug in your setup.

  • DPoP not verified in Inspector — Inspector 0.14.x doesn’t construct DPoP proofs. Even with dpop.enabled: true on AuthPlane and inbound_dpop: {required: true} on your SDK, Inspector’s tokens are plain bearer. To verify DPoP end-to-end, use a client that does construct proofs (a real MCP client, or your own SDK-based test client).
  • 403 on the first tool call from Claude Code but Inspector works fine — Claude Code’s known bug: omits scope on /authorize. See Guides: Connect an MCP client → Known Claude Code quirks. Fix: AUTHPLANE_OAUTH_REQUIRE_SCOPE=false.
  • Inspector connects but you don’t see the tool you just added — check that you’re on the latest revision of your MCP server; Inspector caches tools/list per-session but re-fetches on reconnect.