本节摘要:类型注解描述了「工具需要什么、返回什么」,但没描述「工具会干什么」——它只读吗?有破坏性吗?对外公开吗?这些行为特征影响着宿主的授权策略与模型的调用谨慎度。
ToolAnnotations就是用来声明这些元信息的机制。本节讲清它表达的几个关键行为标志(只读、破坏性、开放等),它们如何被宿主用于授权决策,以及模型如何据此调整调用策略。读完本节,你能让你的工具不只是「能被调用」,还能「被正确地对待」。
类型注解回答了「参数和返回的结构」,但没回答这些行为问题:
这些问题的答案,不影响工具能不能跑,但影响它「该不该被调」「该怎么被调」。考虑两个工具:
工具 A:get_balance(user_id) → int (只读,查余额) 工具 B:transfer(from, to, amount) → bool (破坏性,转账)
光看签名,它们都是「传参数、拿结果」。但宿主对待它们的方式天差地别——get_balance 可以放心自动调,transfer 该让用户确认。这个差别,需要一种机制来表达,这就是 ToolAnnotations。
ToolAnnotations 是一组布尔标志,描述工具的行为特征:
| 标志 | 含义 | 典型工具 |
|---|---|---|
read_only |
只读,不改世界 | 查询、计算、读取 |
destructive |
破坏性,不可逆 | 删除、覆盖、转账 |
idempotent |
幂等,重复调用结果一致 | 设置配置、创建(已存在则不动) |
open_world |
与外部世界交互 | 调外部 API、发邮件、写文件 |
用法:
from mcp.server import MCPServer from mcp.server.mcpserver import ToolAnnotations mcp = MCPServer("Bank") @mcp.tool(annotations=ToolAnnotations( read_only=True, # 查余额是只读 )) def get_balance(user_id: str) -> int: """Get account balance.""" ... @mcp.tool(annotations=ToolAnnotations( destructive=True, # 转账是破坏性 open_world=True, # 涉及外部账本 )) def transfer(from_id: str, to_id: str, amount: float) -> bool: """Transfer money between accounts.""" ...
ToolAnnotations 不是装饰——它会被宿主用于实际决策:
模型决定调用 transfer(...) │ ▼ 宿主检查 ToolAnnotations: destructive=True → 触发「破坏性操作确认」 │ ▼ 宿主弹窗:「确认转账 $100 从 A 到 B?」 │ ▼ 用户确认 → 执行;用户拒绝 → 取消,告知模型
不同标志触发的宿主行为:
| 标志组合 | 宿主的典型反应 |
|---|---|
read_only=True |
放心自动调,无需确认 |
destructive=True |
必须用户确认才执行 |
idempotent=True |
可安全重试(失败了再调一次没事) |
open_world=True |
提示「这会与外部交互」,可能需额外授权 |
这就是为什么准确标注 ToolAnnotations 重要——它直接决定你的工具在宿主里是被「放心自动调」还是「谨慎确认」。
⚠️ 注意:宿主是否真的按这些标志行事,取决于宿主实现。Claude Desktop 可能对
destructive强制确认,另一个宿主可能忽略。但作为工具作者,你的责任是准确声明——这样支持这些标志的宿主能正确对待你的工具,不支持的至少拿到了准确信息。
除了宿主,模型也会读 ToolAnnotations(它会出现在发给模型的工具描述里)。模型据此调整调用策略:
| 标志 | 模型的典型策略 |
|---|---|
read_only=True |
放心调,失败了大不了再查 |
destructive=True |
谨慎调,先确认参数对,必要时先调只读工具核实 |
open_world=True |
知道这会动外部世界,调前会权衡 |
举个例子:模型要删一个文件,如果它看到删除工具标了 destructive=True,可能会先调用一个只读的 file_exists 工具核实,再调删除——这种谨慎行为部分来自 ToolAnnotations 的提示。
ToolAnnotations 的价值依赖准确。错误标注(把破坏性标成只读)会让宿主与模型做出错误决策。实践建议:
| 工具行为 | 建议标注 |
|---|---|
| 纯查询、计算 | read_only=True |
| 创建(可能覆盖) | destructive=True(若会覆盖) |
| 创建(已存在则不动) | idempotent=True |
| 删除 | destructive=True |
| 调外部 API | open_world=True |
| 改本地配置 | idempotent=True(若幂等) |
💡 技巧:拿不准时,优先标「保守」——比如不确定是否破坏性,标
destructive=True让宿主谨慎对待,比标错成只读导致误操作安全。标注的目的是「让工具被正确对待」,保守一点代价小,激进一点代价大。
ToolAnnotations 还能与错误处理协作,提升工具的健壮性。一个常见模式:幂等工具支持安全重试:
@mcp.tool(annotations=ToolAnnotations( idempotent=True, # 幂等,可安全重试 )) def set_config(key: str, value: str) -> bool: """Set a config value (idempotent).""" save_config(key, value) # 同样的 key/value 重复调,结果一样 return True
幂等标志告诉宿主与模型:「这个工具失败了可以重试,不会有副作用累积」。这样当网络抖动导致失败时,宿主能放心重试,而不是「失败就放弃」。
ToolAnnotations 描述行为——后者回答「会干什么」。read_only(只读)、destructive(破坏性)、idempotent(幂等)、open_world(与外部交互)。行为特征清楚了,下一节讲工具失败时如何把错误回传模型。