结论先说:MCP 工具返回的不只是一段字符串。按 2025-11-25 版规范,工具结果同时存在 content(非结构化内容,五类)和 structuredContent(结构化内容)两条通道,客户端必须把「传给模型的内容」和「展示给用户的内容」分开处理,否则图片、音频、资源引用会整块丢失或被误当成文本塞进上下文。
这是很多 MCP 客户端「工具明明返回了结果,界面上却什么都没有」的根因。问题不在服务端,在渲染层。
一、工具结果的两种形态
规范原文的表述是:工具结果可以包含结构化或非结构化内容。非结构化内容放在结果的 content 字段,是一个数组,可以包含多个不同类型的内容项;结构化内容作为 JSON 对象放在 structuredContent 字段。
为向后兼容,规范明确要求:返回结构化内容的工具SHOULD 同时把序列化后的 JSON 放进一个 TextContent 块里。也就是说,老客户端只读 content 也能拿到数据,新客户端则可以直接用 structuredContent 做类型和字段级处理。
{
"jsonrpc": "2.0",
"id": 5,
"result": {
"content": [
{ "type": "text", "text": "{\"temperature\": 22.5, \"conditions\": \"Partly cloudy\"}" }
],
"structuredContent": {
"temperature": 22.5,
"conditions": "Partly cloudy",
"humidity": 65
}
}
}
二、五类 content 逐个说
| 类型 | type 值 | 关键字段 | 渲染要点 |
|---|---|---|---|
| 文本 | text | text | 直接渲染,长文本需折叠 |
| 图片 | image | data(base64)、 mimeType | 解码后内联展示,不要把 base64 塞进模型上下文 |
| 音频 | audio | data(base64)、 mimeType | 给播放器,模型侧只留一句描述 |
| 资源链接 | resource_link | uri 、 name 、 description 、 mimeType | 渲染成可点击条目,按需再拉取 |
| 嵌入资源 | resource | resource.uri / mimeType / text | 内容已内联,直接展示 |
resource_link 与 resource 不是一回事
这是最容易搞混的一对。两者 type 值不同,语义也不同:
resource_link只给一个 URI,客户端可以后续订阅或拉取。规范特别提醒:工具返回的资源链接,不保证会出现在resources/list的结果里——别指望用列表接口兜底。resource是嵌入资源,内容已经内联在结果里(含 uri 、 mimeType 和 text)。规范要求:使用嵌入资源的服务器 SHOULD 实现resources能力。
// resource_link:只给引用
{ "type": "resource_link", "uri": "file:///project/src/main.rs",
"name": "main.rs", "mimeType": "text/x-rust" }
// resource:内容已内联
{ "type": "resource", "resource": {
"uri": "file:///project/src/main.rs",
"mimeType": "text/x-rust",
"text": "fn main() { println!(\"Hello world!\"); }" } }
三、 annotations:决定这块内容给谁看
所有内容类型都支持可选的 annotations,提供三类元数据:
- audience:目标受众数组,如
["user"]或["user", "assistant"]。这是分流渲染的核心依据——只标user的内容,不该进模型上下文。 - priority:优先级数值,如 0.9 、 0.7 。内容超出展示区域时,按它决定谁先被折叠。
- lastModified:ISO 8601 时间戳,如
2025-05-03T14:30:00Z。用于判断缓存是否过期。
四、三层渲染处理
- 标准化:遍历 content 数组,按 type 分派到对应处理器;遇到不认识的 type 要保留原文并提示,不要静默丢弃——规范允许未来扩展类型。
- 分流:按 annotations.audience 决定是否进模型上下文;文本进上下文,图片/音频转为描述性一句话加资源句柄,避免 base64 直接撑爆上下文窗口。
- 降级:结构化内容优先读 structuredContent;它缺失时回退到 content 里的 JSON 文本块解析;都失败则展示原始文本,保证老服务器也能用。
五、 outputSchema:服务端的 MUST 与客户端的 SHOULD
工具可以在定义里提供 outputSchema(可选 JSON Schema,未指定 $schema 时默认 2020-12)。一旦提供,规范给出两条硬要求:
- 服务端 MUST 提供符合该 schema 的结构化结果;
- 客户端 SHOULD 依据该 schema 校验结构化结果。
它的四个作用是:启用严格的响应校验、为编程语言提供类型信息、指导客户端与模型正确解析返回数据、改善文档与开发体验。规范还特别澄清了一点:structuredContent 是服务器产生的结果数据,与模型的 structured outputs(受 schema 约束的生成)不是一回事,不要混用校验逻辑。
六、错误:isError 与协议错误要分开处理
工具用两种机制报错,客户端的处理策略完全不同:
| 机制 | 位置 | 适用场景 | 客户端建议 |
|---|---|---|---|
| 协议错误 | JSON-RPC 的 error 字段 | 未知工具、请求格式错误、服务器错误 | MAY 提供给模型,但恢复概率低 |
| 工具执行错误 | result 里 isError: true | API 失败、入参校验失败、业务逻辑错误 | SHOULD 提供给模型,便于自我纠正重试 |
区别在于:工具执行错误带的是可操作的反馈(比如「出发日期必须在未来,当前日期是 2025-08-08 」),模型拿到后能自行调整参数重试;协议错误说的是请求结构有问题,模型基本修不了。
七、落地清单
- content 数组逐项处理,不要只取第一项——多内容项是常态。
- base64 的 image / audio 不进模型上下文,只留资源句柄与一句描述。
- resource_link 不要假设能通过 resources/list 找到,需要时可主动拉取。
- 有 structuredContent 就优先用;同时兼容 content 里的 JSON 文本块。
- 大结果集要分页或截断,可参考MCP 大数据量结果的分页与不透明游标设计。
- 错误分支单独渲染,isError 的内容明确标为执行失败,不要和正常结果混排。
服务端作者需要注意的三条
二、输出图片音频时带上 mimeType,客户端才能正确解码。
三、工具契约(包括输出结构)一旦发布就要考虑兼容性,改动方式见 MCP 工具契约演进四步法。
客户端侧的完整实现流程,可参考用 Python 从零实现一个 MCP 客户端,其中对结果解析与结构化处理的环节讲得更细。






评论 (0)