Перейти к содержимому
Выберите тему

Сервер для двух эпох

Как один сервер MCP отвечает клиентам Legacy и Modern - что разрешает спецификация, как различить запросы на одном адресе, какие выпущенные SDK это умеют и как проверить

описана редакция 2026-07-28, сверено со спецификацией и выпусками SDK 4 октября 2026

Таблица пересказывает матрицу совместимости из спецификации 2026-07-28.

Клиент Сервер Итог Что происходит
Modern Modern работает несовпадение версий приходит ошибкой -32022, клиент повторяет запрос с общей версией
Modern Legacy не работает сервер может вернуть свою ошибку, промолчать или обработать запрос по старым правилам
Dual-era Modern работает первый запрос Modern удаётся или возвращает ошибку Modern, клиент остаётся в Modern
Dual-era Legacy работает клиент получает ответ 4xx без распознанной ошибки Modern и переходит к initialize
Legacy Modern не работает по HTTP запрос без обязательных заголовков отклоняется со статусом 400
Legacy Dual-era работает сервер отвечает на initialize и обслуживает клиента по согласованной редакции Legacy
Legacy Legacy работает по правилам редакции Legacy

Две строки «не работает» различаются по сути. Клиент Modern перед сервером Legacy может получить понятную ошибку, если начнёт с server/discover: на stdio спецификация говорит, что клиенту следует так делать. У клиента Legacy перед сервером Modern выхода нет. Поэтому совместимость со старыми клиентами - забота сервера.

Может. Сервер, который хочет обслуживать и клиентов Legacy, и клиентов Modern, может реализовать оба поведения. Сервер Dual-era может обслуживать обе эпохи одновременно на одном адресе или в одном процессе.

Обязан. Каждый сервер Modern обязан реализовать server/discover. На запрос с неподдерживаемой версией он обязан ответить ошибкой -32022 со списком версий, которые поддерживает.

Следует. Серверу, который знает только Modern, следует называть поддерживаемые версии в любой ошибке, которую он возвращает на initialize. Это может быть единственная подсказка, которую клиент Legacy покажет человеку.

Сторону Legacy сервер Dual-era реализует по тексту соответствующей старой редакции, например 2025-11-25. То есть для таких клиентов остаются сессии, поток по GET и всё остальное, что было в той редакции. Чем эти формы различаются, показано на странице «Streamable HTTP и переход с HTTP+SSE».

Как различить эпоху запроса на одном адресе

Заголовок раздела «Как различить эпоху запроса на одном адресе»

Спецификация задаёт правило так: сервер Dual-era выбирает поведение по тому, как клиент начинает разговор.

  • Запрос с полями Modern в _meta обслуживается без состояния, по редакции 2026-07-28.
  • Запрос initialize включает правила Legacy. Они действуют в пределах процесса на stdio и в пределах сессии на HTTP.

На HTTP у запроса Modern есть и внешние признаки: заголовок MCP-Protocol-Version: 2026-07-28 и заголовок Mcp-Method. Значение заголовка обязано совпадать с версией в теле.

Запрос initialize редакции 2025-11-25 может прийти без заголовка версии: та редакция требует заголовок в запросах после рукопожатия. Признак здесь - сам метод initialize в теле.

Как именно разбирать запросы, спецификация не предписывает. Один пример из выпущенного SDK: по документации Python SDK 2.3.0 он направляет каждый запрос по заголовку MCP-Protocol-Version.

Правило в редакции 2026-07-28 состоит из двух частей.

  • Сервер, который поддерживает клиентов редакций старше 2025-06-18, может считать запрос без заголовка запросом редакции 2025-03-26. В тех редакциях этого заголовка ещё не было.
  • Сервер, который таких клиентов не поддерживает, обязан отклонить запрос без заголовка: статус 400 и ошибка -32020.

В редакции 2025-11-25 правило было мягче: если заголовка нет и версию нельзя узнать иначе, серверу следует считать версию равной 2025-03-26.

Для сервера Dual-era это место нужно решить явно. Иначе один и тот же запрос без заголовка один сервер примет как Legacy, а другой отклонит.

Что отвечает сервер, который знает только Modern

Заголовок раздела «Что отвечает сервер, который знает только Modern»

Серверу, который поддерживает только 2026-07-28, спецификация говорит, что на трафик старых клиентов следует отвечать так:

Что пришло Ответ
GET или DELETE на адрес MCP 405
заголовок Mcp-Session-Id игнорировать, номера сессий не выдавать и не возвращать
заголовок Last-Event-ID игнорировать, потоки не возобновляются
initialize ошибка, в которой названы поддерживаемые версии

Сервер Dual-era ведёт себя иначе: GET и DELETE для него - часть редакции Legacy, и отвечает он на них по её правилам.

-32022 (UnsupportedProtocolVersionError) - ответ на версию, которую сервер не поддерживает. По HTTP она приходит со статусом 400. В поле data.supported сервер перечисляет свои версии.

Ответ на неподдерживаемую версию
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32022,
"message": "Unsupported protocol version",
"data": {
"supported": ["2026-07-28", "2025-11-25"],
"requested": "1900-01-01"
}
}
}

Пример взят из текста спецификации 2026-07-28 и здесь не запускался.

Клиенту следует выбрать общую версию из списка и повторить запрос. Для клиента Dual-era эта ошибка означает сервер Modern: откат на initialize здесь не нужен. Откат уместен, когда в теле ответа нет распознанной ошибки Modern.

Какие выпущенные SDK дают сервер для двух эпох

Заголовок раздела «Какие выпущенные SDK дают сервер для двух эпох»

В таблице только выпущенные версии, сверенные по метке выпуска. Клиентская сторона SDK здесь не рассматривается.

SDK Выпуск Сервер для двух эпох При какой настройке
TypeScript 2.3.0 да createMcpHandler по умолчанию обслуживает и трафик Legacy (legacy: 'stateless'); legacy: 'reject' его отклоняет
Python 2.3.0 да streamable_http_app() обслуживает обе эпохи всегда; отключить одну из них нельзя
C# 2.2.0 да, зависит от режима см. ниже
Go v1.8.0 да, при включённой настройке по Streamable HTTP запросы 2026-07-28 принимаются только при StreamableHTTPOptions.Stateless = true; по умолчанию настройка выключена, и такие запросы отклоняются. Набор версий сужает ServerOptions.SupportedProtocolVersions
Rust rmcp 3.5.0 да запросы 2026-07-28 всегда обслуживаются без состояния; legacy_session_mode (по умолчанию true) относится только к версиям Legacy
Ruby 1.6.1 да встроенные транспорты обслуживают обе эпохи без настройки
PHP v0.8.1 да один адрес отвечает и на initialize, и на server/discover; withoutModernEra() отключает Modern
Java 2.0.1 нет, только Legacy новейшая редакция в выпуске - 2025-11-25
Kotlin 0.15.0 нет, только Legacy новейшая редакция в выпуске - 2025-11-25
Swift 0.12.1 нет, только Legacy новейшая редакция в выпуске - 2025-11-25

Источники: документация выпусков TypeScript, Python, C#, Go, Ruby, PHP; исходный код выпусков Rust, Java, Kotlin, Swift.

Уточнения к таблице:

  • Старые линии TypeScript 1.x (1.32.0) и Python 1.x (1.30.0) знают только Legacy.
  • PHP SDK сам называет себя экспериментальным до первой основной версии.
  • У Java поддержка 2026-07-28 записана в планах на линию 3.x. На 4 октября 2026 выпусков этой линии нет.
  • У Kotlin в ветке main есть экспериментальные типы для server/discover. В выпуск 0.15.0 они не входят.

В C# поведение задаёт свойство SessionMode. Режимов три.

Режим Запрос 2026-07-28 Клиент с initialize
Stateless (по умолчанию) обслуживается без состояния работает на том же адресе без сессии, в пределах одного POST
Stateful отклоняется ошибкой -32022 получает сессию
StatefulForInitializeClients обслуживается без состояния получает полноценную сессию

В режиме Stateless клиентам Legacy недоступно то, что зависит от сессии: уведомления без запроса, подписки на ресурсы, разделение по клиентам. Документация SDK советует задавать SessionMode явно, а не полагаться на значение по умолчанию.

Достаточно двух запросов. Оба примера схематичные: адрес условный, запросы составлены по тексту спецификации.

Запрос Modern: server/discover
curl -i https://example.com/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":"discover-1","method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"ExampleClient","version":"1.0.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}'

Пример составлен по тексту спецификации 2026-07-28 и здесь не запускался.

Сервер с поддержкой Modern вернёт результат с полем supportedVersions. Если сервер ответил статусом 400 без ошибки JSON-RPC в теле, Modern он не обслуживает.

Запрос Legacy: initialize
curl -i https://example.com/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"ExampleClient","version":"1.0.0"}}}'

Пример составлен по тексту спецификации 2025-11-25 и здесь не запускался.

Как читать ответ на второй запрос:

  • пришёл результат initialize - сервер обслуживает Legacy. Если в ответе есть заголовок MCP-Session-Id, сервер выдал сессию;
  • пришёл статус 400 с ошибкой JSON-RPC - сервер Legacy не обслуживает. Такому серверу следует назвать в ошибке поддерживаемые версии.

Если оба запроса удались, сервер работает как Dual-era. То же самое можно проверить в MCP Inspector, переключая настройку эпохи между legacy и modern.

Срока, после которого сервер обязан отказаться от Legacy, в спецификации нет. Нет и требования поддерживать Legacy: сервер Dual-era разрешён, но не обязателен. Это решение владельца сервера.

На что опираться при решении - это практика, а не правило MCP:

  • какие клиенты ходят к вашему серверу и какую редакцию они поддерживают - см. «Матрица клиентов»;
  • сколько запросов initialize приходит по журналу сервера;
  • нужны ли вам возможности, которые в Modern устроены иначе: сессии, поток по GET, собственные запросы сервера.

Не рассчитывайте на то, что клиент сам справится. Откат на initialize - поведение клиента Dual-era, и не каждый клиент так устроен. Клиент Legacy не умеет ничего, кроме initialize.