私のMCPサーバーは、かつて突然動かなくなることがありました。クラッシュダンプもなければ、ログにスタックトレースもありません。クライアントは何の不満も言わずに接続し続け、数時間後にはすべてが沈黙しました。リクエストは消え、向こう側のAIエージェントには何も届かなくなりました。

これはModel Context Protocol (MCP) エコシステムにおいて、非常に多く見られる、もどかしい問題です。このプロトコルは、AIエージェントが外部ツールをどのように発見し、呼び出すかを定義していますが、仕様上、エラー処理はユーザー自身が行うことが前提となっています。ほとんどのチュートリアルやスターター実装では、その部分は飛ばされがちです。それらはハッピーパス(正常系)、つまり関数にアノテーションを付け、サーバー経由で公開し、クリーンな結果を返す、という流れに集中しています。ネットワークの瞬断が外部APIに発生した場合や、モデルがパラメータ名を間違えて不正な入力を送ってきた場合に何が起こるかについては、ほとんど語られません。その結果、一見正常に見えても、実際には数時間前から死んでいるような、脆弱なサーバーが出来上がってしまうのです。

なぜ「空のレスポンス」はクラッシュよりも厄介なのか

MCPのツールハンドラー内で未処理の例外が発生すると、トランスポート層がそれを飲み込んでしまうことがよくあります。サーバープロセスは生存し続け、ソケットも開いたままですが、クライアントには空のレスポンスが返されます。これは、派手なクラッシュよりも危険です。なぜなら、モニタリングツールが気づかない可能性があるからです。プロセスは実行されており、ポートもリッスン状態のままです。それなのに、すべてのツール呼び出しが何も返さないのです。

AIモデルは、沈黙を「失敗」とは解釈しません。「データが生成されなかった成功した呼び出し」として解釈します。その空のレスポンスは、モデルに即興を学習させてしまいます。モデルは空白を埋めるために事実を捏造(ハルシネーション)し始めたり、同じ壊れた呼び出しを繰り返すループに陥ったりします。一時的なネットワークのタイムアウトや無効なツール引数といった小さな問題が、このような挙動を引き起こすことを決して許してはいけません。

ラッパーパターン:3つの防御策

私は、すべてのツールハンドラーを薄いエラーリカバリ層でラップすることで、この問題を解決しました。このラッパーは、起こりうるすべての失敗を予測しようとするものではありません。エラーを分類し、それに応じて適切に応答するものです。

ConnectionError と TimeoutError
これらは、サーバーが外部APIと通信している際にネットワークが不安定になったときに発生します。直感的な解決策は、MCPサーバーのプロセス全体を再起動することでしょう。しかし、それはしないでください。再起動を行うと、アクティブなクライアント接続が切断され、メモリ内の状態がクリアされ、完全な再初期化を強制することになります。代わりに、接続エラーをキャッチし、ツールが使用しているトランスポート層またはHTTPクライアントのみを再接続してください。これにより、サーバーは稼働状態を維持し、すぐに次のリクエストを受け入れられるようになります。

ValueError
これは、AIクライアントが無効な引数を送ってきたときに発生します。モデルが勝手にパラメータを捏造したり、整数が必要な場所に文字列を渡したり、必須フィールドを忘れたりした場合です。これを未処理のまま上に流してしまうと、クライアントにはクラッシュか空の返答のどちらかが届きます。ラッパー内でこれをキャッチし、何が間違っていたのかをモデルに正確に伝える、明確で具体的なメッセージを構築してください。どのパラメータが失敗し、何が期待されていたのかを説明します。最新のAIモデルの多くは、そのメッセージを読み取り、次のターンで自律的に修正を行います。曖昧なエラーは推論サイクルを無駄にします。正確なエラーは、問題を即座に解決します。

General Exceptions
セーフティネットを用意しておきましょう。エラーが上記のカテゴリに当てはまらない場合は、詳細をログに記録し、クライアントにはクリーンで汎用的な失敗レスポンスを返します。これにより、一つの奇妙なエッジケースによって全員のセッションが終了してしまうのを防ぐことができます。サーバーは生き残り、クライアントには何かが失敗したという信号が伝わり、後でデバッグするための十分なコンテキストをログに残すことができます。

isError フラグは必須事項である

ここに、あなたの修正が実際に機能するかどうかを決定する重要な詳細があります。MCPのレスポンスには、isError というブール値のフィールドが含まれています。例外が発生した際に、isErrortrue に設定せずにエラーメッセージを返すと、クライアントはそのエラーテキストを「成功したツール実行結果」として扱ってしまいます。

例えば、外部APIがレート制限に達したとしましょう。例外をキャッチして "API rate limit exceeded" という文字列を返しますが、isErrorfalse のままにしたとします。すると、クライアントはその文字列を、あたかも本物のツールの出力であるかのようにモデルのコンテキストウィンドウに渡します。モデルはその後、そのテキストをデータであるかのように扱って推論しようとします。要約の中でそのエラーを引用したり、さらに悪いことに、そのエラーテキストと他の事実との間に相関関係を捏造したりするかもしれません。あなたは、一時的なインフラの不具合を、誤情報の源へと変えてしまったのです。

エラーペイロードを返すときは、常に isErrortrue に設定してください。これにより、ツール呼び出しが失敗したことがクライアントに明確に伝わり、モデルがリトライするか、詳細を尋ねるか、あるいは全く別のツールを試すかを判断できるようになります。

キャッチすべきエラーと、停止させるべきエラーを見極める

すべての処理を、あらゆるエラーを飲み込んでしまうような盲目的な try-catch でサーバー全体を囲まないでください。エラーの中には、サーバーを即座に停止させるべきものもあります。起動時に必要な環境変数が不足していたり、設定ファイルが破損していたりする場合、リクエストレベルでいくらキャッチしても解決にはなりません。このような致命的なエラーに対しては、専用の例外クラスを作成し、プロセスをクラッシュさせてください。

ルールは単純です。エラーが一時的なもの、あるいは単一のリクエストに限定されたものであれば、それをキャッチして復旧させてください。もしそのエラーによって、その後のすべてのリクエストが失敗することが確実であるならば、サーバーを派手に停止(die loudly)させてください。起動時に素早く失敗する方が、壊れた状態で数日間もかろうじて動き続けるサーバーよりも、はるかにマシです。

必要になる前にオブザーバビリティ(観測性)を追加する

ラッパーを実装したら、構造化ロギングと組み合わせます。すべてのツール呼び出しとその結果を JSON 形式でログに記録してください。ツール名、生の引数、レイテンシ、そして成功したか、失敗したか、あるいはリトライしたかを含めます。

この習慣はすぐに効果を発揮します。エラーの急増に気づいたとき、ツールごとにフィルタリングして、数分以内にパターンを特定できます。例えば、特定の外部 API が毎日決まった時間にタイムアウトを出し始め、知らなかった定期メンテナンスの時間帯を指し示しているかもしれません。あるいは、あるツールが常に不正な形式の引数を受け取っていることが分かり、上流のプロンプトエンジニアリングの欠陥が露呈するかもしれません。スタックトレースの中に埋もれたプレーンテキストのログでは、こうした調査は苦痛を伴います。構造化された JSON であれば、それは極めて容易になります。

本番環境での結果

私は過去3週間にわたり、2つの本番用 MCP サーバーでこのラッパーパターンを運用してきました。その期間中、サイレントな失敗(silent failures)は一度も発生しませんでした。ラッパーを追加する前は、説明のつかない失敗が平均して毎日1回ほど発生していました。このパターンは複雑ではありませんが、生存可能なノイズと真の問題を切り離してくれるため、その効果は絶大です。

サイレントな失敗は、クラッシュよりも大きな代償を伴います。クラッシュすればアラートシステムが作動します。しかし、沈黙はただ信頼を損なうだけです。ある日は AI エージェントが有用なツールデータを返してくれるのに、翌日には、サーバーが数時間前に応答を停止していたために、勝手に作り話を始めてしまうといったことが起こり得ます。ラッパーパターンはこのギャップを埋めます。軽微な混乱の中でもサーバーを稼働させ続け、モデルが自身のミスを修正するための十分なコンテキストを与え、そして本当に致命的な問題が発生したときには、即座にそれを知らせてくれるのです。

もし今 MCP ツールを構築しているのであれば、まずはラッパーと isError フラグから始めてください。それ以外はすべて、後回しの整理作業に過ぎません。