У MCP два стандартні транспорти — STDIO і Streamable HTTP, — і більшість туториалів пояснює їх так, ніби це питання смаку. Насправді вибір майже завжди визначається одним питанням: де живе сервер і хто ним користується. А ще в 2026 році транспорт суттєво змінився: у ревізії специфікації 2026-07-28 протокол став stateless, і половина старих пояснень про сесії вже не актуальна. У цій статті — як працюють обидва транспорти, що саме змінилося, таблиця вибору за сценаріями і чекліст переходу зі STDIO на HTTP у Spring AI. Мій публічний MCP-сервер для блогу працює через Streamable HTTP, а для STDIO я зібрав окремий тестовий сервер, підключив його до OpenCode і по черзі ламав — тому обидві частини спираються на реальні логи і помилки, а не лише на документацію.
⚡ Коротко
✅ STDIO — клієнт сам запускає сервер як дочірній процес на вашому комп'ютері. Для локальних інструментів і особистого використання.
✅ Streamable HTTP — сервер працює окремо за URL і обслуговує багатьох клієнтів. Для спільних, командних і публічних серверів.
✅ З ревізії 2026-07-28 протокол stateless: немає сесій, немає рукостискання initialize, кожен запит несе весь потрібний контекст.
✅ Старий HTTP+SSE застарів ще в 2025 році — нові сервери на ньому не роблять.
✅ Транспорт ≠ безпека: HTTP вимагає перевірки Origin, автентифікації і лімітів; STDIO — контролю над тим, що саме запускає клієнт.
✅ Головна пастка STDIO мовчазна: зайвий рядок у stdout (банер, логи) не дає жодної помилки — клієнт просто не бачить інструментів.
🎯 Ви отримаєте: розуміння механіки обох транспортів, таблицю вибору під ваш сценарій і чекліст переходу на HTTP.
У MCP два окремі шари. Протокол — це зміст повідомлень: JSON-RPC запити на кшталт tools/list чи tools/call і відповіді на них. Транспорт — це спосіб доставки цих повідомлень між клієнтом і сервером.
Повідомлення однакові незалежно від транспорту. Інструмент search_articles у моєму сервері описується і викликається тим самим JSON, хоч через STDIO, хоч через HTTP. Змінюється лише «труба»: потік вводу-виводу процесу або HTTP-запити.
💡 Чому це важливо: вибір транспорту не змінює логіку ваших інструментів. Це рішення про розгортання — де працює сервер, хто його запускає і хто може до нього достукатися. Тому той самий сервер можна спершу зробити локальним через STDIO, а потім винести в мережу через HTTP, не переписуючи інструменти.
Специфікація визначає два стандартні транспорти — STDIO і Streamable HTTP — і дозволяє власні, але на практиці клієнти підтримують саме ці два.
🖥 Розділ 2. STDIO: сервер як дочірній процес
Як це працює
Клієнт сам запускає сервер як дочірній процес — наприклад, Claude Code виконує команду java -jar my-mcp.jar чи npx some-mcp-server.
Клієнт пише JSON-RPC повідомлення у stdin сервера, сервер відповідає у stdout. Кожне повідомлення — окремий рядок.
Логи сервер може писати в stderr — клієнт їх ігнорує або показує як журнал.
Коли клієнт закривається, він закриває stdin і завершує процес сервера.
Головне правило STDIO зі специфікації: сервер не має права писати в stdout нічого, крім валідних MCP-повідомлень. Будь-який зайвий рядок у stdout — і клієнт отримує «зламаний» JSON.
Мій експеримент: що насправді ламає STDIO у Spring Boot
Я зібрав окремий мінімальний сервер на Spring AI з одним інструментом text_stats, підключив його до OpenCode і по черзі ламав.
Перевірка без клієнта — JSON-рядки в stdin, відповідь у stdout:
Правильний результат — по одному рядку JSON на запит і нічого більше. sleep потрібен, щоб сервер не завершився, поки не відповів.
👤 Пастка 1. «Вимкнути логи» — і отримати помилку в stdout
Порожній шаблон logging.pattern.console= Logback вважає помилкою і пише про це в stdout: Empty or null pattern. Правильно — logging.threshold.console=OFF і логи у файл.
👤 Пастка 2. Банер: сервер живий, але клієнт його не бачить
З увімкненим банером у stdout перед JSON ішли 9 рядків логотипу Spring. Процес працював, запит initialize від OpenCode в лозі був, помилок — жодних. Але інструмента в OpenCode не було: клієнт мовчки відкинув сервер. А модель «порахувала вручну» і впевнено відповіла 36 символів замість 37. Без банера все запрацювало, і відповідь стала точною.
👤 Пастка 3. Перезібрав jar — а працює старий код
Запущений процес не бачить нового jar, а MCP-сервери тримає фоновий сервіс клієнта (opencode-cli serve --service), тож навіть перезапуск вікна OpenCode не допоміг. І навпаки: вручну зупинений сервер клієнт сам не перезапустив — інструмент зник до повного перезапуску.
Ще два спостереження: той самий Spring AI 1.1.3 по HTTP погодив протокол 2025-06-18, а по STDIO — найперший, 2024-11-05; нові поля від свіжих клієнтів (elicitation.form), на яких падав мій HTTP-сервер, по STDIO пройшли без помилки.
Мінімальна конфігурація
spring.ai.mcp.server.stdio=true
spring.main.web-application-type=none # вебсервер не потрібен
spring.main.banner-mode=off # банер іде в stdout
logging.threshold.console=OFF # логи — не в консоль...
logging.file.name=/tmp/mcp-stdio-demo.log # ...а у файл
Плюс: жодного System.out.println у коді, абсолютні шляхи до java і jar у конфігу клієнта, а для важкого застосунку — запас на холодний старт JVM (у Claude Code — змінна MCP_TIMEOUT).
⚠️ Не запускайте основний застосунок через STDIO: клієнт підніме його повністю — з базою, кешем і фоновими задачами. Для STDIO потрібен окремий легкий застосунок лише з MCP-інструментами.
Плюси і мінуси
Плюси
Мінуси
немає порту і мережі — нічого не слухає ззовні
працює лише на тій самій машині, що й клієнт
доступ до локальних файлів і ресурсів користувача
один клієнт — один процес; кожен клієнт запускає свою копію
не потрібні автентифікація, TLS, ліміти
кожен користувач має встановити сервер у себе (Java, Node, Python…)
легко тестувати вручну: JSON у stdin — відповідь у stdout
зламаний stdout не дає явної помилки — клієнт мовчки відкидає сервер
впалий сервер клієнт сам не перезапускає; оновлення jar не підхоплюється до перезапуску
🌐 Розділ 3. Streamable HTTP: один endpoint і POST
Як це працює
Сервер працює самостійно — як звичайний вебзастосунок — і може обслуговувати багатьох клієнтів одночасно.
У нього один endpoint, наприклад https://webscraft.org/mcp.
Кожне повідомлення клієнта — окремий HTTP POST на цей endpoint. Клієнт указує в заголовку Accept, що приймає і application/json, і text/event-stream.
Сервер сам вирішує для кожного запиту, як відповісти: одним JSON (якщо відповідь готова одразу) або SSE-потоком (якщо треба надіслати проміжні повідомлення — наприклад, прогрес довгої операції — а потім фінальну відповідь).
У моєму сервері пошук займає близько пів секунди разом з ембедингом запиту, тому всі відповіді — звичайний JSON. SSE-потоки потрібні, коли інструмент працює довго і має показувати прогрес.
«Це просто REST API» — чому це неточно
Streamable HTTP легко сплутати з REST: та сама мережа, той самий HTTP, той самий JSON. Але різниця суттєва:
Один endpoint замість багатьох ресурсів. У REST дія визначається URL і методом (GET /articles/42). У MCP завжди POST /mcp, а що робити — написано в тілі запиту ("method": "tools/call").
Відповідь може бути потоком. REST зазвичай відповідає одним документом; MCP-сервер може відкрити SSE-потік і надсилати кілька повідомлень.
Протокольні заголовки. Клієнт передає версію протоколу (MCP-Protocol-Version), а в нових версіях — і назву методу та інструмента в заголовках, щоб проксі і балансувальники могли маршрутизувати запити, не розбираючи тіло.
Споживач — не програміст, а AI-клієнт, який сам дізнається, що вміє сервер, через tools/list.
🔄 Розділ 4. Що змінилося у 2026: MCP став stateless
Якщо ви читали туториали 2025 року, там багато про сесії, заголовок Mcp-Session-Id і окремий GET-потік. Ревізія специфікації 2026-07-28 все це прибрала.
2025-03-26 … 2025-11-25
2026-07-28
Початок роботи
рукостискання initialize перед будь-яким викликом
немає рукостискання; версія і можливості клієнта передаються в кожному запиті (_meta)
Сесії
сервер міг видати Mcp-Session-Id, клієнт додавав його до кожного запиту
протокольних сесій немає
Повідомлення від сервера першим
окремий GET-потік, сервер міг надсилати власні запити
GET-потоку немає; запити до клієнта вбудовані у відповідь (Multi Round-Trip Requests), сповіщення про зміни — через окремий запит-підписку
Відновлення обірваного потоку
через Last-Event-ID
не підтримується — просто новий запит
Заголовки
MCP-Protocol-Version (з 2025-06-18)
+ Mcp-Method і Mcp-Name у кожному запиті
Чому так. Сесії заважали масштабуванню: запит мав потрапити саме на той екземпляр сервера, який «пам'ятає» сесію, — потрібні були «липкі» сесії на балансувальнику. Довгі GET-потоки рвалися проксі за таймаутом. Тепер будь-який запит може обробити будь-який екземпляр сервера, а балансувальник маршрутизує за заголовками, не розбираючи тіло.
👤 З мого досвіду: свій сервер я робив на Spring AI 1.1, а його MCP SDK працює з версією протоколу 2025-06-18 — тобто ще з «епохи сесій». Там лишається вибір між режимом із сесіями і stateless, і я обрав stateless: пошук короткий, стрімити нічого, сайт стоїть за Cloudflare і регулярно деплоїться, а сесії при кожному перезапуску зникали б. Ревізія 2026-07-28 зробила такий підхід стандартом для всього протоколу — тож якщо ваш SDK ще дає вибір, обирайте stateless: так ви вже зараз будуєте сервер у тій моделі, куди рухається специфікація. Побічний ефект stateless-режиму, який легко прийняти за помилку: MCP Inspector намагається відкрити GET-потік і отримує 405 Method Not Allowed. Це нормально — для stateless-сервера GET не передбачений, і нова специфікація прямо рекомендує відповідати саме 405.
А як нові клієнти працюють зі старими серверами?
Сумісність передбачена: клієнт, який знає обидві «епохи», спершу надсилає запит у новому форматі, а якщо сервер відповідає помилкою, яку не розпізнає як сучасну, — повертається до старого рукостискання initialize. Мій сервер на старішому SDK саме так і працює з клієнтами: у логах видно, що OpenCode 2.2.6 підключився з версією протоколу 2025-11-25 через звичайний initialize.
Але є нюанс: старий SDK може не розуміти нових полів, які присилають свіжі клієнти, — у мене це закінчилося помилкою Unrecognized field "form" і фільтром сумісності. Детально — у статті «Типові помилки при створенні MCP-сервера»
🕰 Розділ 5. Старий HTTP+SSE: чому його замінили
Якщо ви знайшли туториал, де MCP-сервер має два адреси — одну для GET (SSE-потік) і одну для POST, — це старий транспорт HTTP+SSE з першої ревізії протоколу (2024-11-05).
Як він працював: клієнт відкривав довгий GET-запит, по якому сервер надсилав усі повідомлення, а свої запити надсилав окремими POST на іншу адресу.
Чому відмовилися: постійно відкрите з'єднання на кожного клієнта погано масштабується, рветься проксі і балансувальниками, а дві адреси ускладнюють налаштування. Streamable HTTP прийшов на заміну в ревізії 2025-03-26: одна адреса, кожне повідомлення — окремий запит, потік — лише коли справді потрібен.
💡 Важливе уточнення: застарів транспорт HTTP+SSE, а не сама технологія SSE. Streamable HTTP і далі використовує SSE як формат відповіді, коли серверу треба надіслати кілька повідомлень. Тому фраза «SSE в MCP застарів» — неточна.
Нові сервери на HTTP+SSE робити не варто. Клієнти поки що його підтримують для сумісності, але специфікація позначила його як кандидата на видалення.
📊 Розділ 6. Порівняльна таблиця
STDIO
Streamable HTTP
Хто запускає сервер
клієнт, як дочірній процес
ви, як окремий сервіс
Де працює
на машині користувача
де завгодно: локально, на сервері, в хмарі
Скільки клієнтів
один процес — один клієнт
багато клієнтів одночасно
Хто може підключитися
лише клієнт, який запустив процес
будь-хто, хто дістанеться до URL
Автентифікація
не потрібна — межа безпеки це процес
потрібна для будь-яких непублічних даних (токен, OAuth)
Веб-клієнти (Claude, ChatGPT у браузері)
❌ не підтримують
✅ за публічною адресою
Масштабування
не потрібне
як у звичайного вебсервісу; з 2026-07-28 — без «липких» сесій
Оновлення
на кожній машині окремо
один деплой — оновлено для всіх
Налагодження
JSON-рядки в stdin вручну; логи — лише у файл або stderr
curl, MCP Inspector, логи сервера
Якщо сервер упав
клієнт сам не перезапускає — до перезапуску клієнта
HTTP-клієнти перепідключаються самі
Типова «тиха» помилка
зайве в stdout — клієнт мовчки відкидає сервер
проксі віддає HTML замість JSON
🧭 Розділ 7. Вибір за сценарієм
Сценарій
Транспорт
Чому
Інструмент для себе в IDE: доступ до локальних файлів, локальної бази, git
STDIO
дані на вашій машині, ззовні нічого не відкривається, авторизація не потрібна
Публічні дані для будь-яких AI-клієнтів (мій блог)
Streamable HTTP, відкритий + ліміти
веб-клієнти підтримують лише віддалені сервери; дані публічні, тому авторизація не потрібна, а ліміти — потрібні
Особистий інструмент з приватними даними, який живе на сервері (наприклад, мій SEO-агент з даними Search Console)
Streamable HTTP + токен або окремий легкий STDIO-застосунок
якщо дані вже на сервері — HTTP з токеном простіше; STDIO — лише якщо можна винести логіку в легкий окремий процес
Спільний сервер команди
Streamable HTTP + автентифікація
один деплой замість встановлення в кожного; права можна розділити
SaaS: клієнти підключають ваш продукт до своїх AI (наприклад, пошук по корпоративних документах)
Streamable HTTP + OAuth
корпоративні користувачі підключаються через веб-клієнти, токени вручну ніхто вписувати не буде
Сервер у контейнері поруч з агентом (Docker, Kubernetes)
Streamable HTTP
окремий контейнер — окремий процес, клієнт не може запустити його як дочірній
Коротке правило: якщо сервер має доступ до чогось на машині користувача — STDIO. Якщо до чогось на сервері чи в хмарі або ним користуються кілька людей — Streamable HTTP.
🛡 Розділ 8. Транспорт ≠ безпека
Поширена помилка — вважати, що вибір транспорту сам по собі щось захищає. Кожен транспорт має свою модель загроз.
STDIO: безпечний, поки ви довіряєте тому, що запускаєте
У STDIO немає порту і мережі, тому немає й мережевих атак. Але є інший ризик: STDIO-сервер працює з правами вашого користувача. Сторонній сервер, встановлений «однією командою», може читати ваші файли і секрети зі змінних оточення. Межа безпеки тут — те, що ви дозволяєте клієнту запускати.
Streamable HTTP: обов'язкові заходи
Перевірка Origin. Специфікація вимагає перевіряти заголовок Origin і відповідати 403 на недозволені. Це захист від DNS rebinding: без нього шкідливий сайт у браузері користувача може звертатися до MCP-сервера на його localhost.
Локальний сервер — лише на 127.0.0.1. Якщо запускаєте HTTP-сервер у себе на комп'ютері, прив'язуйте його до localhost, а не до всіх мережевих інтерфейсів — інакше він доступний усім у вашій Wi-Fi-мережі.
TLS для будь-якого сервера в інтернеті.
Автентифікація для будь-яких непублічних даних — від простого токена до OAuth. .
Ліміти запитів — навіть для відкритого сервера з публічними даними, бо кожен виклик може коштувати грошей.
👤 Чесно про мій сервер: ліміти по IP, добовий запобіжник і HTTPS через Cloudflare в мене є. А от перевірки Origin поки немає — ризик для публічного сервера з публічними даними невеликий (DNS rebinding небезпечний насамперед для серверів на localhost), але специфікація вимагає її для всіх, тож це наступний пункт у моєму списку. Як я робив ліміти і чому обрав саме такі — у статті MCP-сервер для сайту.
✅ Розділ 9. Перехід зі STDIO на HTTP у Spring AI: чекліст
Інструменти (@McpTool-методи) переносити не треба — змінюється лише «обв'язка». Що перевірити:
1. Залежності. Стартер STDIO замінюється на вебстартер:
<!-- було -->
<artifactId>spring-ai-starter-mcp-server</artifactId>
<!-- стало -->
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
2. Конфігурація.
# було (STDIO)
spring.ai.mcp.server.stdio=true
spring.main.web-application-type=none
spring.main.banner-mode=off
logging.threshold.console=OFF
# стало (HTTP)
spring.ai.mcp.server.protocol=STATELESS # або STREAMABLE, якщо потрібні сесії
# endpoint за замовчуванням — /mcp
Банер і логи в консоль тепер нічого не ламають — їх можна повернути.
3. Безпека вебрівня. Якщо в проєкті є Spring Security — окремий ланцюжок правил для /mcp: доступ за вашою політикою, CSRF вимкнений (клієнт його не надішле), сесії не створюються. Якщо адмінка захищена перехоплювачем — перевірте, що він не зачіпає /mcp.
4. Автентифікація. У STDIO її не було — тепер вирішіть: відкритий доступ (лише публічні дані), токен чи OAuth. Див. таблицю сценаріїв.
5. Ліміти і реальний IP. Ліміт запитів на IP і загальний; за проксі (Cloudflare, балансувальник) брати IP із заголовків проксі, а не з адреси з'єднання.
6. Перевірка Origin — відповідати 403 на недозволені значення.
7. Проксі і деплой. Для /mcp — без кешування і без перевірок «чи ви людина» на проксі; якщо використовуєте SSE-відповіді — вимкнена буферизація. На Railway, Docker чи будь-якому хостингу — звичайний деплой вебзастосунку.
8. Що перевірити після деплою:
☐ curl з tools/list на продакшн-адресу ззовні повертає JSON, а не HTML-сторінку проксі;
☐ у списку лише ті інструменти, які ви свідомо публікуєте;
☐ підключення з реального клієнта (Claude Code, OpenCode, Cursor) і виклик інструмента;
☐ у логах видно, хто підключився і з якою версією протоколу;
☐ ліміт спрацьовує (HTTP 429), а закриті частини застосунку лишаються закритими.
❓ Часті питання (FAQ)
Який транспорт MCP обрати для початку?
Якщо інструмент для вас самих і працює з локальними даними — STDIO: найпростіше, без мережі і авторизації. Якщо сервером мають користуватися інші люди або веб-клієнти Claude і ChatGPT — одразу Streamable HTTP.
Чи підтримує веб-версія Claude або ChatGPT STDIO-сервери?
Ні. Веб-клієнти підключаються до MCP-серверів зі своїх серверів у хмарі, тому їм потрібна публічна адреса Streamable HTTP. STDIO працює лише з клієнтами на вашому комп'ютері — Claude Code, Cursor, десктопними застосунками.
Чи потрібні ще сесії в Streamable HTTP?
У ревізії специфікації 2026-07-28 протокольні сесії прибрали: кожен запит самодостатній. Сервери на старіших версіях протоколу ще можуть їх використовувати, але для нових серверів stateless — стандарт.
SSE в MCP більше не використовується?
Використовується. Застарів окремий транспорт HTTP+SSE з двома адресами, а SSE як формат відповіді лишився всередині Streamable HTTP — для запитів, які мають надсилати проміжні повідомлення.
Чому мій STDIO-сервер не з'являється в клієнті, хоча процес запущений?
Найчастіше — у stdout потрапляє щось, крім JSON: банер Spring Boot, логи, службові повідомлення бібліотек. Клієнт не може розібрати відповідь і мовчки відкидає сервер, не показуючи помилки. Запустіть сервер вручну, передайте йому JSON-рядок initialize через stdin і перевірте, що у відповідь приходить рівно один рядок JSON. Друга часта причина — клієнт досі працює зі старим процесом сервера після перезбирання: перезапустіть клієнт повністю.
Чи можна один сервер зробити доступним і через STDIO, і через HTTP?
Так: протокол і інструменти однакові, змінюється лише транспорт. Часто роблять так: STDIO для локальної розробки, HTTP — для спільного використання.
✅ Висновки
Вибір транспорту MCP — це не питання смаку, а питання розгортання. STDIO — коли сервер працює з даними на машині користувача і ним користується одна людина. Streamable HTTP — коли сервер живе в мережі, ним користуються кілька людей або веб-клієнти. З ревізією 2026-07-28 Streamable HTTP став простішим: без сесій і рукостискань, кожен запит самодостатній.
Для STDIO найважливіше — чистий stdout: у моєму експерименті один увімкнений банер Spring Boot тихо прибрав сервер із клієнта, а модель замінила інструмент власною неточною відповіддю. Тестуйте STDIO-сервер вручну, до підключення клієнта.
І головне: транспорт не робить сервер безпечним. STDIO вимагає довіри до того, що ви запускаєте; HTTP — перевірки Origin, автентифікації для непублічних даних і лімітів. Як усе це виглядає на реальному відкритому сервері — у статті MCP-сервер для сайту, а чим MCP відрізняється від звичайного API і function calling — у статті «MCP vs API vs Function Calling».