MCP без состояния: изменения 2026-07-28

MCP без состояния (stateless MCP) - модель запросов Model Context Protocol, введённая в версии спецификации 2026-07-28: каждый запрос содержит всё, что нужно серверу для его обработки, а сервер не хранит сессию (session) протокола между запросами. Версия протокола, данные о клиенте и его возможности (capabilities) передаются в поле _meta каждого запроса, а не согласуются один раз при рукопожатии (handshake).

Изменение внесла группа предложений по развитию спецификации (Specification Enhancement Proposals, SEP), в первую очередь SEP-2575, SEP-2567 и SEP-2322. Версия вышла 28 июля 2026 года. В анонсе мейнтейнеры описывают его как переход от двунаправленного протокола с состоянием к протоколу запросов и ответов без состояния. Обзор MCP и краткая сводка версии - в статье Что такое MCP; здесь разобрано каждое изменение и миграция с версии 2025-11-25. Подробности уровня HTTP - в статье Транспорты MCP.

Кратко

Параметр Значение
Где введено Спецификация 2026-07-28, вышла 28 июля 2026 года
Что заменяет Рукопожатие initialize и сессии протокола версий 2025-11-25 и раньше
Основные SEP SEP-2575, SEP-2567, SEP-2322
Метаданные запроса Ключи _meta с префиксом io.modelcontextprotocol/
Обнаружение возможностей server/discover: обязателен для серверов, необязателен для клиентов
Данные от клиента Multi Round-Trip Requests (MRTR)
Уведомления об изменениях Поток subscriptions/listen, подписка на каждый тип отдельно
Совместимость с 2025-11-25 На уровне сообщений несовместимы; реализации обеих эпох поддерживают обе

Какую задачу решает

До версии 2025-11-25 клиент начинал работу с запроса initialize и уведомления notifications/initialized. Рукопожатие фиксировало версию протокола и возможности сторон до конца соединения. В Streamable HTTP сервер мог ещё и назначить сессию заголовком Mcp-Session-Id.

SEP-2575 называет три следствия. Балансировщик с распределением по кругу (round-robin) использовать нельзя: следующий запрос может попасть на экземпляр без сессии, поэтому нужна привязка клиента к экземпляру (sticky sessions). При сбое экземпляра его сессии теряются, и клиенты повторяют рукопожатие.

SEP-2567 добавляет наблюдения о реальных клиентах. Одни хосты (hosts) открывают новую сессию на каждый вызов инструмента (tool), другие держат одну на всё время работы приложения, и почти никто не возобновляет сессию после перезапуска. Автор сервера не может предсказать, чему соответствует сессия. Поскольку tools/list мог отличаться между сессиями, клиенты не могли повторно использовать сохранённый список.

Запросы от сервера к клиенту обходились так же дорого. Чтобы во время вызова инструмента задать вопрос пользователю, сервер держал открытым поток Server-Sent Events, а ответ мог прийти на другой экземпляр. По данным SEP-2322, из-за этого удалённые серверы почти не использовали elicitation, sampling и roots или не могли их использовать.

История

В таблице - шаги от первого предложения до конца срока вывода из употребления.

Дата Событие
18 июня 2025 Создан SEP-2575 «Make MCP Stateless»
25 ноября 2025 Спецификация 2025-11-25, последняя версия с рукопожатием initialize
19 декабря 2025 Мейнтейнеры рабочей группы по транспортам публикуют «Exploring the Future of MCP Transports»
3 февраля 2026 Создан SEP-2322 «Multi Round-Trip Requests»
11 марта 2026 Создан SEP-2567 «Sessionless MCP via Explicit State Handles»
28 июля 2026 Спецификация 2026-07-28. Её поддерживают SDK для TypeScript, Python, Go и C#, SDK для Rust - в бета-версии
28 июля 2027 или позже Первая версия, из которой можно удалить roots, sampling, logging и Dynamic Client Registration

SEP версии

В таблице - SEP, которые определяют модель без состояния и её прямые следствия.

SEP Название Что меняет
SEP-2575 Make MCP Stateless Удаляет initialize; добавляет поля _meta, server/discover, subscriptions/listen
SEP-2567 Sessionless MCP via Explicit State Handles Удаляет сессии и Mcp-Session-Id
SEP-2322 Multi Round-Trip Requests Заменяет запросы сервера результатами input_required
SEP-2549 TTL for List Results Добавляет ttlMs и cacheScope
SEP-2243 HTTP Header Standardization for Streamable HTTP Transport Добавляет Mcp-Method и Mcp-Name
SEP-2663 Tasks Extension Выносит задачи из ядра
SEP-2577 Deprecate Roots, Sampling, and Logging Объявляет устаревшими три функции
SEP-2596 Specification Feature Lifecycle and Deprecation Policy Определяет состояния функций

Что удалено

Рукопожатие и сессия

Запроса initialize, уведомления notifications/initialized и заголовка Mcp-Session-Id больше нет. Первым сообщением клиента может быть любой запрос. Спецификация прямо называет следствие: открытое соединение, в том числе процесс stdio, не является ни диалогом, ни сессией. Клиент может чередовать в одном соединении несвязанные запросы, а сервер не должен делать выводов из предыдущих запросов.

Удалённые методы и механизмы

Часть механизмов существовала из-за сессии или из-за того, что сервер мог отправлять запросы:

  • ping удалён в обе стороны.
  • logging/setLevel удалён; уровень журнала задаётся в _meta каждого запроса.
  • resources/subscribe, resources/unsubscribe и поток HTTP GET заменены на subscriptions/listen.
  • notifications/roots/list_changed удалено.
  • Возобновление потоков через Last-Event-ID удалено. Если поток ответа оборвался, клиент отправляет запрос заново с новым идентификатором.
  • Уведомление notifications/elicitation/complete и поле elicitationId из версии 2025-11-25 удалены.

Сервер вообще не должен инициировать запросы JSON-RPC.

Метаданные запроса

Каждый запрос несёт зарезервированные ключи в params._meta. В таблице они перечислены вместе со связанными ключами результатов и уведомлений.

Ключ Где Обязателен Значение
io.modelcontextprotocol/protocolVersion Запрос Да Версия протокола этого запроса
io.modelcontextprotocol/clientCapabilities Запрос Да Возможности клиента для этого запроса; {} - никаких
io.modelcontextprotocol/clientInfo Запрос Рекомендуется Имя и версия клиента
io.modelcontextprotocol/logLevel Запрос Нет, устарел Минимальный уровень журнала для запроса
progressToken Запрос Нет Включает уведомления о прогрессе
traceparent, tracestate, baggage Любое _meta Нет Контекст трассировки OpenTelemetry
io.modelcontextprotocol/serverInfo Результат Рекомендуется Имя и версия сервера
io.modelcontextprotocol/subscriptionId Уведомление В потоках подписки Идентификатор запроса subscriptions/listen
JSON
{
  "jsonrpc": "2.0",
  "id": 7,
  "method": "resources/read",
  "params": {
    "uri": "file:///project/README.md",
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientInfo": { "name": "example-client", "version": "1.0.0" },
      "io.modelcontextprotocol/clientCapabilities": { "elicitation": { "form": {} } },
      "progressToken": "read-7",
      "traceparent": "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01"
    }
  }
}

Запрос без обязательного ключа отклоняется с ошибкой -32602 (Invalid params), в HTTP - со статусом 400 Bad Request. Сервер не должен брать возможности клиента из предыдущих запросов. Если для обработки нужна возможность, которую запрос не объявил, сервер возвращает MissingRequiredClientCapabilityError с кодом -32021:

JSON
{
  "jsonrpc": "2.0",
  "id": 8,
  "error": {
    "code": -32021,
    "message": "Missing required client capability",
    "data": { "requiredCapabilities": { "elicitation": {} } }
  }
}

Значения clientInfo и serverInfo никто не проверяет. Спецификация предназначает их для отображения, журналов и отладки, а не для решений о безопасности.

Обнаружение возможностей и выбор версии

server/discover возвращает поддерживаемые версии протокола, возможности и данные о сервере. Этот метод обязан реализовать каждый сервер. Клиент может вызвать его первым либо сразу отправить любой запрос и обработать ошибку версии. Параметров у запроса нет, кроме _meta:

JSON
{
  "jsonrpc": "2.0",
  "id": "discover-1",
  "method": "server/discover",
  "params": {
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientInfo": { "name": "example-client", "version": "1.0.0" },
      "io.modelcontextprotocol/clientCapabilities": {}
    }
  }
}

В ответе могут быть instructions - текст для модели о том, как работать с сервером, - и поддерживаемые расширения (extensions):

JSON
{
  "jsonrpc": "2.0",
  "id": "discover-1",
  "result": {
    "resultType": "complete",
    "supportedVersions": ["2026-07-28", "2025-11-25"],
    "capabilities": {
      "tools": { "listChanged": true },
      "prompts": {},
      "extensions": { "io.modelcontextprotocol/tasks": {} }
    },
    "instructions": "Issue tracker server. Search issues before creating new ones.",
    "_meta": {
      "io.modelcontextprotocol/serverInfo": { "name": "example-tracker", "version": "2.4.0" }
    },
    "ttlMs": 3600000,
    "cacheScope": "public"
  }
}

На неизвестную или неподдерживаемую версию сервер отвечает ошибкой UnsupportedProtocolVersionError с кодом -32022. Клиент выбирает версию из data.supported и повторяет запрос:

JSON
{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32022,
    "message": "Unsupported protocol version",
    "data": { "supported": ["2026-07-28", "2025-11-25"], "requested": "1900-01-01" }
  }
}

Многоэтапные запросы

Как проходит обмен

Multi Round-Trip Requests (MRTR), многоэтапные запросы, позволяют серверу получить данные от клиента, не отправляя собственного запроса. Промежуточный результат допустим только в ответ на tools/call, resources/read и prompts/get.

  1. Клиент отправляет запрос, например tools/call.
  2. Серверу не хватает данных, и он возвращает resultType: "input_required".
  3. Клиент выполняет перечисленные запросы, например спрашивает пользователя.
  4. Клиент повторяет исходный запрос с новым id JSON-RPC, ответами и состоянием сервера.
  5. Любой экземпляр сервера обрабатывает повтор и возвращает итоговый результат или снова input_required.

Поля

Каждый результат содержит resultType: "complete" для итогового результата, "input_required" для промежуточного. Расширения могут добавлять свои значения. Неизвестное значение клиент считает недопустимым, а отсутствующее поле, которое присылают серверы прежних версий, - равным "complete".

Промежуточный результат содержит inputRequests, requestState или оба поля. inputRequests - словарь: ключи выбирает сервер, значения - запросы elicitation/create, sampling/createMessage или roots/list. requestState, состояние запроса, - непрозрачная строка, которую клиент обязан вернуть без изменений и не должен разбирать. Ответы клиент кладёт в inputResponses под теми же ключами. Результат input_required показан в статье Что такое MCP; повторный запрос выглядит так:

JSON
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "create_repository",
    "arguments": { "name": "demo" },
    "inputResponses": {
      "github_login": { "action": "accept", "content": { "name": "octocat" } }
    },
    "requestState": "eyJzdGVwIjoyfQ.signature",
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": { "elicitation": { "form": {} } }
    }
  }
}

Сервер может запрашивать только то, что объявил запрос: без возможности elicitation в inputRequests не должно быть elicitation/create. Если ответов не хватает, серверу следует снова вернуть input_required, а не ошибку. Результаты повторных запросов не кешируются.

Состояние без хранилища на сервере

Результат может содержать только requestState. SEP-2322 описывает сброс нагрузки: перегруженный экземпляр прекращает обработку и возвращает накопленное состояние, а немедленный повтор клиента продолжает работу на другом экземпляре.

JSON
{
  "jsonrpc": "2.0",
  "id": 5,
  "result": {
    "resultType": "input_required",
    "requestState": "gAAAAABn-checkpoint-42"
  }
}

Целостность состояния запроса

Состояние проходит через клиента, поэтому сервер обязан считать его данными, которые контролирует атакующий. Если от него зависят авторизация, доступ к ресурсам или бизнес-логика, сервер обязан защищать его целостность, например HMAC или аутентифицированным шифрованием (AEAD), и отклонять состояние, не прошедшее проверку. Против повторного использования защищённые данные должны содержать аутентифицированного субъекта, короткий срок действия и идентификатор исходного запроса. Эти меры не делают состояние одноразовым; одноразовость обеспечивается на стороне сервера. Другие атаки разобраны в статье Безопасность MCP.

Явные идентификаторы состояния

На состояние, которое охватывает несколько вызовов, ссылается явный идентификатор, передаваемый в каждом запросе. Для состояния приложения спецификация описывает приём проектирования инструментов, а не элемент протокола. Идентификатор состояния (state handle) - идентификатор, который возвращает инструмент создания объекта и который модель передаёт аргументом в следующих вызовах:

JSON
{
  "jsonrpc": "2.0",
  "id": 11,
  "result": {
    "resultType": "complete",
    "content": [{ "type": "text", "text": "Created basket bsk_a1b2c3" }],
    "structuredContent": { "basket_id": "bsk_a1b2c3" }
  }
}

Следующий вызов, add_item, получает аргументы { "basket_id": "bsk_a1b2c3", "sku": "shoes" }. Сервер хранит корзину под этим ключом. Спецификация даёт рекомендации по устройству идентификаторов:

  • Авторизация. На сервере с аутентификацией идентификатор - это имя, и при каждом вызове сервер проверяет права вызывающего на объект. Без аутентификации идентификатор работает как токен на предъявителя, поэтому ему нужна достаточная энтропия, например UUIDv4, и ограниченный срок жизни.
  • Непрозрачность. Идентификатор с видимой внутренней структурой провоцирует разбор и угадывание.
  • Срок жизни. Правило хранения записывают в описание инструмента создания, где его видит модель.
  • Ошибки истечения. Вызов с истёкшим идентификатором возвращает ошибку выполнения инструмента, которая прямо об этом говорит.

То же правило действует для списков. Результат tools/list может меняться со временем и зависеть от авторизации запроса, но не от соединения и не от других запросов. Инструмент, который раньше появлялся после connect_database, теперь есть в списке сразу и принимает аргумент connection_id. Проектирование инструментов разобрано в статье Инструменты MCP.

Подписки и уведомления

Уведомления, не связанные с одним запросом, приходят в поток, который открывает subscriptions/listen. Клиент перечисляет нужные типы, и другие типы сервер отправлять не должен:

JSON
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "subscriptions/listen",
  "params": {
    "notifications": {
      "toolsListChanged": true,
      "resourceSubscriptions": ["file:///project/config.json"]
    },
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {}
    }
  }
}

Поля фильтра - toolsListChanged, promptsListChanged, resourcesListChanged и resourceSubscriptions. Первым сообщением в потоке должно быть notifications/subscriptions/acknowledged с теми типами, которые сервер согласился доставлять:

JSON
{
  "jsonrpc": "2.0",
  "method": "notifications/subscriptions/acknowledged",
  "params": {
    "notifications": { "toolsListChanged": true },
    "_meta": { "io.modelcontextprotocol/subscriptionId": 1 }
  }
}

Каждое уведомление в потоке содержит io.modelcontextprotocol/subscriptionId, равный идентификатору запроса подписки, поэтому несколько подписок могут делить один канал. Сервер не хранит состояние подписок между переподключениями, и после потери канала клиент отправляет запрос заново. Уведомления одного запроса, например notifications/progress, по-прежнему приходят в поток ответа на этот запрос.

Подсказки для кеширования

Результаты server/discover, tools/list, prompts/list, resources/list, resources/templates/list и resources/read обязаны содержать два поля. ttlMs - сколько миллисекунд результат остаётся актуальным; 0 означает, что он устаревает сразу. cacheScope равно "public", если в результате нет данных пользователя и его могут хранить шлюзы, или "private", если его можно повторно использовать только в том же контексте авторизации.

JSON
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "resultType": "complete",
    "tools": [
      { "name": "search_issues", "description": "Search issues by text", "inputSchema": { "type": "object" } }
    ],
    "nextCursor": "page-2",
    "ttlMs": 300000,
    "cacheScope": "private"
  }
}

Ключ кеша - метод вместе с параметрами, которые влияют на результат, например uri или cursor. Каждая страница кешируется отдельно. TTL - подсказка об актуальности, а не интервал опроса, и уведомление об изменении сразу делает сохранённый результат устаревшим. Серверу следует возвращать инструменты в детерминированном порядке; спецификация связывает это с кешем клиента и кешем промптов (prompt cache) модели.

Вывод из употребления и жизненный цикл функций

SEP-2596 определяет три состояния функции. Active - функция входит в текущую версию. Deprecated - функция остаётся в спецификации с описанным путём миграции, а новым реализациям не следует её использовать. Removed - функции нет в следующей версии. Между объявлением устаревшей и правом на удаление проходит не меньше двенадцати месяцев, считая от выхода версии, которая её объявила; при активной угрозе безопасности срок можно сократить до девяноста дней.

В таблице - функции, объявленные устаревшими в 2026-07-28.

Функция Путь миграции Самое раннее удаление
Roots Параметры инструментов, URI ресурсов или конфигурация сервера Первая версия 28 июля 2027 года или позже
Sampling Прямые вызовы API провайдеров моделей Первая версия 28 июля 2027 года или позже
Logging stderr для stdio, OpenTelemetry Первая версия 28 июля 2027 года или позже
Dynamic Client Registration Client ID Metadata Documents Первая версия 28 июля 2027 года или позже

SEP-2577 называет причины: низкое распространение, размытый смысл roots, сложность sampling и наличие стандартных средств журналирования. В реестре также есть транспорт HTTP+SSE, устаревший с версии 2025-03-26 и переклассифицированный по новой политике. В течение срока формат сообщений не меняется. SDK уровня Tier 1 обязаны помечать устаревшие API штатным механизмом своего языка.

Совместимость версий

Спецификация называет версии с метаданными в каждом запросе (2026-07-28 и новее) современными (modern), а версии с рукопожатием initialize (2025-11-25 и раньше) - прежними (legacy). Реализация, которая поддерживает и те и другие, - реализация обеих эпох (dual-era). В таблице - исход каждого сочетания.

Клиент Сервер Исход
Современный Современный Работает; несовпадение версий решается повтором
Современный Прежний Не работает; клиент показывает ошибку
Обеих эпох Современный Работает в современном режиме
Обеих эпох Прежний Работает после перехода на initialize
Прежний Современный Не работает; прежний клиент не может перейти на новую версию
Прежний Обеих эпох Работает по прежней версии

Клиент обеих эпох определяет эпоху сервера по правилам транспорта. В stdio он первым отправляет server/discover: DiscoverResult или распознанная современная ошибка означают современный сервер, любая другая ошибка или отсутствие ответа - прежний. В HTTP клиент отправляет современный запрос и перед откатом изучает тело ответа 400 Bad Request. Результат клиенту следует запомнить на время жизни процесса сервера или для его origin.

Сервер обеих эпох выбирает режим по первому сообщению: современные поля _meta означают обработку без состояния, initialize включает прежнюю семантику. Серверу только с современными версиями следует перечислять поддерживаемые версии в ошибке, которую он возвращает на initialize.

Миграция с 2025-11-25

Анонс ожидает основные затраты у приложений, которые зависели от идентификаторов сессий. SEP-2567 приводит автоматический анализ 1000 серверов с открытым кодом: у 90% не было обращений к идентификатору сессии на уровне приложения, у 2,5% состояние приложения хранилось по этому идентификатору.

Серверы

  1. Реализовать server/discover с версиями, возможностями, serverInfo и подсказками для кеширования.
  2. Читать версию и возможности из _meta каждого запроса; возвращать -32602, -32022 или -32021, как описано выше.
  3. Добавить resultType в каждый результат.
  4. Перестать выдавать Mcp-Session-Id. Перенести состояние, привязанное к сессии, на идентификаторы состояния или на аутентифицированного субъекта.
  5. Сделать списки независимыми от соединения, упорядочить их детерминированно и добавить ttlMs и cacheScope.
  6. Заменить запросы к клиенту результатами input_required и защитить requestState.
  7. Обслуживать subscriptions/listen вместо потока GET и resources/subscribe.
  8. Перенести экспериментальные задачи в расширение io.modelcontextprotocol/tasks.
  9. Сохранить ветку initialize, если прежние клиенты должны продолжать работать.

Клиенты

  1. Добавлять protocolVersion, clientCapabilities и clientInfo в _meta каждого запроса.
  2. Обрабатывать -32022 повтором с поддерживаемой версией или заранее вызывать server/discover.
  3. Разбирать результат по resultType и реализовать цикл MRTR с новым id и неизменённым requestState.
  4. Открывать subscriptions/listen для уведомлений об изменениях и открывать заново после потери канала.
  5. Кешировать списки по ttlMs и cacheScope и сбрасывать кеш по уведомлениям.
  6. Повторять запрос с новым идентификатором, если его поток ответа оборвался.
  7. Перестать отправлять ping, logging/setLevel и Mcp-Session-Id.
  8. Сохранять идентификаторы состояния в контексте при сжатии диалога.

Проверка изменений контракта между версиями описана в статье Регрессионное тестирование MCP-сервера.

Состояние в протоколе без состояния

Отсутствие состояния относится к протоколу, а не к приложениям. SEP-2575 сравнивает это с HTTP: приложения хранят состояние, а запрос несёт само состояние или ссылку на него. В таблице такие ссылки сравниваются с удалённой сессией.

Механизм Где хранится состояние Срок жизни Кто передаёт ссылку
Сессия протокола, удалена Сервер, под Mcp-Session-Id Задаёт хост, не определён Заголовок транспорта
Идентификатор состояния Хранилище сервера Правило в описании инструмента Модель, аргументом инструмента
requestState Внутри самой строки Один логический запрос Клиент, возвращает без изменений
Идентификатор задачи Сервер, надёжно ttlMs задачи Клиент, в tasks/get
Подписка Открытый поток До отмены или разрыва Идентификатор запроса подписки

Сессия давала одну область на соединение. Идентификаторы дают столько областей, сколько создаст модель, и каждую можно разделить или изолировать: по данным SEP-2567, субагенты могут пользоваться одной корзиной и отдельными контекстами браузера. Идентификаторы задач относятся к расширению Tasks, описанному в статье Расширения MCP.

Термины

Термин Значение
Протокол без состояния Протокол, в котором каждый запрос содержит всё нужное для его обработки
server/discover Обязательный метод сервера, который возвращает поддерживаемые версии, возможности и данные о сервере
resultType Поле результата, которое помечает результат как итоговый, промежуточный или тип расширения
Состояние запроса Непрозрачная строка с ходом обработки, которую клиент возвращает без изменений при повторе
Идентификатор состояния Идентификатор, который возвращает инструмент и передаёт обратно модель, чтобы сослаться на состояние на сервере
Подсказки для кеширования Поля ttlMs и cacheScope: как долго и кто может кешировать результат
Реализация обеих эпох Клиент или сервер, который поддерживает и 2026-07-28, и версии с рукопожатием
Срок вывода из употребления Не меньше двенадцати месяцев между объявлением функции устаревшей и правом на её удаление

Источники

Серия

Model Context Protocol (MCP)
  1. Что такое MCP (Model Context Protocol)
  2. Инструменты (tools) в MCP
  3. Ресурсы и шаблоны ресурсов в MCP
  4. Промпты (prompts) в MCP
  5. Транспорты MCP: stdio и Streamable HTTP
  6. MCP без состояния: изменения 2026-07-28Текущая
  7. Авторизация в MCP
  8. Безопасность MCP
  9. Расширения MCP
  10. MCP-сервер поверх существующего API
  11. Тестирование MCP-серверов: основы
  12. Оценка MCP-сервера на моделях
  13. Регрессионное тестирование MCP-сервера
  14. Публикация MCP-сервера в MCP Registry

Статьи Wiki пишет и поддерживает команда ComplexQA.