Unit-тесты инструментов, интеграционное тестирование через MCP-клиент, отладка.
Тестирование MCP-сервера включает два уровня: unit-тесты (логика инструментов) и интеграционные тесты (весь MCP-стек). В этой теме разберём оба подхода и инструменты отладки.
Unit-тесты проверяют бизнес-логику. Интеграционные тесты проверяют, что MCP-протокол работает корректно.
Декоратор @mcp.tool() не меняет функцию — она остаётся обычной Python-функцией. Это значит, что unit-тест вызывается напрямую:
# server.py
from fastmcp import FastMCP, ErrorTool
mcp = FastMCP("MathServer")
@mcp.tool()
def divide(a: int, b: int) -> float:
"""Делит a на b."""
if b == 0:
return ErrorTool("Деление на ноль недопустимо")
return a / b# test_server.py
import pytest
from server import divide
from fastmcp import ErrorTool
class TestDivide:
def test_divide_positive(self):
"""Корректное деление."""
result = divide(10, 2)
assert result == 5.0
def test_divide_negative(self):
"""Деление отрицательных чисел."""
result = divide(-10, 2)
assert result == -5.0
def test_divide_by_zero(self):
"""Деление на ноль возвращает ErrorTool."""
result = divide(10, 0)
assert isinstance(result, ErrorTool)
assert "деление на ноль" in str(result).lower()
def test_divide_floats(self):
"""Деление с плавающей точкой."""
result = divide(7, 2)
assert result == 3.5Unit-тесты быстрые — не нужно запускать сервер, подключать клиент, сериализовать данные. Только бизнес-логика.
Когда инструмент зависит от внешних систем (БД, API), мокируем зависимости:
# server.py
import requests
@mcp.tool()
def get_user_by_email(email: str) -> str:
"""Ищет пользователя по email через внешний API."""
response = requests.get(f"https://api.example.com/users", params={"email": email})
response.raise_for_status()
data = response.json()
if not data:
return ErrorTool(f"Пользователь с email {email} не найден")
return str(data[0])# test_server.py
from unittest.mock import patch, MagicMock
from server import get_user_by_email
from fastmcp import ErrorTool
class TestGetUserByEmail:
@patch("server.requests.get")
def test_user_found(self, mock_get):
"""Пользователь найден — возвращаем данные."""
mock_response = MagicMock()
mock_response.json.return_value = [{"id": 1, "name": "Алиса", "email": "alice@test.com"}]
mock_response.raise_for_status = MagicMock()
mock_get.return_value = mock_response
result = get_user_by_email("alice@test.com")
assert "Алиса" in result
mock_get.assert_called_once_with(
"https://api.example.com/users",
params={"email": "alice@test.com"}
)
@patch("server.requests.get")
def test_user_not_found(self, mock_get):
"""Пользователь не найден — ErrorTool."""
mock_response = MagicMock()
mock_response.json.return_value = []
mock_response.raise_for_status = MagicMock()
mock_get.return_value = mock_response
result = get_user_by_email("nobody@test.com")
assert isinstance(result, ErrorTool)
@patch("server.requests.get")
def test_api_error(self, mock_get):
"""Ошибка API — выбрасывает исключение."""
mock_response = MagicMock()
mock_response.raise_for_status.side_effect = requests.HTTPError("500 Server Error")
mock_get.return_value = mock_response
with pytest.raises(requests.HTTPError):
get_user_by_email("alice@test.com")Мокирование позволяет тестировать все сценарии: успех, не найдено, ошибка сервера, таймаут — без реального API.
Интеграционный тест запускает сервер и подключается через MCP-клиент. Это проверяет весь стек: сериализацию, транспорт, десериализацию:
import pytest
import pytest_asyncio
from fastmcp import FastMCP
from fastmcp.client import Client
# Создаём тестовый сервер
test_mcp = FastMCP("TestServer")
@test_mcp.tool()
def add(a: int, b: int) -> int:
"""Складывает два числа."""
return a + b
@test_mcp.tool()
def greet(name: str) -> str:
"""Приветствует пользователя."""
return f"Hello, {name}!"
@pytest_asyncio.fixture
async def mcp_client():
"""Фикстура: запускает сервер и подключает клиент."""
client = Client(test_mcp)
async with client:
yield client
@pytest.mark.asyncio
async def test_add_tool(mcp_client):
"""Интеграционный тест: инструмент add через MCP-протокол."""
result = await mcp_client.call_tool("add", {"a": 3, "b": 5})
assert result == 8
@pytest.mark.asyncio
async def test_greet_tool(mcp_client):
"""Интеграционный тест: инструмент greet через MCP-протокол."""
result = await mcp_client.call_tool("greet", {"name": "Мир"})
assert result == "Hello, Мир!"
@pytest.mark.asyncio
async def test_tools_list(mcp_client):
"""Интеграционный тест: список инструментов."""
tools = await mcp_client.list_tools()
tool_names = [t.name for t in tools]
assert "add" in tool_names
assert "greet" in tool_namesИнтеграционный тест медленнее unit-теста (запуск сервера, транспорт), но проверяет, что MCP-протокол работает корректно.
MCP Inspector — веб-интерфейс для отладки MCP-серверов. Запуск:
npx @modelcontextprotocol/inspector python server.pyInspector открывает веб-страницу, где можно:
Это незаменимый инструмент для первичной отладки — особенно когда нужно понять, какие параметры сервер ожидает и что возвращает.
Включите подробное логирование FastMCP для диагностики проблем:
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger("fastmcp")
logger.setLevel(logging.DEBUG)Логи покажут:
Проверьте, что декоратор применён до вызова mcp.run():
# Неправильно — run() вызван до декоратора
mcp.run()
@mcp.tool()
def my_tool():
pass
# Правильно — декоратор до run()
@mcp.tool()
def my_tool():
pass
mcp.run()Убедитесь, что аннотации типов указаны корректно. Без аннотаций FastMCP не может сгенерировать JSON Schema:
# Неправильно — нет аннотаций
@mcp.tool()
def add(a, b):
return a + b
# Правильно — аннотации типов
@mcp.tool()
def add(a: int, b: int) -> int:
return a + bПроверьте, что порт свободен и хост корректен:
# Неправильно — порт может быть занят
mcp.run(transport="sse", port=8000)
# Правильно — обработка ошибки
try:
mcp.run(transport="sse", host="127.0.0.1", port=8765)
except OSError as e:
logger.error("Не удалось запустить SSE-сервер: %s", e)Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.