mcptest docs GitHub

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:

  1. A static bearer token you already hold, read from an env var. This is the common CI path.
  2. 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:

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:

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

See also