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
108 lines
3.9 KiB
Markdown
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)**.
|