キャンセル
クライアントは呼び出しを途中で諦めることがあります。ユーザーが停止ボタンを押した場合や、タイムアウトの時間が切れた場合です。
そのとき、SDK はハンドラーをキャンセルします。ハンドラーが待機している await が例外を送出し、関数は巻き戻され、戻り値は何も送信されません。ほとんどのハンドラーでは、これについて何もする必要はありません。
対応が必要なのは 2 種類です。クリーンアップするものがあるハンドラーと、通常の def で書かれたハンドラーです。
async def ツールでクリーンアップする
クリーンアップは finally に書いてください。
import anyio
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
holds: set[str] = set()
async def take_payment(title: str) -> None:
await anyio.sleep(30) # the customer is typing a card number
async def release_hold(title: str) -> None:
await anyio.sleep(0.1) # a round trip to the stock system
holds.discard(title)
@mcp.tool()
async def order_book(title: str) -> str:
"""Hold a copy of a book while the customer pays for it."""
holds.add(title)
try:
await take_payment(title)
return f"Ordered {title!r}."
finally:
with anyio.move_on_after(5, shield=True):
await release_hold(title)
finallyは、ツールがどのように終了しても実行されます。値を返した場合も、例外を送出した場合も、キャンセルされた場合もです。awaitが必要なクリーンアップにはshield=Trueが必要です。キャンセルされたハンドラーでは、以降のawaitもすべて例外を送出するため、シールドがなければrelease_holdは最初の行で止まってしまいます。- シールドされたブロックは何からもキャンセルできないので、制限時間を設定してください。ここでは
5秒です。
Tip
except ではなく finally を使ってください。クリーンアップが終わったあとも、キャンセルは上位へ伝わり続ける必要があります。finally ならそれができます。
通常の def ツールで早めに停止する
通常の def ツールはスレッドで実行され、スレッドを外部から中断する手段はありません。ツールの側から確認する必要があります。
import time
import anyio.from_thread
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
offline: set[str] = set()
def index_book(title: str) -> None:
time.sleep(1) # slow work with nothing to await
@mcp.tool()
def rebuild_index(titles: list[str]) -> str:
"""Take search offline and rebuild its index, one book at a time."""
offline.add("search")
try:
for title in titles:
anyio.from_thread.check_cancelled()
index_book(title)
return f"Indexed {len(titles)} books."
finally:
offline.discard("search")
anyio.from_thread.check_cancelled()は、呼び出しが有効な間は何もせず、キャンセルされたあとは例外を送出します。作業の区切りごとに呼び出してください。- ここでもクリーンアップは
finallyに書きます。スレッド内では何も await しないので、シールドは不要です。 - 一度も確認しない
defツールは最後まで実行され、その結果は破棄されます。
適用される範囲
プロンプトとリソースの関数も、ツールとまったく同じようにキャンセルされます。
stdio でも Streamable HTTP でも同じように動作します。この SDK の Client で呼び出しを諦めるとは、call_tool を await しているタスクをキャンセルするか、read_timeout_seconds が切れるのに任せることです。
Warning
Streamable HTTP の 2 つのオプションでは、キャンセルがハンドラーに伝わりません。2026-07-28 の接続での json_response=True と、レガシー接続での stateless_http=True です。この場合、クライアントが何をしても、ハンドラーは最後まで実行されます。
まとめ
- クライアントが呼び出しを諦めると、SDK はハンドラーをキャンセルします。ツール、プロンプト、リソースのいずれも対象です。
async def:finallyでクリーンアップし、await を伴うクリーンアップはanyio.move_on_after(seconds, shield=True)の中に置いてください。- 通常の
def:作業の区切りごとにanyio.from_thread.check_cancelled()を呼び出してください。そうしないと、ツールは最後まで実行されます。クリーンアップは通常のfinallyでできます。 json_response=True(モダンな接続)とstateless_http=True(レガシー接続)は、キャンセルを無効にします。
進捗とキャンセルは、実行中のツールとその「呼び出し側」との間のやり取りです。サーバーを運用する「自分」に向けてツールが記録するログは別のチャネルで、それが ロギング です。