Skip to main content
Glama

Два MCP-сервера, один продукт, две ревизии протокола

Одна и та же корзина покупок, реализованная дважды: один раз на старой спецификации MCP с сохранением состояния (2025-11-25), второй — на новой, без сохранения состояния (2026-07-28). Запустите их рядом и посмотрите, как один из них падает.

Цель — не рабочий код, а понимание почему спецификация изменилась. Каждый эксперимент здесь устроен так, чтобы сбой был громким, а причина видна на проводе.

Что такое MCP? (три предложения)

MCP — Model Context Protocol — это стандартный способ для ИИ-приложения вызывать инструменты, написанные кем-то другим. Он заменяет N приложений × M интеграций на N + M, так же как Language Server Protocol заменил каждому редактору собственную поддержку TypeScript. Конкретно это сообщения JSON-RPC 2.0 с согласованными именами методов, передаваемые через stdio или HTTP.

Более длинная версия, если это пролетело слишком быстро: docs/01 — почему существует MCP.

Related MCP server: Online Boutique AI Assistant MCP Server

Что демонстрирует этот репозиторий

Пять инструментов — catalog_list, cart_create, cart_add_item, cart_view, cart_checkout — с одинаковыми именами в обоих серверах и одинаковой бизнес-логикой в общем пакете cart-core, который ничего не знает о MCP. Единственное различие между двумя серверами — уровень протокола, который и является предметом изучения.

Четыре вещи, за которыми можно наблюдать:

  1. server-old не может завершить собственное рукопожатие за обычным round-robin балансировщиком нагрузки. server-new даже не замечает, что балансировщик существует.

  2. Перезапустите server-old в середине разговора — и корзина исчезнет навсегда, без какого-либо запроса, который клиент мог бы отправить для её восстановления.

  3. Вопрос «подтверждаете эту сумму?» обходится server-old в удержание открытого сокета на всё время человеческого размышления (измерено 1522 мс). server-new делает это двумя независимыми запросами, 4 мс + 15 мс, и может завершиться на другой машине, чем начался.

  4. Стабильный порядок списков и подсказки кэша — и арифметика, показывающая, почему отсутствующий .sort() стоит около $4 200 в год.

Серверы намеренно не отрефакторины для совместного использования кода протокола. Дублирование между ними сделано специально, чтобы вы могли прочитать каждый насквозь и сравнить их.

Обязательно посмотрите на провод

Оба сервера печатают каждый запрос на уровне HTTP: метод, путь, все MCP-заголовки, метод и параметры JSON-RPC, какой экземпляр его обработал, и ответ, включая resultType. Ничего не скрыто за абстракциями SDK. Если во время эксперимента вы прочитаете только одно — читайте цветные строки лога.

Предварительные требования

  • Node.js 20 или новее (разработано на 25.5). Проверьте node -v.

  • Терминал, поддерживающий ANSI-цвета — логи сильно на них полагаются.

  • Порты 3000–3002, 3011, 3012 свободны.

  • Никакой базы данных, никакого Docker, никакого облачного аккаунта. Общее состояние — это JSON-файл.

Никакого Python в этом репозитории.

Установка

git clone <this repo>
cd mcp-server
npm install
npm run typecheck    # should print nothing and exit 0

npm install настраивает npm-воркспейс, содержащий два поколения MCP SDK одновременно. У них разные имена пакетов, поэтому они сосуществуют без трюков с алиасами:

Пакет

Версия

Используется

@modelcontextprotocol/sdk

1.30.0

server-old, старый клиент

@modelcontextprotocol/{core,server,client,node}

2.0.0

server-new, новый клиент

Четыре эксперимента по порядку

Каждый — одна команда. Каждый запускает и останавливает свои собственные серверы — второй терминал не нужен. Прочитайте связанную статью после запуска; каждая объясняет, что вы только что увидели и почему.

Порядок

Команда

Чему учит

1

npm run exp:01

Два экземпляра за балансировщиком нагрузки — старый сервер не может даже закончить приветствие; новый не обеспокоен. Начните здесь.

2

npm run exp:02

Перезапуск в середине разговора — где на самом деле жила корзина и почему «просто добавьте Redis» работает лишь наполовину.

3

npm run exp:03

Подтверждение перед оформлением — 1522 мс удержания сокета против двух запросов по 4 мс, и почему старый способ никогда не сможет работать на serverless.

4

npm run exp:04

Подсказки кэша и стабильный порядок — доказательство попаданий в кэш счётчиком, а не секундомером, и денежный аргумент за .sort().

Затем прочитайте архитектурные документы, которые связывают все четыре вместе:

Управление вручную

Стоит сделать хотя бы раз, потому что вы выбираете темп и можете читать каждую строку лога по мере её появления.

# Old server (port 3001)
npm run old:server
npm run client -- --target old --scenario basic
npm run client -- --target old --scenario checkout
npm run client -- --target old --scenario checkout --decline

# New server (port 3002)
npm run new:server
npm run client -- --target new --scenario basic
npm run client -- --target new --scenario checkout
npm run client -- --target new --scenario discover    # server/discover — new spec only

Два экземпляра плюс балансировщик нагрузки, вручную:

PORT=3002 INSTANCE_ID=A npm run new:server
PORT=3012 INSTANCE_ID=B npm run new:server
PORT=3000 TARGETS=http://localhost:3002,http://localhost:3012 npm run lb
npm run client -- --target new --url http://localhost:3000/mcp --scenario basic

Смотрите на оба терминала серверов: один и тот же идентификатор корзины появляется в запросах, обработанных каждым, и никому нет дела.

Эксперименты с curl

Самый прямой способ почувствовать, насколько самодокументируемым является запрос новой спецификации. Запустите npm run new:server, затем:

# A complete, valid request — note how much has to be in it
curl -s -X POST http://localhost:3002/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H 'mcp-protocol-version: 2026-07-28' \
  -H 'mcp-method: tools/call' \
  -H 'mcp-name: catalog_list' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{
        "name":"catalog_list","arguments":{},
        "_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28",
                 "io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1"},
                 "io.modelcontextprotocol/clientCapabilities":{}}}}'

Теперь ломайте его по частям и наблюдайте, как меняется ошибка:

Изменение

Ожидаемый результат

-H 'mcp-method: tools/list' (тело всё ещё tools/call)

-32020 HeaderMismatch

-H 'mcp-name: cart_view'

-32020, с указанием на разногласие

уберите заголовок mcp-method

-32020, «требуемый заголовок Mcp-Method отсутствует»

уберите блок _meta

-32602, с перечислением отсутствующих ключей обёртки

mcp-protocol-version: 2099-01-01 и в заголовке, и в _meta

-32022 Неподдерживаемая версия протокола

curl http://localhost:3002/mcp (GET)

405 — конечная точка GET исчезла

Проделайте то же самое со старым сервером — и вам скажут сначала выполнить initialize.

Структура репозитория

packages/
  cart-core/     the actual product. zero MCP knowledge. shared by both servers.
  server-old/    MCP 2025-11-25. sessions, handshake, held-open streams.
  server-new/    MCP 2026-07-28. stateless, handles, MRTR, cache hints.
  client-demo/   both clients — one per SDK generation.
  round-robin/   ~50-line load balancer. no stickiness, on purpose.
experiments/     four runnable scripts + a write-up each.
docs/            the four architecture notes.
.cart-store/     server-new's shared state. a JSON file. delete it freely.

Порядок чтения кода: cart-core/src/cart.ts (что делает продукт) → server-old/src/index.ts → server-new/src/index.ts. Код уровня протокола в обоих серверах прокомментирован построчно; обвязка — нет.

npm run clean удаляет результаты сборки и .cart-store.


Глоссарий

Термины, используемые повсюду, в порядке, в котором они вас укусят.

Балансировщик нагрузки — устройство перед несколькими идентичными копиями вашего сервера, которое распределяет входящие запросы между ними. Политика по умолчанию — round-robin: отправлять каждый запрос следующей копии в списке. Он предполагает, что любая копия может ответить на любой запрос, — и именно это предположение старая спецификация MCP нарушила. Здесь это packages/round-robin, около 50 строк.

Сессия — серверная память о клиенте, охватывающая несколько запросов. В 2025-11-25 сервер выпускал Mcp-Session-Id во время рукопожатия, клиент повторял его в каждом запросе, а сервер использовал его как ключ в карте в памяти. Идентификатор сессии — это указатель в кучу одного процесса, и именно отсюда берутся все проблемы.

Без сохранения состояния — сервер ничего не хранит между запросами. Каждый запрос несёт всё необходимое для его обслуживания. Обратите внимание, что это не означает: корзина всё ещё есть, и она всё ещё хранится. Исчезло состояние, удерживаемое в конкретном процессе, неявно, по ключу соединения. Состояние приложения в общей базе данных полностью совместимо с протоколом без сохранения состояния.

Липкая сессия (привязка сессии) — настройка балансировщика нагрузки так, чтобы все запросы от одного клиента возвращались к одной и той же копии сервера, обычно путём хеширования cookie или заголовка. Стандартный обходной путь для протокола с сохранением состояния. Он работает, но стоит вам равномерного распределения нагрузки, безболезненных развёртываний, полезного автомасштабирования и балансировщика, которому не нужно понимать ваш прикладной протокол. Список затрат в docs/02.

Элиситация — сервер задаёт конечному пользователю вопрос в середине операции («сумма $180.36, подтвердить?»). В старой спецификации сервер отправлял собственный запрос клиенту через удерживаемый открытый поток и блокировался внутри обработчика инструмента, пока человек думал. Эта единственная функция требовала живого процесса, открытого сокета и гарантированной маршрутизации обратно на ту же машину.

MRTR (Multi Round-Trip Requests) — как 2026-07-28 делает элиситацию вместо этого. Сервер возвращает обычный 200 с resultType: "input_required", вопросы в inputRequests и непрозрачный подписанный requestState. Этот запрос завершён — ничего не удерживается. Клиент собирает ответы и отправляет новый запрос (новый идентификатор JSON-RPC) с inputResponses и тем же requestState. Состояние в полёте путешествовало через клиента, а не сидело в процессе, поэтому второй раунд может обслужить совершенно другая машина.

Хэндл — идентификатор, выпущенный сервером, возвращаемый как обычный вывод инструмента, а затем передаваемый обратно как обычный аргумент. cart_create возвращает cartId; cart_add_item принимает его. Так 2026-07-28 заменяет состояние сессии, и разница со старым дизайном в том, кто держит ключ: транспорт, невидимо, против клиента, в значении, которое модель может прочитать и передать дальше. Оговорка: хэндл сам по себе — это токен-носитель; его нужно ограничить аутентифицированным пользователем, что docs/04 честно освещает.

Кэширование подсказок — LLM-провайдеры кэшируют префикс подсказки: отправьте те же начальные байты снова, и провайдер переиспользует своё вычисленное состояние вместо повторной обработки этих токенов, примерно за десятую часть цены ввода. Два свойства делают его хрупким: совпадение по точным байтам и позиционность от начала. Поэтому если ваш список инструментов или каталог находится в префиксе и две записи меняются местами, вы теряете скидку на каждый токен после перестановки. Вот почему 2026-07-28 говорит, что серверы ДОЛЖНЫ возвращать списки в детерминированном порядке, и почему listProducts() сортирует по уникальному id, а не по имени или цене — уникальный ключ даёт полный порядок без связей, которые сортировка могла бы разрешить по-разному. Рабочий пример с ценами: эксперимент 04.


Если запомнить только три вещи

  1. «Без сохранения состояния» не означает «без состояния» — это означает отсутствие состояния, привязанного к процессу. Корзина всё ещё существует. Она переехала туда, куда может добраться любой экземпляр.

  2. Липкие сессии были настоящим исправлением с реальными затратами, и одна из этих затрат — необходимость заставить вашу инфраструктуру разбирать ваш прикладной протокол.

  3. Именно MRTR, а не отсутствие состояния, открыло дорогу serverless. Отсутствие состояния вывело MCP за балансировщик нагрузки. Элиситация всё ещё требовала, чтобы процесс оставался живым, пока человек читает диалог — и именно это serverless устранил.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    Related MCP Connectors

    Related MCP Servers