From zero to passing tests in 5 minutes. Every example is copy-pasteable and runnable.
pip install mcp-test-harness
# Creates tests/test_mcp_server_example.py + mcp-test.yaml
mcp-test init --server-command "python my_server.py"
# Or check your server health first (no tests):
mcp-test doctor
mcp-test
# Output:
# [PASS] test_server_has_tools (45.2ms)
# [PASS] test_echo_tool (120.8ms)
#
# 2 passed, 0 failed, 0 errored, 0 skipped
# Total time: 312.5ms
from mcp_test_harness import (
assert_tool_call, assert_capabilities,
assert_resource_read, assert_tool_schema,
)
async def test_capabilities(mcp_server):
"""Server advertises tools and resources."""
await assert_capabilities(mcp_server, {"tools": {}, "resources": {}})
async def test_echo(mcp_server):
"""Call echo tool, validate response."""
result = await assert_tool_call(
mcp_server, "echo", {"message": "hello"},
expected=[{"text": "hello", "isError": False}]
)
async def test_schema(mcp_server):
"""Tool input schema is valid JSON Schema."""
await assert_tool_schema(mcp_server, "echo", {
"type": "object",
"properties": {"message": {"type": "string"}},
"required": ["message"]
})
async def test_config_resource(mcp_server):
"""Read a resource and check MIME type."""
await assert_resource_read(
mcp_server, "file:///config.json",
expected_mime_type="application/json"
)
from pathlib import Path
from mcp_test_harness import assert_snapshot, assert_tool_idempotent
async def test_stable_output(mcp_server):
"""Response matches stored snapshot."""
result = await mcp_server.call_tool("generate_report", {})
await assert_snapshot(result, "report_output", test_file=Path(__file__))
async def test_masked_snapshot(mcp_server):
"""Snapshot with volatile fields masked."""
result = await mcp_server.call_tool("with_ids", {})
await assert_snapshot(
result, "noisy",
test_file=Path(__file__),
ignore_fields=["requestId", "timestamp"],
mask_patterns=[r"req_[a-f0-9]+"],
)
async def test_idempotent(mcp_server):
"""Same input always produces same output."""
await assert_tool_idempotent(
mcp_server, "calculate", {"expr": "2+2"}, iterations=5
)
from mcp_test_harness import assert_latency, assert_throughput, marker
@marker(tags=["perf"])
async def test_echo_p95(mcp_server):
"""Echo responds under 200ms at p95."""
await assert_latency(
mcp_server, "echo", {"message": "hi"},
p95_ms=200, iterations=20, warmup=3
)
@marker(tags=["perf"])
async def test_echo_p99(mcp_server):
"""Echo responds under 500ms at p99."""
await assert_latency(
mcp_server, "echo", {"message": "hi"},
p99_ms=500, iterations=50, warmup=5
)
@marker(tags=["perf", "load"])
async def test_throughput(mcp_server):
"""Sustain 10 RPS with p95 under 300ms."""
await assert_throughput(
mcp_server, "echo", {"message": "load"},
target_rps=10, duration_s=10, p95_ms=300
)
# mcp-test.yaml - full configuration example
server:
command: python my_server.py
transport: stdio # stdio | sse | http
transport_options:
# headers:
# Authorization: "Bearer token"
test:
dirs: [tests/]
timeout: 30 # per-test timeout (seconds)
parallel: false
workers: 4
report:
format: junit # json | junit | html
output: reports/results.xml
schema_validation: true # validate JSON-RPC responses
plugins: [] # custom plugin modules
# .github/workflows/mcp-tests.yml
name: MCP Server Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: pip install mcp-test-harness
- run: mcp-test --report-format junit --report-output results.xml
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: results.xml
Each pack includes tests, YAML config, and report output. Clone and run.
assert_tool_call, assert_capabilities, assert_resource_read, schema validation. The basics.
Snapshots, idempotency, ignore_fields, mask_patterns. Catch silent changes.
assert_latency p95/p99, warmup, assert_throughput, SLO gates.
Governance evidence, audit artifacts, compliance-oriented test patterns.
Safety checks, boundary testing, repeatable evidence for governance programs.
One .py file per assertion/feature. Copy the one you need.
| Assertion | Mode | What it does |
|---|---|---|
| assert_tool_call | Functional | Invoke a tool, validate response content |
| assert_resource_read | Functional | Read a resource, check content and MIME type |
| assert_prompt | Functional | Get a prompt, validate messages |
| assert_capabilities | Functional | Verify server capabilities object |
| assert_tool_schema | Functional | Validate tool inputSchema against expected |
| assert_protocol_version | Functional | Check MCP protocol version string |
| assert_tool_call_validates_input | Functional | Verify args validated against inputSchema |
| assert_tool_denied | Security | Verify tool rejects unauthorized calls |
| assert_authorization_boundary | Security | Test RBAC boundaries |
| assert_snapshot | Regression | Compare response to stored snapshot |
| assert_tool_idempotent | Regression | Same input produces same output N times |
| assert_latency | Performance | p95/p99/mean/median over N iterations + warmup |
| assert_throughput | Performance | Concurrent load, target RPS, latency gate |
pip install, init, run. Under 5 minutes.