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

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

Страница: https://mcpdoc.ru/deployment/dual-era/
Указатель сайта: https://mcpdoc.ru/llms.txt

Сервер Dual-era - это сервер, который отвечает и клиентам Modern (редакция `2026-07-28`), и клиентам Legacy (редакция `2025-11-25` и старше). Спецификация такое разрешает, в том числе на одном адресе и в одном процессе.

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

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

Совместимость Dual-era страница описывает обе эпохи

- **Страница описывает редакцию:** 2026-07-28

- **Затрагивает устаревшее:** нет

- **Затрагивает удалённое в редакции 2026-07-28:** рукопожатие initialize, сессии

- **Проверено:** 2026-10-04

- **Источник:** [modelcontextprotocol.io](https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning)

## Что получится при каждом сочетании

Таблица пересказывает [матрицу совместимости](https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning) из спецификации `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`](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports). То есть для таких клиентов остаются сессии, поток по GET и всё остальное, что было в той редакции. Чем эти формы различаются, показано на странице [«Streamable HTTP и переход с HTTP+SSE»](https://mcpdoc.ru/deployment/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](https://github.com/modelcontextprotocol/python-sdk/blob/v2.3.0/docs/run/legacy-clients.md) он направляет каждый запрос по заголовку `MCP-Protocol-Version`.

## Запрос без `MCP-Protocol-Version`

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

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

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

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

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

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

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

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

## Ошибка -32022

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

```json title="Ответ на неподдерживаемую версию"
{
  "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 | Выпуск | Сервер для двух эпох | При какой настройке |
|---|---|---|---|
| [TypeScript](https://mcpdoc.ru/sdk/typescript/) | 2.3.0 | да | `createMcpHandler` по умолчанию обслуживает и трафик Legacy (`legacy: 'stateless'`); `legacy: 'reject'` его отклоняет |
| [Python](https://mcpdoc.ru/sdk/python/) | 2.3.0 | да | `streamable_http_app()` обслуживает обе эпохи всегда; отключить одну из них нельзя |
| [C#](https://mcpdoc.ru/sdk/csharp/) | 2.2.0 | да, зависит от режима | см. ниже |
| [Go](https://mcpdoc.ru/sdk/go/) | v1.8.0 | да, при включённой настройке | по Streamable HTTP запросы `2026-07-28` принимаются только при `StreamableHTTPOptions.Stateless = true`; по умолчанию настройка выключена, и такие запросы отклоняются. Набор версий сужает `ServerOptions.SupportedProtocolVersions` |
| [Rust](https://mcpdoc.ru/sdk/rust/) | rmcp 3.5.0 | да | запросы `2026-07-28` всегда обслуживаются без состояния; `legacy_session_mode` (по умолчанию `true`) относится только к версиям Legacy |
| [Ruby](https://mcpdoc.ru/sdk/ruby/) | 1.6.1 | да | встроенные транспорты обслуживают обе эпохи без настройки |
| [PHP](https://mcpdoc.ru/sdk/php/) | v0.8.1 | да | один адрес отвечает и на `initialize`, и на `server/discover`; `withoutModernEra()` отключает Modern |
| [Java](https://mcpdoc.ru/sdk/java/) | 2.0.1 | нет, только Legacy | новейшая редакция в выпуске - `2025-11-25` |
| [Kotlin](https://mcpdoc.ru/sdk/kotlin/) | 0.15.0 | нет, только Legacy | новейшая редакция в выпуске - `2025-11-25` |
| [Swift](https://mcpdoc.ru/sdk/swift/) | 0.12.1 | нет, только Legacy | новейшая редакция в выпуске - `2025-11-25` |

Источники: документация выпусков [TypeScript](https://github.com/modelcontextprotocol/typescript-sdk/blob/v2.3.0/docs/protocol-versions.md), [Python](https://github.com/modelcontextprotocol/python-sdk/blob/v2.3.0/docs/run/legacy-clients.md), [C#](https://github.com/modelcontextprotocol/csharp-sdk/blob/v2.2.0/docs/concepts/stateless/stateless.md), [Go](https://github.com/modelcontextprotocol/go-sdk/blob/v1.8.0/docs/protocol.md), [Ruby](https://github.com/modelcontextprotocol/ruby-sdk/blob/v1.6.1/docs/protocol-versions.md), [PHP](https://github.com/modelcontextprotocol/php-sdk/blob/v0.8.1/docs/run/protocol-eras.md); исходный код выпусков [Rust](https://github.com/modelcontextprotocol/rust-sdk/blob/rmcp-v3.5.0/crates/rmcp/src/transport/streamable_http_server/tower.rs), [Java](https://github.com/modelcontextprotocol/java-sdk/blob/v2.0.1/mcp-core/src/main/java/io/modelcontextprotocol/spec/ProtocolVersions.java), [Kotlin](https://github.com/modelcontextprotocol/kotlin-sdk/blob/0.15.0/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt), [Swift](https://github.com/modelcontextprotocol/swift-sdk/blob/0.12.1/Sources/MCP/Base/Versioning.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# SDK 2.2.0

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

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

В режиме `Stateful` сервер на C# SDK 2.2.0 отвечает на запрос `2026-07-28` ошибкой `-32022`. Документация SDK называет `StatefulForInitializeClients` режимом перехода: сессии остаются для старых клиентов, новые обслуживаются без состояния на том же адресе.

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

## Как проверить свой сервер

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

```bash title="Запрос 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 он не обслуживает.

```bash title="Запрос 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](https://mcpdoc.ru/tools/inspector/), переключая настройку эпохи между `legacy` и `modern`.

## Когда отключать Legacy

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

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

- какие клиенты ходят к вашему серверу и какую редакцию они поддерживают - см. [«Матрица клиентов»](https://mcpdoc.ru/reference/clients/);
- сколько запросов `initialize` приходит по журналу сервера;
- нужны ли вам возможности, которые в Modern устроены иначе: сессии, поток по GET, собственные запросы сервера.

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

## Что читать дальше

- [Streamable HTTP и переход с HTTP+SSE](https://mcpdoc.ru/deployment/http-sse/) - чем различаются формы транспорта по редакциям.
- [Редакции и версии протокола](https://mcpdoc.ru/protocol/versions/) - как клиент определяет эпоху сервера.
- [Переход на редакцию 2026-07-28](https://mcpdoc.ru/protocol/migration/) - что меняется в самом сервере.
- [SDK](https://mcpdoc.ru/sdk/) - состояние каждого официального SDK.
