My MCP server used to simply stop working. No crash dump. No stack trace in the logs. Clients connected without complaint, then after a few hours the whole thing went mute. Requests disappeared and the AI agent on the other end received nothing but blank air.

This is a frustratingly common story in the Model Context Protocol (MCP) ecosystem. The protocol defines how AI agents discover and call external tools, but the specification assumes you will handle errors yourself. Most tutorials and starter implementations skip past that part. They focus on the happy path: annotate a function, expose it through the server, and return a clean result. They rarely show you what happens when a network blip hits your external API, or when the model hallucinates a parameter name and sends garbage input. The result is a brittle server that looks healthy but has actually been dead for hours.

למה תגובות ריקות גרועות יותר מקריסות

כשחריגה (exception) שלא טופלה מחליקה דרך ב-tool handler של ה-MCP, שכבת ה-transport לרוב בולעת אותה. תהליך השרת נשאר חי, ה-socket נשאר פתוח, אך הלקוח מקבל תגובה ריקה. זה מסוכן יותר מקריסה רועשת, כי ייתכן שהניטור שלכם לא יבחין בכך. התהליך עדיין רץ. הפורט עדיין מאזין. ובכל זאת, כל קריאה לכלי לא מחזירה דבר.

מודל ה-AI לא מפרש שתיקה ככישלון. הוא מפרש שתיקה כקריאה מוצלחת שלא הניבה נתונים. התגובה הריקה הזו מאמנת את המודל לאלתר. הוא מתחיל להמציא עובדות כדי למלא את החלל, או שהוא נכנס ללולאה של ניסיונות חוזרים לאותה קריאה שבורה. בעיות קטנות כמו timeout רשת זמני או ארגומנט לא תקין לכלי לעולם לא צריכות להיגרם כתוצאה מהתנהגות כזו.

תבנית ה-Wrapper: שלושה קווי הגנה

תיקנתי את זה על ידי עטיפה של כל tool handler בשכבת התאוששות משגיאות דקה. ה-wrapper לא מנסה לחזות כל כישלון אפשרי. הוא מסווג אותם ומגיב בהתאם.

ConnectionError ו-TimeoutError
אלו נוצרים כאשר השרת שלכם מדבר עם API חיצוני והרשת רועדת. התיקון האינסטינקטיבי הוא להפעיל מחדש את כל תהליך שרת ה-MCP. אל תעשו זאת. הפעלה מחדש מנתקת חיבורי לקוח פעילים, מנקה כל מצב (state) שנמצא בזיכרון, ומכריחה אתחול מלא. במקום זאת, תפסו את כשל החיבור וחברו מחדש רק את שכבת ה-transport או את לקוח ה-HTTP שהכלי שלכם משתמש בו. השרת נשאר "חם" ומוכן לבקשה הבאה באופן מיידי.

ValueError
זה מה שרואים כשלקוח ה-AI שולח ארגומנטים לא תקינים. אולי המודל המציא פרמטר, העביר מחרוזת (string) במקום מספר שלם (integer), או שכח שדה חובה. אם תתנו לזה לעלות למעלה (bubble up) ללא טיפול, הלקוח יקבל או קריסה או תשובה ריקה. תפסו את זה בתוך ה-wrapper, ואז בנו הודעה ברורה וספציפית שאומרת למודל בדיוק מה השתבש. הסבירו איזה פרמטר נכשל ומה היה מצופה. רוב מודלי ה-AI המודרניים יקראו את ההודעה הזו ויבצעו תיקון עצמי כבר בסיבוב הבא. שגיאה מעורפלת מבזבזת מחזור חשיבה. שגיאה מדויקת פותרת את הבעיה באופן מיידי.

General Exceptions
שמרו על רשת ביטחון. אם שגיאה חורגת מהקטגוריות לעיל, תעדו (log) את הפרטים עבור עצמכם והחזירו תגובת כישלון גנרית ונקייה ללקוח. זה מונע ממקרה קצה מוזר אחד להרוס את הסשן עבור כולם. השרת שורד, הלקוח מקבל סימן שמשהו נכשל, ואתם שומרים מספיק הקשר ביומנים (logs) שלכם כדי לדבג מאוחר יותר.

הדגל isError הוא הכרחי

הנה הפרט שבאמת קובע אם התיקון שלכם יעבוד. תגובות MCP כוללות שדה בוליאני בשם isError. אם מתרחשת חריגה ואתם מחזירים הודעת שגיאה מבלי להגדיר את isError כ-true, הלקוח יתייחס לטקסט השגיאה הזה כתוצאה מוצלחת של הכלי.

דמיינו שה-API החיצוני שלכם מגיע למגבלת קצב (rate limit). אתם תופסים את החריגה ומחזירים את המחרוזת "API rate limit exceeded" אך משאירים את isError כ-false. הלקוח מעביר את המחרוזת הזו לחלון ההקשר (context window) של המודל כאילו הייתה פלט אמיתי של הכלי. המודל מנסה אז להסיק מסקנות מהטקסט הזה כאילו היה נתונים. הוא עשוי לצטט את השגיאה בסיכום, או גרוע מכך, הוא עשוי להמציא קשרים בין טקסט השגיאה הזה לבין עובדות אחרות. הפכתם תקלה תשתיתית זמנית למקור של מידע מוטעה.

Always set isError to true when you are returning an error payload. This gives the client a clear signal that the tool call failed, which lets the model decide whether to retry, ask for clarification, or try a different tool entirely.

Know What to Catch and What to Kill

Do not wrap your entire server in a blind try-catch that swallows everything. Some errors mean the server should stop immediately. If a required environment variable is missing on startup, or your configuration file is corrupt, no amount of request-level catching will help. Create a specific exception class for fatal errors like these and let them crash the process.

The rule is simple. If the error is temporary or isolated to a single request, catch it and recover. If the error means every subsequent request is guaranteed to fail, let the server die loudly. A fast failure on startup is infinitely better than a server that limps along for days in a broken state.

Add Observability Before You Need It

Once you have the wrapper in place, pair it with structured logging. Log every tool call and its outcome in JSON format. Include the tool name, the raw arguments, the latency, and whether it succeeded, failed, or retried.

This discipline pays off quickly. When you notice a spike in errors, you can filter by tool and spot patterns in minutes. Maybe a specific external API starts throwing timeouts at the same time every day, pointing to a scheduled maintenance window you did not know about. Maybe one tool receives consistently malformed arguments, revealing a prompt engineering flaw upstream. Plain text logs buried in stack traces make this detective work painful. Structured JSON makes it trivial.

The Production Result

I have run this wrapper pattern on two production MCP servers for the past three weeks. In that window, I have seen zero silent failures. Before adding the wrapper, I averaged roughly one unexplained failure every day. The pattern is not complex, but its impact is outsized because it separates survivable noise from real problems.

Silent failures cost more than crashes. A crash triggers your alerting system. Silence just erodes trust. One day your AI agent returns useful tool data, and the next day it starts making things up because the server stopped answering hours ago. The wrapper pattern closes that gap. It keeps your server running through minor turbulence, gives the model enough context to fix its own mistakes, and ensures that when something truly fatal goes wrong, you hear about it immediately.

If you are building MCP tools today, start with the wrapper and the isError flag. Everything else is just cleanup.