【随笔】MCP工具注解的信任边界:四个提示如何参与调用决策

【随笔】MCP工具注解的信任边界:四个提示如何参与调用决策

上一篇拆开了MCP协议错误与工具执行错误。接下来还有一个常见问题:调用前,客户端怎样知道工具会不会修改数据,重复调用是否安全,是否可能访问外部世界?

ToolAnnotations提供一组描述行为的提示。它们能帮助应用组织说明和交互,但调用许可还需要结合可信来源、实际实现、用户意图与业务范围。读到一个true,不能跳过这些核验。

本文依据2026年10月7日查阅的MCP规范2026-07-28版本。下方Python3.12示例为应用侧策略模型,在Python3.12.14实际运行;它不连接MCP服务器,也不冒充某个SDK提供的接口。

一、注解在工具描述的哪个位置

MCP工具描述包含名称、说明、输入模式等信息,还可以携带annotations。模型与客户端据此了解工具的用途,应用再决定怎样呈现、检查和执行。

例如,一个工具可以提供如下描述片段;这里省略输入模式等其他字段,仅观察注解位置:

json 复制代码
{
  "name": "lookup_order",
  "annotations": {
    "title": "查询订单",
    "readOnlyHint": true,
    "openWorldHint": false
  }
}

title是便于展示的名称。它与后面几个Hint字段一样,都不会赋予工具权限。完整结构参考MCP工具规范。

二、四个Hint分别提示什么

字段 true所提示的含义 省略时的默认值
readOnlyHint 工具不修改环境 false
destructiveHint 工具可能执行破坏性更新 true
idempotentHint 相同参数重复调用不产生额外效果 false
openWorldHint 工具可能与开放的外部世界交互 true

destructiveHint与idempotentHint仅在readOnlyHint为false时有意义。前者为false描述的是只做增加性更新,仍然可能修改数据。后者提示重复调用的效果,不能替代实际实现中的幂等约束。

openWorldHint=false描述封闭域,不代表工具离线运行、不访问数据库,或已经通过安全检查。字段的适用范围与默认值应以ToolAnnotations官方定义为准。

三、把描述放在核验之前

服务器提供工具描述,也负责实现工具。一个不可信的服务器可以声明readOnlyHint=true,实际执行其他操作。因此,注解与执行合同的来源必须分开看。

规范明确提醒:除非来自可信服务器,客户端必须将工具注解视为不可信;也不应基于不可信服务器的注解决定是否使用工具。这一要求见工具规范的注解说明与注解结构中的安全提醒。

图中的核验关口表示应用自己的决策过程。注解提供描述;可信工具清单、实现审查与当前请求范围提供其他判断依据。相同工具名出现在不同服务器上,也应分别识别。

四、一个应用侧策略示例

下面把两件事分开:display_hints按默认值整理展示信息;decide读取应用维护的已审查记录。模拟工具描述里的Hint不参与权限决策。

示例只实现几条教学规则:未知工具进入核验;超出本次用户意图时拒绝;写操作需要对应授权;结果不明的重试先核对状态。write_approved表示应用已经记录到的授权,可能来自此前明确授权,具体交互应与产品场景匹配。

生产系统还需要验证身份、参数与数据范围,并落实权限执行。这里的intent_allowed等布尔量代表这些检查的输入,没有替代真正的鉴权实现。

保存为annotation_policy_demo.py:

python 复制代码
"""Application policy example; no MCP SDK or remote tool is invoked."""
from dataclasses import dataclass

DEFAULTS = {
    "readOnlyHint": False,
    "destructiveHint": True,
    "idempotentHint": False,
    "openWorldHint": True,
}


def display_hints(annotations):
    return {
        name: annotations[name]
        if type(annotations.get(name)) is bool else default
        for name, default in DEFAULTS.items()
    }


@dataclass(frozen=True)
class ReviewedPolicy:
    read_only: bool
    retry_contract: bool


# These entries represent separately reviewed implementation contracts.
REVIEWED = {
    ("internal", "lookup_order"): ReviewedPolicy(True, True),
    ("internal", "create_ticket"): ReviewedPolicy(False, False),
}


def decide(server, tool, intent_allowed, write_approved=False,
           retry=False, outcome_unknown=False):
    policy = REVIEWED.get((server, tool))
    if policy is None:
        return "REVIEW_UNKNOWN_TOOL"
    if not intent_allowed:
        return "DENY_OUT_OF_SCOPE"
    if not policy.read_only and not write_approved:
        return "NEED_WRITE_APPROVAL"
    if retry and (outcome_unknown or not policy.retry_contract):
        return "RECONCILE_BEFORE_RETRY"
    return "ALLOW_BY_REVIEWED_POLICY"


if __name__ == "__main__":
    print("defaults:", display_hints({}))
    print("invalid-string:", display_hints({"readOnlyHint": "false"}))
    print("unknown:", decide("external", "claim_read_only", True))
    print("read:", decide("internal", "lookup_order", True))
    print("out-of-scope:", decide("internal", "lookup_order", False))
    print("write:", decide("internal", "create_ticket", True))
    print("approved-write:", decide("internal", "create_ticket", True,
                                    write_approved=True))
    print("uncertain-retry:", decide("internal", "lookup_order", True,
                                     retry=True, outcome_unknown=True))

执行:

bash 复制代码
python annotation_policy_demo.py

本次实际输出:

text 复制代码
defaults: {'readOnlyHint': False, 'destructiveHint': True, 'idempotentHint': False, 'openWorldHint': True}
invalid-string: {'readOnlyHint': False, 'destructiveHint': True, 'idempotentHint': False, 'openWorldHint': True}
unknown: REVIEW_UNKNOWN_TOOL
read: ALLOW_BY_REVIEWED_POLICY
out-of-scope: DENY_OUT_OF_SCOPE
write: NEED_WRITE_APPROVAL
approved-write: ALLOW_BY_REVIEWED_POLICY
uncertain-retry: RECONCILE_BEFORE_RETRY

"false"是字符串,并非JSON布尔值false。示例只接受真实bool,并为非法值使用保守默认值;这是示例的展示处理方式,严格的协议输入校验也可以直接拒绝非法描述。

未知工具即便声称只读,也进入核验。已审查的查询工具在范围内可以调用;写工具只有取得对应授权后才通过。这里的规则来自应用维护的记录,工具自己提供的Hint无法修改它们。

五、幂等提示与失败重试,怎样衔接

幂等性需要实现合同 。相同参数的效果要由服务实现与业务规则保证。应用可以结合已核实的合同制定重试策略,不能只看到idempotentHint=true就反复提交写请求。

遇到超时,调用方可能还不知道服务端是否已经执行。先查询任务状态、核对业务幂等键或结果,再决定是否重试,能减少重复动作。示例对结果不明的场景采用保守处理,具体系统可以依据可靠的状态查询与去重机制细化。

图中readOnlyHint卡片是待核对的描述。导师猫查看另一份应用记录,学习猫等待核验结果;这个过程强调描述与可信依据各有来源。

给使用者清楚的控制入口。让用户知道可用工具、调用内容与结果,并能拒绝或中止相关操作,有助于检查模型的工具选择。MCP工具规范建议保留适当的人在回路与确认交互,具体交互方式由应用设计;注解没有规定一个统一的审批界面。

六、落地时的三个检查点

先按服务器身份与工具名识别工具,检查版本及实际实现。更新描述或实现后,原先的审查结论也可能需要重新确认。

再核对参数、用户意图与数据范围。读取也可能暴露数据;只读属性不等于任何查询都被允许。

最后把工具执行结果与业务状态一起检查。注解无法说明这一次调用是否真正完成,尤其不能代替写操作之后的状态核对。

七、🧠 思维导图

#mermaid-svg-jtzuIqQHvFgLJyQQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jtzuIqQHvFgLJyQQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jtzuIqQHvFgLJyQQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jtzuIqQHvFgLJyQQ .error-icon{fill:#552222;}#mermaid-svg-jtzuIqQHvFgLJyQQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jtzuIqQHvFgLJyQQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jtzuIqQHvFgLJyQQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jtzuIqQHvFgLJyQQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jtzuIqQHvFgLJyQQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jtzuIqQHvFgLJyQQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jtzuIqQHvFgLJyQQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jtzuIqQHvFgLJyQQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jtzuIqQHvFgLJyQQ .marker.cross{stroke:#333333;}#mermaid-svg-jtzuIqQHvFgLJyQQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jtzuIqQHvFgLJyQQ p{margin:0;}#mermaid-svg-jtzuIqQHvFgLJyQQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-jtzuIqQHvFgLJyQQ .cluster-label text{fill:#333;}#mermaid-svg-jtzuIqQHvFgLJyQQ .cluster-label span{color:#333;}#mermaid-svg-jtzuIqQHvFgLJyQQ .cluster-label span p{background-color:transparent;}#mermaid-svg-jtzuIqQHvFgLJyQQ .label text,#mermaid-svg-jtzuIqQHvFgLJyQQ span{fill:#333;color:#333;}#mermaid-svg-jtzuIqQHvFgLJyQQ .node rect,#mermaid-svg-jtzuIqQHvFgLJyQQ .node circle,#mermaid-svg-jtzuIqQHvFgLJyQQ .node ellipse,#mermaid-svg-jtzuIqQHvFgLJyQQ .node polygon,#mermaid-svg-jtzuIqQHvFgLJyQQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-jtzuIqQHvFgLJyQQ .rough-node .label text,#mermaid-svg-jtzuIqQHvFgLJyQQ .node .label text,#mermaid-svg-jtzuIqQHvFgLJyQQ .image-shape .label,#mermaid-svg-jtzuIqQHvFgLJyQQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-jtzuIqQHvFgLJyQQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-jtzuIqQHvFgLJyQQ .rough-node .label,#mermaid-svg-jtzuIqQHvFgLJyQQ .node .label,#mermaid-svg-jtzuIqQHvFgLJyQQ .image-shape .label,#mermaid-svg-jtzuIqQHvFgLJyQQ .icon-shape .label{text-align:center;}#mermaid-svg-jtzuIqQHvFgLJyQQ .node.clickable{cursor:pointer;}#mermaid-svg-jtzuIqQHvFgLJyQQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-jtzuIqQHvFgLJyQQ .arrowheadPath{fill:#333333;}#mermaid-svg-jtzuIqQHvFgLJyQQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-jtzuIqQHvFgLJyQQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-jtzuIqQHvFgLJyQQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jtzuIqQHvFgLJyQQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-jtzuIqQHvFgLJyQQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jtzuIqQHvFgLJyQQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-jtzuIqQHvFgLJyQQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-jtzuIqQHvFgLJyQQ .cluster text{fill:#333;}#mermaid-svg-jtzuIqQHvFgLJyQQ .cluster span{color:#333;}#mermaid-svg-jtzuIqQHvFgLJyQQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-jtzuIqQHvFgLJyQQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-jtzuIqQHvFgLJyQQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-jtzuIqQHvFgLJyQQ .icon-shape,#mermaid-svg-jtzuIqQHvFgLJyQQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jtzuIqQHvFgLJyQQ .icon-shape p,#mermaid-svg-jtzuIqQHvFgLJyQQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-jtzuIqQHvFgLJyQQ .icon-shape .label rect,#mermaid-svg-jtzuIqQHvFgLJyQQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jtzuIqQHvFgLJyQQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-jtzuIqQHvFgLJyQQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-jtzuIqQHvFgLJyQQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} MCP工具注解
行为提示
readOnlyHint
destructiveHint
idempotentHint
openWorldHint
信任边界
描述不保证实现
核对服务器身份
应用决策
已审查工具记录
用户意图与参数范围
写操作对应授权
执行核对
幂等需要实现合同
结果不明先查状态

八、总结

总结要点

注解描述预期行为。四个Hint有不同的默认值与适用范围,能帮助应用呈现工具属性,不能自动授予执行权限。

可信依据需要独立核验。将服务器身份、工具实现、用户意图与参数范围放进应用决策,避免由工具自己的声明决定是否执行。

重试还要核对结果。幂等提示无法回答一次超时之后发生了什么。可靠的实现合同与业务状态,才能支持下一步动作。

下一篇继续看MCP进度通知:长任务执行时,怎样让客户端知道进展,并区分进度提示与最终结果。

👉 如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏、分享!😊

相关推荐
慢云智慧空间2 小时前
从设备智能到空间理解,慢云科技如何重新定义智慧建筑
python·科技
浮链序2 小时前
用 Claude Haiku 5.5 做子智能体路由,把 Agent 成本砍掉六成
人工智能·python·llm
冯一川2 小时前
安装Pyside 6
python
网络毒刘3 小时前
为 MCP 写集成测试:mock stdio、断言 tool schema,防止升级后静默坏掉
测试·schema·mcp·atomgit
估值探索者3 小时前
【Python量化系统工程实战 #08】从脚本到生产:量化系统上线 checklist 的最小闭环
java·开发语言·jvm·python·数据挖掘·数据·股票数据api接口
李可以量化3 小时前
如何开通基于 AI 打造的 A 股量化策略(下):特征工程与大模型决策落地
python
万联WANFLOW3 小时前
从Agent到Agentic Collaboration:企业AI工作流背后的技术架构
llm·agent·mcp
万年咸鱼3 小时前
解决C语言的内存释放机制
java·c语言·python
李可以量化3 小时前
如何开通基于 AI 打造的 A 股量化策略(上):三层策略骨架与代码落地
python