Данило (Dayfing)
Назад до публікацій
1 912 слів9 хв

MCP 2026-07-28: stateless-сервери та безпечна міграція

Редакція Model Context Protocol від 28 липня 2026 року змінює спосіб роботи віддаленого MCP. Ядро протоколу тепер не має стану й працює з окремими запитами. Кожен запит містить дані, потрібні для обробки, тому балансувальник може надіслати наступний виклик іншому екземпляру сервера. Реліз також додає server/discover, багатоетапні запити Multi Round-Trip Requests (MRTR), обов’язкові заголовки маршрутизації, підказки кешування, framework розширень і посилені правила авторизації. Нормативні деталі цього матеріалу взято з офіційного повідомлення про реліз та журналу змін специфікації.

Це зміна стану протоколу, а не вимога зробити весь бізнес без стану. Інструмент і надалі може використовувати базу даних, чергу або довговічний workflow. Зникає прихований стан, прив’язаний до транспортної сесії MCP, що важливо під час переходу від одного процесу до кількох реплік.

Що змінилося порівняно з протоколом 2025 року

Раніше життєвий цикл починався запитом initialize, за яким надходило сповіщення notifications/initialized. У Streamable HTTP сервер міг видати Mcp-Session-Id і прив’язати наступні повідомлення до цього з’єднання. У 2026-07-28 обмін initialize та заголовок протокольної сесії вилучено. Кожен запит оголошує версію протоколу й можливості клієнта в _meta. Клієнту варто передавати io.modelcontextprotocol/clientInfo, а серверу варто вказувати свою ідентичність у метаданих результату.

Порівняння допомагає спланувати перехід:

Область Поведінка 2025 року Поведінка 2026-07-28
Життєвий цикл initialize і notifications/initialized Протокольного handshake немає
Сесія Необов’язковий Mcp-Session-Id в HTTP Протокольної сесії немає
Можливості Узгоджуються один раз Оголошуються в кожному запиті
Виявлення Після initialize або за домовленістю Сучасний сервер мусить мати server/discover, виклик клієнта необов’язковий
Сервер до клієнта Запити через відкритий канал MRTR повертає запити введення у відповіді
Маршрутизація HTTP Шлюз часто розбирає JSON Заголовки Mcp-Method і Mcp-Name за потреби
Списки та читання Свіжість визначає клієнт Підказки ttlMs і cacheScope
Відновлення SSE міг використовувати ID подій Last-Event-ID не відновлює потік, потрібен новий запит
Реєстрація DCR зазвичай був автоматичним шляхом CIMD рекомендовано, DCR лишається для сумісності

Редакція переносить Tasks у розширення io.modelcontextprotocol/tasks, замінює старий потік змін на subscriptions/listen і позначає Roots, Sampling, Logging та старий HTTP+SSE як застарілі. Політика дає щонайменше дванадцять місяців, але нові реалізації не повинні їх додавати.

Що означає stateless на практиці

У сучасному Streamable HTTP сервер відкриває одну MCP-точку, що приймає POST. Клієнт надсилає один JSON-RPC-запит або сповіщення на кожен POST. Відповіддю може бути один JSON-об’єкт або SSE-потік, пов’язаний лише з цим запитом. Сервер не створює ідентифікатор сесії, а перерваний потік не має історії для відновлення. В HTTP закриття потоку відповіді є сигналом скасування.

Заголовки транспорту й тіло описують одну операцію. Мінімальний виклик інструмента має такий вигляд:

POST /mcp HTTP/1.1
Accept: application/json, text/event-stream
Content-Type: application/json
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/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{},"io.modelcontextprotocol/clientInfo":{"name":"catalog-app","version":"1.0.0"}}}}

Значення MCP-Protocol-Version має збігатися зі значенням у _meta. Сучасний сервер відхиляє відсутній або невідповідний обов’язковий заголовок із HTTP 400 і кодом HeaderMismatch, -32020. Після роботи шлюзу сервер мусить перевірити заголовки знову. Це не дає проксі маршрутизувати один інструмент, коли застосунок виконує інший.

Stateless змінює масштабування, але не бізнес-семантику. Якщо workflow має продовжуватися, поверніть з інструмента явний handle і вимагайте його в наступному виклику. Зберігайте авторитетний стан у базі чи workflow-сервісі, зв’яжіть handle з користувачем і операцією та встановіть строк дії. Неперевірений handle, requestState або аргумент інструмента не є доказом дозволу.

server/discover і сумісність версій

Кожен сучасний сервер має реалізувати RPC server/discover. Його результат оголошує підтримувані версії та можливості й може містити необов’язкові інструкції. Клієнт може викликати його першим, щоб обрати версію, або одразу надіслати сучасний запит. Отже, discovery корисний, але не є обов’язковим handshake на боці клієнта.

Якщо потрібна версія не підтримується, сервер повертає UnsupportedProtocolVersionError зі списком підтримуваних версій. Клієнт обирає спільну версію та повторює запит. Клієнт, який підтримує обидві епохи, повинен обережно класифікувати пробний запит. Порожня відповідь 400 або 400 без розпізнаної сучасної JSON-RPC-помилки може вказувати на стару точку входу. Відома сучасна помилка означає, що запит треба виправити або узгодити знову. Помилки авторизації й інфраструктури не доводять, що сервер старий.

Сучасний SDK може перевірити з’єднання standard input і зафіксувати епоху для нього. Сервер може залишити legacy-маршрут, поки нові клієнти використовують сучасні запити. Не визначайте епоху лише за успішним TCP-з’єднанням або загальним 404.

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

Сучасний формат вилучає канал JSON-RPC-запитів від сервера до клієнта. Інструменту, якому потрібне підтвердження, відсутнє значення або крок за участю моделі, не потрібно утримувати потік відкритим. Він повертає проміжний результат з resultType: "input_required" і картою inputRequests. Клієнт відповідає на них і повторює початковий метод з inputResponses. Повтор є новим запитом і може потрапити на іншу репліку.

Потік підтвердження може мати такий вигляд:

{
  "resultType": "input_required",
  "inputRequests": {
    "confirm": {
      "type": "elicitation",
      "message": "Delete three files?",
      "schema": {"type": "boolean"}
    }
  },
  "requestState": "signed-opaque-state"
}

Потім клієнт надсилає inputResponses.confirm і повертає requestState побайтно. Сервер має знову увійти в обробник як у новий запит. Зробіть обробник ідемпотентним, виводьте поточний крок із перевіреного стану й запитуйте лише дані, яких ще бракує. Не позначайте руйнівну дію завершеною до перевірки підтвердження.

requestState не є захищеним контейнером. Він проходить через клієнта, тому його слід вважати введенням, контрольованим атакувальником. Підписуйте його HMAC або використовуйте автентифіковане шифрування, прив’яжіть до principal, початкового методу, важливих параметрів і строку дії, а підробку відхиляйте до запуску обробника. Підпис не приховує вміст, тому не зберігайте там секрети. TypeScript SDK надає codec стану запиту та hook перевірки. Його legacy shim може перетворити той самий обробник input_required на старі elicitation/create, sampling/createMessage і roots/list, доки підтримуються клієнти 2025 року.

Для довгих операцій застосовуйте Tasks із довговічним task handle, опитуванням tasks/get і tasks/update, коли потрібне введення клієнта.

Підказки кешу та детерміновані каталоги

Сучасні результати tools/list, prompts/list, resources/list, resources/templates/list і resources/read містять ttlMs та cacheScope. ttlMs є невід’ємною підказкою свіжості в мілісекундах, подібною до HTTP max-age. Нуль означає негайне застарівання. Для старих серверів відсутнє значення слід трактувати як нуль. Додатне значення повідомляє, коли клієнт може не виконувати повторне отримання, але не гарантує незмінність даних. Перевіряйте свіжість під час використання даних, а не перетворюйте TTL на безперервний polling.

Ключ кешу має включати метод і кожен параметр, що впливає на результат, зокрема URI ресурсу та cursor посторінкового списку. Не кешуйте відповідь із inputResponses або requestState, бо простий ключ списку не містить її контексту. cacheScope: "public" дозволяє ділитися результатом між контекстами авторизації, тому використовуйте його лише для даних без користувацьких або permission-залежних полів. Навіть кешований каталог не скасовує авторизацію кожного інструмента.

Серверу варто повертати інструменти в детермінованому порядку. Стабільний порядок зберігає сталість prompt моделі й покращує повторне використання кешу після перепідключення. Сповіщення listChanged може доповнити TTL, але не замінює коректну підказку.

OAuth та зміни безпеки

Авторизація для MCP загалом необов’язкова. HTTP-сервер, який захищає ресурси, має дотримуватися профілю OAuth 2.1 зі специфікації 2026 року. Він працює як resource server і мусить публікувати OAuth Protected Resource Metadata за RFC 9728. Відповідь 401 повинна вказувати на ці метадані через WWW-Authenticate і, коли доречно, містити scope challenge. Клієнти мають підтримувати URL із заголовка та обидві well-known форми.

Метадані можуть назвати кілька authorization server. Клієнти мають підтримувати OAuth Authorization Server Metadata за RFC 8414 і OpenID Connect Discovery. Для issuer зі шляхом спочатку перевіряйте форми зі вставленням шляху, потім OpenID-форму з доданим шляхом.

Реєстрація тепер надає перевагу Client ID Metadata Documents. Попередня реєстрація також чинна. Dynamic Client Registration є застарілим запасним механізмом. Якщо використовується DCR, передавайте правильний application_type для desktop або CLI-клієнта. Перевіряйте PKCE, використовуйте S256, реєструйте точні redirect URI та застосовуйте HTTPS, крім дозволеного localhost callback.

Клієнт має зберегти перевірений issuer разом із транзакцією PKCE. Якщо відповідь авторизації містить iss, порівняйте його до обміну коду. Облікові дані прив’язані до issuer, який їх видав, і не можуть повторно використовуватися з іншим authorization server. Передавайте канонічний URI MCP-сервера як resource RFC 8707 у запитах авторизації та токена. Сервер мусить перевіряти audience і відхиляти токени іншого ресурсу.

У Streamable HTTP перевіряйте Origin, щоб заблокувати DNS rebinding. Локальний сервер повинен слухати localhost, а не всі інтерфейси. Не записуйте bearer-токени в query string або логи, вимагайте згоди перед розкриттям приватних ресурсів і викликом інструментів, а описи та annotations вважайте ненадійними, якщо сервер не перевірений. 401 означає відсутню або недійсну авторизацію. 403 означає недостатні дозволи й за можливості має містити insufficient_scope.

Міграція TypeScript SDK

TypeScript SDK v2 розділяє пакети клієнта, сервера, ядра й runtime. Ознайомтеся з посібником SDK для 2026-07-28 і посібником переходу з v1 на v2. Саме оновлення не гарантує сучасні байти на дроті. Клієнт v2 за замовчуванням узгоджує legacy-режим, тому явно ввімкніть versionNegotiation.

Для клієнта, який підтримує обидві епохи, документація показує таку форму:

import { Client } from '@modelcontextprotocol/client';

const client = new Client(
  { name: 'catalog-app', version: '1.0.0' },
  { versionNegotiation: { mode: 'auto' } },
);
await client.connect(transport);

mode: 'auto' перевіряє server/discover і повертається до handshake 2025 року лише зі справді legacy-піром. Зафіксуйте 2026-07-28, якщо fallback може приховати несумісність. createMcpHandler(factory) створює сучасний HTTP-сервер для кожного запиту й може обслуговувати обидві епохи. Для вибору епохи в stdio використовуйте serveStdio(() => buildServer()).

Замість реєстрації обробників v1 через схеми використовуйте рядки методів, наприклад setRequestHandler('tools/call', handler). Код, що читає ctx.sessionId, замініть явними application handles або перевіреним requestState. Push-elicitation замініть на inputRequired(...). Сучасний запит не надсилає log-сповіщення без io.modelcontextprotocol/logLevel. Codemod SDK є механічною допомогою, а не тестом сумісності.

Для перевірки запускайте createMcpHandler через fetch-подібний тестовий транспорт і окремо перевіряйте legacy-handshake. Тестуйте заголовки, _meta, ідемпотентність повторів, область кешу, audience та HTTP-статус реальними старим і сучасним клієнтами.

Чекліст міграції

  1. Складіть перелік клієнтів, серверів, транспортів, сховищ сесій, SSE event store та читання Mcp-Session-Id.
  2. Визначте endpoint для сучасного трафіку і маршрут, який тимчасово залишиться legacy.
  3. Оновіть SDK і зафіксуйте фактичні версії в lockfile.
  4. Додайте server/discover і політику узгодження версій.
  5. Зробіть кожен запит самодостатнім та перевіряйте _meta і дзеркальні заголовки.
  6. Замініть стан за сесією явними handles або підписаним requestState зі строком дії.
  7. Перепишіть серверну взаємодію через MRTR і зробіть повтори безпечними.
  8. Додайте ttlMs, cacheScope та детермінований порядок каталогам і ресурсам.
  9. Оновіть шлюзи, WAF, метрики й трасування для Mcp-Method і Mcp-Name.
  10. Реалізуйте перевірки issuer, resource, audience, PKCE, redirect, Origin і scopes.
  11. Не додавайте HTTP+SSE, Roots, Sampling, Logging або DCR у новий код.
  12. Розгортайте поступово з observability, порівняйте помилки modern і legacy, а код сумісності видаляйте після міграції споживачів.

Діагностика

Симптом Ймовірна причина Дія
HTTP 400 з -32020 Заголовок відсутній або не збігається з тілом Перерахуйте MCP-Protocol-Version, Mcp-Method і Mcp-Name з одного об’єкта
HTTP 400 і помилка версії Пір не підтримує потрібну редакцію Оберіть версію з supported або legacy-маршрут
HTTP 404 і method-not-found Endpoint сучасний, але методу немає Перевірте метод і розширення, не запускайте initialize навмання
HTTP 401 або 403 під час discovery Авторизація заблокувала пробу Виправте credentials і метадані, цей статус не доводить legacy
Немає log-сповіщень Відсутній io.modelcontextprotocol/logLevel Увімкніть його для запиту або використовуйте stderr і OpenTelemetry
Повторні побічні ефекти Потік більше не можна відновити Використовуйте idempotency keys і новий ID запиту
Дані користувача потрапляють в інший кеш Результат помилково позначено public Використайте private і збережіть перевірки principal
Старий клієнт отримує 405 на GET Він очікує HTTP+SSE Тимчасово залиште legacy-маршрут

Контекст архітектури наведено в матеріалах production AI agent architecture і MCP server with TypeScript and OAuth.

Джерела

Матеріал спирається на специфікацію MCP 2026-07-28, журнал змін і застарілих можливостей, вимоги Streamable HTTP, SEP-2575 про MCP без стану, статтю про реліз 2026-07-28, специфікацію авторизації MCP і документацію міграції TypeScript SDK.

Інші публікації