mcptest docs GitHub

Headless auth

How to authenticate against an OAuth-protected MCP server when no browser exists: a coding agent, a CI job, or any other non-interactive caller. The short version: provision a token into an env var before the loop starts, name that env var everywhere, and use mcptest doctor when a 401 or 403 appears.

The decision tree

  1. A token already exists (issued by the IdP, a service account, or a human who logged in elsewhere): export it into an env var and name the var via bearer_token_env. This is the headless path; everything below shows where the name goes.
  2. The server's IdP supports the client-credentials grant (machine to machine): declare auth.oauth with grant: client_credentials in the suite YAML. mcptest exchanges the client id and secret for a token at run time with no human involved. See Auth in tests.
  3. The IdP advertises device_authorization_endpoint (RFC 8628) and the machine has no browser at all (SSH box, container): run mcptest login --device. mcptest prints a verification URL and a short code, a human opens the URL on any other device and enters the code, and mcptest polls until the grant lands. See the section below.
  4. Only the authorization-code (browser) flow exists: a human must run mcptest login <url> once. --no-browser prints the authorization URL for copy-paste instead of opening a browser. The resulting token is cached and refreshed automatically; headless runs on the same machine reuse it.

Naming the env var

One token, one env var, four surfaces that accept the name:

# Suite YAML: the runner attaches the token to every request.
servers:
  api:
    url: https://mcp.example.com
    auth:
      bearer_token_env: MCP_TOKEN
# Imperative CLI commands take the same name as a flag.
mcptest tools --url https://mcp.example.com --bearer-token-env MCP_TOKEN
mcptest doctor --url https://mcp.example.com --bearer-token-env MCP_TOKEN
// mcp-server agent verbs (list_tools, scaffold_suite, propose_assertions, ...)
{ "url": "https://mcp.example.com", "bearer_token_env": "MCP_TOKEN" }

The value never appears in YAML, flags, or reports; only the name travels. .env files are loaded automatically (see Authentication).

Refresh mid-run (already shipped)

Tokens minted by mcptest login or the client-credentials grant are cached under ~/.mcptest/auth/ with their absolute expiry and refresh token. The runner refreshes proactively near expiry and once on a 401, under a file lock so parallel runs do not race. Nothing to configure. Details in Auth: OAuth refresh.

When no token exists: what the agent should ask for

An agent that hits the 401 below and has no way to mint a token should stop and ask the human for exactly one thing:

A bearer token for <server URL>, exported into an env var (for example MCP_TOKEN), or credentials for a client-credentials grant (client id, secret env var, token URL). If only browser login exists, please run mcptest login <server URL> once on this machine.

Do not retry without new credentials, and never paste a token into YAML or a command line; pass the env var name instead.

What a failure looks like

Every front-door verb and introspection command reports an auth rejection as one actionable message, not a bare 401:

auth failed: HTTP 401 from https://mcp.example.com, server advertises
Bearer (realm="mcp"). env var `MCP_TOKEN` (named by `bearer_token_env`) is
not set or is empty in this process; export a valid token into it.
Diagnose with: mcptest doctor --url https://mcp.example.com --bearer-token-env MCP_TOKEN

The message carries the status, the scheme the server advertised in WWW-Authenticate (including RFC 9728 resource_metadata when present), which input to set, and the doctor one-liner.

Diagnosing with doctor

mcptest doctor --url https://mcp.example.com --bearer-token-env MCP_TOKEN

The AUTH layer of the report distinguishes the credential states, one hint each:

StateHint says
No credentials suppliedpass --bearer-token-env <VAR> or run mcptest login <url>
Env var named but unset or emptythe var name, and to export a token into it
Token sent, server says 401the token was rejected; mint a fresh one or re-login
Token sent, server says 403the token lacks permission; check scope or audience
Cached mcptest login token expiredrun mcptest login <url> to refresh it

Device-code flow (RFC 8628): mcptest login --device

When the IdP advertises device_authorization_endpoint in its RFC 8414 metadata, mcptest login --device mints a token without a local browser or loopback listener:

mcptest login --device --url https://mcp.example.com

The command prints a verification URL and a short code:

To sign in, open this URL on any device:
  https://idp.example.com/activate
and enter the code: WDJB-MJHT

(IdPs that return verification_uri_complete get it printed on its own line, with the code already embedded.) mcptest then polls the token endpoint with the device-code grant until the human approves:

Discovery is the same RFC 9728 + RFC 8414 walk the browser flow uses, and the resulting token caches and refreshes exactly like a PKCE one. If the IdP does not advertise the endpoint, the command says so and suggests mcptest login --no-browser instead.

Two caveats: