Ревизия Model Context Protocol от 2026-07-28 меняет эксплуатационную модель удалённого MCP. Ядро протокола теперь работает без состояния и обрабатывает отдельные запросы. В каждом запросе есть данные, необходимые для обработки, поэтому балансировщик может отправить следующий вызов на другой экземпляр сервера. В релиз также вошли server/discover, многораундовые запросы Multi Round-Trip Requests (MRTR), обязательные заголовки маршрутизации, подсказки кэширования, framework расширений и более строгие правила авторизации. Нормативные детали этого материала взяты из официальной статьи о релизе и журнала изменений спецификации.
Это изменение состояния протокола, а не требование сделать без состояния всю бизнес-систему. Инструмент по-прежнему может использовать базу данных, очередь или надёжный workflow. Исчезает скрытое состояние, связанное с транспортной сессией MCP, что важно при переходе от одного процесса к нескольким репликам.
Что изменилось по сравнению с протоколом 2025 года
Раньше жизненный цикл начинался запросом initialize, за ним следовало уведомление notifications/initialized, а Streamable HTTP мог выдавать Mcp-Session-Id. Сервер связывал последующие сообщения с соединением. В 2026-07-28 обмен initialize и заголовок протокольной сессии удалены. Каждый запрос сообщает версию протокола и возможности клиента в _meta. Клиенту следует указывать io.modelcontextprotocol/clientInfo, а серверу следует добавлять свою идентификацию в метаданные результата.
Эта таблица помогает спланировать переход:
| Область | Поведение 2025 года | Поведение 2026-07-28 |
|---|---|---|
| Жизненный цикл | initialize и notifications/initialized |
Протокольного рукопожатия нет |
| Сессия | Необязательный Mcp-Session-Id в HTTP |
Протокольной сессии нет |
| Возможности | Согласуются один раз | Объявляются в каждом запросе |
| Обнаружение | После initialize или по соглашению | Современный сервер обязан иметь server/discover, клиент может его вызвать |
| Сервер к клиенту | Запросы по открытому каналу | MRTR возвращает запросы ввода в ответе |
| Маршрутизация HTTP | Шлюз часто разбирает JSON | Заголовки Mcp-Method и применимый Mcp-Name |
| Списки и чтение | Свежесть определяет клиент | Подсказки ttlMs и cacheScope |
| Возобновление | SSE мог использовать ID событий | Last-Event-ID не поддерживается, нужен новый запрос |
| Регистрация | Обычно автоматический DCR | Предпочтителен CIMD, DCR остаётся для совместимости |
Ревизия переносит Tasks в расширение io.modelcontextprotocol/tasks, заменяет старый поток изменений на subscriptions/listen и объявляет устаревшими Roots, Sampling, Logging и старый HTTP+SSE. Политика даёт минимум двенадцать месяцев, но новые реализации не должны добавлять устаревшие возможности.
Как работает протокол без состояния
Современный Streamable HTTP-сервер публикует одну MCP-точку входа, принимающую POST. Клиент отправляет один JSON-RPC-запрос или уведомление на каждый POST. Ответом может быть один JSON-объект или поток SSE, относящийся к этому запросу. Сервер не выдаёт идентификатор сессии, а прерванный поток ответа не имеет истории для возобновления. В HTTP закрытие потока ответа является сигналом отмены.
Заголовки транспорта и тело описывают одну операцию. Минимальный вызов инструмента выглядит так:
POST /mcp HTTP/1.1
Accept: application/json, text/event-stream
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search","arguments":{"q":"otters"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{},"io.modelcontextprotocol/clientInfo":{"name":"catalog-app","version":"1.0.0"}}}}
Значение заголовка MCP-Protocol-Version должно совпадать со значением в _meta. Современный сервер отклоняет отсутствующий или несовпадающий обязательный заголовок с HTTP 400 и кодом HeaderMismatch, -32020. После работы шлюза сервер должен снова проверить заголовки. Так прокси не сможет направить запрос по одному имени инструмента, пока приложение выполнит другой.
Протокол без состояния меняет масштабирование, но не бизнес-смысл. Если workflow требует продолжения, верните из инструмента явный идентификатор и потребуйте его в следующем вызове. Храните источник истины в базе данных или workflow-сервисе, свяжите идентификатор с пользователем и операцией и задайте срок действия. Непроверенный идентификатор, requestState или аргумент инструмента нельзя считать доказательством права доступа.
server/discover и совместимость версий
Каждый современный сервер обязан реализовать RPC server/discover. Его результат объявляет поддерживаемые версии и возможности, а также необязательные инструкции. Клиент может вызвать его первым, чтобы выбрать версию, или сразу отправить обычный современный запрос. Поэтому discovery полезен, но не является обязательным рукопожатием на стороне клиента.
Если версия не поддерживается, сервер возвращает UnsupportedProtocolVersionError со списком поддерживаемых версий. Клиент выбирает общую версию и повторяет запрос. Клиенту, поддерживающему обе эпохи протокола, нужно осторожно классифицировать пробный запрос. Пустой ответ 400 или ответ без известной современной JSON-RPC-ошибки может означать старую точку входа. Известная современная ошибка означает, что запрос нужно исправить или согласовать заново. Ошибки авторизации и инфраструктуры не доказывают, что сервер старый.
Современный SDK может выполнить пробу через стандартный ввод и закрепить эпоху за соединением. Сервер может сохранить старый маршрут, пока новые клиенты используют современные запросы. Нельзя определять эпоху только по успешному TCP-соединению или общему 404.
MRTR вместо запросов сервера к клиенту
Современный формат удаляет канал JSON-RPC-запросов от сервера к клиенту. Инструменту, которому нужно подтверждение, недостающее значение или шаг с участием модели, больше не нужно удерживать поток открытым. Он возвращает промежуточный результат с resultType: "input_required" и картой inputRequests. Клиент отвечает на эти запросы и повторяет исходный метод с inputResponses. Повторный вызов является новым запросом и может попасть на другую реплику.
Пример потока подтверждения:
{
"resultType": "input_required",
"inputRequests": {
"confirm": {
"type": "elicitation",
"message": "Delete three files?",
"schema": {"type": "boolean"}
}
},
"requestState": "signed-opaque-state"
}
Затем клиент отправляет inputResponses.confirm и побайтно возвращает requestState. Сервер должен повторно войти в обработчик как в новый запрос. Сделайте обработчик идемпотентным, выводите текущий шаг из проверенного состояния и запрашивайте только ещё отсутствующие данные. Разрушительное действие нельзя считать завершённым до проверки подтверждения.
requestState не является защищённым контейнером. Он проходит через клиента и считается данными, контролируемыми атакующим. Подпишите его HMAC или примените аутентифицированное шифрование, свяжите с субъектом, исходным методом, важными параметрами и сроком действия, а подделку отклоняйте до запуска обработчика. Подпись не скрывает содержимое, поэтому секреты в нём хранить нельзя. TypeScript SDK предоставляет кодек состояния запроса и hook проверки. Legacy shim SDK может преобразовать тот же обработчик input_required в старые запросы elicitation/create, sampling/createMessage и roots/list, пока поддерживается клиент 2025 года.
Для длительных операций используйте расширение Tasks с постоянным идентификатором задачи, опросом tasks/get и tasks/update, когда требуется ввод клиента.
Подсказки кэша и детерминированные каталоги
Современные результаты tools/list, prompts/list, resources/list, resources/templates/list и resources/read содержат ttlMs и cacheScope. ttlMs является неотрицательной подсказкой свежести в миллисекундах, аналогичной HTTP max-age. Ноль означает немедленную устарелость. Для старых серверов отсутствующее значение следует считать нулём. Положительное значение говорит клиенту, когда можно не делать новый запрос, но не гарантирует неизменность данных. Клиент должен проверять свежесть при обращении к данным, а не превращать TTL в бесконечный polling.
Ключ кэша должен включать метод и каждый параметр, влияющий на результат. Это URI ресурса и cursor постраничного списка. Ответ с inputResponses или requestState кэшировать нельзя, потому что контекст не выражен простым ключом списка. cacheScope: "public" разрешает совместное использование между контекстами авторизации. Ставьте его только для данных без пользовательских или permission-зависимых полей. Даже при кэшированном каталоге нужна авторизация каждого инструмента.
Серверу следует возвращать инструменты в детерминированном порядке. Стабильный порядок уменьшает шум в сравнениях, сохраняет промпты модели и улучшает повторное использование кэша после переподключения. Уведомление listChanged дополняет TTL, но не отменяет правильную подсказку.
OAuth и изменения безопасности
Авторизация для MCP в целом необязательна. HTTP-сервер, защищающий ресурсы, должен следовать профилю OAuth 2.1 из спецификации 2026 года. Он работает как resource server и обязан публиковать OAuth Protected Resource Metadata по RFC 9728. Ответ 401 должен указывать на эти метаданные через WWW-Authenticate и, когда возможно, содержать требуемые scopes. Клиент должен поддерживать URL из заголовка и обе well-known формы.
Метаданные указывают один или несколько authorization server. Клиент должен поддерживать OAuth Authorization Server Metadata по RFC 8414 и OpenID Connect Discovery. Для issuer с путём сначала проверяйте формы с вставкой пути, затем OpenID-форму с добавлением пути.
Регистрация клиента теперь предпочитает Client ID Metadata Documents. Допустима и предварительная регистрация. Dynamic Client Registration остаётся устаревшим запасным вариантом. При DCR передавайте правильный application_type для desktop или CLI-клиента. Проверяйте PKCE, используйте S256, регистрируйте точные redirect URI и применяйте HTTPS, кроме разрешённого localhost callback.
Клиент должен сохранить проверенный issuer вместе с транзакцией PKCE. Если в ответе авторизации есть iss, сравните его до обмена кода. Учётные данные привязаны к выдавшему их issuer, поэтому их нельзя повторно использовать у другого authorization server. Передавайте канонический URI MCP-сервера как RFC 8707 resource в запросах авторизации и токена. Сервер обязан проверять audience и отклонять токены другого ресурса.
Для Streamable HTTP проверяйте Origin, чтобы предотвратить DNS rebinding. Локальный сервер следует привязывать к localhost, а не ко всем интерфейсам. Не помещайте bearer-токены в query string или логи, запрашивайте согласие перед передачей приватных ресурсов и вызовом инструментов, считайте описания и annotations недоверенными, если сервер не проверен. 401 означает отсутствие или недействительность авторизации. 403 означает нехватку разрешений и по возможности должен содержать challenge insufficient_scope.
Миграция TypeScript SDK
TypeScript SDK v2 разделяет пакеты клиента, сервера, ядра и runtime. Изучите руководство SDK по поддержке 2026-07-28 и руководство перехода с v1 на v2. Одно обновление SDK не означает, что в сеть автоматически пойдут современные байты. Клиент v2 по умолчанию согласует legacy-режим, поэтому явно включите versionNegotiation.
Для клиента, работающего с обеими эпохами, документация показывает такую форму:
import { Client } from '@modelcontextprotocol/client';
const client = new Client(
{ name: 'catalog-app', version: '1.0.0' },
{ versionNegotiation: { mode: 'auto' } },
);
await client.connect(transport);
mode: 'auto' пробует server/discover и возвращается к рукопожатию 2025 года только с действительно legacy-пиром. Зафиксируйте 2026-07-28, если fallback может скрыть несовместимость. createMcpHandler(factory) создаёт новый современный HTTP-сервер для каждого запроса и способен обслуживать обе эпохи. Для выбора эпохи в stdio используйте serveStdio(() => buildServer()).
Регистрацию обработчиков v1 через схемы замените строками методов, например setRequestHandler('tools/call', handler). Код, читающий ctx.sessionId, замените явными идентификаторами приложения или проверенным requestState. Push-style elicitation замените inputRequired(...). Учтите, что современный запрос не отправляет уведомление логирования без io.modelcontextprotocol/logLevel. Codemod SDK используйте только как механическую помощь, а не как проверку совместимости.
Для проверки прогоняйте createMcpHandler через fetch-подобный транспорт и отдельно покрывайте legacy-рукопожатие. Проверяйте заголовки, _meta, идемпотентность повторов, область кэша, audience и HTTP-статус реальными старыми и современными клиентами.
Чек-лист миграции
- Составьте список клиентов, серверов, транспортов, session store, event store SSE и чтения
Mcp-Session-Id. - Решите, какая точка входа принимает modern-трафик, а какая временно оставляет legacy.
- Обновите SDK и зафиксируйте фактические версии в lockfile.
- Добавьте
server/discoverи политику согласования версий. - Сделайте каждый запрос самодостаточным и проверяйте
_metaи зеркальные заголовки. - Замените состояние по сессии явными идентификаторами или подписанным
requestStateсо сроком действия. - Перепишите серверное взаимодействие на MRTR и сделайте повторы безопасными.
- Добавьте
ttlMs,cacheScopeи детерминированный порядок каталогам и ресурсам. - Обновите шлюзы, WAF, метрики и трассировку для
Mcp-MethodиMcp-Name. - Реализуйте проверки issuer, resource, audience, PKCE, redirect, Origin и scopes.
- Не добавляйте в новый код HTTP+SSE, Roots, Sampling, Logging и DCR.
- Выпускайте поэтапно с наблюдаемостью, сравните ошибки modern и legacy, а затем удаляйте совместимость после миграции потребителей.
Диагностика
| Симптом | Вероятная причина | Действие |
|---|---|---|
HTTP 400 и -32020 |
Заголовок отсутствует или не совпадает с телом | Заново вычислите MCP-Protocol-Version, Mcp-Method и Mcp-Name из одного объекта запроса |
| HTTP 400 и ошибка версии | Пир не обслуживает запрошенную ревизию | Выберите версию из supported или legacy-маршрут |
| HTTP 404 и method-not-found | Точка входа современная, но метод удалён или неизвестен | Проверьте метод и расширение, не запускайте вслепую initialize |
| HTTP 401 или 403 при discovery | Проба заблокирована авторизацией | Исправьте credentials и discovery метаданных, статус auth не доказывает legacy |
| Нет уведомлений логирования | В запросе нет io.modelcontextprotocol/logLevel |
Включите его на запрос или используйте stderr и OpenTelemetry |
| Повторные побочные эффекты | Поток больше нельзя возобновить | Используйте idempotency keys и новый ID запроса |
| Данные одного пользователя видны в другом кэше | Результат ошибочно помечен public |
Поставьте private и сохраняйте проверки principal |
| Старый клиент получает 405 на GET | Он ожидает HTTP+SSE | Временно оставьте legacy-точку или обновите клиента до Streamable HTTP |
Контекст архитектуры см. в материалах production AI agent architecture и MCP server with TypeScript and OAuth.
Источники
Материал опирается на спецификацию MCP 2026-07-28, журнал изменений и устаревшие возможности, требования Streamable HTTP, SEP-2575 о MCP без состояния, статью о релизе 2026-07-28, спецификацию авторизации MCP и документацию миграции TypeScript SDK.