コンテンツにスキップ

キャンセル

機械翻訳

このページは英語版ドキュメントから自動翻訳されたものであり、正式な版は英語版のページです。不自然な箇所があれば、翻訳についてで報告の方法を説明しています。

クライアントは呼び出しを途中で諦めることがあります。ユーザーが停止ボタンを押した場合や、タイムアウトの時間が切れた場合です。

そのとき、SDK はハンドラーをキャンセルします。ハンドラーが待機している await が例外を送出し、関数は巻き戻され、戻り値は何も送信されません。ほとんどのハンドラーでは、これについて何もする必要はありません。

対応が必要なのは 2 種類です。クリーンアップするものがあるハンドラーと、通常の def で書かれたハンドラーです。

async def ツールでクリーンアップする

クリーンアップは finally に書いてください。

server.py
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 ツールはスレッドで実行され、スレッドを外部から中断する手段はありません。ツールの側から確認する必要があります。

server.py
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(レガシー接続)は、キャンセルを無効にします。

進捗とキャンセルは、実行中のツールとその「呼び出し側」との間のやり取りです。サーバーを運用する「自分」に向けてツールが記録するログは別のチャネルで、それが ロギング です。