取消
客户端可以放弃一次调用:用户按了停止,或者超时时间到了。
这时,SDK 会取消你的处理函数。它正在等待的 await 会抛出异常,函数逐层退出,它返回的任何内容都不会发送出去。大多数处理函数不需要为此做任何事。
有两类需要:有东西要清理的处理函数,以及用普通 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
用 finally,不要用 except。清理完成后,取消必须继续向上传播,而 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 的任务,或者让它的 read_timeout_seconds 耗尽。
Warning
有两个 Streamable HTTP 选项会让处理函数收不到取消的消息: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(旧版连接)会关闭取消。
进度和取消发生在运行中的工具和它的调用方之间。它为你(运维这台服务器的人)记录的日志走的是另一条通道:日志。