AmigaОбучение ИИ
Модуль 3 · Транспорты и коммуникация · урок 8 из 13

Типы JSON-сообщений

12 мин чтения▶ есть видеоверсия

SDK прячет от вас сообщения, и это удобно ровно до первой непонятной ошибки. Тогда приходится смотреть в поток и понимать, что там. Этот урок — про формат этого потока. К концу вы будете читать любой обмен между клиентом и сервером без документации и понимать, что означает каждое поле.

JSON-RPC 2.0 в одном абзаце

JSON-RPC — старый и очень простой протокол удалённого вызова: одна сторона отправляет JSON-объект с именем метода и параметрами, другая отвечает объектом с результатом или ошибкой. Никакой привязки к HTTP: сообщения можно передавать чем угодно, лишь бы доходили. MCP взял его целиком, добавил несколько ограничений и договорился об именах методов. Каждое сообщение — отдельный JSON-объект; пакетов из нескольких сообщений в одном массиве в MCP нет.

Четыре вида сообщений

Запрос — есть id, method и необязательные params:

json
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": { "name": "build_report", "arguments": { "sprint": "2026-09" } }
}

Два правила про id, которых нет в базовом JSON-RPC. Он не может быть null — только строка или число. И он не должен совпадать с id любого другого запроса той же стороны, на который ещё не пришёл ответ. Уникальность нужна, чтобы получатель, у которого в работе несколько запросов, сопоставил ответы.

Результат — тот же id и поле result:

json
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "resultType": "complete",
    "content": [{ "type": "text", "text": "Отчёт по спринту 2026-09 ..." }]
  }
}

Поле resultType появилось в ревизии 2026-07-28. Значение complete означает обычный результат, input_required — что серверу нужен ввод от клиента (паттерн MRTR из уроков про sampling и roots). Если поля нет — сервер старой ревизии, и клиент трактует ответ как complete.

Ошибка — тот же id и объект error с целым code, строкой message и необязательными data:

json
{
  "jsonrpc": "2.0",
  "id": 3,
  "error": { "code": -32602, "message": "Invalid params", "data": { "field": "sprint" } }
}

Уведомление — метод и параметры без id. Ответа на него не бывает.

Все четыре вида вы уже видели в предыдущих уроках; здесь только формализация.

Коды ошибок

Стандартные коды JSON-RPC: -32700 (невалидный JSON), -32600 (невалидный запрос), -32601 (метод не найден), -32602 (неверные параметры), -32603 (внутренняя ошибка). Диапазон -32000…-32099 JSON-RPC отдаёт под ошибки реализации, и MCP его поделил: -32000…-32019 — историческое наследие, новых кодов там не заводят; -32020…-32099 — зарезервировано за спецификацией. Сейчас определены три:

  • -32020 HeaderMismatch — заголовки HTTP не совпали с телом (об этом в уроке про Streamable HTTP);
  • -32021 MissingRequiredClientCapability — сервер попытался использовать возможность, которую клиент не объявил;
  • -32022 UnsupportedProtocolVersion — сервер не поддерживает запрошенную версию протокола.

Код -32002 («ресурс не найден») из старых ревизий больше не отправляют, но принимать его от старых серверов клиенты должны. Свои коды для прикладных ошибок стоит выбирать вне диапазона -32768…-32000.

Важно различать ошибки протокола и ошибки инструмента. Если инструмент упал, сервер не отправляет JSON-RPC-ошибку: он возвращает нормальный результат с isError: true и текстом ошибки в content, чтобы модель могла его прочитать и попробовать иначе. JSON-RPC-ошибка — это когда не получилось даже дойти до инструмента: неизвестный метод, неверные параметры, неподдерживаемая версия.

Поле _meta

У любого запроса и любого результата есть необязательное поле _meta — место для метаданных, которые не относятся к самому методу. Ключи с префиксом io.modelcontextprotocol/ зарезервированы спецификацией, свои ключи принято именовать в обратной DNS-нотации (ru.amiga/trace). Два ключа вы уже знаете: progressToken для прогресса и io.modelcontextprotocol/logLevel для логов. Ещё три появляются в следующем разделе.

Как стороны договариваются: старый способ

До ревизии 2025-11-25 включительно соединение начиналось с рукопожатия. Клиент отправляет запрос initialize:

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-11-25",
    "capabilities": { "roots": { "listChanged": true }, "sampling": {}, "elicitation": {} },
    "clientInfo": { "name": "amiga-assistant", "version": "0.4.0" }
  }
}

Сервер отвечает своей версией, возможностями и описанием:

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-11-25",
    "capabilities": {
      "logging": {},
      "tools": { "listChanged": true },
      "resources": { "subscribe": true, "listChanged": true },
      "prompts": { "listChanged": true }
    },
    "serverInfo": { "name": "reports", "version": "1.2.0" },
    "instructions": "Сначала вызывай list_sprints, потом build_report."
  }
}

Клиент завершает рукопожатие уведомлением notifications/initialized, и только после этого начинается нормальная работа. Версию клиент предлагает свою; если сервер её не поддерживает, он отвечает другой версией, и клиент либо соглашается, либо отключается.

Возможности (capabilities) — это объявление того, что сторона умеет. Клиентские: roots, sampling, elicitation. Серверные: tools, resources, prompts, logging, completions. У некоторых есть подфлаги: listChanged (сервер умеет сообщать об изменении списка), subscribe у ресурсов (можно подписаться на изменения конкретного ресурса). Обе стороны обязаны использовать только то, что было объявлено. Поле instructions — подсказка для модели о том, как пользоваться сервером; хосты обычно добавляют её в системный промпт.

Как стороны договариваются: новый способ

Ревизия 2026-07-28 рукопожатие убрала. Логика такая: сервер не должен ничего помнить о клиенте между запросами, а значит, всё, что раньше сообщалось один раз при initialize, теперь сообщается в каждом запросе — в _meta:

json
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "build_report",
    "arguments": { "sprint": "2026-09" },
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": { "elicitation": {} },
      "io.modelcontextprotocol/clientInfo": { "name": "amiga-assistant", "version": "0.5.0" }
    }
  }
}

Версия и возможности обязательны в каждом запросе; без них сервер отвечает -32602. Информация о клиенте необязательна, но её рекомендуют слать. Сервер в свою очередь кладёт io.modelcontextprotocol/serverInfo в _meta каждого результата.

Если сервер не поддерживает версию, он отвечает -32022 со списком поддерживаемых, и клиент повторяет запрос с одной из них. Отдельного шага согласования нет — но есть необязательный запрос server/discover, который любой сервер обязан реализовать. Он возвращает то, что раньше возвращал initialize: supportedVersions, capabilities, instructions, а также ttlMs и cacheScope — сколько и кому можно кэшировать ответ. Клиент может вызвать его первым, чтобы показать пользователю, что умеет сервер, а может и не вызывать.

Ещё одно следствие: в новой ревизии сервер не отправляет клиенту запросов вообще. Раньше sampling/createMessage и roots/list были запросами сервера; теперь они вкладываются в результат input_required. Сервер посылает только результаты, ошибки и уведомления.

Три паттерна обмена

Из четырёх видов сообщений спецификация собирает три паттерна. Запрос-ответ — базовый. Многошаговый запрос (MRTR) — сервер возвращает input_required, клиент повторяет запрос с ответами. Подписка — клиент отправляет subscriptions/listen, а ответом служит долгий поток уведомлений об изменениях. Любой транспорт обязан уметь все три, и все три построены из тех же четырёх видов сообщений — поэтому новые паттерны не требуют менять транспорты.

Попробуйте сами

10–15 мин на рабочем месте
  1. Запустите любой свой сервер под Inspector и найдите в его интерфейсе историю сообщений. Определите вид каждого: запрос, результат, ошибка, уведомление. Найдите _meta и посмотрите, какие ключи там есть.
  2. Вызовите инструмент с намеренно неверными аргументами (строку вместо числа) и с исключением внутри. Сравните ответы: в одном случае это JSON-RPC-ошибка, в другом — результат с isError. Объясните себе, почему так.
  3. Подключитесь к своему серверу клиентом SDK, вызовите client.server_capabilities и client.protocol_version и сравните с тем, что вы ожидали увидеть.

Коротко

  • MCP использует JSON-RPC 2.0: запрос (id, method, params), результат (id, result), ошибка (id, error), уведомление (без id).
  • id не может быть null и не должен повторять id незавершённого запроса той же стороны.
  • В 2026-07-28 результат содержит resultType: complete или input_required.
  • Стандартные коды ошибок JSON-RPC плюс -32020…-32022 от MCP; ошибка инструмента — это результат с isError, а не JSON-RPC-ошибка.
  • До 2025-11-25: рукопожатие initialize → результат → notifications/initialized, обмен возможностями один раз.
  • С 2026-07-28: рукопожатия нет, версия и возможности клиента — в _meta каждого запроса; server/discover — необязательный способ узнать сервер.
  • Три паттерна: запрос-ответ, многошаговый запрос, подписка.

Видеоверсия

Сценарий озвучки · 471 слово, ≈ 4 мин

SDK прячет от вас сообщения протокола, и это удобно ровно до первой непонятной ошибки. Тогда приходится смотреть в поток и понимать, что там. Этот урок — про формат этого потока.

В основе MCP лежит JSON-RPC версии два — старый и очень простой протокол удалённого вызова. Одна сторона отправляет джейсон-объект с именем метода и параметрами, другая отвечает объектом с результатом или ошибкой. К транспорту он не привязан: сообщения можно передавать чем угодно.

Видов сообщений четыре. Запрос: у него есть идентификатор, имя метода и параметры. Результат: тот же идентификатор и поле result. Ошибка: тот же идентификатор и объект с целым кодом, текстом и необязательными данными. И уведомление: метод и параметры без идентификатора, ответа на него не бывает. Про идентификатор два правила, которых нет в базовом JSON-RPC: он не может быть пустым и не должен повторять идентификатор другого незавершённого запроса той же стороны. Иначе получатель не сопоставит ответы, когда в работе несколько запросов.

Про коды ошибок. Стандартные — невалидный джейсон, неизвестный метод, неверные параметры, внутренняя ошибка. Плюс три кода, которые определил сам MCP: заголовки не совпали с телом, у клиента нет нужной возможности, и версия протокола не поддерживается. Важно различать ошибку протокола и ошибку инструмента. Если инструмент упал, сервер не отправляет ошибку протокола, а возвращает обычный результат с флагом «это ошибка» и текстом, чтобы модель могла прочитать и попробовать иначе. Ошибка протокола — это когда не получилось даже дойти до инструмента.

У любого запроса и результата есть поле мета — место для метаданных, не относящихся к самому методу. Там живут токен прогресса, уровень логов и, в новой ревизии, самое важное: версия протокола и возможности клиента.

Теперь о том, как стороны договариваются. В старых ревизиях соединение начиналось с рукопожатия. Клиент отправляет запрос initialize с версией протокола, своими возможностями и именем. Сервер отвечает своей версией, своими возможностями, именем и инструкцией для модели. Клиент подтверждает уведомлением, и начинается работа. Возможности — это объявление того, что сторона умеет: клиент — корни, sampling, элиситацию; сервер — инструменты, ресурсы, промпты, логирование. Обе стороны обязаны использовать только объявленное.

Ревизия июля две тысячи двадцать шестого рукопожатие убрала. Логика такая: сервер не должен ничего помнить о клиенте между запросами. Значит, всё, что раньше сообщалось один раз, теперь сообщается в каждом запросе — в поле мета. Версия и возможности клиента обязательны в каждом запросе, имя клиента желательно. Если сервер не поддерживает версию, он отвечает ошибкой со списком поддерживаемых, и клиент повторяет запрос с одной из них. Есть ещё необязательный запрос «обнаружить сервер», который любой сервер обязан реализовать: он возвращает то же, что раньше возвращал initialize. Клиент может вызвать его первым, а может и не вызывать. И ещё одно следствие: в новой ревизии сервер вообще не отправляет клиенту запросов. Только результаты, ошибки и уведомления.

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

Отметка хранится только в вашем браузере