エラーの処理
ツールの失敗には 2 通りあり、SDK はそれぞれをまったく違う形で扱います。
通常の例外を送出すると、モデルがそれを目にします。MCPError を送出すると、プロトコルがそれを目にします。
このページは、そのどちらを選ぶかについてです。
モデルが直せるエラー
何かを検索するツールを用意し、その検索を空振りさせてみます。
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
CATALOG = {"Dune": "Frank Herbert", "Neuromancer": "William Gibson"}
@mcp.tool()
def get_author(title: str) -> str:
"""Look up the author of a book in the catalog."""
if title not in CATALOG:
raise ValueError(f"No book titled {title!r} in the catalog.")
return CATALOG[title]
この 2 行に MCP らしいところは何もありません。get_author は、どんな Python 関数でもそうするように、ただの ValueError を送出しているだけです。
カタログにないタイトルで呼び出して、結果を見てみましょう。
result.is_error # True
result.content # [TextContent(text="Error executing tool get_author: No book titled 'Nothing' in the catalog.")]
result.structured_content # None
- リクエストは成功しています。結果が返っており、呼び出し側では何も送出されていません。
is_errorはTrueで、例外のメッセージ(ツール名が前に付きます)がcontentに入っています。まさにモデルが読む場所です。structured_contentはNoneです。失敗した呼び出しには、構造化すべき戻り値がありません。
これがツールエラーで、ツールが送出する「あらゆる」例外のデフォルトの扱いです。そして、ほとんどの場合これこそが望む挙動です。
ツールを呼び出しているのはモデルです。引数を選んだのもモデルです。つまりツールエラーは会話の 1 ターンになります。モデルは「No book titled 'Nothing' in the catalog.」を読み、タイトルを推測し損ねたことに気づき、もっと良いタイトルで呼び直します。raise を 1 つ書いただけで、自己修正するエージェントが手に入りました。
Tip
ツールからエラーメッセージを return しないでください。返された文字列は is_error=False なので、モデルにとっても(そしてあらゆるクライアント UI にとっても)ツールは正常に動作し、その文字列が答えだったように見えます。raise してください。シグナルはこのフラグです。
モデルが直せないエラー
今度は ValueError を MCPError に置き換えます。
from mcp import MCPError
from mcp.server import MCPServer
from mcp.types import INVALID_PARAMS
mcp = MCPServer("Bookshop")
CATALOG = {"Dune": "Frank Herbert", "Neuromancer": "William Gibson"}
@mcp.tool()
def get_author(title: str) -> str:
"""Look up the author of a book in the catalog."""
if title not in CATALOG:
raise MCPError(code=INVALID_PARAMS, message=f"No book titled {title!r} in the catalog.")
return CATALOG[title]
MCPError は SDK のプロトコルエラーです。ツールのラッパーが捕捉しない唯一の例外で、そのまま伝播し、tools/call リクエスト全体が結果ではなく JSON-RPC エラーで失敗します。
{
"code": -32602,
"message": "No book titled 'Nothing' in the catalog."
}
- 結果がありません。
contentもis_errorもなく、モデルが読めるものは何もありません。 - 代わりにホストアプリケーションがエラーを受け取ります。ツールがそもそも存在しなかった場合と同じ扱いです。
code、message、dataはそのまま届きます。INVALID_PARAMSは-32602です。mcp.typesはこれを含む JSON-RPC のエラーコード(INVALID_REQUEST、INTERNAL_ERRORなど)を定数としてエクスポートしているので、マジックナンバーを手で打つ必要はありません。
Check
同じ検索、同じ空振りですが、今度はクライアント側で呼び出しが結果を返す代わりに「送出」します。
mcp.shared.exceptions.MCPError: No book titled 'Nothing' in the catalog.
最初のバージョンは、モデルが反応できる一文を渡しました。こちらは何も渡しません。get_author にとってこれは明らかに改悪であり、それが次のセクションの要点です。
どちらを送出するか
2 つの経路は、2 つの異なる問いに答えるものです。
- 「実行」の失敗、つまりツールがやろうとしたことがうまくいかなかった場合は、任意の例外を送出します。呼び出しを選んだのはモデルなので、モデルがその結果を目にし、立て直す機会を得るべきです。綴りの間違ったタイトル、タイムアウトした上流の API、存在しない行。どれもツールエラーです。
- 「リクエストそのもの」を拒否すべきときは
MCPErrorを送出します。ツールが依存するケイパビリティをクライアントが持っていない、サーバーが誰にも応答できる状態にない、呼び出し側が必要な手順を飛ばした。どれもモデルが再試行しても直らないので、メッセージを渡しても得るものはありません。
決め手になる問いは 1 つです。もっと賢いモデルならこれを避けられたか。はい → 通常の例外。いいえ → MCPError。
この基準で見ると、get_author の 2 番目のバージョンは選択を誤っています。より良いタイトルで直るのですから、モデルはメッセージを見るべきでした。あれは仕組みを見せるためのもので、推奨するためのものではありません。
Info
MCPError は from mcp import MCPError でインポートでき、code、message、省略可能な data ペイロードを受け取ります。そこに入れた内容がそのままクライアントに届きます。SDK は送出された MCPError をサニタイズせず、そのまま転送します。
存在しないリソース
リソースも同じ線引きをします。そして、よくあるケースのために名前付きの例外を 1 つ用意しています。
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ResourceNotFoundError
mcp = MCPServer("Bookshop")
CATALOG = {"Dune": "Frank Herbert", "Neuromancer": "William Gibson"}
@mcp.resource("books://{title}")
def book(title: str) -> str:
"""The catalog entry for one book."""
if title not in CATALOG:
raise ResourceNotFoundError(f"No book titled {title!r} in the catalog.")
return f"{title} by {CATALOG[title]}"
books://{title} はテンプレートです。「あらゆる」タイトルにマッチするので、「URI が正しい形式か」と「その本が存在するか」は別の問いであり、2 番目に答えられるのはこの関数だけです。
答えられないときは ResourceNotFoundError を送出してください。SDK はこれを、仕様が存在しないリソースに割り当てているプロトコルエラーに変換します。-32602 で、リクエストされた URI が data に入るので、クライアントは「どの」読み取りが失敗したのかがわかります。
{
"code": -32602,
"message": "No book titled 'Nothing' in the catalog.",
"data": {"uri": "books://Nothing"}
}
ここには is_error=True のような中間的な結果がないことに注目してください。リソースの読み取りは、内容を返すか失敗するかのどちらかです。リソースにはプロトコルの経路しかありません。テンプレートをはじめ、リソースに関するその他すべては リソース にあります。
送出する必要のないエラー
不正な引数が関数に届くことはありません。
get_author に文字列ではない title を送ると、SDK は関数を呼び出す前に入力スキーマと照合して拒否します。その結果は同じ種類の is_error=True のツールエラーなので、モデルが読んで修正できます。ツール では、Field(le=50) 制約で同じ拒否の様子を示しています。
つまり、書かなくてよい raise 文がまるごと一群あるということです。自分の型ヒントを改めて検証しないでください。
Info
このページの内容はすべてクライアントから見えるものです。テストを書くときに使うインメモリの Client にも、まったく同じものが見えます。raise_exceptions=True でもツールエラーがトレースバックに戻ることはありません。このフラグが作用できる時点では、例外はすでに is_error=True の結果になっています。結果に対してアサートしてください。このパターンは テスト で扱っています。
まとめ
- ツールの中で任意の例外を送出する → 呼び出しは
is_error=Trueを返し、メッセージがcontentに入ります。モデルはそれを読み、再試行できます。これがデフォルトです。 MCPErrorを送出する → 呼び出しそのものが JSON-RPC エラーで失敗します。モデルには何も見えず、ホストが対処します。code、message、dataはそのまま残ります。- 決め手の問い:「もっと賢いモデルならこれを避けられたか」。はい → 例外。いいえ →
MCPError。 - リソースのハンドラーから
ResourceNotFoundErrorを送出する → プロトコルの-32602になり、URI がdataに入ります。 - 不正な引数は関数が実行される前にスキーマと照合して拒否されます。そのために
raiseする必要はありません。 from mcp import MCPErrorでインポートします。エラーコードの定数はmcp.typesから取得します。
エラーの処理はここまでです。サーバーが「公開する」ものはこれですべてです。すべてのハンドラーが実行中に読み取れるもの、そして実行中にクライアントに対して行えることは、次のセクション ハンドラーの中で で扱います。
遭遇する可能性が最も高い SDK エラーの正確な文面、それぞれの意味、そしてそれぞれを一手で直す方法は トラブルシューティング にあります。