文章介绍工具行为注解等常被忽视的 MCP 特性,说明如何让客户端识别只读、幂等、破坏性及外部访问行为。合理提供这些元数据可帮助 Agent 更安全、准确地调用工具。
人人都在构建 MCP server。
大多数讨论都集中在 transports、身份验证、OAuth 和工具暴露上。这些话题确实很重要,但最近在构建几个 MCP server 时,我开始关注规范中的另一个部分——它似乎远没有得到应有的重视。
有意思的是,这些并不是什么隐藏 API。它们早已是 Model Context Protocol 规范或其官方扩展的一部分。
下面这些功能都不会改变你的工具做什么。
它们改变的是 client 能否更清楚地理解你的 server,以及如何与它交互。
正是这个功能启发了我写这篇文章。
MCP 允许你通过 readOnlyHint、idempotentHint、destructiveHint 和 openWorldHint 等 annotations 来描述工具的行为。
{
"annotations": {
"readOnlyHint": true,
"idempotentHint": true,
"destructiveHint": false,
"openWorldHint": false
}
}
你不仅是在告诉 client 工具能做什么,也是在告诉它这个工具会如何行动。
readOnlyHint 表示工具不会修改其运行环境。
idempotentHint 表示:使用相同参数重复调用一个写入工具,不会对环境产生额外影响。
destructiveHint 用于区分可能具有破坏性的更新与增量式更新。
openWorldHint 表示工具是否可能与开放世界中的外部实体交互。
例如,Web 搜索工具运行在开放世界中,而访问固定本地记忆存储的工具则运行在封闭领域中。
这些可能看起来只是几个简单的布尔值,但它们可以在 client 规划执行过程、向用户展示工具或设计确认流程时,提供非常有用的上下文。
这里有几个细节值得注意。
idempotentHint 和 destructiveHint 只对非只读工具有意义。此外,destructiveHint: false 表示该工具只会执行增量式更新。
最重要的是,这些只是 hints,并非安全保证。client 不应该根据来自不受信任 server 的 annotations 做出涉及安全的决策。身份验证、授权、用户同意以及确定性的安全防护依然不可或缺。
📖 MCP 规范:Tools
许多 MCP 工具仍然只返回文本。
Found 12 matching documents.
对人类而言,这完全没问题。
但如果同时获得底层的结构化数据,client 往往能从中受益。
MCP 工具可以通过 structuredContent 返回一个 JSON 值。
{
"structuredContent": {
"documents": [
{
"title": "...",
"url": "...",
"lastModified": "..."
}
]
}
}
这个值不一定非要是对象。它可以是任何有效的 JSON 值,包括数组、字符串、数字、布尔值或 null。
结构化数据可以让 client 更容易验证结果、渲染界面、将数据传入其他工作流,也能让模型直接处理各个字段,而不必解析自然语言响应。
为了向后兼容,规范建议同时在普通的文本 content block 中返回序列化后的 JSON。
这里需要区分一个术语:MCP structuredContent 指的是结构化工具结果。它与 LLM structured output 或受 schema 约束的模型生成并不是一回事。
📖 MCP 规范:Structured Content
如果要返回结构化数据,就应该告诉 client 数据长什么样。
当 client 通过 tools/list 发现工具时,它会获得每个工具的 inputSchema。
工具还可以暴露 outputSchema。
{
"name": "search_documents",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string"
}
},
"required": ["query"]
},
"outputSchema": {
"type": "object",
"properties": {
"documents": {
"type": "array"
}
},
"required": ["documents"]
}
}
这意味着 client 可以在调用工具之前,先检查 structuredContent 的预期结构。
如果定义了 output schema,server 就必须返回符合该 schema 的结构化内容。同时也鼓励 client 对结果进行验证。
更可预测的 client 端处理方式
更清晰的工具文档
我喜欢这样理解:
结构化内容告诉 client 你返回了什么。输出 schema 则在返回结果之前,告诉 client 应该期待什么。
这两个功能可以很好地互相补充。
📖 MCP 规范:Output Schema
并不是所有操作都能在一秒内完成。
想象一下为整个代码仓库建立索引、处理数千个文件,或者运行一个大型 AI 工作流。
如果没有进度报告,用户可能只能盯着加载动画,却不知道任务是否仍在继续。
MCP 支持为耗时请求发送可选的进度通知。
当 client 希望接收进度更新时,它会在请求的 metadata 中加入一个唯一的 progressToken。
{
"_meta": {
"progressToken": "index-repository-123"
}
}
之后,server 可以发送与该 token 关联的 notifications/progress 消息。
{
"method": "notifications/progress",
"params": {
"progressToken": "index-repository-123",
"progress": 42,
"total": 100,
"message": "Indexing repository"
}
}
即使 client 提供了 token,server 也不一定必须发送进度通知。如果 server 决定发送,那么每次通知中的 progress 值都必须递增。当总工作量未知时,total 可以省略。
有时候,好的 UX 并不意味着让任务执行得更快。
而是让等待不再那么令人困惑。
📖 MCP 规范:Progress
有些操作不应该一直保持请求连接,直到工作全部完成。
MCP Tasks 为 CI pipelines、批处理、外部任务、审批工作流或模型训练等耗时操作提供了异步执行能力。
Tasks 是 MCP 的官方扩展,而不是核心协议的一部分。client 和 server 都必须声明自己支持这项扩展。
当 server 判断一个受支持的请求将长时间运行时,可以返回一个持久化的 task handle,而不是最终结果。
接收 task identifier
通过 tasks/get 轮询 task
重新连接后恢复轮询
通过 tasks/update 提供请求所需的输入
通过 tasks/cancel 请求取消任务
在 task 完成后获取最终结果
Tasks 还可以暴露 working、input_required、completed、failed 和 cancelled 等状态。
轮询是默认机制。如果双方支持,server 还可以额外提供 task notifications。
当底层操作可能比网络连接存活得更久,或者需要暂停并等待人工输入时,这种模型尤其有用。
📖 MCP Tasks 扩展
人们很容易把 MCP 看作一种暴露工具的协议。
但我认为它远不止如此。
它同样是一种帮助 client 理解和操作这些工具的协议。
如果一个 client 知道工具是否会修改环境、重复调用是否会产生额外影响、应该期待怎样的输出结构、如何使用结构化数据,以及如何跟踪运行时间较长的任务,那么与只获得名称和描述的 client 相比,它拥有的上下文要丰富得多。
这正是最吸引我的地方。
这个协议不仅描述了能力。
它也在描述行为和执行模式。
其中一些功能只需要少量工作就能实现。另一些功能——例如 Tasks——则需要 client 和 server 双方提供更深入的支持。
尽管如此,它们仍然值得了解,因为它们可以带来更可预测的集成效果,并为用户提供更好的体验。
如果你正在构建一个 MCP server,那么除了工具名称、描述和 input schemas 之外,值得再多花一点时间,看看还有哪些能力可以利用。
这样一来,你的 client 或许能更好地理解 server,你的用户也可能因此获得更加透明、可靠的体验。
我很想听听,在你看来,MCP 还有哪些部分值得得到更多关注。
作为后续措施,你可以考虑屏蔽此人和/或举报滥用行为。