Inspector
Inspectorbeta

Reach a protected server

rc
v0.1.0-rc.14

Find out why a server refused you, walk the OAuth discovery chain including the step that failed, get a token with a browser sign-in or with an ID-JAG from your enterprise IdP, bind it to a DPoP key, or present an AAuth agent identity.

A refusal is not one problem. "No OAuth here" and "it advertises OAuth but its authorization server could not be read" look identical from a client, and have completely different fixes. The auth lab exists to tell them apart.

What came back, and why

bash
mcpg inspector auth https://gw.example/mcp

auth sends one deliberately credential-free request and reports:

  • the status and the WWW-Authenticate challenge;
  • every step of the RFC 9728 → RFC 8414 discovery walk, including the one that failed;
  • a one-line verdict naming the next action.

A half-walked chain reads as a half-walked chain. That is the whole point — it needs no connected session, because the case you care about is the one where connecting failed.

When the chain reaches an authorization server, the report also says how a client can get in:

  • enterprise_managed_authorization — advertised when the server lists the ID-JAG grant profile, jwt-bearer-only when it lists the JWT bearer grant without it, unsupported otherwise. issuer beside it is the value an ID-JAG's audience must carry, byte for byte.
  • DPoP — the proof algorithms the server takes, and, from the resource's own metadata, whether it takes only DPoP-bound tokens. A 401 that carries a WWW-Authenticate: DPoP challenge beside the Bearer one shows it as dpop_challenge.
  • RAR — the authorization_details types the server and the resource take.
  • The authorization endpoint, dynamic registration, client-metadata documents, client authentication methods, and whether the server names itself in every authorization response (RFC 9207).

The verdict turns that into the next step: which logins the server takes, and whether you need --dpop-key.

It also reports AAuth posture, which lives in a separate channel: AAuth-Requirement, Signature-Error, and the target's /.well-known/aauth-resource.json.

In the web UI this is the first diagnose tab; in the TUI, the diagnose screen.

Getting a token

Once auth has found an authorization server, login gets you a token — with a browser, or without one from your enterprise IdP. Either way the access token is printed on stdout and nothing else is, so it passes straight back:

bash
TOKEN="$(mcpg inspector login https://gw.example/mcp)"
mcpg inspector list https://gw.example/mcp tools --bearer "$TOKEN"

--token-file PATH writes it to a file only you can read instead, and prints no token at all. The token is audience-bound to this server via RFC 8707 resource indicators, so it is not replayable at another resource that trusts the same authorization server.

With a browser

bash
mcpg inspector login https://gw.example/mcp

It registers a client dynamically (RFC 7591), opens a browser for an authorization-code grant with PKCE, catches the redirect on loopback, checks that the redirect's iss is this authorization server (RFC 9207 — a redirect from another one is a mix-up, and its code is never used), and exchanges the code.

FlagWhat it does
--client-idUse a client you registered — or the https:// URL of your client-metadata document — instead of registering one
--client-secret, --client-key, --client-authAuthenticate a confidential client: a secret, a private_key_jwt key (PEM or JWK), and the method when the rest does not decide it
--registration-tokenThe initial access token of a server that does not take anonymous registrations
--scopeOverride what to ask for; the default is whatever the challenge named
--authorization-detailsAsk for RFC 9396 details: a JSON array, or @file
--no-browserPrint the URL instead of opening one — for SSH sessions

Against an mcpg gateway with interactive sign-in, this is the gateway's consent page and then your IdP. A client the operator registered with a loopback redirect URI (http://127.0.0.1/callback) works on whatever port the inspector listens on:

bash
mcpg inspector login https://gw.example/mcp --client-id acme-mcp-cli

From your enterprise IdP (ID-JAG)

Enterprise-managed authorization — Okta's Cross App Access — reaches a server with the identity your IdP already holds, and no browser. Hand login the user's ID token and your IdP's token endpoint:

bash
export MCPG_INSPECTOR_CLIENT_SECRET="$MCPG_TEST_SECRET"      # mcpg-test at the MCP server
export MCPG_INSPECTOR_IDP_CLIENT_SECRET="$TEST_AGENT_SECRET" # the test agent at the IdP
mcpg inspector login https://gw.example/mcp \
  --client-id mcpg-test \
  --idp-token-url https://acme.okta.com/oauth2/v1/token \
  --idp-client-id "$TEST_AGENT_ID" \
  --subject-token-file - < id_token.jwt

It exchanges the ID token at the IdP for an ID-JAG addressed to the server's authorization server (RFC 8693), then redeems the ID-JAG there with the JWT bearer grant (RFC 7523). --client-id is the client as registered at the MCP server's authorization server — the ID-JAG names it, so it is required — and --client-secret (or MCPG_INSPECTOR_CLIENT_SECRET) or --client-key authenticate it there, as that client is registered. The IdP client has its own flags: --idp-client-id, --idp-client-secret (or MCPG_INSPECTOR_IDP_CLIENT_SECRET), --idp-client-key; its assertion is addressed to the token endpoint, which is what Okta expects.

The ID token must be one the IdP issued to that IdP client: Okta exchanges an ID token only for the app it was issued to. Against an mcpg gateway with Okta, Test with your own agent sets up the test agent, the mcpg-test client and the sign-in that yields id_token.jwt.

Already have an ID-JAG, from xaa.dev or your own IdP call? Redeem it as it is:

bash
export MCPG_INSPECTOR_CLIENT_SECRET="$MCPG_TEST_SECRET"
mcpg inspector login https://gw.example/mcp \
  --client-id mcpg-test --id-jag-file id-jag.jwt

Before redeeming, the inspector reads the ID-JAG's claims and warns about what the server will refuse: an aud that is not the issuer (a trailing slash makes another issuer), a client_id that is not yours, a resource that is not this server, an expired grant. A refusal comes back with those warnings attached, so "invalid_grant" arrives with a reason. --json shows the grant's iss, aud, client_id, resource, scope and expiry — never the grant itself.

Bound to a key (DPoP)

A server that binds tokens to a key (RFC 9449) is signed into with --dpop-key:

bash
mcpg inspector login https://gw.example/mcp --dpop-key ./dpop.jwk
mcpg inspector list https://gw.example/mcp tools --bearer "$TOKEN" --dpop-key ./dpop.jwk

The first login creates the key file, readable by you only. The code, the token request and every later MCP request carry a proof of that key, and a server nonce is fetched and used when the server asks for one. Keep the key with the token: a DPoP-bound token without its key is refused. When the resource takes only DPoP-bound tokens, login says so before opening a browser.

AAuth: an agent identity

AAuth gives an agent a portable cryptographic identity — aauth:local@domain bound to an Ed25519 key — and signs every request with it, so a captured credential is useless without the key. mcpg verifies it through the dev.mcpg.identity.aauth plugin; the inspector can present it.

There is no registration step. Trust is domain control plus a published key, so getting an identity means generating one and serving two static documents:

bash
mcpg inspector aauth-keygen \
  --agent aauth:inspector@yourdomain.example \
  --publish ./site

That writes ./site/.well-known/aauth-agent.json and ./site/.well-known/jwks.json, and prints the private seed. Serve the two documents under https://yourdomain.example, then sign with:

bash
mcpg inspector list https://gw.example/mcp tools \
  --aauth-key "$MCPG_INSPECTOR_AAUTH_KEY" \
  --aauth-agent aauth:inspector@yourdomain.example \
  --aauth-issuer https://yourdomain.example

--aauth-key reads MCPG_INSPECTOR_AAUTH_KEY, so the seed need not appear in a process listing. If you already have an agent token from an agent provider, pass it with --aauth-token and the token's own sub names the agent.

The inspector implements the identity-based access mode — the agent token is the credential — which is the mode mcpg verifies. The modes that require a person server and a human approval are reported by auth but not driven.

Two details that decide whether it works

  • Content-Digest is signed by default. Every MCP call is POST /mcp, so without it the signature would say nothing about which call was made.
  • EdDSA vs Ed25519. The JOSE alg is the fully-specified Ed25519. The polymorphic EdDSA is forbidden by the draft and refused by mcpg, so there is no switch for it. Do not confuse it with the RFC 9421 alg inside Signature-Input, a different registry spelled lowercase ed25519 — this is the most common interop failure in AAuth.

Keeping credentials off the command line

A command line is readable by every process on the box. Where a credential is involved, the inspector takes it from the environment or a file instead:

  • MCPG_INSPECTOR_AAUTH_KEY for the AAuth seed;
  • MCPG_INSPECTOR_BEARER for --bearer;
  • MCPG_INSPECTOR_CLIENT_SECRET, MCPG_INSPECTOR_IDP_CLIENT_SECRET and MCPG_INSPECTOR_REGISTRATION_TOKEN for the client secrets and the registration token of login;
  • --subject-token-file and --id-jag-file (or - for stdin) for the user's ID token and a ready ID-JAG; --client-key, --idp-client-key and --dpop-key are always files;
  • MCPG_INSPECTOR_TOKEN when attaching a TUI to a running inspector;
  • the gateway's own token when it supervises a sidecar, which is why --inspector needs nothing pasted.

--help never shows the value of a secret's environment variable, and when a server's error echoes a secret login sent, the error shows [redacted] in its place.

Next steps