Scenario 11: test behind authentication
You have an MCP server that only answers when the caller presents a token. You want to run the same conformance suite you would run against an open server, but now every request has to carry an Authorization: Bearer <token> header. You also want to do this without ever writing the token into the YAML.
The hosted test server makes this easy to rehearse. The endpoint https://test.mcptest.sh/secure/mcp runs the exact same conformant handler as the open /mcp endpoint, with the same tools (greet, get_forecast, and friends), but it rejects any request that does not carry a bearer token. That lets you practice the token-gated path against a real server before you point the suite at your own.
There are two ways to get a token in:
- A static bearer token you already hold, read from an env var. This is the common CI path.
- The OAuth 2.1 flow via
mcptest login, which opens a browser, walks PKCE, and caches the resulting token for later runs. This is the developer-machine path.
This page walks both, and shows what a 401 looks like when the token is missing or expired.
The YAML
Save this as tests/secure.yml:
# yaml-language-server: $schema=https://mcptest.sh/schema/v1.json
servers:
secure:
url: "https://test.mcptest.sh/secure/mcp"
auth:
bearer_token_env: "MCPTEST_TEST_TOKEN"
tools:
- name: "greet works once authenticated"
server: secure
tool: greet
args:
name: "Ada"
expect:
- target: "result.content[0].text"
matcher:
exact: "Hello, Ada!"
- name: "forecast works once authenticated"
server: secure
tool: get_forecast
args:
city: "Portland"
expect:
- target: "result.content[0].text"
matcher:
contains: "Portland"
What is happening here:
auth.bearer_token_envpoints at the env varMCPTEST_TEST_TOKEN. mcptest reads the value at connect time and sends it asAuthorization: Bearer <value>on every request to this server. The token itself never appears in the YAML, so the file is safe to commit.- The resolved token is promoted to the redaction registry the moment it is read, so it never shows up in reporter output, debug logs, or recorded cassettes.
- The two tools are the same conformant tools the open
/mcpendpoint exposes. The only difference is the/secure/path requires the header. Once authenticated, the suite behaves exactly as it would against the open server.
Run it
The hosted server ships a public demo token, mcptest-demo-token, so you can exercise the path without minting anything. Put it in the env var the YAML names, then run:
MCPTEST_TEST_TOKEN=mcptest-demo-token mcptest --config tests/secure.yml run
The token lives only in the environment for the duration of the command. It is never written to the YAML and never echoed back.
If you would rather use a fresh, short-lived token than the static demo one, the server's mock OAuth endpoint mints one with the client-credentials grant:
TOKEN=$(curl -s -X POST https://test.mcptest.sh/oauth/token \
-d grant_type=client_credentials | jq -r .access_token)
MCPTEST_TEST_TOKEN="$TOKEN" mcptest --config tests/secure.yml run
Either token authenticates against the same handler. The static demo token is convenient for a quick check; the minted token is closer to what a real machine-to-machine client would do.
OAuth instead of a static token (mcptest login)
When the server is fronted by an interactive identity provider, you do not hold a long-lived token. You log in once and let mcptest cache the result. The hosted test server advertises the standard OAuth metadata so you can rehearse this flow too:
GET /.well-known/oauth-protected-resource(RFC 9728) tells a client which authorization server protects the resource.GET /.well-known/oauth-authorization-server(RFC 8414) advertises the authorization and token endpoints and S256 PKCE support./oauth/authorizeruns the authorization step (add?deny=1to simulate a user declining), and/oauth/tokenexchanges the code.
mcptest login drives all of this for you:
mcptest login --url https://test.mcptest.sh/secure/mcp
The command discovers the authorization server from the well-known metadata, registers a client via Dynamic Client Registration, opens your browser, runs the PKCE authorization-code flow, and caches the returned token under ~/.mcptest/auth/. After that, a plain run picks the cached token up automatically, with no auth: block and no env var needed:
mcptest --config tests/secure.yml run
On a headless box where no browser can open, add --no-browser. The CLI prints the authorization URL for you to open elsewhere and still catches the callback on its loopback listener:
mcptest login --url https://test.mcptest.sh/secure/mcp --no-browser
For CI, prefer the static bearer token (or a minted client-credentials token) over the interactive flow. There is no human at a browser when a build runs. The static-token path at the top of this page is the CI path.
Expected output
A run with the token present passes both tools:
mcptest --config tests/secure.yml run
PASS greet works once authenticated (243ms)
PASS forecast works once authenticated (251ms)
2 passed, 0 failed in 0.5s
A run with the env var unset fails fast. The server returns 401 for every request because no bearer header was sent:
mcptest --config tests/secure.yml run
FAIL greet works once authenticated (188ms)
connection to server 'secure' failed: HTTP 401 Unauthorized
from https://test.mcptest.sh/secure/mcp
hint: set MCPTEST_TEST_TOKEN, or run `mcptest login --url ...`
0 passed, 1 failed in 0.2s
The 401 surfaces as a connection failure, not a per-assertion mismatch, because the handshake itself was rejected before any tool was called.
Troubleshooting
- 401 Unauthorized. The token is missing, wrong, or expired. For a static token, confirm
MCPTEST_TEST_TOKENis set in the same shell or CI step that runs mcptest (echo ${MCPTEST_TEST_TOKEN:+set}should printset). The public demo token ismcptest-demo-token; a token minted from/oauth/tokenis short-lived, so mint a fresh one if a previous one has aged out. MCPTEST_TEST_TOKENis unset. The runner connects with no bearer header and the server answers 401. Export the var before the run, or switch to themcptest loginpath so the token comes from the cache instead of the environment.- Login succeeds but the run still 401s. The cached token may be stale. Re-run
mcptest login --url ... --forceto clear the cache and walk the flow again. - Testing the denial path. Hitting
/oauth/authorize?deny=1simulates a user who declines consent. The authorization server returns anaccess_deniederror instead of a code, andmcptest loginreports the denial rather than caching a token. Use this to confirm your error handling when a developer says no.
See also
docs/auth.md, the full auth reference (bearer tokens, OAuth 2.1 + PKCE, custom headers, transport-level schemes).docs/auth-in-tests.md, how a suite picks per-server auth and the precedence rules between them.- Previous: Grade against the spec.
- Next: Multi-server suites.