Что случилось 28 июля?
Касается ли это вас прямо сейчас: если у вас работают MCP-серверы или клиент, то да - но не так, как кажется. Ломается не всё сразу, и проверка своей стороны занимает пятнадцать минут. Команды для неё - в третьем разделе.
Предыдущая стабильная ревизия датирована 2025-11-25. Восемь месяцев без изменений для протокола, вокруг которого выросла целая индустрия интеграций, - это долго, и накопленное вышло одним куском.
Что убрали:
- рукопожатие
initializeи уведомлениеnotifications/initialized; - заголовок
Mcp-Session-Idи вместе с ним протокольные сессии; - методы
pingиlogging/setLevel, уведомлениеnotifications/roots/list_changed; - возобновляемость потока: заголовок
Last-Event-IDи идентификаторы событий.
Что добавили:
server/discover- метод обнаружения, который сервер обязан реализовать;subscriptions/listen- один долгоживущий поток вместо подписок на ресурсы;- шаблон многораундовых запросов MRTR: сервер возвращает
input_required, клиент повторяет запрос с данными; - обязательное поле
resultTypeв каждом результате; - поля кэширования
ttlMsиcacheScopeв списочных методах.
Каждое изменение в спецификации снабжено номером предложения и ссылкой на обсуждение: SEP-2567, SEP-2575, SEP-2322, SEP-2663, SEP-2596, SEP-2549. Проверяйте формулировки по ним, а не по пересказам.
Почему «я обновил SDK» ещё не значит «я мигрировал»?
Источник первый - дефолт SDK. Документ миграции TypeScript SDK говорит прямо:
Ничто в v2 по умолчанию не кладёт в провод ни одного байта ревизии 2026-07-28: собранные вручную
Client,ServerиMcpServerпродолжают говорить на протоколе эпохи 2025, под который были написаны.
Режим legacy стоит по умолчанию. Чтобы клиент попробовал новую ревизию, ему нужен явный versionNegotiation: { mode: 'auto' }. Локальная проверка на паре свежих пакетов дала такой вывод:
DEFAULT (no versionNegotiation) => era: legacy | tools: ping_tool
mode:'auto' => era: modern | tools: ping_tool
mode:{pin:'2026-07-28'} => era: modern | tools: ping_toolИнструменты работают во всех трёх случаях. Разница видна только через getProtocolEra().
Источник второй - имя пакета в npm. Мажорную двойку получило новое семейство, а привычное имя осталось на единице:
| Пакет | Версия | Опубликован |
|---|---|---|
@modelcontextprotocol/sdk | 1.30.0 | 27.07, 17:56 UTC |
@modelcontextprotocol/core | 2.0.0 | 27.07, 23:55 UTC |
@modelcontextprotocol/client | 2.0.0 | 27.07, 23:55 UTC |
@modelcontextprotocol/server | 2.0.0 | 27.07, 23:55 UTC |
@modelcontextprotocol/node | 2.0.0 | 27.07, 23:55 UTC |
@modelcontextprotocol/inspector | 2.0.0 | 28.07, 07:33 UTC |
Рядом лежат ещё два пакета: codemod для автоматической миграции кода и server-legacy - замороженная копия старого транспорта. При установке последнего npm печатает предупреждение: пакет существует только ради миграции и новых возможностей не получит.
Источник третий - константа внутри самих пакетов v2. Она осталась легаси-словарём:
LATEST_PROTOCOL_VERSION = 2025-11-25
SUPPORTED_PROTOCOL_VERSIONS = ["2025-11-25","2025-06-18","2025-03-26","2024-11-05","2024-10-07"]Строки 2026-07-28 в списке поддерживаемых нет вообще, хотя тот же пакет экспортирует функции новой ревизии. Смысл у константы узкий: она обозначает последнюю рукопожатную ревизию, а рукопожатия в новой эре больше нет. Определять по ней версию протокола нельзя.
Как узнать, на какой ревизии ваш сервер?
Способ первый, для современного сервера. Три заголовка обязательны, без них придёт ошибка:
curl -s -X POST https://ваш-сервер/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: server/discover' \
-d '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'В ответе придёт supportedVersions со списком ревизий.
Способ второй, для сервера с обычным stdio. Он же самый показательный. Спросите сервер новой ревизией и посмотрите, что он ответит:
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2026-07-28","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}' \
| npx -y ваш-mcp-сервер 2>/dev/null | head -1На живом сервере это дало вот что:
{"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"Playwright","version":"1.62.0-alpha"}}}Клиент попросил 2026-07-28. Сервер ответил 2025-11-25, и рукопожатие прошло успешно. Ни ошибки, ни предупреждения: сервер молча согласовал ту ревизию, которую знает сам. Клиент, который не читает поле protocolVersion из ответа, узнает об этом по странному поведению через неделю.
Здесь же прячется ирония, ради которой стоит запомнить эту команду. Спросить сервер таким способом можно только методом initialize - тем самым рукопожатием, которое новая ревизия удаляет. Проверка работает ровно до тех пор, пока ваш сервер живёт на старой эре.
Способ третий - два бинарных признака транспорта. Современный сервер отвечает 405 на GET и DELETE к своей точке входа, старый отдал бы на GET поток событий:
curl -s -o /dev/null -w "%{http_code}\n" -X GET https://сервер/mcp # 405
curl -s -o /dev/null -w "%{http_code}\n" -X DELETE https://сервер/mcp # 405Способ четвёртый - из кода клиента. getProtocolEra() возвращает modern или legacy и остаётся единственным способом узнать правду о собственном подключении.
Способ пятый - инспектор. В версии 2.0.0 эра подключения показана в интерфейсе переключателем Protocol Era. Это самый наглядный путь, если сервер уже подключён к инспектору.
Почему server/discover говорит не всю правду?
Если вы пишете клиент, который по supportedVersions решает «этот сервер старый, работать не буду», вы отсеете сервер, который вас прекрасно обслужил бы по старым правилам. Проверять надо обе стороны: сначала современный запрос, потом откат на рукопожатие.
Что ломается молча?
Первое. Спецификация прямо описывает, как ведёт себя старый сервер, получивший современный запрос:
Сервер может отклонить запрос ошибкой на своё усмотрение, промолчать или даже обработать метод, неоднозначный между эрами, по правилам старой эры.
То есть tools/call может просто выполниться, но по старым правилам. Лечится обязательной пробой server/discover первым запросом: она даёт детерминированный отказ вместо неопределённости.
Второе. Отсутствие поля трактуется как успех:
Ради обратной совместимости с серверами прежних ревизий, которые не присылают
resultType, клиенты ОБЯЗАНЫ считать отсутствующийresultTypeзначением «complete».
Старый сервер, который на самом деле ждёт от клиента дополнительных данных, для нового клиента выглядит завершившимся штатно.
Третье. Заголовки старого мира выбрасываются без единого слова:
Заголовок
Mcp-Session-Idв запросе - игнорировать, идентификаторы сессий не выпускать и не отражать обратно. ЗаголовокLast-Event-ID- игнорировать; потоки не возобновляемы.
Старый клиент отправляет идентификатор сессии, ошибки не получает - и всё состояние, которое он на эту сессию вешал, тихо перестаёт существовать.
Четвёртое - дефолт legacy в собственном SDK, с которого начинался разбор. Он же самый частый.
Что ломается громко?
| Ситуация | HTTP | Код | Что смотреть |
|---|---|---|---|
Нет _meta в теле запроса | 400 | -32602 | заголовок обещает новую ревизию, а конверта в теле нет |
| Заголовок не совпал с телом | 400 | -32020 | Mcp-Method называет один метод, тело - другой |
| Версия не поддержана | 400 | -32022 | в data.supported придёт список того, что сервер умеет |
| Метод неизвестен | 404 | -32601 | метод удалён из ревизии либо не реализован |
| Не хватает возможности | 400 | -32021 | в data.requiredCapabilities перечислено, чего не хватило |
GET или DELETE к точке входа | 405 | - | признак современного сервера |
Диапазон -32020..-32099 теперь зарезервирован за самой спецификацией, а -32000..-32019 остаётся за реализациями. Отдельно поменялся код «ресурс не найден»: он переехал с -32002 на -32602, и это единственное изменение кодов, которое ломает работающее ветвление на стороне клиента.
Кто с кем совместим?
| Сочетание | Исход |
|---|---|
| Обе стороны в одной эре | работает |
| Одна из сторон умеет обе эры | работает, с откатом на рукопожатие |
| Новый клиент против старого сервера | падает |
| Старый клиент против нового сервера | падает |
Отличить старый сервер от современного помогает одно поле. Старый на неподдержанную версию отвечает кодом -32000 и человекочитаемой строкой, где список версий перечислен текстом. Современный отвечает -32022 и кладёт список в data.supported. Спецификация прямо запрещает завязывать откат на конкретный код ошибки: смотреть надо на наличие поля data.supported, а не на число.
Куда девать состояние, которое лежало в сессиях?
Инженер Кевин Ридль из Wavect перечисляет, что искать в своём коде перед миграцией: карты сессий, липкие cookie, объекты транспорта, переиспользуемые между запросами, списки инструментов на соединение и хранилища событий для возобновляемых потоков. Его вывод короче любого чек-листа:
Именно поэтому «мы не храним сессии MCP» - недостаточная проверка перед миграцией.
Спецификация про requestState высказывается жёстко: сервер обязан считать его вводом, контролируемым атакующим, и защищать целостность, а клиент не имеет права разбирать и менять его содержимое.
Второй капкан - хэндлы без проверки владельца. Если один инструмент выдал идентификатор корзины, а другой принимает его на веру, вы получили уязвимость по дизайну: любой, кто угадает или перехватит идентификатор, поработает с чужими данными. Хэндл в новой схеме - это допуск, и обращаться с ним надо как с допуском: проверять на каждом вызове, что предъявитель имеет на него право.
Отсюда же вытекает главная ошибка переезда - пустить все вызовы через одну широкую сервисную учётную запись. Авторизация в новой ревизии переехала на каждый запрос, и общий кредит поверх неё делает все остальные меры бессмысленными. Если вы уже размечали, что агенту можно трогать, а что нет, эта работа теперь смотрит на каждый вызов - подробнее про допуски мы разбирали в статье про права агента, а про границы периметра - в проверке песочницы.
Почему ретрай теперь может списать деньги дважды?
Формулировка спецификации не оставляет вариантов: идентификатор JSON-RPC у повтора обязан отличаться от исходного, потому что это независимые запросы. Ридль формулирует последствие одной строкой:
Повтор после оборванного потока не должен списать, отправить или удалить дважды.
Практический вывод: до включения многораундовых запросов сделайте операции идемпотентными. Ключ идемпотентности в аргументах инструмента, проверка на стороне сервера, срок жизни ключа. Обратный порядок - сначала MRTR, потом идемпотентность - это готовый инцидент.
Что стало с Roots, Sampling и Logging?
Замены выглядят так: вместо Roots - передавать файлы и каталоги обычными параметрами инструмента или адресами ресурсов; вместо Logging - писать в стандартный поток ошибок либо использовать OpenTelemetry, для которого ревизия отдельно описала соглашения о передаче контекста трассировки.
Здесь и прячется самое недооценённое. Сервер, который раньше переиспользовал модель клиента, после миграции превращается в самостоятельного потребителя платного API. В бюджете появляется новая строка, и посчитайте её до переезда, а не после первого счёта. Сколько именно уходит на токены и почему счёт растёт быстрее ожиданий, мы разбирали в тексте про контекстное окно.
Отдельная деталь: поле, которым заменили logging/setLevel, помечено устаревшим в той же самой ревизии, где его ввели.
Сколько у вас времени на самом деле?
Работающие интеграции в августе не рассыплются: обе эры живут параллельно, а SDK по умолчанию держат старую. Но и год в запасе - иллюзия. Сократить окно короче года политика разрешает только при активной угрозе безопасности с опубликованным advisory, зато удалить возможность сразу после истечения срока могут без отдельного повода.
Пятнадцать минут на проверку своей стороны сегодня, миграция в план на квартал.
А протокол вообще нужен?
Эйсо Кант, глава Poolside, в подкасте Latent.Space 23 июля высказался против самой связки MCP и вызова инструментов и уточнил, что держится этой позиции около двух лет. Его прогноз: через год мы не увидим ни одного системного промпта, набитого двумя-тремя десятками инструментов. Возразить на это просто: его же компания продолжает поддерживать и вызов инструментов, и MCP.
Со стороны безопасности в трекере спецификации 31 июля появился разбор с семью претензиями к дизайну - от инъекции через описания инструментов до теневых имён, которые, по мнению автора, молча конфликтуют между серверами. Он подчёркивал, что говорит о дефектах спецификации, а не реализаций. Через два дня обсуждение закрыл сопровождающий проекта, отклонив разбор как сгенерированный ИИ пересказ уже известных векторов атаки. То есть претензии не проигнорировали - их сочли не новыми.
Обе позиции полезно держать в голове, но ни одна из них не отменяет практической задачи: интеграции, которые у вас уже работают, написаны под протокол, и его основа изменилась.
Что сделать сегодня
- Спросите каждый сервер запросом
initializeс новой ревизией и запишите, какую ревизию он назвал в ответе. - Проверьте свой клиент через
getProtocolEra(). Если тамlegacyпри свежих пакетах, вы ещё в старой эре. - Найдите в коде карты сессий, липкие cookie и переиспользуемые объекты транспорта. Составьте список того, что придётся вынести в явные аргументы.
- Проверьте, что списания, отправки и удаления защищены ключом идемпотентности.
- Если пользуетесь Sampling - посчитайте, во сколько обойдётся прямой доступ к API провайдера.
Источники
- Спецификация MCP
2026-07-28, список изменений - Спецификация MCP
2026-07-28, транспорт Streamable HTTP - Спецификация MCP
2026-07-28, шаблон MRTR - Реестр устаревших возможностей
- Политика жизненного цикла возможностей
- Релизы спецификации на GitHub
- Кевин Ридль, Wavect: руководство по миграции сервера
- Agentic AI Foundation: чего миграция требует от прав доступа
- Гайд миграции TypeScript SDK v2 на ревизию 2026-07-28
- Подкаст Latent.Space с Эйсо Кантом, Poolside
- Appwrite: разбор ревизии 2026-07-28
- AWS: поддержка ревизии в AgentCore Gateway
- Разбор с претензиями к безопасности ревизии, issue #3180
- Реестр npm: пакеты
@modelcontextprotocol