MCPMCP Python SDK 2.2 · Python 3.10+
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
24 small wins to finish your pathNext lesson

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.

Exampletest_shop.py, part 1
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 connected

The 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.

Exampletest_shop.py, part 2
@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 True

One 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.

Example
pytest -q

A test catching a change

Someone edits the error message to a shorter "Order not found.":

Example
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.

Try it yourself
  • Add a test that list_tools returns exactly lookup_order and search_help.
  • Test that search_help with topic="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.