Files
modelcontextprotocol--pytho…/docs/get-started/testing.md
T
wehub-resource-sync 49b9bb6724
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:10:27 +08:00

108 lines
3.9 KiB
Markdown

# 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)**.