# MCP 2026 став stateless: будуємо сучасний MCP server на TypeScript

> Спека MCP 2026-07-28 прибрала handshake і сесії: кожен запит самодостатній, сервер масштабується як звичайний HTTP-сервіс. Розбираємо нову архітектуру, SDK v2 і міграцію старого сервера.

- Автор: Юра Скиба (https://cookiesoftware.io)
- Опубліковано: 2026-10-01
- Категорія: AI в роботі розробника
- Canonical: https://cookiesoftware.io/blog/mcp-2026-stateless-typescript

---

**Коротка відповідь.** Специфікація MCP від 28 липня 2026 перетворила протокол із двонаправленого stateful на запит-відповідь stateless: `initialize`-handshake і `Mcp-Session-Id` видалені, усе необхідне їде в кожному запиті через `_meta` і HTTP-заголовки. Для тебе це означає: MCP server тепер пишеться і деплоїться як звичайний HTTP-сервіс – за load balancer-ом, без sticky sessions, із кешованими списками інструментів. Нижче – що саме змінилося, робочий сервер на SDK v2 і міграція старого.

Якщо ти взагалі вперше чуєш про MCP – спочатку прочитай [що таке MCP простими словами](/blog/mcp-prostymy-slovamy): тут ми не пояснюємо основи, а одразу йдемо в архітектуру.

## Що прибрали і чому це важливо

До цієї спеки MCP жив як розмова: клієнт відкривав з'єднання, відбувався `initialize`/`initialized` handshake, сервер видавав `Mcp-Session-Id`, і далі обидві сторони пам'ятали стан розмови. Красиво для desktop-сценарію «Claude ↔ локальний сервер», боляче для production HTTP: кожен запит мусив потрапити на той самий інстанс або сервер мусив тягати стан через Redis.

Спека 2026-07-28 вирізала це повністю:

- **Жодного handshake.** Перший запит – одразу робочий.
- **Жодного `Mcp-Session-Id`.** Запити можуть лягати на будь-який інстанс за round-robin.
- **Самоописні запити.** Версія протоколу, ідентичність клієнта і його capabilities їдуть у `_meta` кожного запиту.

Ось як виглядає виклик інструмента тепер (приклад зі спеки):

```
POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search

{"jsonrpc":"2.0","id":1,"method":"tools/call",
 "params":{"name":"search","arguments":{"q":"otters"},
 "_meta":{"io.modelcontextprotocol/clientInfo":{"name":"my-app","version":"1.0"}}}}
```

Зверни увагу на заголовки `Mcp-Method` і `Mcp-Name` – тепер вони обов'язкові для Streamable HTTP. Це подарунок інфраструктурі: gateway, WAF чи rate limiter можуть роутити й тарифікувати виклики, не парсячи JSON-тіло.

## server/discover: capabilities на вимогу

Handshake зник, але іноді клієнту треба знати можливості сервера наперед. Для цього є новий RPC `server/discover` – необов'язковий: якщо клієнт і так знає, що робити, він просто робить. Це інверсія старої моделі: замість «спершу познайомимось» – «запитуй, коли справді треба».

## MRTR: що замінило серверні запити до клієнта

У stateful-світі сервер міг сам ініціювати запит до клієнта – наприклад, попросити підтвердження в користувача посеред виконання інструмента. Для цього потрібен був відкритий stream. У stateless-моделі це неможливо, тому з'явилися **Multi Round-Trip Requests**:

1. Інструмент розуміє, що йому бракує відповіді користувача, і повертає `resultType: "input_required"` зі списком запитань.
2. Клієнт показує запитання користувачу і **повторює той самий виклик**, додавши відповіді в `inputResponses`.

Типовий кейс: інструмент видалення даних перед виконанням повертає «підтверди, що видаляємо проєкт X вартістю Y». Жодних відкритих з'єднань – просто ще один HTTP round-trip.

## Кешовані списки інструментів

`tools/list`, `prompts/list`, `resources/list` і `resources/read` тепер повертають `ttlMs` і `cacheScope`. Клієнт більше не мусить смикати список інструментів на кожну розмову – сервер сам каже, скільки відповідь жива і в якому скоупі її можна шерити. Для серверів із сотнями інструментів це помітно знімає навантаження.

## Auth: менше магії, більше RFC

Коротко, бо тема на окрему статтю: authorization server тепер зобов'язаний повертати `iss` за RFC 9207 і клієнт мусить його валідувати; креденшели клієнта прив'язані до issuer-а (жодного перевикористання між серверами); Dynamic Client Registration формально деприкований на користь Client ID Metadata Documents (CIMD). Якщо твій сервер за OAuth – міграційний гайд читати обов'язково.

## Мінімальний stateless server на SDK v2

TypeScript SDK v2 вийшов разом зі спекою і розбитий на окремі пакети `@modelcontextprotocol/server` і `@modelcontextprotocol/client`. Реєстрація інструмента – через zod-схему, яку SDK валідує до виклику хендлера:

```ts
import { McpServer } from '@modelcontextprotocol/server';
import { z } from 'zod';

function buildServer() {
  const server = new McpServer({ name: 'blog-tools', version: '1.0.0' });

  server.registerTool(
    'search-articles',
    {
      description: 'Пошук статей блогу за запитом',
      inputSchema: z.object({
        query: z.string().min(2),
        limit: z.number().int().min(1).max(20).default(5),
      }),
    },
    async ({ query, limit }) => {
      const results = await searchIndex(query, limit);
      return {
        content: [{ type: 'text', text: JSON.stringify(results) }],
      };
    }
  );

  return server;
}
```

Ключовий момент деплою: сервер створюється **фабрикою на кожен запит** – саме так працює stateless-ідіома. Для HTTP є адаптери під Express, Hono і Fastify (`@modelcontextprotocol/express` тощо), які загортають `buildServer` у handler; за замовчуванням вони обслуговують і клієнтів попередньої ери протоколу. Жодного стану в замиканнях – усе, що треба між запитами, живе в базі або кеші, як у будь-якому нормальному API.

Чесна примітка: приклад зібраний за офіційною документацією SDK v2; точний API `ttlMs`/`cacheScope` на рівні SDK я не перевіряв на виконання – звіряйся з доками в день імплементації.

## Міграція старого сервера: чекліст

1. **Онови SDK до v2** і розведи імпорти на `@modelcontextprotocol/server` / `client`. v1.x отримує лише фікси ще мінімум пів року.
2. **Знайди все, що залежить від session-id.** Це головне джерело breaking changes: логіка «запам'ятати клієнта між викликами» переїжджає в явний стан (БД/кеш) або зникає.
3. **Прибери server-initiated патерни** – перепиши на MRTR (`input_required` → retry).
4. **Віддавай `ttlMs` для списків** – безкоштовне зниження трафіку.
5. **Перевір деприкації:** Roots, Sampling і Logging ще працюють мінімум 12 місяців, але нові фічі на них не будуй; старий HTTP+SSE-транспорт теж у річному вікні деприкації.
6. **Auth:** валідація `iss`, issuer-bound credentials, план переходу з DCR на CIMD.

## Типові помилки

- **Тягнути сесійний стан у stateless-сервер** через глобальні змінні модуля. На одному інстансі «працює», за load balancer-ом – ні. Стан – тільки явний.
- **Ігнорувати `Mcp-Method`/`Mcp-Name` заголовки** у власному транспорті – частина інфраструктури (gateway, логування) почне поводитись непередбачувано.
- **Вважати `server/discover` обов'язковим** і робити його перед кожним викликом – це знову handshake, тільки збоку.
- **Кешувати tools/list назавжди**, ігноруючи `ttlMs` – після деплою нового інструмента клієнти його не побачать.

Якщо дивитись ширше, ця еволюція – класика розподілених систем: ми вже проходили той самий шлях від stateful до stateless у [суперечці моноліта з мікросервісами](/blog/monolith-vs-microservices). MCP просто наздогнав дорослі патерни. А як перевіряти, що твій AI-агент із цими інструментами реально працює, – у [статті про eval-и агентів](/blog/ai-agent-evals).

## Практичне завдання

Візьми будь-який свій (або знайомий open-source) MCP server на v1 і проведи аудит за чеклістом міграції вище: випиши кожне місце, де код покладається на сесію чи server-initiated запит, і для кожного запиши stateless-альтернативу. Навіть без самої міграції цей список покаже, скільки прихованого стану жило у твоєму «простому» сервері.
