Типы JSON-сообщений
SDK прячет от вас сообщения, и это удобно ровно до первой непонятной ошибки. Тогда приходится смотреть в поток и понимать, что там. Этот урок — про формат этого потока. К концу вы будете читать любой обмен между клиентом и сервером без документации и понимать, что означает каждое поле.
JSON-RPC 2.0 в одном абзаце
JSON-RPC — старый и очень простой протокол удалённого вызова: одна сторона отправляет JSON-объект с именем метода и параметрами, другая отвечает объектом с результатом или ошибкой. Никакой привязки к HTTP: сообщения можно передавать чем угодно, лишь бы доходили. MCP взял его целиком, добавил несколько ограничений и договорился об именах методов. Каждое сообщение — отдельный JSON-объект; пакетов из нескольких сообщений в одном массиве в MCP нет.
Четыре вида сообщений
Запрос — есть id, method и необязательные params:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": { "name": "build_report", "arguments": { "sprint": "2026-09" } }
}Два правила про id, которых нет в базовом JSON-RPC. Он не может быть null — только строка или число. И он не должен совпадать с id любого другого запроса той же стороны, на который ещё не пришёл ответ. Уникальность нужна, чтобы получатель, у которого в работе несколько запросов, сопоставил ответы.
Результат — тот же id и поле result:
{
"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:
{
"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 — зарезервировано за спецификацией. Сейчас определены три:
-32020HeaderMismatch — заголовки HTTP не совпали с телом (об этом в уроке про Streamable HTTP);-32021MissingRequiredClientCapability — сервер попытался использовать возможность, которую клиент не объявил;-32022UnsupportedProtocolVersion — сервер не поддерживает запрошенную версию протокола.
Код -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:
{
"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" }
}
}Сервер отвечает своей версией, возможностями и описанием:
{
"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:
{
"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 мин на рабочем месте- Запустите любой свой сервер под Inspector и найдите в его интерфейсе историю сообщений. Определите вид каждого: запрос, результат, ошибка, уведомление. Найдите
_metaи посмотрите, какие ключи там есть. - Вызовите инструмент с намеренно неверными аргументами (строку вместо числа) и с исключением внутри. Сравните ответы: в одном случае это JSON-RPC-ошибка, в другом — результат с
isError. Объясните себе, почему так. - Подключитесь к своему серверу клиентом 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. Клиент может вызвать его первым, а может и не вызывать. И ещё одно следствие: в новой ревизии сервер вообще не отправляет клиенту запросов. Только результаты, ошибки и уведомления.
Из четырёх видов сообщений спецификация собирает три паттерна: запрос-ответ, многошаговый запрос, когда серверу нужен ввод от клиента, и подписка, когда ответом служит долгий поток уведомлений. Любой транспорт обязан уметь все три. В следующем уроке разберём первый из транспортов — стандартный ввод-вывод.
