mcptest docs GitHub

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:

  1. auth.bearer_token_env (an explicit static token)
  2. auth.oauth (client credentials)
  3. a cached mcptest login token for the server URL
  4. none (the server is reached unauthenticated)

Secret hygiene

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