Продвинутый практикум

Как тестировать согласование возможностей между MCP-клиентом и сервером

До 10 минут Продвинутый уровень Результат: матрица контрактных тестов

Успешное соединение еще не означает совместимость. Клиент может принять версию протокола, но затем вызвать не объявленный сервером метод; сервер — отправить запрос, для которого клиент не заявил поддержку; HTTP-прокси — удалить заголовок с согласованной версией. Контрактные тесты должны обнаруживать такие расхождения до развертывания.

Что именно требуется согласовать

Model Context Protocol (MCP) разделяет обязательный базовый протокол и необязательные возможности. Инициализация устанавливает три части контракта:

  1. версию протокола, используемую в соединении;
  2. возможности клиента, например roots, sampling или elicitation;
  3. возможности сервера, например tools, resources, prompts, logging и completions.

Наличие верхнеуровневой возможности и ее подфункции — разные утверждения. Например, "resources": {} разрешает операции с ресурсами, но не подтверждает подписки. Для них требуется "resources": {"subscribe": true}. Аналогично, tools.listChanged сообщает о поддержке уведомлений об изменении списка, а не просто о наличии инструментов.

Шаг 1. Зафиксируйте политику совместимости

Не начинайте с сетевого теста. Сначала представьте политику клиента как данные. Ниже — пример, а не требование спецификации:

{
  "supportedProtocolVersions": [
    "2025-11-25",
    "2025-06-18"
  ],
  "requiredServerCapabilities": [
    "tools"
  ],
  "optionalServerCapabilities": [
    "resources",
    "resources.subscribe",
    "tools.listChanged"
  ]
}

Разделение обязательных и необязательных возможностей предотвращает две крайности: запуск заведомо неработоспособной сессии и отказ из-за функции, без которой приложение умеет работать.

Условие Ожидаемое решение клиента
Версия ответа входит в поддерживаемый список Принять ее как версию сессии
Версия ответа не поддерживается Не отправлять notifications/initialized, закрыть соединение
Нет обязательной возможности Завершить запуск с диагностикой
Нет необязательной возможности Отключить зависимую функцию
Подфункция явно равна false или отсутствует Не использовать подфункцию
Присутствует неизвестное поле Не падать; сохранить совместимость с расширяемой схемой

Шаг 2. Проверьте форму и порядок инициализации

Первым обычным взаимодействием должен быть запрос initialize. Клиент передает поддерживаемую версию, свои capabilities и clientInfo. После успешного ответа он отправляет уведомление notifications/initialized.

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-11-25",
    "capabilities": {
      "roots": {
        "listChanged": true
      }
    },
    "clientInfo": {
      "name": "contract-test-client",
      "version": "0.0.0"
    }
  }
}

Имена и версии в этом фрагменте — тестовые значения. Они не обозначают существующий продукт.

Зафиксируйте в тестовом транспорте все исходящие сообщения и проверьте:

  1. initialize отправлен раньше прикладных запросов;
  2. идентификатор ответа совпадает с идентификатором запроса;
  3. ответ содержит строковый protocolVersion, объект capabilities и корректный serverInfo;
  4. notifications/initialized появляется только после полной валидации ответа;
  5. при ошибке, тайм-ауте или несовместимой версии уведомление не отправляется.

Шаг 3. Вынесите решение в чистую функцию

Сетевой код трудно покрыть всей матрицей. Практичнее отдельно тестировать функцию, которая превращает ответ сервера в решение: принять соединение, включить ограниченный режим или отказать.

// negotiation.mjs — пример
function hasCapability(capabilities, path) {
  let value = capabilities;

  for (const part of path.split(".")) {
    if (value === null || typeof value !== "object" ||
        !Object.prototype.hasOwnProperty.call(value, part)) {
      return false;
    }
    value = value[part];
  }

  return value !== false && value !== undefined;
}

export function negotiate(policy, initializeResult) {
  if (!policy.supportedProtocolVersions.includes(
    initializeResult.protocolVersion
  )) {
    return {
      ok: false,
      reason: "unsupported_protocol_version"
    };
  }

  const capabilities = initializeResult.capabilities ?? {};
  const missing = policy.requiredServerCapabilities.filter(
    capability => !hasCapability(capabilities, capability)
  );

  if (missing.length > 0) {
    return {
      ok: false,
      reason: "missing_required_capabilities",
      missing
    };
  }

  return {
    ok: true,
    protocolVersion: initializeResult.protocolVersion,
    enabledOptionalCapabilities:
      policy.optionalServerCapabilities.filter(
        capability => hasCapability(capabilities, capability)
      )
  };
}

Эта функция демонстрирует архитектуру теста, но не заменяет официальную схему MCP. В рабочем коде сначала валидируйте типы ответа, а уже затем применяйте продуктовую политику.

Шаг 4. Составьте минимальную матрицу контрактных тестов

Для воспроизводимости можно использовать встроенный в Node.js модуль node:test. Команда не устанавливает пакеты и не обращается к сети:

node --test negotiation.test.mjs
// negotiation.test.mjs — пример
import test from "node:test";
import assert from "node:assert/strict";
import { negotiate } from "./negotiation.mjs";

const policy = {
  supportedProtocolVersions: ["2025-11-25", "2025-06-18"],
  requiredServerCapabilities: ["tools"],
  optionalServerCapabilities: [
    "resources",
    "resources.subscribe",
    "tools.listChanged"
  ]
};

test("принимает поддерживаемую версию и обязательную capability", () => {
  const result = negotiate(policy, {
    protocolVersion: "2025-11-25",
    capabilities: { tools: {} },
    serverInfo: { name: "fixture", version: "0.0.0" }
  });

  assert.equal(result.ok, true);
  assert.equal(result.protocolVersion, "2025-11-25");
});

test("отклоняет неизвестную клиенту версию", () => {
  const result = negotiate(policy, {
    protocolVersion: "2099-01-01",
    capabilities: { tools: {} },
    serverInfo: { name: "fixture", version: "0.0.0" }
  });

  assert.deepEqual(result, {
    ok: false,
    reason: "unsupported_protocol_version"
  });
});

test("отклоняет ответ без обязательной capability", () => {
  const result = negotiate(policy, {
    protocolVersion: "2025-11-25",
    capabilities: { resources: {} },
    serverInfo: { name: "fixture", version: "0.0.0" }
  });

  assert.deepEqual(result, {
    ok: false,
    reason: "missing_required_capabilities",
    missing: ["tools"]
  });
});

test("не включает subscribe по одному наличию resources", () => {
  const result = negotiate(policy, {
    protocolVersion: "2025-11-25",
    capabilities: {
      tools: {},
      resources: {}
    },
    serverInfo: { name: "fixture", version: "0.0.0" }
  });

  assert.equal(result.ok, true);
  assert.deepEqual(result.enabledOptionalCapabilities, ["resources"]);
});

test("переносит неизвестные поля без ошибки", () => {
  const result = negotiate(policy, {
    protocolVersion: "2025-11-25",
    capabilities: {
      tools: {},
      "com.example/future": { mode: "fixture" }
    },
    serverInfo: {
      name: "fixture",
      version: "0.0.0",
      extraField: true
    }
  });

  assert.equal(result.ok, true);
});

Добавьте еще четыре интеграционных сценария на уровне транспорта:

  • тайм-аут ответа на initialize приводит к отмене ожидания и закрытию транспорта;
  • JSON-RPC error не интерпретируется как успешный результат;
  • серверный запрос, зависящий от не объявленной клиентом capability, отклоняется предсказуемо;
  • прикладной метод не отправляется до завершения рукопожатия.

Шаг 5. Отдельно протестируйте Streamable HTTP

Тесты JSON-RPC не обнаружат ошибку reverse proxy или HTTP-адаптера. Для HTTP-транспорта проверьте как минимум заголовки, тип ответа и идентификатор сессии, если сервер его выдает.

После инициализации запросы должны использовать согласованную версию в заголовке MCP-Protocol-Version. Не подставляйте автоматически последнюю известную клиенту версию: сервер мог выбрать другую поддерживаемую редакцию.

POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2025-11-25

{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}

Это образец формы запроса, а не команда к реальному адресу. В тесте направляйте его только на локальный стенд или контролируемый тестовый endpoint. Не помещайте токены, cookie и рабочие URL в фикстуры.

Контракт HTTP-адаптера должен проверять:

  • заголовок версии отсутствует в первоначальном initialize, но присутствует в последующих запросах;
  • его значение совпадает с версией из принятого ответа сервера;
  • Accept допускает application/json и text/event-stream;
  • клиент корректно обрабатывает оба разрешенных типа ответа;
  • заголовки сессии, если они применяются в выбранной редакции и возвращены сервером, не смешиваются между параллельными соединениями.

Проверка результата

Набор готов, если каждый переход состояния наблюдаем и имеет отрицательный сценарий. Полезная итоговая проверка выглядит так:

NEW
  → initialize sent
  → response validated
  → version accepted
  → required capabilities present
  → notifications/initialized sent
  → READY

Любой отказ до состояния READY должен исключать прикладные вызовы. В журнале диагностики достаточно записывать выбранную версию, названия отсутствующих capabilities и категорию ошибки. Полные тела сообщений могут содержать чувствительные данные, поэтому не включайте их в логи по умолчанию.

Запустите одну и ту же матрицу минимум против локального транспорта-двойника, HTTP-адаптера и сборки, максимально близкой к production. Именно повторное использование одинаковых контрактов выявляет различия окружений.

Типовые ошибки

Сравнение версий как дат или чисел
Версия MCP — строковый идентификатор редакции. Поддерживайте явное множество допустимых значений, а не условие вида «не старше».
Проверка только верхнего уровня
Наличие resources не доказывает поддержку resources.subscribe; наличие tools не доказывает tools.listChanged.
Capabilities трактуются как список методов
Методы и capabilities связаны правилами конкретной редакции спецификации. Храните такую связь в одном слое, а не размазывайте по обработчикам.
Неизвестное поле считается ошибкой
Объекты возможностей расширяемы. Валидатор должен строго проверять обязательные поля, но не ломаться только из-за дополнительного корректно сформированного поля.
Уведомление initialized отправляется слишком рано
Сначала проверьте схему, версию и обязательные возможности. Иначе сервер считает сессию готовой, хотя клиент уже решил завершить ее.
Моки повторяют реализацию клиента
Фикстуры должны описывать сообщения на границе протокола. Если мок вызывает те же внутренние функции, что и production-код, тест не обнаружит ошибку сериализации или транспорта.

Ограничения подхода

Capabilities подтверждают заявленную поддержку, но не корректность реализации. Сервер может объявить tools и ошибаться на tools/list; это проверяется отдельными поведенческими тестами. Контрактные тесты также не заменяют проверки авторизации, повторного подключения, отмены, ограничений размера сообщений и безопасности данных.

Редакции MCP различаются. Поля, обязательность заголовков и экспериментальные функции следует привязывать к конкретной версии схемы. Не смешивайте правила текущей, старой и draft-редакций в одном безусловном валидаторе.

Контрольный список

  • поддерживаемые версии заданы явным списком;
  • обязательные и необязательные capabilities разделены;
  • проверены порядок initialize и notifications/initialized;
  • есть отрицательные тесты версии, схемы, тайм-аута и capabilities;
  • подвозможности проверяются отдельно;
  • неизвестные расширения не вызывают аварийного отказа;
  • HTTP-заголовок содержит именно согласованную версию;
  • до состояния READY прикладные методы заблокированы;
  • фикстуры не содержат секретов и production-адресов;
  • одна матрица выполняется во всех целевых окружениях.

Материалы

Продолжить настройку интеграции можно в разделе практических гайдов, а определения протокольных терминов собраны в глоссарии Agent Lab Journal.

Для сверки контрактов используйте первичные источники: схему MCP 2025-11-25, описание жизненного цикла и согласования и требования к транспортам.