Testing an MCP server with pytest
The in-memory client used in every lesson is how the SDK tests its own docs. Put it in a pytest fixture and each tool's promises become tests.
import pytest
from mcp import Client
from shop import mcp
@pytest.fixture
def anyio_backend():
return "asyncio"
@pytest.fixture
async def client():
async with Client(mcp, raise_exceptions=True) as connected:
yield connectedThe tests are async, so they need a runner. anyio, installed with mcp, includes a pytest plugin: @pytest.mark.anyio runs a test in an event loop, and the anyio_backend fixture picks asyncio.
The client fixture connects once per test. raise_exceptions=True matters only for failures outside a tool, such as a broken server setup: over this connection the SDK would otherwise replace their message with a generic Internal server error, and in a test you want the real one.
@pytest.mark.anyio
async def test_lookup_returns_the_order(client):
result = await client.call_tool("lookup_order", {"order_id": "A17"})
assert result.is_error is False
assert result.structured_content == {"id": "A17", "item": "blue mug", "status": "shipped", "total": 12.5}
@pytest.mark.anyio
async def test_unknown_order_tells_the_model_what_to_send(client):
result = await client.call_tool("lookup_order", {"order_id": "Z9"})
assert result.is_error is True
assert "Order ids look like A17" in result.content[0].text
@pytest.mark.anyio
async def test_search_limit_is_enforced(client):
result = await client.call_tool("search_help", {"query": "refund", "limit": 50})
assert result.is_error is TrueOne test for the promised data, one for the error text a model depends on, one for a limit. Comparing the whole structured_content catches a field that appears, disappears or changes type.
pytest -qA test catching a change
Someone edits the error message to a shorter "Order not found.":
pytest -q --tb=short--tb=short keeps the failure report to the lines that matter. The test fails and names the text that went missing: the part that told a model what a valid order id looks like. A message written for a model is behaviour, and it deserves a test like any other.
- Add a test that
list_toolsreturns exactlylookup_orderandsearch_help. - Test that
search_helpwithtopic="refunds"returns only refund articles. - Add a test for a resource read from lesson 9, using
pytest.raises(MCPError)for a missing order.
You understood something today that you didn't yesterday.