Auth in tests
How a test suite authenticates to the MCP server(s) it exercises. The guiding rule: secrets never live in the YAML. A suite references a credential by the name of an environment variable; mcptest resolves it at run time and redacts it from every output. You commit the suite; you keep the secret in your shell, your .env, or your CI secret store.
The shapes
A server's auth: block picks how that server is authenticated. Each test names its server, so auth is per-server: a multi-server suite can hit one server with a static token and another with OAuth.
Static bearer token
The simplest case: a token you already have, in an env var.
servers:
api:
url: https://api.example.com/mcp
auth:
bearer_token_env: API_TOKEN # mcptest reads $API_TOKEN, sends `Authorization: Bearer ...`
API_TOKEN=secret-xyz mcptest run
Use this when a CI secret or a long-lived token is already available in the environment. Nothing is stored; the token is read once at startup.
OAuth client credentials (machine-to-machine)
For server-to-server auth with no browser, declare the client-credentials grant. mcptest exchanges it for an access token at connect time and uses that token for the run.
servers:
api:
url: https://api.example.com/mcp
auth:
oauth:
grant: client_credentials
client_id: my-service
token_endpoint: https://auth.example.com/oauth/token
client_secret_env: API_CLIENT_SECRET # the secret, by env-var name
scope: "mcp.read mcp.write" # optional
API_CLIENT_SECRET=... mcptest run
The token is fetched once per server per run and reused for every test, so a large suite makes one token call, not one per request. "Setup" is just the secret being present in the environment; "teardown" is automatic, since the token is ephemeral and never written to disk.
Interactive login (developer machines)
For an OAuth server that needs a human (authorization-code flow), authenticate once out of band:
mcptest login --url https://api.example.com/mcp
mcptest login discovers the authorization server (RFC 9728 + RFC 8414), registers a client (dynamic client registration), runs the PKCE authorization-code dance in your browser, and caches the token under ~/.mcptest/auth/. A later mcptest run against the same URL picks up the cached token automatically, with no auth: block needed. This is the developer-machine path; for CI prefer client credentials or a static token.
Suite-level default
To avoid repeating the same auth: block on every server, declare a default at the top level. Any URL server that does not set its own auth: inherits it; a server's own auth: overrides the default as a whole (no partial merge).
auth: # the default for every URL server
oauth:
grant: client_credentials
token_endpoint: https://auth.example.com/oauth/token
client_id: my-service
client_secret_env: API_CLIENT_SECRET
servers:
reports:
url: https://reports.example.com/mcp # inherits the default above
admin:
url: https://admin.example.com/mcp
auth: # overrides: this server uses a static token
bearer_token_env: ADMIN_TOKEN
Precedence
When more than one source could apply to a server, the highest wins:
auth.bearer_token_env(an explicit static token)auth.oauth(client credentials)- a cached
mcptest logintoken for the server URL - none (the server is reached unauthenticated)
Secret hygiene
- A secret is referenced by env-var name, never written inline. There is no field that takes a literal secret value.
- The resolved token and the client secret are redacted from every reporter (pretty, json, junit, and the rest), from debug logs, and from error messages. The audit test runs an authenticated suite under
--debugand asserts that neither the client secret nor the minted bearer appears in any output stream. - Recorded cassettes store JSON-RPC envelopes, never HTTP headers, so the
Authorizationheader (the bearer) is never written to a cassette at all. A recording of an authenticated run is secret-free by construction.
Offline replay of an authenticated run
Because mcptest can replay a recorded cassette, an authenticated suite records once against the live server and then replays forever with no network and no secret:
# Record once, with the credential present.
API_CLIENT_SECRET=... mcptest run --record
# Replay in CI with a cassette: server. No secret, no token endpoint, no network.
mcptest run --config replay.yaml
See Cassettes for the recorder and the cassette: server source.
Custom and non-OAuth flows
When a credential needs a custom mint (a signed request, a non-standard endpoint, a one-off provisioning call), mint it before the run and pass it through bearer_token_env. A server's connection auth is resolved from the environment at startup, so the mint has to happen first, in your shell or CI step, not in an in-suite hook:
# Mint the credential however your system requires, then run.
export API_TOKEN="$(./scripts/mint-token.sh)"
mcptest run
# ... and revoke it afterward.
./scripts/revoke-token.sh "$API_TOKEN"
servers:
api:
url: https://api.example.com/mcp
auth:
bearer_token_env: API_TOKEN # the freshly-minted credential
The beforeAll: / afterAll: hooks run after the servers connect, so they are the place for setup and teardown operations (seed a fixture record before the tests, delete it after) and for injecting env or vars that tests consume, not for providing a server's connection credential. Keep the mint/revoke of the connection token in the wrapper around mcptest run, as above.
Status
- Static
bearer_token_env, the cachedmcptest logintoken,auth.oauthclient credentials (machine-to-machine, at connect), the suite-levelauth:default with per-server override, secret redaction across reporters/logs, and the header-free cassette guarantee all work today. - An expired cached
mcptest logintoken is now refreshed in place at connect (using its refresh token and the endpoint stamped at login), so logging in once keeps later runs working without a re-login. A client-credentials token is minted fresh each run, so it never goes stale mid-run. - A literal refresh-on-401 mid-run (a token expiring partway through a single long run) is the one remaining edge case; it is rare given the above and is tracked as a follow-up. Token-endpoint auto-discovery for the client-credentials grant (so
token_endpointcould be omitted) is also a follow-up; specify it for now.