Testing with MCP Inspector
TL;DR —
npx @modelcontextprotocol/inspector http://localhost:8080/mcpruns 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-Authenticatechallenge and error body.
Install and run
npx @modelcontextprotocol/inspector http://localhost:8080/mcp
- URL must include the
/mcppath (or whatever endpoint you configured — some SDKs default to something else).http://localhost:8080alone 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:
resourcematches the URL you gave Inspector, byte-for-byteauthorization_serverscontains a URL for your AuthPlane instancescopes_supportedlists 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:
issuermatches the URL Inspector is calling (no mismatch — one character off = every future token rejected)token_endpoint,authorization_endpoint,registration_endpoint,jwks_uriall presentgrant_types_supportedincludesauthorization_codecode_challenge_methods_supported: ["S256"](noplain— 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.
5. Consent
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 useraud— should equal your resource URIscope— should include what you approved on the consent screeniss— should equal AuthPlane’s issuerexp— 15 minutes ahead by defaultcnf.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.
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-resourcerequest. Response body will be your server’s 404 page. - Discovery works but authorize 500s → response body has the error. Common:
session.secretnot set in production config. - Consent approves but
/oauth/tokenreturnsinvalid_grant→code_verifiermismatch (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-Authenticateheader 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 whatrequire_scopedemands.
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: trueon AuthPlane andinbound_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). 403on the first tool call from Claude Code but Inspector works fine — Claude Code’s known bug: omitsscopeon/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/listper-session but re-fetches on reconnect.
Related
- Quickstart — walks through Inspector as the smoke test at the end
- Guides: Connect an MCP client — Claude Desktop / Cursor / VS Code configs
- Guides: Enable DPoP end-to-end — why Inspector doesn’t help verify DPoP
- Compatibility matrix — the full list of what Inspector tests
- Reference: Errors — decoding the responses Inspector shows you
- Troubleshooting: Debugging — when Inspector fails but you don’t know at which step