Authentication
How to point mcptest at a server that requires credentials, without ever putting a secret in a YAML file or a CI log. This page is the authoritative reference for the auth surface in v1.0.
Which auth do I need?
Most authentication questions answer themselves once you know two things: what transport the server uses, and whether you control the deployment.
┌───────────────────────────────────┐
│ Is the server a local subprocess │
│ (stdio transport) or a URL? │
└─────────────────┬─────────────────┘
│
┌─────────────────┴──────────────────┐
│ │
▼ ▼
stdio target URL target
│ │
▼ ▼
No auth needed. ┌────────────────────┐
The transport is │ Is the URL on a │
the trust boundary. │ developer machine, │
Skip the rest of │ or behind real │
this page. │ auth? │
└─────────┬──────────┘
│
┌──────────────────┴──────────────────┐
│ │
▼ ▼
Open dev URL (no auth): Production / staging
http://localhost:8000, URL behind real auth
ngrok preview without (Cloudflare Access,
protection. Skip the rest IAP, OAuth, API
of this page. gateway, custom).
│
▼
┌──────────────────────┐
│ Does the server │
│ accept a long-lived │
│ bearer token, or do │
│ you log in to get a │
│ short-lived one? │
└──────────┬───────────┘
│
┌───────────────────┴─────────────────┐
│ │
▼ ▼
Long-lived bearer token Login required
(service account, PAT, (interactive user,
CI deploy key). SSO behind it).
│ │
▼ ▼
Use `auth.bearer_token_env:` Use `auth.oauth:` plus
and put the value in an `mcptest login` to
env var. See populate the token
[Bearer token via env var] cache. See
(#bearer-token-via-env-var). [OAuth 2.1 + PKCE]
(#oauth-21--pkce).
The two leaves under "production URL" are not mutually exclusive across servers. A suite that talks to two URL servers may use a bearer token for one and OAuth for the other. Each entry under servers: configures its own auth.
If the same server accepts both bearer tokens and OAuth, prefer the bearer form. It is simpler, has fewer moving parts, and runs unattended in CI without a logged-in human anywhere in the loop. Save OAuth for servers that demand it.
Bearer token via env var
The most common production setup is a server that accepts a long-lived bearer token. The token lives in an env var; the YAML points at the env var by name. mcptest reads the var at request time and sends Authorization: Bearer <value> for you.
servers:
remote_api:
url: "https://mcp.example.com/v1"
auth:
bearer_token_env: "MCPTEST_REMOTE_API_TOKEN"
Required field:
bearer_token_env(string). Name of the env var holding the token. The token itself never appears in the YAML.
The runner promotes the resolved value to the redaction registry the moment it reads it, so the token never appears in reporter output, cassette dumps, or doctor logs. The redaction policy is documented at docs/security/redaction.md.
Env var setup, local
For local development, export the var in your shell or put it in a .env file next to mcptest.yml:
# in your shell
export MCPTEST_REMOTE_API_TOKEN="sk-live-..."
mcptest run
# or in a .env file (loaded automatically by dotenvy)
echo 'MCPTEST_REMOTE_API_TOKEN=sk-live-...' >> .env
mcptest run
The .env file should be in your .gitignore. Commit a .env.example with the variable names but no values so other developers know which vars they need.
CI integration
mcptest reads the env var from whatever process the CI runner spawned it under. Every major CI platform has a way to expose secrets as env vars. Three worked examples follow.
GitHub Actions
# .github/workflows/mcptest.yml
name: mcptest
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: cargo install mcptest --locked
- run: mcptest run
env:
MCPTEST_REMOTE_API_TOKEN: ${{ secrets.MCPTEST_REMOTE_API_TOKEN }}
Define the secret at the repository level under Settings, Secrets and variables, Actions. The expression ${{ secrets.NAME }} is the GitHub Actions equivalent of "read this from the encrypted secret store and expose it to this step only."
GitLab CI
# .gitlab-ci.yml
mcptest:
image: rust:1.78
script:
- cargo install mcptest --locked
- mcptest run
variables:
MCPTEST_REMOTE_API_TOKEN: $MCPTEST_REMOTE_API_TOKEN
GitLab masks variables marked "Masked" in the project's CI settings. Mark every secret variable as masked so it does not echo into job logs.
CircleCI
# .circleci/config.yml
version: 2.1
jobs:
mcptest:
docker:
- image: cimg/rust:1.78
steps:
- checkout
- run: cargo install mcptest --locked
- run: mcptest run
workflows:
test:
jobs:
- mcptest:
context: mcptest-secrets
The context block (here mcptest-secrets) is a CircleCI primitive for sharing secrets across jobs. Define the context once in the organization settings, list the env var there, and any job that opts into the context inherits it.
Token rotation
Long-lived tokens should rotate on a schedule. mcptest does not rotate for you; it reads whatever value the env var holds at request time. The recommended pattern is to issue a new token, set it in your CI secret store, redeploy the env, and revoke the old one a day or two later. The runner picks up the new value on the next run with no code change.
If you want the rotation to be invisible to your YAML (rotating tokens with versioned names like MCPTEST_TOKEN_V3), use ${SECRET_VAR_NAME} interpolation in the bearer_token_env field itself:
servers:
api:
url: "https://api.example.com/mcp"
auth:
bearer_token_env: "${MCPTEST_ACTIVE_TOKEN_VAR}"
Now the env var named in MCPTEST_ACTIVE_TOKEN_VAR is the one that gets read. Change MCPTEST_ACTIVE_TOKEN_VAR=MCPTEST_TOKEN_V4 to switch.
OAuth 2.1 + PKCE
For URL servers behind an interactive identity provider, mcptest implements OAuth 2.1 with PKCE per the MCP specification. The model:
- You run
mcptest login <server-name>once per developer machine. - The runner opens a browser, walks the OAuth flow with PKCE, and writes the resulting access and refresh tokens to a per-server file under
~/.mcptest/auth/. - Subsequent
mcptest runinvocations read the cache, refresh on 401, and present the access token as a bearer header on each request.
The login flow lands with the runner work. The token cache and refresh-on-401 layer already shipped ( in docs/auth-refresh.md), so the cache layout this page describes is real today even though the login command is not yet wired up. Author your YAML against the shape below; it will work the day the login command lands.
The YAML side
servers:
remote_api:
url: "https://mcp.example.com/v1"
auth:
oauth:
client_id_env: "MCPTEST_OAUTH_CLIENT_ID"
authorization_url: "https://auth.example.com/oauth/authorize"
token_url: "https://auth.example.com/oauth/token"
scopes:
- "mcp:read"
- "mcp:invoke"
Required fields under oauth:
client_id_env(string). Env var holding the OAuth client ID. The matching client secret resolves from${client_id_env}_SECRETby convention. PKCE-only public clients can leave the secret env var unset.authorization_url(string). RFC 6749 section 3.1 endpoint.token_url(string). RFC 6749 section 3.2 endpoint.
Optional:
scopes(array of strings). Scopes to request. Omit when the authorization server has a default.
Dynamic Client Registration (RFC 7591)
When your authorization server supports RFC 7591, mcptest will register itself automatically on first login. You skip the manual "create an app" step on the IdP, and client_id_env is populated behind the scenes; you only need to point authorization_url and token_url at the right endpoints.
The intended flow:
mcptest login remote_api- The CLI POSTs an RFC 7591 client metadata document to the server's registration endpoint (discovered via the standard
/.well-known/oauth-authorization-servermetadata). - The IdP returns a client ID (and optionally a client secret), which mcptest writes into the token cache alongside the access and refresh tokens.
- The PKCE flow proceeds with the freshly issued client ID.
If RFC 7591 is unavailable on your IdP, fall back to the manual "create an app, paste the client ID into an env var" workflow. The two paths coexist; the runner uses whichever metadata it finds.
The mcptest login command
# log into a single server
mcptest login remote_api
# log into every URL server that uses oauth
mcptest login --all
# log out (delete the cached tokens)
mcptest logout remote_api
Behind the scenes mcptest login:
- Reads the
oauth:block for the named server from your YAML. - Spawns a temporary listener on
127.0.0.1:<random port>to catch the redirect. - Opens your default browser at the authorization URL with a
code_challengederived from a fresh PKCE verifier. - Catches the redirect, exchanges the code at the token endpoint, and writes the result to the per-server cache file.
The default browser opens via xdg-open on Linux, open on macOS, and start on Windows. If the browser cannot open (headless CI, locked-down desktop), the CLI prints the authorization URL and waits for you to paste back the redirected URL.
Refresh on 401 (shipped)
While mcptest run is executing, the cached access token may expire mid-suite. The refresh layer handles this transparently:
- The server returns 401.
- The runner reads the cached refresh token.
- The runner POSTs to
token_urlwithgrant_type=refresh_tokenand the cached refresh token. - On success, the cache is rewritten atomically and the original request retries once with the new access token.
- On failure (refresh token revoked, IdP unreachable, server still returns 401 with the new token), the runner aborts with a clear error and a hint to re-run
mcptest login.
The refresh path holds an advisory file lock so two concurrent runs against the same server do not duplicate the refresh request. See docs/auth-refresh.md for the full mechanism.
Token cache at ~/.mcptest/auth/
~/.mcptest/auth/
<sha256(server_url)>.json # cached Token document, chmod 0600 on Unix
<sha256(server_url)>.lock # advisory lock, sibling to the cache
One file per MCP server URL. The filename is a hex SHA-256 of the URL so two servers cannot collide and the filename is filesystem-safe.
The JSON content (see ):
{
"access_token": "...",
"refresh_token": "...",
"expires_at_unix": 1700000000,
"token_type": "Bearer",
"scope": "mcp:read",
"last_refreshed": "2026-05-15T00:00:00Z"
}
Move the cache to a different root with TokenCache::with_root(path) in tests, or by setting MCPTEST_AUTH_DIR in the environment.
OAuth in CI
CI is the awkward case for OAuth: nobody is sitting at a browser when a build runs. Three workable strategies:
- Pre-populate the cache. Log in once on a controlled host, copy
~/.mcptest/auth/<hash>.jsoninto a CI secret, and write it to the runner beforemcptest run. The refresh path will keep it alive for as long as the refresh token is valid. - Switch to a bearer token for CI. Many IdPs let you mint a long-lived machine token alongside the interactive OAuth flow. Use the bearer-token form for CI, and the OAuth form for local dev. Two
servers:entries pointing at the same URL, picked via--profile. - Use a CI-only OAuth client with service account credentials. Some IdPs support a non-interactive grant for machine clients (client credentials flow). Configure the OAuth block to use that grant type; no human in the loop. v1.0 only ships the interactive PKCE flow; the client credentials grant is on the roadmap.
Pick whichever fits your IdP and audit posture.
Custom headers
For auth schemes that are not bearer tokens or OAuth (Cloudflare Access, GCP IAP signed JWTs, AWS API Gateway API keys), use the headers: block on the server. The headers: block sits at the server level, not under auth:, because it serves both auth and non-auth use cases (tenant routing, tracing IDs). The full shape is documented in docs/yaml-reference.md#custom-headers-and-http-transport. The relevant rules for auth:
servers:
protected:
url: "https://mcp.example.com/v1"
headers:
X-API-Key:
env: PROTECTED_API_KEY
CF-Access-Client-Id:
env: CF_ACCESS_CLIENT_ID
CF-Access-Client-Secret:
env: CF_ACCESS_CLIENT_SECRET
Each entry maps a header name to a value. The value is either a literal string (with ${VAR} interpolation) or an object {env: NAME} that the runner reads from the environment at connect time.
Literal vs env: suffix: when to use which
Picking between literal and env-backed comes down to one rule: if revealing the value in the YAML or a reporter line would be bad, use the env form. Otherwise use literal.
| Header purpose | Use literal | Use env |
|---|---|---|
Tenant identifier (X-Tenant: acme) | yes | no |
Plan or feature flag (X-Plan: enterprise) | yes | no |
Trace ID (X-Trace-Id: ${run_id}) | yes (via interpolation) | no |
| API key / shared secret | no | yes |
| Service account JWT | no | yes |
| Signed access token | no | yes |
| OIDC client credentials | no | yes |
Values read via env: go through the same redaction registry as bearer tokens, so they never appear in reporter output. Literal values are not redacted.
A useful rule of thumb: if you would not paste the value into a public Slack channel, use env:.
The headers block is forbidden from setting Authorization or Proxy-Authorization; use the auth: block for those. The schema rejects them at validation time.
The behind-the-scenes ticket is .
CLI flags for headers
Two global flags map to the same surface so a single YAML can target multiple environments. Both are repeatable.
--header NAME=VALUE: append a literal header.--header-env NAME=VAR: append an env-backed header, readingVARfrom the environment at connect time.
Useful when the same suite runs against staging (one set of headers) and production (a different set) without forking the YAML.
Troubleshooting
"OAuth flow hung"
Symptoms: mcptest login opens a browser, you authorize, the page spins indefinitely.
Causes and fixes:
- The redirect listener could not bind. Some corporate firewalls block
127.0.0.1listeners. Re-run withMCPTEST_LOGIN_PORT=53321 mcptest login server. If the explicit port still fails, your network blocks loopback redirects; ask your IdP for a device authorization grant or fall back to a bearer token. - The browser opened in a different profile. Some browsers open a new tab in a profile that is not logged into the IdP. Copy the printed authorization URL into your usual browser profile manually.
- The redirect URL is wrong. Some IdPs require the redirect URL to be pre-registered. The CLI uses
http://127.0.0.1:<port>/callback. Register that exact path on the IdP side, or use RFC 7591 to let mcptest register it automatically.
"401 after login"
Symptoms: mcptest login succeeds, you can see the cache file, but mcptest run immediately returns 401 from the server.
Causes and fixes:
- Scope mismatch. The token you got back does not include a scope the server requires. Add the missing scope to
oauth.scopesand re-runmcptest login. - Audience mismatch. The token is for one audience and the server validates another. Look at the IdP-side app config and make sure the audience matches the server URL.
- Server's clock is off. A few minutes of clock skew can fail validation. NTP-sync the server.
- Wrong client ID for the wrong tenant. Some IdPs partition clients by tenant or environment; the dev-tenant client cannot produce tokens valid against the prod-tenant server.
"Token expired mid-run"
Symptoms: the suite runs partway, then fails with a token-expired error.
This is what the refresh layer is for. If you see this error, one of three things happened:
- The refresh token is also expired. Re-run
mcptest login. - The refresh token was revoked. Re-run
mcptest login. If the IdP rotated all tokens for the user, you may need to coordinate with security. - The refresh request itself failed. Check
--debugoutput for the response body fromtoken_url. The server may be misbehaving (502, 503) or the IdP may have policy changes (new MFA requirement, IP allowlist, conditional access).
The runner's behavior on refresh failure is to abort the run with a clear error rather than continue with stale credentials. This is deliberate.
"Secret leaked in log"
Symptoms: you see what looks like a bearer token in --verbose output or in a JUnit report.
The redaction layer should prevent this. If you see a raw secret, file a GitHub issue against the mcptest repo with the smallest reproducer you can produce. In the meantime:
- Confirm that the secret was registered via
bearer_token_env,auth.oauth.client_id_env, orheaders.<NAME>.env. Plain${NAME}interpolation does not register the value for redaction; that is by design (the value may be a non-secret). - Read
docs/security/redaction.mdfor the seven secret categories the redactor matches against. If your secret does not fall into any of them, you can register it explicitly via the--redact-env NAMEflag for the run. - Rotate the leaked secret. The redactor missing a value is a bug on our side, but the value is still out there. Treat it as compromised.
Worked examples
Five concrete configurations for common edge production setups.
Cloudflare Access
Cloudflare Access protects an internal URL with two custom headers issued from a service token.
servers:
protected:
url: "https://mcp.internal.example.com/v1"
headers:
CF-Access-Client-Id:
env: CF_ACCESS_CLIENT_ID
CF-Access-Client-Secret:
env: CF_ACCESS_CLIENT_SECRET
Set CF_ACCESS_CLIENT_ID and CF_ACCESS_CLIENT_SECRET in your env. Both values are treated as secrets and never appear in reporter output.
Google Cloud IAP
GCP Identity-Aware Proxy fronts a Cloud Run service with a JWT header. Mint the JWT however your shop normally does, drop it into an env var, and reference it:
servers:
gcp:
url: "https://iap.example.com/mcp"
headers:
Proxy-Authorization-IAP:
env: GCP_IAP_TOKEN
http:
timeout: 60s
The 60-second timeout is generous; IAP cold starts can run long.
AWS API Gateway
AWS API Gateway routes a custom domain to a Lambda-backed MCP endpoint with an API key header.
servers:
awsg:
url: "https://abc123.execute-api.us-east-1.amazonaws.com/prod/mcp"
headers:
x-api-key:
env: AWSG_API_KEY
API Gateway is picky about header case in some configurations. The example above uses the lowercase form AWS publishes; if your stage is case-sensitive and rejects the request, try X-Api-Key.
Multi-tenant SaaS
A multi-tenant SaaS that scopes requests by tenant header plus a shared bearer token.
servers:
saas:
url: "https://api.example.com/mcp"
auth:
bearer_token_env: "SAAS_API_TOKEN"
headers:
X-Tenant: "acme"
X-Plan: "enterprise"
The bearer token is shared across tenants; the tenant header is the literal tenant slug. Switch tenants by overriding --header X-Tenant=other on the CLI rather than editing the YAML.
Internal CA bundle
An internal endpoint signed by a corporate CA. No bearer needed, but the TLS verification needs the bundle:
servers:
internal:
url: "https://mcp.corp.example.com/v1"
http:
tls:
ca_bundle_path: "/etc/ssl/corp-internal-ca.pem"
min_version: "1.3"
This is "auth" in the sense that the TLS handshake is the trust anchor. If your internal endpoint also wants a token, add a normal auth: block on top.
Transport-level client auth
Three transport-level schemes sign or authenticate the connection itself, rather than carrying a token in a header. They compose with the header-level schemes above: an mTLS connection can still carry a bearer token, and a SigV4-signed request can still carry static headers.
mTLS (client certificates)
For a server that requires a client certificate at the TLS handshake. mcptest loads the cert and key and presents them on every request.
servers:
internal:
url: "https://mcp.corp.example.com/v1"
http:
tls:
mtls:
client_cert_path: "/etc/mcptest/client.pem"
client_key_path: "/etc/mcptest/client.key"
ca_bundle_path: "/etc/ssl/corp-internal-ca.pem"
What it is for: mutual-TLS endpoints behind a corporate CA or a service mesh that pins client identity to a certificate.
Fields under mtls:
client_cert_path(string). PEM file with the client certificate (and any intermediates).client_key_path(string) orclient_key_env(string). The private key, from a file or from an env var holding the PEM bytes. Set exactly one. A key file more permissive than0600raises a warning.ca_bundle_path(string, optional). Extra root certificate trusted for server verification, for an internal CA not in the platform store.
AWS SigV4 signed requests
For an MCP endpoint fronted by API Gateway or a Lambda Function URL that authorizes with AWS IAM. mcptest signs each request with SigV4.
servers:
awsg:
url: "https://abc123.execute-api.us-east-1.amazonaws.com/prod/mcp"
auth:
aws_sigv4:
region: "us-east-1"
service: "execute-api"
credentials:
source: from_environment
What it is for: IAM-authorized API Gateway / Lambda endpoints, signed per request so the time-bound signature is always fresh.
Fields under aws_sigv4:
region(string) andservice(string). The AWS region and signing name (execute-apifor API Gateway,lambdafor Function URLs).credentials.source(string).from_environmentreads the standardAWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_SESSION_TOKENenv vars;explicitreadsaccess_key_id_env/secret_access_key_env/session_token_env(for non-standard names);profilereads a named profile from your AWS files (see below).
Named profiles
The profile source resolves credentials from a named profile in your AWS files, the same files the AWS CLI reads:
servers:
awsg:
url: "https://abc123.execute-api.us-east-1.amazonaws.com/prod/mcp"
auth:
aws_sigv4:
service: "execute-api"
credentials:
source: profile
name: prod
mcptest reads the access key, secret key, optional session token, and region from ~/.aws/credentials, and falls back to ~/.aws/config for the region. Set AWS_SHARED_CREDENTIALS_FILE to point at a different credentials file and AWS_CONFIG_FILE for a different config file; mcptest honors both.
The credentials file uses bare [profile-name] sections:
[prod]
aws_access_key_id = AKIA...
aws_secret_access_key = ...
aws_session_token = ...
region = us-east-1
The config file uses [profile profile-name] sections (and a bare [default] for the default profile):
[profile prod]
region = us-east-1
You can omit region from the YAML when the profile supplies one: the profile region fills an empty config region, and an explicit region in the YAML or --aws-sigv4-region still wins. The secret key and session token are read at sign time and never logged.
On the command line, --profile <NAME> selects the same source:
mcptest run --profile prod --aws-sigv4-region us-east-1 --service execute-api
Web Bot Auth (signed HTTP message bot identity)
For a server that verifies an RFC 9421 HTTP Message Signature to identify the agent. mcptest signs each request with an Ed25519 key.
servers:
bot:
url: "https://mcp.example.com/v1"
auth:
web_bot_auth:
key_path: "/etc/mcptest/bot.ed25519"
directory_url: "https://mcptest.sh/.well-known/http-message-signatures-directory"
signature_agent: "https://mcptest.sh"
What it is for: presenting a verifiable bot identity to origins that check Web Bot Auth signatures.
Fields under web_bot_auth:
key_path(string) orkey_env(string). The private key in PKCS#8 PEM, from a file or an env var. Set exactly one. Ed25519 and RSA keys are both accepted; the key type must matchalgorithm.directory_url(string). Where the public-key directory is published.signature_agent(string). TheSignature-Agentvalue identifying the bot.algorithm(string, optional).ed25519(default) orrsa-pss. The RSA-PSS path signs withrsa-pss-sha512per RFC 9421.components(array, optional). Components covered by the signature. Defaults to["@authority", "@target-uri", "content-digest"].expires_in_seconds(integer, optional). Signature validity window; default 30.
Both Ed25519 and RSA-PSS (rsa-pss-sha512) are supported. Ed25519 is the default and the draft's recommendation for new deployments; choose rsa-pss when you already manage RSA keys. For RSA-PSS, point key_path or key_env at an RSA PKCS#8 PEM and set algorithm: rsa-pss:
servers:
bot:
url: "https://mcp.example.com/v1"
auth:
web_bot_auth:
key_env: "MCPTEST_BOT_RSA_KEY"
directory_url: "https://mcptest.sh/.well-known/http-message-signatures-directory"
signature_agent: "https://mcptest.sh"
algorithm: rsa-pss
Doctor pre-flight (local, no network)
mcptest doctor runs a local pre-flight for any transport-level auth scheme your config declares. It validates from files and environment only, makes no network call, and never prints a secret. Run it before a real run to catch a misconfiguration up front:
mcptest doctor
What it checks, per scheme:
- mTLS: the client cert and key load and parse, the cert is not expired (with a warning when it expires within 30 days), and on Unix the private key file is not more permissive than
0600(a warning, not a failure). - SigV4: which AWS credential source resolves (environment variables, the explicit env vars named in the config, or a named profile read from
~/.aws/credentials) and the resolved region. It fails only when no credential source resolves at all. In the default offline mode it does not make a live STS call (see live probes below). - Web Bot Auth: the signing key loads and parses for the configured algorithm (Ed25519 or RSA-PSS). It does not fetch the key directory in the default offline mode (see live probes below).
The pre-flight only reports non-secret facts: file paths, expiry dates, the derived public-key id, the algorithm name, and "credentials found in environment". Secret access keys, private keys, and signing keys never appear. A failed check makes mcptest doctor exit non-zero.
Live probes (opt-in, network)
By default the pre-flight is offline. Add --probe to make one real network call per scheme so you can confirm the credentials actually authenticate, not just that they parse:
mcptest doctor --probe
What each probe does:
- SigV4: signs and sends a real
sts:GetCallerIdentityto the regional STS endpoint. A200confirms the credentials authenticate. A403means the request signed but the identity was rejected (check the keys and the clock). The signedAuthorizationheader never appears in the output. - Web Bot Auth: fetches the configured
directory_url. A2xxconfirms the key directory is reachable. The fetch carries no signature.
Probe rows print under a live probes (network) heading, one line per scheme, and report only a status code or a secret-free transport error. A failed probe makes mcptest doctor exit non-zero. Leave --probe off to keep the run fully offline and deterministic.
CLI flags
Every field above can also come from a mcptest run flag. A flag overrides the matching YAML field (CLI beats YAML), or populates the scheme when the YAML did not declare one. Secrets still come from environment variables: the *-env / *-key-env flags name a variable, they never take a raw secret as a value.
| Flag | Overrides |
|---|---|
--client-cert <PATH> | http.tls.mtls.client_cert_path |
--client-key <PATH> | http.tls.mtls.client_key_path |
--client-key-env <VAR> | http.tls.mtls.client_key_env |
--ca-bundle <PATH> | http.tls.mtls.ca_bundle_path |
--aws-sigv4-region <REGION> | auth.aws_sigv4.region |
--service <NAME> | auth.aws_sigv4.service |
--profile <NAME> | auth.aws_sigv4.credentials named-profile source |
--webbot-key <PATH> | auth.web_bot_auth.key_path |
--webbot-key-env <VAR> | auth.web_bot_auth.key_env |
--webbot-agent <URL> | auth.web_bot_auth.signature_agent |
--webbot-algorithm <ALG> | auth.web_bot_auth.algorithm |
--profile <NAME> selects the named-profile credential source. The keys resolve from ~/.aws/credentials (and ~/.aws/config for the region) at sign time, never from the command line. See "Named profiles" above.
A flag combination that produces an invalid block (a cert with no key, an empty SigV4 region, an unknown Web Bot Auth algorithm) is rejected before the run starts, with the same validator that runs at config load.
Web Bot Auth key directory
A server operator publishes a .well-known/http-message-signatures-directory document so verifiers can find a bot's public key. mcptest is a client, but it can build that document for your signing key so you can publish it:
mcptest web-bot-auth directory --key bot.ed25519
# RSA-PSS keys: add --algorithm rsa-pss.
# Env-sourced key: --key-env MCPTEST_BOT_KEY.
The command prints a JWK Set ({"keys": [...]}) carrying only the public key and the same non-secret key id the signer advertises in Signature-Input. The private key is never printed. See examples/auth-fixtures/ for a full local round trip that verifies the signature against this directory.
Deferred polish (still open)
The schemes above sign and connect correctly today. RSA-PSS for Web Bot Auth (algorithm: rsa-pss), SigV4 named profiles, live doctor probes (--probe), and the key-directory command all shipped. Some surrounding polish is still open:
- Cassette / cache secret-key handling nuances.
Open a GitHub issue if you need one of these.
See also
docs/auth-refresh.md, the OAuth refresh mechanics that already ship.docs/yaml-reference.md, the full YAML schema reference.docs/security/redaction.md, the secret-handling policy that protects credentials in output.docs/guides/ci-integration.md, CI patterns that consume these auth blocks.