Reach a protected server
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
mcpg inspector auth https://gw.example/mcp
auth sends one deliberately credential-free request and reports:
- the status and the
WWW-Authenticatechallenge; - 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—advertisedwhen the server lists the ID-JAG grant profile,jwt-bearer-onlywhen it lists the JWT bearer grant without it,unsupportedotherwise.issuerbeside it is the value an ID-JAG'saudiencemust 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: DPoPchallenge beside the Bearer one shows it asdpop_challenge. - RAR — the
authorization_detailstypes 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:
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
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.
| Flag | What it does |
|---|---|
--client-id | Use a client you registered — or the https:// URL of your client-metadata document — instead of registering one |
--client-secret, --client-key, --client-auth | Authenticate a confidential client: a secret, a private_key_jwt key (PEM or JWK), and the method when the rest does not decide it |
--registration-token | The initial access token of a server that does not take anonymous registrations |
--scope | Override what to ask for; the default is whatever the challenge named |
--authorization-details | Ask for RFC 9396 details: a JSON array, or @file |
--no-browser | Print 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:
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:
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:
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:
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:
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:
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-Digestis signed by default. Every MCP call isPOST /mcp, so without it the signature would say nothing about which call was made.EdDSAvsEd25519. The JOSEalgis the fully-specifiedEd25519. The polymorphicEdDSAis forbidden by the draft and refused by mcpg, so there is no switch for it. Do not confuse it with the RFC 9421alginsideSignature-Input, a different registry spelled lowercaseed25519— 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_KEYfor the AAuth seed;MCPG_INSPECTOR_BEARERfor--bearer;MCPG_INSPECTOR_CLIENT_SECRET,MCPG_INSPECTOR_IDP_CLIENT_SECRETandMCPG_INSPECTOR_REGISTRATION_TOKENfor the client secrets and the registration token oflogin;--subject-token-fileand--id-jag-file(or-for stdin) for the user's ID token and a ready ID-JAG;--client-key,--idp-client-keyand--dpop-keyare always files;MCPG_INSPECTOR_TOKENwhen attaching a TUI to a running inspector;- the gateway's own token when it supervises a sidecar, which is why
--inspectorneeds 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
- Debug a server — now that you can connect.
- Run it for a team — per-user workspaces and the hosted profile.
- Authorization — what the gateway does with the identity you present.
- Enterprise-managed authorization and interactive sign-in — the gateway side of the two logins.
mcpg inspectorreference — everyloginflag.