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

Якщо ти взагалі вперше чуєш про MCP – спочатку прочитай що таке MCP простими словами: тут ми не пояснюємо основи, а одразу йдемо в архітектуру.

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

До цієї спеки 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 валідує до виклику хендлера:

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 у суперечці моноліта з мікросервісами. MCP просто наздогнав дорослі патерни. А як перевіряти, що твій AI-агент із цими інструментами реально працює, – у статті про eval-и агентів.

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

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