Scenario 6: URL target against staging
You have an MCP server deployed to a staging environment. It is behind a bearer token, it routes by tenant header, and it can take a few seconds to wake up after a deploy. You want one YAML that runs the same suite locally (against a stdio binary) and in CI (against the staging URL), with credentials picked up from the environment.
The trick is to keep the same tools: block and swap only the servers: entry between runs. Two ways to do that: two named servers in one file picked by --server, or one server overridden via CLI flags. Both work; pick by team preference.
The YAML
Save this as tests/staging.yml:
# yaml-language-server: $schema=https://mcptest.sh/schema/v1.json
servers:
local:
command: ["./target/debug/my-mcp-server"]
staging:
url: "https://mcp.staging.example.com/v1"
auth:
bearer_token_env: "MCPTEST_STAGING_TOKEN"
headers:
X-Tenant:
env: MCPTEST_STAGING_TENANT
X-Trace-Id: "${run_id}"
wait_for_ready: "https://mcp.staging.example.com/healthz"
http:
timeout: 30s
connect_timeout: 5s
variables:
run_id:
from_env: "GITHUB_RUN_ID"
default: "local-dev"
tools:
- name: "lookup happy path"
server: staging
tool: "lookup_record"
args:
id: "rec_abc123"
expect:
- target: "result.content[0].text"
matcher:
contains: "rec_abc123"
- name: "lookup error path"
server: staging
tool: "lookup_record"
args:
id: "rec_missing"
expect:
- target: "result.isError"
matcher:
exact: true
What is happening here:
- The
stagingserver'sauth.bearer_token_envpoints at the env varMCPTEST_STAGING_TOKEN. mcptest reads the value at request time and sends it asAuthorization: Bearer .... The value never appears in the YAML. headers.X-Tenantreads fromMCPTEST_STAGING_TENANTand goes on the wire as a custom header. The env form means the value goes through the redaction registry, so it does not appear in reporter output (useful when tenants are sensitive identifiers).headers.X-Trace-Idis literal but uses${run_id}interpolation so each CI run gets a distinct trace ID. Locally,run_idfalls back tolocal-dev.wait_for_readypolls a health endpoint until it returns 2xx before the runner sends any MCP request. Useful after a fresh deploy.http.timeout: 30sis generous; staging cold starts can run long.
Local invocation
To run against the local binary instead of staging, override the server with the CLI:
mcptest run --server-command "./target/debug/my-mcp-server" tests/staging.yml
The --server-command flag overrides the command of every server in the suite with the supplied stdio command, so the tool tests now run against your local binary without you editing the YAML. The argument is split with POSIX shell rules, so --server-command "./dev-server --debug" parses to two argv elements.
Alternatively, point the suite at a different URL endpoint:
mcptest run --server-url "http://localhost:8080/v1" tests/staging.yml
The --server-url flag overrides the url of every server in the suite. It is mutually exclusive with --server-command.
CI invocation
# .github/workflows/staging.yml
name: staging integration
on:
schedule:
- cron: "0 */4 * * *" # every four hours
workflow_dispatch:
jobs:
staging:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- run: cargo install mcptest --locked
- run: mcptest run tests/staging.yml
env:
MCPTEST_STAGING_TOKEN: ${{ secrets.MCPTEST_STAGING_TOKEN }}
MCPTEST_STAGING_TENANT: "acme"
Two GitHub Actions secrets are referenced: the bearer token and the tenant slug. Both are read into env vars before mcptest run. The runner picks them up automatically.
Expected output
mcptest run tests/staging.yml
WAIT staging -> https://mcp.staging.example.com/healthz (ready in 2.1s)
PASS lookup happy path (412ms)
PASS lookup error path (387ms)
2 passed, 0 failed in 3.0s (1 readiness probe)
The WAIT line shows the readiness probe; the per-test lines show the actual request time. If the readiness probe fails (server never becomes ready in the configured window), the run aborts with a structured error before any test runs.
Troubleshooting
MCPTEST_STAGING_TOKENis unset. The runner aborts at load time with a clear error pointing at the field that required it. Set the env var.- 401 from staging. The token is expired or wrong. Rotate the token in your CI secret store. If the secret is fresh, check that you set it on the correct repo / org / environment.
wait_for_readytimes out. The probe polls the health URL until it returns 2xx. If your staging deploy is slow to wake, give the connection more room withhttp.connect_timeoutandhttp.timeouton the server block, and confirm the health URL is reachable from CI.- Trace ID does not appear in staging logs. Some load balancers strip unknown headers. Add
X-Trace-Idto your LB's allowlist, or use thetraceparentheader (W3C standard) which most ingresses pass through unchanged.
See also
docs/auth.md, the full auth reference (bearer tokens, OAuth, custom headers).docs/yaml-reference.md#custom-headers-and-http-transport, the headers and HTTP transport surface.- Previous: CI quality gate.
- Next: LLM-judge preview.