首页 › tutorials › MCP工具注解:声明风险让客户端安心

MCP工具注解:声明风险让客户端安心

概述

当你的 MCP 工具被 Claude Code、Cursor、Codex CLI 等不同客户端调用时,客户端需要回答一个关键问题:这个工具该不该弹确认框?能不能自动执行?失败后能不能重试? 仅靠工具的 name 和 description 无法回答这些——这正是工具注解(Tool Annotations)要解决的。本教程带你用官方 Python SDK 内置的 FastMCP 模块,从零构建一个"日志服务器",为每个工具标注正确的风险元数据,再用官方 MCP Inspector 可视化验证注解是否生效。

适用人群:已经能写基本 MCP 工具、想让自己的工具被多客户端安全调用的开发者;以及需要把控"哪些操作可自动放行、哪些必须人工确认"的工程负责人。

前置条件


第一步:理解问题——为什么客户端需要"风险词典"

一个模型发现工具时,看到的三样东西是:工具名、描述、入参 schema。但这回答不了真正重要的问题:

没有这些信息,客户端只能做最保守的假设:把每个工具都当成"破坏性、不可重试、连外网"来对待。结果是——每次调用都弹确认框,体验极差;或者干脆不允许自动执行。

工具注解就是 MCP 给出的标准化"风险词典"。它是在 2025-03-26 规范修订中正式引入的,让服务器用一个结构化的 annotations 字段主动声明工具的行为属性。

关键认知:注解是 hint(提示)而非 contract(契约)。规范明确写道——除非来自可信服务器,否则客户端必须把注解视为不可信。这意味着注解影响的是交互体验(要不要确认、要不要高亮、要不要重试),而不是安全强制。一个恶意/有 bug 的服务器完全可以把删除工具标记成 readOnlyHint: true 来骗过确认框。所以注解是用来优化 UX 的,真正的安全边界仍要靠权限沙箱和可信来源管理。


第二步:用官方 FastMCP 模块造第一个工具

很多人不知道:官方 Python SDK(pip install mcp)内部就自带了一个 fastmcp 高级模块,路径是 mcp.server.fastmcp。它用装饰器风格写工具,几行就能跑起来,不必单独装第三方 FastMCP 包。

操作说明

新建项目目录并安装依赖:

mkdir log-mcp && cd log-mcp
uv init --python 3.11
uv add "mcp[cli]"

创建 server.py:

# server.py
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("Log Explorer")

@mcp.tool()
def search_logs(keyword: str, limit: int = 50) -> str:
    """按关键词搜索应用日志。只读取,不修改任何数据。"""
    # 演示用:真实场景接日志后端(ELK/Loki)
    fake = [
        f"[{i}] user=alice action=login ip=10.0.0.{i} status=ok",
        for i in range(3)
    ]
    matched = [line for line in fake if keyword in line]
    return "\n".join(matched[:limit]) or f"未命中关键词: {keyword}"

if __name__ == "__main__":
    mcp.run()   # 默认走 stdio 传输

上面的列表推导为了示例简洁省略了循环体,实际代码请用完整的 for 语句。

预期结果

直接运行能看到进程挂起等待 stdio 输入(正常,stdio 传输下它由宿主驱动):

uv run server.py
# 无报错,进程等待 JSON-RPC 消息

常见问题


第三步:四大注解详解——风险词典的每个词

ToolAnnotations 接口定义了四个布尔属性加一个人类可读标题。重点不是去背字段名,而是理解每个"默认值"背后的安全哲学。

注解 默认值 含义 典型设值
readOnlyHint false 工具是否不修改环境 查询类设 true
destructiveHint true 非只读工具的修改是否破坏性 删除/覆盖设 true;追加设 false
idempotentHint false 同参数多次调用是否幂等 set 值设 true;append 设 false
openWorldHint true 是否与外部世界交互 查内网库设 false;抓 URL 设 true

谨慎默认值的逻辑:一个未加任何注解的工具,在四个维度上都会被当成最坏情况——会改数据、破坏性、不可重试、连外网。这是规范故意偏向安全的设计:服务器懒得标注,客户端就把它当危险品。

逐个拆解:

1. readOnlyHint —— 最影响 UX 的一个。 设为 true 是"可自动放行"的强信号,因为不该触发确认框。但要诚实:一个"搜索"工具如果顺带把搜索词写进了分析库,那它就不是只读的,哪怕主目的是读。判断标准是"调用后环境状态有没有变",而不是"用户以为它在干嘛"。

2. destructiveHint —— 默认值是 true,注意这个方向。 它不是问"会不会删数据",而是"非只读操作里,这个修改能不能轻松撤销"。删用户、覆盖文件、吊销 token、关闭 issue,都算破坏性。"创建一条记录""追加一行日志"是良性的,设 false。

3. idempotentHint —— 影响重试策略。 幂等指 f(x) 调两次和调一次结果一样。"把用户名改成 X"幂等;"往购物车加一件"不幂等(会变成两件);"切换开关"不幂等(会反复横跳)。超时后,幂等工具可以放心重试,非幂等工具盲目重试可能导致重复付款、重复记录。

4. openWorldHint —— 唯一一个"关注调用之后"的注解。 前三个回答"调用前要不要确认",它回答"返回的内容可不可信"。一个抓取用户给定 URL 的工具是 openWorldHint: true——它返回的网页内容可能藏着提示注入。查公司内部库的工具是 false——封闭域,相对可信。客户端据此决定要不要对返回内容做额外净化。


第四步:给工具加上正确注解

操作说明

在 server.py 里引入 ToolAnnotations,为"搜索"和"删除"两个工具标注截然不同的风险画像:

# server.py(完整版)
from mcp.server.fastmcp import FastMCP
from mcp.types import ToolAnnotations

mcp = FastMCP("Log Explorer")

@mcp.tool(
    annotations=ToolAnnotations(
        title="搜索日志",          # 人类可读标签,UI 里展示
        read_only_hint=True,      # 只读,可自动放行
        destructive_hint=False,
        idempotent_hint=True,     # 查询天然幂等
        open_world_hint=False,    # 只在封闭的日志库内
    )
)
def search_logs(keyword: str, limit: int = 50) -> str:
    """按关键词搜索应用日志。只读取,不修改任何数据。"""
    return f"命中关键词 '{keyword}' 的日志共 N 条(演示)"

@mcp.tool(
    annotations=ToolAnnotations(
        title="清理旧日志",
        read_only_hint=False,
        destructive_hint=True,    # 删除,破坏性
        idempotent_hint=True,     # 再删一次也是空,幂等
        open_world_hint=False,
    )
)
def purge_logs(before_date: str) -> str:
    """永久删除指定日期之前的日志,操作不可逆。"""
    return f"已清理 {before_date} 之前的日志(演示)"

if __name__ == "__main__":
    mcp.run()

预期结果

ToolAnnotations 的字段名是 snake_case(read_only_hint),序列化成 JSON-RPC 时会自动转成 spec 要求的 camelCase(readOnlyHint)。这是 SDK 帮你做的转换,你不必手写驼峰。

常见问题


第五步:用 MCP Inspector 可视化验证注解

写完注解,怎么确认它真的发出去了、字段值对不对?靠肉眼读源码不算数——要看线上序列化后的 JSON-RPC 响应。官方 MCP Inspector 就是这个用途的交互式调试器。

操作说明

在项目目录下,用 npx 直接拉起 Inspector 并指向你的服务器(无需全局安装):

# 用 uv 运行你的 Python 服务器
npx @modelcontextprotocol/inspector uv run server.py

# 或直接用 python
npx @modelcontextprotocol/inspector python server.py

Inspector 启动后会打印两个地址:

浏览器打开 6274 后:

  1. 左侧点 Connect,Inspector 会通过代理与你的 server.py 完成 initialize 握手;
  2. 切到 Tools 标签,列出 search_logs 和 purge_logs;
  3. 点开 search_logs,在右侧详情里能看到完整的 annotations 块;
  4. 在参数表单里填 keyword=login,点 Run Tool,下方直接看返回。

预期结果

search_logs 的注解块应该是:

"annotations": {
  "title": "搜索日志",
  "readOnlyHint": true,
  "destructiveHint": false,
  "idempotentHint": true,
  "openWorldHint": false
}

purge_logs 则是 destructiveHint: true、read_only_hint 为 false。如果 Inspector 里看到的是这个 camelCase 结构且值正确,说明注解已正确序列化。如果 annotations 字段根本不存在,说明注解没传进去——回到第四步检查导入路径和拼写。

常见问题


第六步:接入 Claude Code,看注解的实际效果

注解验证无误后,把它接进真实客户端,对比"只读工具"和"破坏性工具"在交互上的差异。

操作说明

用 CLI 注册(推荐用绝对路径,避免工作目录漂移):

claude mcp add log-explorer -- uv run /abs/path/log-mcp/server.py

或在项目根放 .mcp.json(团队共享):

{
  "mcpServers": {
    "log-explorer": {
      "command": "uv",
      "args": ["run", "/abs/path/log-mcp/server.py"]
    }
  }
}

启动 Claude Code,输入:

帮我在 log-explorer 里搜一下 login 相关的日志

再输入:

把 2024 年以前的旧日志清理掉

预期结果

这就是注解落地的价值——同一台服务器上的两个工具,因为风险画像不同,获得了不同的放行策略,而这一切由你用四个布尔值声明。

常见问题


实战案例:一个完整的"日志服务器"

把上面的内容整合成一个可直接 uv run server.py 跑起来的最小项目。结构如下:

log-mcp/
├── pyproject.toml      # 依赖 mcp[cli]
├── server.py           # FastMCP 服务器,含两个带注解的工具
└── .mcp.json           # Claude Code 接入配置

server.py 用第四步的完整版即可。它的设计要点恰好示范了注解的风险词典用法:

这两条工具共享同一个服务器,但通过注解让客户端精准区分该不该打扰用户。这就是"声明风险让客户端安心"的完整闭环:写工具 → 标注 → Inspector 验证 → 客户端落地。


工作流程

flowchart TD
    A["写 FastMCP 工具<br/>@mcp.tool + 类型注解 + docstring"] --> B["标注 ToolAnnotations<br/>四维度风险画像"]
    B --> C{"注解是否完整?"}
    C -- 否 --> B
    C -- 是 --> D["MCP Inspector 验证<br/>npx @modelcontextprotocol/inspector"]
    D --> E{"annotations 序列化正确?<br/>camelCase 值对吗?"}
    E -- 否 --> B
    E -- 是 --> F["接入客户端<br/>claude mcp add / .mcp.json"]
    F --> G["客户端按风险画像决策<br/>只读→放行 / 破坏性→确认"]

    style B fill:#e8f5e9
    style D fill:#fff3e0
    style G fill:#e3f2fd

延伸阅读


训练营适用性评估


参考来源

  1. MCP Tool Annotations Explained: Hints, Trust, and the Risk Vocabulary — ChatForest — 四大注解的默认值、含义与 hint 非 contract 哲学的详解,引用了 2025-03-26 规范修订。
  2. Tool Annotations as Risk Vocabulary — MCP 官方博客 — 官方对注解作为风险词典、不可信语义的权威说明。
  3. MCP Inspector — Model Context Protocol 官方文档 — Inspector 的启动命令、端口与调试流程。
  4. FastMCP — GitHub (PrefectHQ) — Pythonic 装饰器风格 MCP 框架,工具注解的进阶用法与 API。
  5. MCP Python SDK — GitHub — 官方 SDK 的 mcp.server.fastmcp 模块与 mcp.types.ToolAnnotations 定义。

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入