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.tsserver-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 устранил.

Maintenance

ActivityMaintained
ResponsivenessSyncing

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ritik913553/mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server