Инженерия агентов
Контрактные тесты MCP-инструментов: как обнаруживать несовместимость до запуска агента
MCP-сервер может продолжать запускаться после несовместимого изменения, а агент — обнаружить проблему только при реальном вызове инструмента. Причиной бывает переименованный аргумент, новый обязательный параметр, изменившаяся структура результата или другой способ сообщить об ошибке. Исправим это проверками, которые выполняются до подключения модели.
Что именно считать контрактом
Контрактный тест проверяет наблюдаемое соглашение между поставщиком и потребителем интерфейса. Для MCP-инструмента поставщиком выступает сервер, а потребителем — клиент и код, который преобразует результат вызова в контекст агента.
Минимальный контракт включает четыре слоя:
- Обнаружение: имя инструмента и его присутствие в списке доступных инструментов.
- Ввод: схема аргументов, обязательные поля, типы, ограничения и поведение при лишних полях.
- Вывод: структура успешного результата и типы элементов содержимого, которые понимает клиент.
- Сбои: различие между ошибкой протокольного вызова, ошибочным результатом инструмента и транспортным отказом.
Описание инструмента тоже влияет на выбор модели, но его изменение не всегда является машинно несовместимым. Поэтому полезно разделить проверки на блокирующие и информационные: схема и формат результата блокируют сборку, а изменение описания создаёт отчёт для ревью.
Шаг 1. Зафиксируйте артефакты контракта
Создайте отдельный каталог, который можно хранить вместе с сервером или в репозитории интеграционных тестов:
mkdir -p contract/tools contract/cases
touch contract/tools.json
touch contract/tools/lookup_record.schema.json
touch contract/cases/lookup_record.json
Команды создают только новые каталоги и пустые файлы в текущей рабочей директории. Если такие файлы уже существуют, touch не удаляет их содержимое, но перед работой всё равно проверьте путь командой pwd.
Получите список инструментов через тот же транспорт и с той же версией протокола, которые использует ваш клиент. Сохраните полученный документ без токенов, пользовательских данных, временных идентификаторов и абсолютных путей. Способ вызова зависит от реализации сервера, поэтому универсальная команда здесь намеренно не приводится.
В contract/tools.json храните нормализованное представление интерфейса. Пример:
{
"protocolRevision": "PINNED_BY_PROJECT",
"tools": [
{
"name": "lookup_record",
"inputSchemaFile": "tools/lookup_record.schema.json"
}
]
}
Значение PINNED_BY_PROJECT — заполнитель. Замените его фактическим значением, которое фиксирует ваш проект. Не копируйте версию из этой статьи и не допускайте автоматического выбора «последней» версии в контрактном тесте.
Пример отдельной схемы входа:
{
"type": "object",
"properties": {
"recordId": {
"type": "string",
"minLength": 1
}
},
"required": ["recordId"],
"additionalProperties": false
}
Эта схема является примером, а не требованием MCP. В реальном контракте сохраните схему, опубликованную вашим сервером, включая используемую версию JSON Schema и все значимые ограничения.
Шаг 2. Опишите случаи глазами клиента
Снимок схемы показывает, что сервер объявляет. Набор случаев показывает, чем действительно пользуется клиент. Для каждого инструмента нужны как минимум успешный ввод, отклоняемый ввод и ожидаемая классификация ошибки.
Пример contract/cases/lookup_record.json:
{
"tool": "lookup_record",
"acceptedArguments": [
{
"recordId": "example-record"
}
],
"rejectedArguments": [
{},
{
"recordId": 42
},
{
"recordId": "example-record",
"unexpected": true
}
],
"acceptedSuccessShape": {
"content": [
{
"type": "text",
"text": "example result"
}
]
},
"acceptedToolErrorShape": {
"isError": true,
"content": [
{
"type": "text",
"text": "example error"
}
]
}
}
Здесь строки example result и example error — синтетические данные. Не делайте контракт зависимым от точного текста сообщения, если клиенту важны только признак ошибки и тип содержимого. Точный текст обычно меняется чаще структуры.
Если клиент читает структурированный результат, зафиксируйте именно используемые поля и их типы. Не утверждайте, что необязательное поле обязательно, только потому что оно встретилось в одном ответе.
Шаг 3. Добавьте проверку без запуска агента
Следующий пример использует только встроенные возможности Node.js. Он не заменяет полноценный валидатор JSON Schema: задача скрипта — показать базовую проверку снимка и потребительских ожиданий без сетевых вызовов.
Сохраните пример как contract/check-contract.mjs:
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
const readJson = async (path) =>
JSON.parse(await readFile(new URL(path, import.meta.url), "utf8"));
const manifest = await readJson("./tools.json");
const cases = await readJson("./cases/lookup_record.json");
const schema = await readJson("./tools/lookup_record.schema.json");
const tool = manifest.tools.find((item) => item.name === cases.tool);
assert.ok(tool, `Missing tool: ${cases.tool}`);
assert.equal(schema.type, "object");
assert.ok(schema.properties?.recordId, "Missing recordId property");
assert.ok(
schema.required?.includes("recordId"),
"recordId is no longer required"
);
assert.equal(
schema.properties.recordId.type,
"string",
"recordId must remain a string"
);
const success = cases.acceptedSuccessShape;
assert.ok(Array.isArray(success.content), "Success content must be an array");
assert.equal(success.content[0]?.type, "text");
assert.equal(typeof success.content[0]?.text, "string");
const toolError = cases.acceptedToolErrorShape;
assert.equal(toolError.isError, true);
assert.ok(Array.isArray(toolError.content));
console.log("Contract checks passed");
Запустите проверку из корня проекта:
node contract/check-contract.mjs
Ожидаемый результат примера:
Contract checks passed
Скрипт проверяет только явно записанные ожидания условного клиента. Для производственного набора подключите валидатор той версии JSON Schema, которую фактически публикует сервер, и зафиксируйте его версию в менеджере зависимостей проекта.
Шаг 4. Сопоставьте объявленную схему с поведением сервера
Статическая проверка не обнаружит сервер, который публикует одну схему, но выполняет другую. Поэтому добавьте отдельный тестовый режим сервера и вызывайте каждый инструмент данными из acceptedArguments и rejectedArguments.
Адаптер теста должен выполнить следующую последовательность:
- Запустить сервер в изолированном тестовом окружении.
- Согласовать закреплённую проектом версию протокола.
- Запросить список инструментов и нормализовать нестабильные поля.
- Сравнить имена и схемы с утверждённым снимком.
- Вызвать инструмент каждым допустимым набором аргументов.
- Убедиться, что клиент способен разобрать каждый успешный ответ.
- Передать каждый недопустимый набор и проверить категорию отказа.
- Завершить сервер и очистить только созданные тестом ресурсы.
Конкретный код адаптера должен использовать реальный SDK или транспорт вашего проекта. Не подменяйте его вымышленным клиентом: иначе тест проверит модель протокола из теста, а не производственную интеграцию.
Проверяйте ошибки по категориям
Разделите ожидания как минимум на три класса:
- Транспортный отказ: соединение не установлено или было прерванo.
- Ошибка протокольного запроса: вызов не был корректно обработан на уровне протокола.
- Ошибочный результат инструмента: вызов дошёл до инструмента, но операция завершилась ожидаемым прикладным отказом.
Не сводите эти состояния к одной строке error. Клиент может повторять транспортный запрос, исправлять аргументы после ошибки валидации и передавать прикладной отказ модели — это разные ветви управления.
Шаг 5. Отделите совместимое изменение от несовместимого
Для входной схемы обычно требуют ручного решения или блокировки:
- удаление инструмента;
- переименование аргумента;
- добавление обязательного аргумента без значения по умолчанию на стороне сервера;
- сужение допустимого типа или диапазона;
- запрет ранее разрешённых дополнительных полей, если клиент их отправляет.
Для результата потенциально несовместимы:
- удаление поля, которое читает клиент;
- смена типа поля;
- замена поддерживаемого клиентом типа содержимого на неподдерживаемый;
- перенос прикладной ошибки в другую категорию;
- возврат успешного статуса с формой данных, которую клиент считает ошибочной.
Добавление необязательного поля часто совместимо, но только если клиент игнорирует неизвестные поля. Контракт должен проверять реальное поведение потребителя, а не предполагать его.
Шаг 6. Встройте контракт в CI
Запускайте статическую проверку при каждом изменении клиента, схем и сервера. Поведенческие тесты запускайте там, где доступна изолированная тестовая конфигурация.
Пример нейтрального фрагмента shell-задачи:
set -eu
node contract/check-contract.mjs
npm test -- --runInBand contract
Вторая команда является примером и применима только в проекте, где npm test действительно настроен и поддерживает указанные параметры. Замените её фактической командой вашего тестового раннера.
Обновление снимка не должно происходить автоматически после падения теста. Сначала изучите различие, определите затронутых потребителей, затем обновите сервер, клиент или утверждённый контракт отдельным ревью.
Как проверить результат
Набор готов, если вы можете воспроизвести оба сценария:
- На неизменённой версии сервера статические и поведенческие тесты проходят, а клиент разбирает успешный и ошибочный результат.
-
В тестовой ветке вы намеренно меняете тип
recordIdсо строки на число либо удаляете инструмент, после чего CI завершается ошибкой до запуска агента.
После проверки отмените намеренное изменение обычным способом контроля версий. Не запускайте для этого команды, стирающие весь рабочий каталог или несохранённые изменения.
Полезный отчёт о несовместимости должен отвечать на три вопроса:
- какой инструмент и какая часть контракта изменились;
- какое ожидание клиента нарушено;
- требуется ли миграция клиента, сервера или обоих компонентов.
Типовые ошибки
Сравнение сырого JSON как текста
Порядок ключей и форматирование не определяют совместимость. Разбирайте JSON, нормализуйте его и сравнивайте семантически значимые поля.
Один «золотой» ответ
Полный снимок ответа ломается от времени, идентификаторов и текста. Проверяйте обязательную структуру, типы и инварианты; точные значения фиксируйте только там, где они являются частью соглашения.
Проверка только счастливого пути
Агент особенно уязвим к изменению ошибок: повторяет необратимую операцию, передаёт модели пустой результат или скрывает причину отказа. Негативные случаи должны быть такими же явными, как успешные.
Автоматическое принятие нового снимка
Если CI сам обновляет ожидаемую схему, проверка подтверждает любое изменение. Новый контракт должен приниматься осознанно вместе с оценкой потребителей.
Тестирование другим клиентским стеком
Универсальный диагностический клиент полезен для локализации проблемы, но потребительский контракт обязан проходить через тот же декодер и те же преобразования, что и рабочий агент.
Секреты в фикстурах и логах
Контрактные данные должны быть синтетическими. Перед сохранением снимка удаляйте заголовки авторизации, персональные данные, внутренние адреса и чувствительные фрагменты ошибок.
Ограничения подхода
- Контрактные тесты не оценивают, правильно ли модель выбрала инструмент.
- Они не доказывают смысловую корректность данных, если проверяется только форма ответа.
- Снимки не обнаруживают расхождение поведения, пока не добавлен реальный вызов сервера.
- Тестовый стенд может отличаться от производственного по правам, лимитам и зависимостям.
- Потоковые ответы, прогресс, отмена и конкурентные вызовы требуют отдельных сценариев.
- Совместимость JSON Schema зависит от используемой версии спецификации и возможностей конкретного валидатора.
Поэтому контрактные тесты следует дополнять интеграционными тестами транспорта, тестами разрешений и небольшим набором сценариев агента. Их главная функция — быстро остановить известный класс несовместимых интерфейсных изменений.
Итоговый контрольный список
- Версия протокола закреплена проектом.
- Список инструментов и входные схемы сохранены без секретов.
- Для каждого используемого инструмента есть допустимые и недопустимые аргументы.
- Клиент проверяет фактически используемую форму успешного результата.
- Транспортные, протокольные и прикладные ошибки различаются.
- Поведенческий тест вызывает реальный сервер в безопасном окружении.
- Несовместимое изменение воспроизводимо останавливает CI.
- Снимок обновляется только после ревью.
Дополнительные практические материалы собраны в руководствах Agent Lab Journal, а определения MCP, JSON Schema и других терминов — в глоссарии.