# Testing The Python SDK ships a `Client` class with an **in-memory transport**: pass it your server object and it connects to it directly. No subprocess. No port. No transport at all. It's the same idea as FastAPI's `TestClient`. ## Basic usage Let's assume you have a simple server with a single tool: ```python title="server.py" --8<-- "docs_src/testing/tutorial001.py" ``` To run the test below you'll need two extra (development) dependencies: === "uv" ```bash uv add --dev pytest inline-snapshot ``` === "pip" ```bash pip install pytest inline-snapshot ``` !!! info These docs assume you already know [`pytest`](https://docs.pytest.org/en/stable/). [`inline-snapshot`](https://15r10nk.github.io/inline-snapshot/latest/) is what the test below uses to assert on the whole result object in one line. It records the output of a test as the `snapshot(...)` literal you see. If you'd rather not use it, drop the import and assert on the fields you care about (`result.content[0].text == "3"`) like in any other test. Now the test: ```python title="test_server.py" import pytest from inline_snapshot import snapshot from mcp import Client from mcp_types import CallToolResult, TextContent from server import mcp @pytest.fixture def anyio_backend(): # (1)! return "asyncio" @pytest.fixture async def client(): # (2)! async with Client(mcp, raise_exceptions=True) as c: yield c @pytest.mark.anyio async def test_call_add_tool(client: Client): result = await client.call_tool("add", {"a": 1, "b": 2}) assert result == snapshot( CallToolResult( content=[TextContent(type="text", text="3")], structured_content={"result": 3}, ) ) ``` 1. If you are using `trio`, return `"trio"` instead. See the [anyio documentation](https://anyio.readthedocs.io/en/stable/testing.html#specifying-the-backends-to-run-on) for the details. 2. The fixture yields a connected client. Every test that takes `client` gets a fresh in-memory connection to the same server. There you go! You can now extend your tests to cover more scenarios. ## Why `raise_exceptions=True`? Two different things can go wrong, and this flag only touches one of them. An exception inside one of **your tools** is not a protocol failure. It becomes a normal result with `is_error=True`, and the model reads the message. `raise_exceptions` doesn't change that: with or without it, `call_tool` returns the same `is_error=True` result. There's a whole page on it: **[Handling errors](../servers/handling-errors.md)**. A failure **outside** a tool body is different. On the connection `Client(mcp)` gives you, the server sanitises it into a generic `"Internal server error"` before the client sees it. You should never leak the details of an unexpected crash to a remote caller. In a test that is exactly what you *don't* want, and it is what `raise_exceptions=True` changes: your test sees the real message instead of the sanitised one. Leave it on in tests. It has no meaning in production code. ## In-process by default !!! note `Client(mcp)` connects in-process and is **era-neutral** by default: it probes the server and picks the appropriate protocol path. Pin `mode="legacy"` if your test exercises legacy-specific semantics (sampling or elicitation push, `message_handler`), and drop `raise_exceptions=True` there: a legacy connection never sanitises in the first place, and the flag re-raises the failure inside the server task instead of in your test. That one line is also why these docs can promise you that their examples work: every example file is exercised by the SDK's own test suite, almost all of them through exactly this client. You're using the same tool the SDK uses on itself. You have a working, tested server. Putting it inside a real application (Claude Desktop, an IDE) is **[Connect to a real host](real-host.md)**; every other way to serve it is **[Running your server](../run/index.md)**.