Mój serwer MCP po prostu przestawał działać. Żadnych zrzutów pamięci (crash dump). Żadnych śladów stosu (stack trace) w logach. Klienci łączyli się bez skarg, a po kilku godzinach wszystko milkło. Zapytania znikały, a agent AI po drugiej stronie otrzymywał jedynie pustkę.

To frustrująco częsta historia w ekosystemie Model Context Protocol (MCP). Protokół definiuje, w jaki sposób agenci AI odkrywają i wywołują zewnętrzne narzędzia, ale specyfikacja zakłada, że sam zajmiesz się obsługą błędów. Większość samouczków i podstawowych implementacji pomija ten etap. Skupiają się na tzw. "happy path": anotuj funkcję, udostępnij ją przez serwer i zwróć czysty wynik. Rzadko pokazują jednak, co się dzieje, gdy w zewnętrznym API wystąpi chwilowy problem z siecią lub gdy model wygeneruje halucynację nazwy parametru i wyśle błędne dane wejściowe. Rezultatem jest kruchy serwer, który wygląda na sprawny, ale w rzeczywistości od godzin nie działa.

Dlaczego puste odpowiedzi są gorsze niż awarie

Gdy w obsłudze narzędzia MCP prześlizgnie się nieobsłużony wyjątek, warstwa transportowa często go "połyka". Proces serwera pozostaje aktywny, gniazdo (socket) pozostaje otwarte, ale klient otrzymuje pustą odpowiedź. Jest to bardziej niebezpieczne niż głośna awaria, ponieważ Twój monitoring może tego nie zauważyć. Proces wciąż działa. Port wciąż nasłuchuje. Mimo to każde wywołanie narzędzia zwraca nic.

Model AI nie interpretuje ciszy jako błędu. Interpretuje ją jako udane wywołanie, które nie zwróciło żadnych danych. Taka pusta odpowiedź uczy model improwizacji. Zaczyna on halucynować fakty, aby wypełnić lukę, lub wpada w pętlę ponawiania tego samego błędnego wywołania. Małe problemy, takie jak przejściowe przekroczenie czasu oczekiwania na sieć (timeout) lub nieprawidłowy argument narzędzia, nigdy nie powinny prowadzić do takiego zachowania.

Wzorzec Wrapper: Trzy linie obrony

Rozwiązałem ten problem, owijając każdy handler narzędzia w cienką warstwę odzyskiwania błędów. Wrapper nie próbuje przewidzieć każdej możliwej awarii. Zamiast tego kategoryzuje je i odpowiada stosownie do sytuacji.

ConnectionError i TimeoutError
Pojawiają się, gdy Twój serwer komunikuje się z zewnętrznym API, a sieć staje się niestabilna. Instynktownym rozwiązaniem jest restart całego procesu serwera MCP. Nie rób tego. Restart przerywa aktywne połączenia klientów, czyści stan w pamięci i wymusza pełną reinicjalizację. Zamiast tego przechwyć błąd połączenia i połącz ponownie tylko warstwę transportową lub klienta HTTP, z którego korzysta Twoje narzędzie. Serwer pozostanie "rozgrzany" i natychmiast gotowy do kolejnego zapytania.

ValueError
To sytuacja, gdy klient AI wysyła błędnie sformatowane argumenty. Być może model wymyślił parametr, przekazał ciąg znaków (string) tam, gdzie wymagana była liczba całkowita (integer), lub zapomniał o wymaganym polu. Jeśli pozwolisz temu błędu "wypłynąć" bez obsługi, klient otrzyma albo awarię, albo pustą odpowiedź. Przechwyć go wewnątrz wrappera, a następnie skonstruuj jasną, konkretną wiadomość, która powie modelowi dokładnie, co poszło nie tak. Wyjaśnij, który parametr zawiódł i czego oczekiwano. Większość nowoczesnych modeli AI przeczyta tę wiadomość i skoryguje błąd w następnej turze. Niejasny błąd marnuje cykl rozumowania. Precyzyjny błąd natychmiast rozwiązuje problem.

General Exceptions
Zadbaj o siatkę bezpieczeństwa. Jeśli błąd nie mieści się w powyższych kategoriach, zaloguj szczegóły dla siebie i zwróć klientowi czystą, ogólną odpowiedź o niepowodzeniu. Zapobiega to sytuacji, w której jeden nietypowy przypadek brzegowy (edge case) przerywa sesję wszystkim użytkownikom. Serwer przetrwa, klient otrzyma sygnał, że coś nie zadziałało, a Ty zachowasz wystarczający kontekst w logach, aby móc to później zdebugować.

Flaga isError jest bezdyskusyjna

Oto szczegół, który faktycznie decyduje o tym, czy Twoja poprawka zadziała. Odpowiedzi MCP zawierają pole logiczne isError. Jeśli wystąpi wyjątek, a Ty zwrócisz wiadomość o błędzie bez ustawienia isError na true, klient potraktuje ten tekst błędu jako udany wynik działania narzędzia.

Wyobraź sobie, że Twoje zewnętrzne API osiąga limit zapytań (rate limit). Przechwytujesz wyjątek i zwracasz ciąg znaków "API rate limit exceeded", ale pozostawiasz isError jako false. Klient przekazuje ten ciąg do okna kontekstowego modelu tak, jakby był to rzeczywisty wynik narzędzia. Model próbuje wtedy wyciągnąć wnioski z tego tekstu, jakby były to dane. Może przytoczyć błąd w podsumowaniu, a co gorsza, może zacząć halucynować powiązania między tym tekstem błędu a innymi faktami. Zamieniłeś tym samym chwilowy problem z infrastrukturą w źródło dezinformacji.

Zawsze ustawiaj isError na true, gdy zwracasz ładunek błędu (error payload). Daje to klientowi jasny sygnał, że wywołanie narzędzia zakończyło się niepowodzeniem, co pozwala modelowi zdecydować, czy spróbować ponownie, poprosić o wyjaśnienie, czy spróbować użyć zupełnie innego narzędzia.

Wiedź, co przechwycić, a co ubić

Nie owijaj całego serwera w ślepy blok try-catch, który pochłania wszystko. Niektóre błędy oznaczają, że serwer powinien natychmiast się zatrzymać. Jeśli podczas uruchamiania brakuje wymaganej zmiennej środowiskowej lub plik konfiguracyjny jest uszkodzony, żadne przechwytywanie na poziomie żądania nie pomoże. Utwórz dedykowaną klasę wyjątków dla takich błędów krytycznych i pozwól im przerwać działanie procesu.

Zasada jest prosta. Jeśli błąd jest tymczasowy lub dotyczy tylko pojedynczego żądania, przechwyć go i odzyskaj sprawność. Jeśli błąd oznacza, że każde kolejne żądanie na pewno zakończy się niepowodzeniem, pozwól serwerowi „umrzeć głośno”. Szybka awaria podczas uruchamiania jest nieskończenie lepsza niż serwer, który przez wiele dni ledwo funkcjonuje w uszkodzonym stanie.

Dodaj obserwowalność, zanim jej będziesz potrzebować

Gdy już wdrożysz wrapper, połącz go ze strukturalnym logowaniem. Loguj każde wywołanie narzędzia i jego wynik w formacie JSON. Uwzględnij nazwę narzędzia, surowe argumenty, opóźnienie (latency) oraz informację, czy operacja zakończyła się sukcesem, niepowodzeniem czy ponowieniem próby.

Ta dyscyplina szybko się opłaca. Gdy zauważysz nagły wzrost liczby błędów, możesz przefiltrować je według narzędzia i w kilka minut wykryć wzorce. Być może konkretne zewnętrzne API zaczyna zwracać przekroczenia czasu oczekiwania (timeouts) o tej samej porze każdego dnia, co wskazuje na zaplanowane okno serwisowe, o którym nie wiedziałeś. Być może jedno z narzędzi stale otrzymuje błędnie sformatowane argumenty, co ujawnia błąd w inżynierii promptów (prompt engineering) na wcześniejszym etapie. Logi tekstowe ukryte w stosach wywołań (stack traces) sprawiają, że taka praca detektywistyczna jest uciążliwa. Strukturalny JSON czyni ją trywialną.

Wynik w środowisku produkcyjnym

Przez ostatnie trzy tygodnie stosowałem ten wzorzec wrappera na dwóch produkcyjnych serwerach MCP. W tym czasie nie odnotowałem ani jednej cichej awarii. Przed dodaniem wrappera średnio codziennie zdarzał się jeden niewyjaśniony błąd. Wzorzec ten nie jest skomplikowany, ale jego wpływ jest ogromny, ponieważ oddziela przejściowy szum od realnych problemów.

Ciche awarie kosztują więcej niż błędy krytyczne. Awaria uruchamia system powiadomień. Cisza jedynie podkopuje zaufanie. Pewnego dnia Twój agent AI zwraca przydatne dane z narzędzi, a następnego dnia zaczyna zmyślać, ponieważ serwer przestał odpowiadać kilka godzin wcześniej. Wzorzec wrappera niweluje tę lukę. Pozwala serwerowi działać mimo drobnych turbulencji, daje modelowi wystarczający kontekst, aby mógł naprawić własne błędy, i zapewnia, że gdy wydarzy się coś naprawdę krytycznego, dowiesz się o tym natychmiast.

Jeśli budujesz dziś narzędzia MCP, zacznij od wrappera i flagi isError. Reszta to tylko sprzątanie.