Коли я робив MCP-сервер для свого блогу, найбільше часу пішло не на сам пошук, а на дрібниці навколо нього: клієнт не підключався через версію протоколу, локальні тести давали зовсім інші цифри, ніж продакшн, а одна необережна конфігурація могла викласти назовні приватні дані аналітики. У цій статті — всі такі моменти. Для кожного пояснюю не лише «як виправити», а й чому так відбувається, як вирішив я і які є альтернативи. Що таке MCP і як він влаштований — у основній статті про MCP-сервер для сайту.
⚡ Коротко
✅ Більшість проблем — не в коді пошуку, а навколо нього: версії протоколу, проксі, безпека фреймворку за замовчуванням, різниця між локальним середовищем і продакшном.
✅ Найнебезпечніша помилка мовчазна: фреймворк може сам опублікувати приватні інструменти — без жодного попередження.
✅ Модель — теж користувач: їй потрібні зрозумілі описи, статуси замість винятків і відповіді, які не з'їдають її контекст.
✅ Чесно позначаю: 👤 — на це я наступив сам, 🛡 — це я передбачив заздалегідь.
🎯 Ви отримаєте: список проблем, які варто перевірити до запуску, пояснення, звідки вони беруться, і чек-лист перед деплоєм.
👇 Нижче — 19 помилок у п'яти групах: підключення, безпека, ліміти, якість пошуку і «модель не так розуміє сервер».
📚 Зміст статті
📌 Розділ 1. Підключення: клієнт не бачить сервер
📌 Розділ 2. Безпека: що випадково виходить назовні
📌 Розділ 3. Ліміти і гроші
📌 Розділ 4. Якість пошуку
📌 Розділ 5. Модель не так розуміє сервер
✅ Чек-лист перед запуском MCP-сервера
❓ Часті питання (FAQ)
✅ Висновки
🔌 Розділ 1. Підключення: клієнт не бачить сервер
Найнеприємніший тип помилок: сервер працює, curl відповідає, а клієнт не підключається. Майже завжди причина — у розбіжності очікувань між клієнтом і сервером.
Симптом. MCP Inspector при підключенні показав помилку:
Unrecognized field "form" (class io.modelcontextprotocol.spec.McpSchema$ClientCapabilities$Elicitation),
not marked as ignorable (0 known properties: ])
Чому так. MCP розвивається: у специфікації кілька версій (2025-06-18, 2025-11-25 і новіші). При підключенні клієнт повідомляє серверу свої можливості, і свіжий клієнт додає туди нове поле capabilities.elicitation.form. MCP SDK на моєму сервері знав лише версію 2025-06-18 і розбирав JSON суворо: незнайоме поле — помилка, з'єднання розірвано. Це не проблема лише Inspector: така сама помилка траплялася в Claude Code при підключенні до серверів на старому Java SDK. Тобто частина реальних клієнтів просто не змогла б підключитися.
Як я виправив. Поставив перед SDK невеликий фільтр, який у запиті initialize замінює можливості клієнта на порожній об'єкт {}. Це безпечно саме для мого сервера: він лише віддає інструменти і не використовує жодну з можливостей клієнта — ні elicitation (запит даних у користувача), ні sampling, ні roots. Решта запитів проходить без змін. Заодно фільтр пише в лог, який клієнт підключився і на якій версії протоколу, — корисно для налагодження.
Альтернативи.
- Оновити SDK або фреймворк — правильне рішення. У моєму випадку це Spring AI 2.x із підтримкою 2025-11-25, але він вимагає Spring Boot 4, тобто міграції всього сайту. Тому фільтр — тимчасовий, до міграції.
- Старіша версія Inspector — прибирає помилку лише в Inspector. Справжні клієнти все одно не підключаться.
- Якщо ваш сервер використовує можливості клієнта (наприклад, просить у користувача підтвердження через elicitation), вирізати їх не можна — лише оновлювати SDK.
Як не повторити. Тестуйте найсвіжішими версіями клієнтів, а не тими, що встановлені пів року тому, і логуйте initialize: версію протоколу і назву клієнта.
1.2. 👤 MCP Inspector не підключається: сервер додали як stdio замість Streamable HTTP
Симптом. Форма додавання сервера в Inspector просить Command, Arguments і Environment, а поля для URL немає.
Чому так. У MCP два принципово різні способи підключення. stdio — клієнт сам запускає сервер як локальну програму і спілкується з нею через стандартний ввід/вивід (так працюють, наприклад, сервери для доступу до файлів на вашому комп'ютері). Streamable HTTP — сервер уже працює десь у мережі, клієнт ходить до нього за URL. Я клонував приклад-шаблон, а він виявився stdio.
Як виправив. У полі Transport обрав Streamable HTTP — замість Command з'явилося поле URL.
Як не повторити. Просте правило: якщо у вашого сервера є адреса (http://…/mcp), це завжди Streamable HTTP. stdio — лише для програм, які клієнт запускає сам.
1.3. 🛡 localhost не працює в Claude і ChatGPT
Чому так. Веб-версії Claude і ChatGPT підключаються до MCP-сервера зі своїх серверів у хмарі, а не з вашого браузера. Для них localhost — це їхній власний сервер, а не ваш ноутбук.
Як роблю я. Локально перевіряю через curl і MCP Inspector, а також через клієнти, які працюють на моєму комп'ютері (Claude Code, OpenCode, Cursor) — вони бачать localhost. Веб-клієнти — лише після деплою.
Альтернатива — тунель (ngrok, Cloudflare Tunnel): він дає тимчасову публічну адресу для локального сервера. Зручно, але пам'ятайте: якщо сервер без авторизації, до нього на цей час може підключитися будь-хто, хто дізнається адресу. Для сервера з публічними даними це не страшно, для приватних — небезпечно.
1.4. 🛡 Замість JSON приходить HTML-сторінка: захист від ботів на проксі
Симптом. Локально все працює, а на продакшні клієнт не підключається; curl замість JSON повертає HTML-сторінку з перевіркою «чи ви людина».
Чому так. Якщо сайт стоїть за Cloudflare чи іншим проксі із захистом від ботів, запити від MCP-клієнтів виглядають саме як боти: це сервери, а не браузери. Проксі показує їм сторінку-перевірку, яку програма пройти не може. Друга можлива проблема — кешування: проксі може віддати збережену відповідь замість справжньої.
Як виправити. Для адреси /mcp — правило-виняток: без кешування і без перевірки на ботів. Захист від зловживань при цьому лишається на рівні застосунку (ліміти — у розділі 3).
Як не повторити. Після кожного деплою — curl на продакшн-адресу ззовні, не з самого сервера.
1.5. 🛡 403 або редирект на логін: безпека фреймворку за замовчуванням
Чому так. Фреймворки безпеки (Spring Security, Django, Laravel та інші) за замовчуванням захищають POST-запити CSRF-токеном, перенаправляють неавторизованих на сторінку входу і створюють сесію на кожен запит. MCP-клієнт CSRF-токен не надішле, логінитися не буде, а сесії від ботів лише засмічують пам'ять.
Як у мене. У моєму застосунку адмінка захищена не фреймворком безпеки, а власним перехоплювачем запитів на /admin/**. Тому я перевірив головне: що перехоплювач зареєстрований саме на /admin/**, а не на всі адреси з винятками. Інакше /mcp отримував би редирект на логін.
Якщо у вас Spring Security (або аналог) — для /mcp окремий ланцюжок правил: доступ усім, CSRF вимкнений, сесії не створюються. Додавати /mcp в основну конфігурацію сайту теж можна, але тоді ви змішуєте правила сайту й API, і сесії все одно створюватимуться.
⚠️ Чого не робити: не додавайте фреймворк безпеки в проєкт «заради MCP», якщо його там не було. Spring Security за замовчуванням закриє формою входу весь сайт, включно з блогом.
Як перевірити: curl з initialize повертає 200 без заголовка Set-Cookie, а адмінка при цьому лишається закритою.
🔐 Розділ 2. Безпека: що випадково виходить назовні
Ці помилки не дають жодних симптомів. Сервер працює, клієнти задоволені — а назовні тим часом віддається те, чого віддавати не можна.
Чому так. MCP-стартери фреймворків зручні: позначили метод анотацією — і він уже інструмент. Але так само зручно вони збирають усі інструменти, які знайдуть у застосунку. У моєму застосунку, крім блогу, працює SEO-агент з інструментами для даних Search Console і Google Analytics. Достатньо одного рядка конфігурації, щоб ці інструменти потрапили в загальний реєстр, — і будь-хто з інтернету міг би питати в Claude про мої кліки, позиції і трафік. Без помилок і попереджень.
Як я захистився.
- MCP-інструменти — лише через окрему анотацію і лише в окремому пакеті. Інструменти SEO-агента підключаються до свого агента напряму і ніколи не реєструються як загальні.
- Першу перевірку
tools/list я зробив ще до того, як написав хоч один інструмент. Відповідь була порожньою — значить, стартер нічого зайвого не підхопив.
- Після кожного деплою — той самий
tools/list: у списку мають бути рівно три інструменти блогу.
Альтернативи. Найнадійніше — винести MCP в окремий сервіс з доступом лише на читання до потрібних таблиць: приватних інструментів там фізично немає. Компроміс — явний білий список назв інструментів у конфігурації.
2.2. 🛡 Чернетки й видалені сторінки у векторній базі
Чому так. Векторна база живе своїм життям: стаття могла бути проіндексована як чернетка, знята з публікації або видалена, а її шматки (чанки) лишилися. Пошук по векторах про статус публікації нічого не знає.
Як роблю я. Кожен пошуковий запит з'єднує знайдені чанки з таблицею статей і пропускає лише ті, де статус «опубліковано» і дата публікації вже настала. Навіть якщо у векторній базі лишилося сміття, назовні воно не потрапить.
Альтернатива — видаляти чанки з векторної бази при знятті статті з публікації. Це правильно, але покладається на те, що індексатор ніколи не помиляється. Фільтр на рівні запиту — страховка, яка працює завжди.
2.3. 🛡 Інструменти без позначки «лише читання»
Чому так. MCP дозволяє позначити інструмент підказками: чи він лише читає (readOnlyHint), чи може щось видалити (destructiveHint). Клієнти по-різному реагують на відсутність позначок — ChatGPT, наприклад, вважає інструмент без позначки «лише читання» таким, що може змінювати дані, і поводиться з ним обережніше.
Як роблю я. Усі три інструменти позначені: readOnlyHint = true, destructiveHint = false, idempotentHint = true.
💡 Важливо: підказка — це обіцянка клієнту, а не захист. Якщо інструмент насправді щось змінює, позначка «лише читання» — це обман, і відповідальність на вас.
💸 Розділ 3. Ліміти і гроші
Відкритий сервер, де кожен виклик пошуку коштує грошей (ембединг запиту), без лімітів — це рахунок без стелі. Тому тут три лінії захисту: ліміт на IP, загальний добовий ліміт і ліміт витрат у провайдера. І одне непросте рішення — що робити, коли відмовляє сам механізм лімітів.
3.1. 🛡 Ліміт рахує IP проксі замість реального клієнта
Чому так. Коли сайт стоїть за Cloudflare, усі запити приходять на сервер з IP-адрес Cloudflare. Якщо рахувати ліміт за адресою з'єднання, «один клієнт» — це весь Cloudflare. Результат: при першому ж активному користувачі ліміт вичерпується для всіх одразу.
Як роблю я. Беру IP по ланцюжку: заголовок CF-Connecting-IP (його ставить Cloudflare) → перший IP із X-Forwarded-For → адреса з'єднання. Ліміт — 30 запитів на хвилину з одного IP; при перевищенні сервер відповідає HTTP 429 із заголовком Retry-After.
Альтернатива — ліміти на рівні самого проксі (правила rate limiting у Cloudflare). Це знімає навантаження із застосунку, але на безкоштовних планах можливості обмежені, і логіка розмазується між двома місцями. Я лишив ліміти в застосунку, де їх легко змінювати і логувати.
3.2. 🛡 Підроблений CF-Connecting-IP при зверненні напряму до сервера
Чому так. Заголовку CF-Connecting-IP можна довіряти лише тоді, коли запит справді пройшов через Cloudflare. Але у хостингу зазвичай є власна адреса сервера (у моєму випадку — технічний домен Railway), доступна напряму. Зловмисник може звернутися туди і в кожному запиті підставити новий «IP» — ліміт по IP перестає працювати.
Як у мене зараз. Чесно: прямий доступ до сервера я ще не закрив. Від витрат у цьому сценарії захищає загальний добовий ліміт (наступний пункт): він рахує всі запити разом, тому підроблені IP його не обходять. А останньою лінією лишається ліміт витрат у провайдера ембедингів.
Як правильно: приймати запити лише з IP-адрес Cloudflare, використовувати перевірку справжності запитів від проксі (authenticated origin pulls) або не публікувати технічну адресу сервера взагалі.
3.3. 🛡 Немає добового запобіжника
Чому так. Ліміт на IP захищає від одного настирливого клієнта, але не від тисячі різних: ботнет чи просто популярність сервера дадуть стільки викликів, скільки IP-адрес.
Як роблю я. Окремий лічильник на весь сервер — 5000 викликів на добу за UTC. Коли його перевищено, сервер відповідає «добовий ліміт вичерпано» і пошук не виконується, тобто ембединги не оплачуються.
А що, якщо впаде сам Redis із лічильниками?
Обидва лічильники — і по IP, і добовий — живуть у Redis. Якщо він недоступний, доводиться обирати між двома стратегіями, і обидві мають ціну:
| Fail-open (пропускати) | Fail-closed (відмовляти) |
| Що відбувається | запити обробляються без лімітів | сервер відповідає «тимчасово недоступний» |
| Що зберігає | доступність сервісу | бюджет |
| Ризик | поки Redis лежить, лімітів немає зовсім — ні по IP, ні добового | проблема з кешем валить увесь MCP, навіть коли зловживань немає |
| Коли підходить | публічні дані, дешеві операції, короткі збої | платний API, дорогі AI-операції, генерація через LLM за ваш рахунок |
Я обрав fail-open — і чесно про ціну: поки Redis недоступний, не працює жоден ліміт застосунку. Для мого блогу це прийнятно: один виклик — це лише ембединг запиту (частки цента), Redis падає рідко і ненадовго, а недоступний MCP заради теоретичного зловживання — гірший сценарій. Якби інструмент генерував відповіді через LLM, я б обрав fail-closed без вагань.
Що робить fail-open безпечнішим:
- Ліміт витрат у провайдера ембедингів — обов'язкова страховка, яка не залежить ні від Redis, ні від вашого коду. Навіть якщо всі ліміти застосунку відмовлять, рахунок не вийде за встановлену суму. Найкраще — окремий API-ключ саме для MCP зі своїм лімітом: тоді зловживання через MCP не зачепить інші частини сайту, які користуються тим самим провайдером.
- Сповіщення про бюджет — щоб дізнатися про проблему раніше, ніж про неї розповість рахунок.
- Сповіщення про сам збій Redis — щоб «працюємо без лімітів» тривало хвилини, а не дні.
- Лічильник у пам'яті як запасний варіант. Коли Redis недоступний, застосунок переходить на простий лічильник у власній пам'яті. Він не синхронізується між інстансами і обнуляється при перезапуску, але дає хоч якусь стелю замість жодної. Це компроміс між двома стратегіями: сервіс доступний, але й без повністю відкритого крана.
💡 Загальне правило: обирайте стратегію за ціною одного виклику. Дешевий виклик і публічні дані — fail-open плюс ліміт у провайдера. Дорогий виклик (LLM, платні API, дії з даними) — fail-closed: краще кілька хвилин недоступності, ніж непередбачений рахунок.
🔍 Розділ 4. Якість пошуку
4.1. 👤 Connection refused :11434 — локальна модель ембедингів не запущена
Симптом. Тестовий пошук локально впав з помилкою 500:
I/O error on POST request for "http://localhost:11434/api/embed": Connection refused
Чому так. Пошук спершу перетворює запит на вектор, і для цього потрібна модель ембедингів. Локально в мене це Ollama (порт 11434), на продакшні — хмарна модель. Ollama просто не була запущена. Банально, але показово: пошук — це не лише база даних, а й зовнішній сервіс, який може бути недоступним.
Як виправив. Запустив Ollama. А головне — в інструменті MCP така помилка перехоплюється і перетворюється на статус unavailable з повідомленням «пошук тимчасово недоступний», а не на незрозумілий збій для моделі (докладніше — у пункті 5.1).
⚠️ Пов'язана пастка: модель ембедингів для пошуку має бути тією самою, якою індексувалась база. Інша модель — інша розмірність векторів (у мене 768 локально і 1536 на продакшні) і помилка «different vector dimensions», або, гірше, тихо неправильні результати.
4.2. 👤 Локальні й продакшн-ембединги дають різний score — поріг не працює
Чому так. Щоб не віддавати нерелевантне, пошук відсікає результати нижче порогу схожості. Але «схожість 0.5» — не універсальна величина: кожна модель ембедингів має свою шкалу. Локальна модель видавала для тих самих запитів схожість 0.7+, а на продакшн-моделі релевантні результати — близько 0.6. Поріг, підібраний на одній моделі, на іншій або все пропускає, або все відсікає.
Як роблю я. Поріг винесений у конфігурацію: локально — 0 (дивлюся лише на логіку), на продакшні — калібрую. Калібрування просте: кілька запитів трьох типів і їхні цифри в логах. На попередньому проєкті з тією самою продакшн-моделлю вийшло так: стаття точно є — 0.60–0.63, сусідня тема — 0.43–0.49, зовсім не про сайт — 0.33–0.34. Поріг ставлю між «зовсім не про сайт» і «сусідньою темою». Значення задається змінною оточення, тому змінити його можна без нового деплою.
Як не повторити: ніколи не переносьте поріг схожості між моделями і середовищами — калібруйте там, де працює справжня модель.
4.3. 🛡 П'ять фрагментів однієї статті замість п'яти різних статей
Чому так. Довга стаття ділиться на десятки чанків, і найближчі до запиту часто належать одній статті. Наївне «топ-5 найближчих чанків» дає п'ять шматків одного тексту — модель бачить одне джерело замість п'яти.
Як роблю я. Беру 50 найближчих чанків (тут працює векторний індекс), нумерую їх усередині кожної статті, лишаю не більше двох на статтю і групую за статтею. Порядок «спершу 50 найближчих, потім групування» важливий: якщо групувати одразу, база перебиратиме всю таблицю.
Альтернатива — переранжування на різноманітність (MMR): алгоритм штрафує результати, схожі на вже обрані. Гнучкіше, але складніше; для блогу «не більше двох на статтю» дає той самий ефект простіше.
4.4. 👤 Фрагменти починаються з обрізка слова
Симптом. Фрагменти на зразок «ma на слабкому залізі…» чи «тті Топ-10 моделей…».
Чому так. При нарізці тексту на чанки сусідні шматки перекриваються, щоб не втратити контекст на межі, — і межа часто проходить посеред слова.
Як виправив. Якщо фрагмент починається з малої літери, відрізаю все до першого пробілу (але не більше 30 символів, щоб не з'їсти зайве). Кінець обрізаю по межі слова і ставлю «…».
Альтернатива — правильніша, але дорожча: різати текст на чанки по межах речень ще при індексації. Тоді обрізків не буде взагалі, але доведеться переіндексувати всю базу.
4.5. 🛡 Повна стаття у відповіді з'їдає контекст клієнта
Чому так. Стаття — це 5–10 тисяч токенів. Один такий виклик — і помітна частина контекстного вікна моделі зайнята текстом, з якого їй потрібен один абзац. А модель клієнта ще має пам'ятати розмову з користувачем.
Як роблю я. Пошук віддає фрагменти до ~900 символів, не більше двох на статтю. Окремий інструмент get_article віддає не повний текст, а «карту» статті: заголовок, короткий зміст і список розділів. Модель розуміє, що ще є в статті, а за повним текстом людина йде за посиланням.
Компроміс: повний текст зручніший для користувача, фрагменти — для контексту моделі і для кліків на сайт. Для документації, де людина рідко переходить на сайт, повний текст розділу може бути кращим вибором.
🤖 Розділ 5. Модель не так розуміє сервер
Користувач MCP-сервера — модель. Їй, як і людині, потрібні зрозумілі повідомлення.
5.1. 🛡 Винятки замість статусів: «немає даних» і «сервер зламався» виглядають однаково
Чому так. Якщо інструмент кидає виняток, клієнт отримує загальну помилку виклику. Модель не знає, що сталося: статті на цю тему немає, чи сервер тимчасово лежить? Від цього залежить, що вона скаже людині: «на цьому сайті про це не пишуть» чи «спробуйте пізніше».
Як роблю я. Кожна відповідь має поле status:
{"status": "ok", "articles": [...]}
{"status": "not_found", "message": "Нічого не знайдено на webscraft.org за цим запитом."}
{"status": "unavailable", "message": "Пошук тимчасово недоступний. Спробуйте пізніше."}
{"status": "error", "message": "Query is empty."}
Усі винятки перехоплюються всередині інструмента і пишуться в лог, а модель отримує зрозумілий статус.
5.2. 🛡 Розмиті описи: модель не викликає інструмент
Чому так. Модель вирішує, чи викликати інструмент, лише за його назвою, описом і параметрами. Опис «пошук по статтях» нічого не каже про те, яких статтях і коли вони корисні. Модель просто відповість із власних знань.
Як роблю я. В описі чотири речі: про що дані (україномовний технічний блог: AI, локальні LLM, Spring Boot, RAG, SEO), коли викликати (питання «як зробити» в цих темах або прохання дати джерело), як формулювати запит (українською — так пошук точніший) і що робити з результатом (це фрагменти — завжди давати посилання).
Як перевірити: подивіться на опис очима клієнта в MCP Inspector, а потім поставте реальному клієнту питання, не згадуючи інструмент, і перевірте в логах сервера, чи був виклик. [ПЕРЕВІРИТИ НА ПРОДІ: чи викликає Claude пошук без прямої підказки]
5.3. 👤 Кирилиця в конфігурації ламає instructions
Чому так. MCP-сервер при підключенні віддає клієнту інструкцію — короткий текст про те, що це за сервер. Я задаю його в конфігураційному файлі .properties, а цей формат історично читається не в UTF-8. Кирилиця там легко перетворюється на «кракозябри» — я вже наступав на це раніше з іншими налаштуваннями.
Як роблю я. Інструкцію і описи інструментів пишу англійською: моделі однаково добре розуміють обидві мови, а проблема кодування зникає.
Альтернативи: YAML-конфігурація (читається в UTF-8), змінні оточення або текст прямо в коді.
✅ Чек-лист перед запуском MCP-сервера
- ☐
tools/list повертає лише ті інструменти, які ви свідомо публікуєте.
- ☐
initialize на продакшн-адресу ззовні повертає JSON, а не HTML-сторінку проксі.
- ☐ Відповідь без
Set-Cookie; адмінка й інші закриті розділи лишаються закритими.
- ☐ Підключення перевірене найсвіжішими версіями клієнтів (Inspector, Claude Code, веб-клієнти).
- ☐ Ліміт рахує реальний IP клієнта, а не IP проксі; при перевищенні — 429 з
Retry-After.
- ☐ Є загальний добовий ліміт на витрати.
- ☐ Назовні потрапляють лише опубліковані дані (фільтр за статусом у самому запиті).
- ☐ Інструменти, що лише читають, позначені
readOnlyHint.
- ☐ Поріг релевантності відкалібровано на продакшн-моделі ембедингів.
- ☐ Фрагменти короткі, без обрізків слів, не більше 1–2 на джерело.
- ☐ Інструменти повертають статуси (
ok / not_found / unavailable), а не винятки.
- ☐ Описи інструментів відповідають на питання «про що», «коли викликати» і «що робити з результатом».
- ☐ У логах видно, хто підключився, який запит сформулювала модель і скільки тривав виклик.
❓ Часті питання (FAQ)
Чому клієнт не бачить інструменти мого MCP-сервера?
Найчастіше — одна з трьох причин: клієнт і сервер на різних версіях протоколу (помилка в initialize), запит перехоплює проксі чи фреймворк безпеки (HTML або редирект замість JSON), або інструменти просто не зареєструвалися. Перевірте tools/list через curl — це одразу звузить пошук.
Як дізнатися, яку версію протоколу використовує клієнт?
Клієнт передає її в запиті initialize у полі protocolVersion, разом із назвою і версією клієнта в clientInfo. Залогуйте ці поля — і ви завжди знатимете, хто і як підключається.
Чи обов'язковий Redis для лімітів?
Ні. Для одного інстансу сервера достатньо лічильника в пам'яті. Redis потрібен, коли інстансів кілька або ліміти мають переживати перезапуск.
Як перевірити, що назовні не виходить нічого зайвого?
Викликайте tools/list після кожного деплою і порівнюйте зі списком, який ви свідомо публікуєте. Найкраще — автоматично, у перевірці після деплою.
Чи можна перевірити сервер у ChatGPT чи Claude без деплою?
Лише через тунель (ngrok, Cloudflare Tunnel), який дає локальному серверу тимчасову публічну адресу. Для локальних тестів простіше Inspector і клієнти на вашому комп'ютері — Claude Code, Cursor, OpenCode.
✅ Висновки
Сам пошук у MCP-сервері — найпростіша частина. Більшість помилок живе на межах: між версіями протоколу, між сервером і проксі, між локальним середовищем і продакшном, між вашим кодом і «зручною» автоматикою фреймворку. Тому головне правило — перевіряти не код, а те, що бачить клієнт: initialize, tools/list і реальні виклики ззовні, після кожного деплою.
Як влаштований MCP і які архітектурні рішення варто прийняти до того, як писати код, — у основній статті про MCP-сервер для сайту. А якщо потрібен MCP для вашого сайту чи документів компанії — напишіть нам.