Мій MCP-сервер раніше просто переставав працювати. Ніяких дампів пам'яті. Ніяких stack trace у логах. Клієнти підключалися без жодних скарг, а через кілька годин усе просто замовкало. Запити зникали, а ШІ-агент на іншому кінці не отримував нічого, крім порожнечі.
Це прикро поширена історія в екосистемі Model Context Protocol (MCP). Протокол визначає, як ШІ-агенти знаходять та викликають зовнішні інструменти, але специфікація передбачає, що ви будете обробляти помилки самостійно. Більшість туторіалів та початкових реалізацій пропускають цю частину. Вони зосереджуються на «щасливому шляху» (happy path): анотуйте функцію, відкрийте її через сервер і поверніть чистий результат. Вони рідко показують, що стається, коли у вашого зовнішнього API виникає мережевий збій або коли модель галюцинує назву параметра і надсилає некоректні дані. Результатом є крихкий сервер, який виглядає справним, але насправді вже кілька годин як «мертвий».
Чому порожні відповіді гірші за збої
Коли в обробнику інструментів MCP проскакує необроблений виняток, транспортний рівень часто поглинає його. Процес сервера залишається живим, сокет відкритим, але клієнт отримує порожню відповідь. Це небезпечніше за явний збій, оскільки ваша система моніторингу може цього не помітити. Процес усе ще працює. Порт усе ще прослуховується. Проте кожен виклик інструменту нічого не повертає.
ШІ-модель не сприймає тишу як помилку. Вона сприймає тишу як успішний виклик, який не повернув жодних даних. Така порожня відповідь привчає модель до імпровізації. Вона починає галюцинувати факти, щоб заповнити прогалину, або входить у цикл повторних спроб одного й того самого зламаного виклику. Не можна дозволяти таким дрібницям, як тимчасовий таймаут мережі або невірний аргумент інструменту, спричиняти таку поведінку.
Патерн «Обертка» (Wrapper Pattern): три лінії захисту
Я виправив це, обгорнувши кожен обробник інструментів у тонкий шар відновлення помилок. Обертка не намагається передбачити кожен можливий збій. Вона класифікує їх і відповідає відповідним чином.
ConnectionError та TimeoutError
Вони виникають, коли ваш сервер взаємодіє із зовнішнім API, а мережа працює нестабільно. Інстинктивне рішення — перезапустити весь процес MCP-сервера. Не робіть цього. Перезапуск розриває активні з'єднання клієнтів, очищує будь-який стан у пам'яті та змушує систему пройти повну реініціалізацію. Замість цього перехоплюйте помилку з'єднання та перепідключайте лише транспортний рівень або HTTP-клієнт, який використовує ваш інструмент. Сервер залишатиметься «теплим» і готовим до наступного запиту негайно.
ValueError
Це те, що ви бачите, коли ШІ-клієнт надсилає некоректні аргументи. Можливо, модель вигадала параметр, передала рядок там, де потрібне ціле число, або забула обов'язкове поле. Якщо ви дозволите цій помилці просунутися далі без обробки, клієнт отримає або збій, або порожню відповідь. Перехопіть її всередині обгортки, а потім сформулюйте чітке, конкретне повідомлення, яке точно пояснить моделі, що пішло не так. Поясніть, який параметр не пройшов перевірку і що саме очікувалося. Більшість сучасних ШІ-моделей прочитають це повідомлення і самостійно виправлять помилку вже під час наступного кроку. Нечітка помилка марнує цикл міркувань. Точна помилка миттєво вирішує проблему.
Загальні винятки (General Exceptions)
Створіть страховочну сітку. Якщо помилка не підпадає під вищезазначені категорії, залогуйте деталі для себе та поверніть клієнту чисту, загальну відповідь про помилку. Це не дозволить одному дивному граничному випадку зіпсувати сесію для всіх. Сервер виживає, клієнт отримує сигнал про те, що щось пішло не так, а ви зберігаєте достатньо контексту в логах для подальшого налагодження.
Прапорець isError не підлягає обговоренню
Ось деталь, яка насправді визначає, чи спрацює ваше виправлення. Відповіді MCP містять булеве поле isError. Якщо виникає виняток і ви повертаєте повідомлення про помилку, не встановивши isError у значення true, клієнт сприйматиме цей текст помилки як успішний результат роботи інструменту.
Уявіть, що ваш зовнішній API досяг ліміту запитів (rate limit). Ви перехоплюєте виняток і повертаєте рядок "API rate limit exceeded", але залишаєте isError як false. Клієнт передає цей рядок у контекстне вікно моделі так, ніби це реальний результат роботи інструменту. Потім модель намагається міркувати над цим текстом, наче це дані. Вона може процитувати помилку в резюме або, що ще гірше, почати галюцинувати зв'язки між цим текстом помилки та іншими фактами. Ви перетворили тимчасовий збій інфраструктури на джерело дезінформації.
Завжди встановлюйте isError у значення true, коли повертаєте payload з помилкою. Це дає клієнту чіткий сигнал про те, що виклик інструмента завершився невдачею, що дозволяє моделі вирішити, чи варто повторити спробу, чи попросити уточнення, чи спробувати зовсім інший інструмент.
Знайте, що перехоплювати, а що припиняти
Не огортайте весь свій сервер у сліпий try-catch, який поглинає все підряд. Деякі помилки означають, що сервер має негайно зупинитися. Якщо під час запуску відсутня необхідна змінна середовища або файл конфігурації пошкоджено, жодне перехоплення на рівні запиту не допоможе. Створіть окремий клас винятків для таких фатальних помилок і дозвольте їм зупинити процес.
Правило просте. Якщо помилка є тимчасовою або стосується лише одного запиту, перехопіть її та відновіть роботу. Якщо ж помилка означає, що кожен наступний запит гарантовано завершиться невдачею, дозвольте серверу «впасти» гучно. Швидкий збій під час запуску набагато кращий за сервер, який днями працює у зламаному стані.
Додайте спостережуваність (observability) до того, як вона вам знадобиться
Щойно ви впровадите обгортку (wrapper), поєднайте її зі структурованим логуванням. Логуйте кожен виклик інструмента та його результат у форматі JSON. Вказуйте назву інструмента, сирі аргументи, затримку (latency), а також те, чи був виклик успішним, чи завершився помилкою, чи було здійснено повторну спробу.
Така дисципліна швидко окупиться. Коли ви помітите сплеск помилок, ви зможете відфільтрувати їх за інструментом і за лічені хвилини виявити закономірності. Можливо, певний зовнішній API починає видавати тайм-аути в один і той самий час щодня, що вказує на заплановане технічне обслуговування, про яке ви не знали. Можливо, один інструмент постійно отримує некоректні аргументи, що виявляє недолік у промпт-інжинірингу на попередньому етапі. Звичайні текстові логи, занурені в стек-трейси (stack traces), роблять таку детективну роботу болісною. Структурований JSON робить її тривіальною.
Результат у продакшені
Протягом останніх трьох тижнів я використовував цей патерн обгортки на двох production MCP-серверах. За цей час я не зафіксував жодного прихованого збою. До додавання обгортки в мене щодня траплялася приблизно одна незрозуміла помилка. Цей патерн не є складним, але його вплив величезний, оскільки він відокремлює допустимий шум від реальних проблем.
Приховані збої коштують дорожче, ніж падіння системи. Падіння активує вашу систему сповіщень. Тиша ж просто підриває довіру. Одного дня ваш ШІ-агент повертає корисні дані інструмента, а наступного — починає вигадувати відповіді, тому що сервер перестав відповідати ще кілька годин тому. Патерн обгортки усуває цю прогалину. Він дозволяє серверу працювати під час незначних турбулентностей, надає моделі достатньо контексту для виправлення власних помилок і гарантує, що коли трапиться щось справді фатальне, ви дізнаєтеся про це негайно.
Якщо ви створюєте MCP-інструменти сьогодні, почніть з обгортки та прапорця isError. Все інше — це лише доопрацювання.
