Перейти к содержанию

Тестирование

Машинный перевод

Эта страница переведена с английской документации автоматически, и основной версией остаётся английская страница. Если что-то читается неправильно, на странице Переводы объясняется, как об этом сообщить.

В Python SDK есть класс Client со встроенным in-memory транспортом: передайте ему объект сервера, и он подключится к нему напрямую.

Ни подпроцесса. Ни порта. Вообще никакого транспорта. Та же идея, что и TestClient в FastAPI.

Базовое использование

Предположим, есть простой сервер с одним инструментом:

server.py
from mcp.server import MCPServer

mcp = MCPServer("Calculator")


@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b

Чтобы запустить тест ниже, понадобятся две дополнительные зависимости (для разработки):

uv add --dev pytest inline-snapshot
pip install pytest inline-snapshot

Info

Эта документация предполагает, что вы уже знакомы с pytest.

inline-snapshot — то, с помощью чего тест ниже проверяет весь объект результата одной строкой. Библиотека записывает вывод теста в виде литерала snapshot(...), который вы видите. Если не хотите её использовать, уберите импорт и проверяйте нужные поля (result.content[0].text == "3"), как в любом другом тесте.

Теперь сам тест:

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})
    # Drop the server identity stamp in `_meta`; it is not what this test is about.
    result.meta = None
    assert result == snapshot(
        CallToolResult(
            content=[TextContent(type="text", text="3")],
            structured_content={"result": 3},
        )
    )
  1. Если используете trio, верните вместо этого "trio". Подробности — в документации anyio.
  2. Фикстура отдаёт подключённый клиент. Каждый тест, принимающий client, получает свежее in-memory подключение к тому же серверу.

Готово! Теперь можно расширять тесты, чтобы покрыть больше сценариев.

Зачем raise_exceptions=True?

Пойти не так могут две разные вещи, и этот флаг касается только одной из них.

Исключение внутри одного из ваших инструментов — не сбой протокола. Оно превращается в обычный результат с is_error=True, и модель читает сообщение. raise_exceptions этого не меняет: с ним или без него call_tool возвращает один и тот же результат с is_error=True. Этому посвящена целая страница: Обработка ошибок.

Сбой вне тела инструмента — другое дело. На подключении, которое даёт Client(mcp), сервер очищает его до обобщённого "Internal server error", прежде чем оно дойдёт до клиента. Детали неожиданного падения никогда не должны утекать к удалённому вызывающему. В тесте это ровно то, чего вы не хотите, и именно это меняет raise_exceptions=True: тест видит настоящее сообщение вместо очищенного.

В тестах оставляйте его включённым. В продакшен-коде он не имеет смысла.

Внутри процесса по умолчанию

Note

Client(mcp) подключается внутри процесса и по умолчанию не привязан к поколению протокола: он опрашивает сервер и выбирает подходящий путь протокола. Зафиксируйте mode="legacy", если тест проверяет семантику, специфичную для подключений старого поколения (push-сообщения сэмплирования (sampling) или элицитации (elicitation), message_handler), и уберите там raise_exceptions=True: подключение старого поколения вообще ничего не очищает, а флаг повторно выбрасывает сбой внутри задачи сервера, а не в вашем тесте.

Именно благодаря этой одной строке документация может обещать, что её примеры работают: каждый файл с примером прогоняется собственным набором тестов SDK, и почти все — ровно через этот клиент. Вы пользуетесь тем же инструментом, которым SDK проверяет сам себя.

У вас есть работающий, протестированный сервер. Как поместить его в настоящее приложение (Claude Desktop, IDE) — на странице Подключение к настоящему хосту; все остальные способы его запустить — в разделе Запуск сервера.