SEP-2106:工具的 `inputSchema` 与 `outputSchema` 符合 JSON Schema 2020-12
社区译文;来源: MCP 29affa6b
| 字段 | 值 |
|---|---|
| SEP | 2106 |
| 标题 | 工具的 inputSchema 与 outputSchema 符合 JSON Schema 2020-12 |
| 状态 | Final |
| 类型 | 标准跟踪 |
| 创建日期 | 2026-01-06 |
| 作者 | John McBride(@jpmcb)— 原始提案作者;Ola Hungerford(@olaservo)— 当前负责人,负责 SEP-1850 后的转换 |
| 发起人 | Ola Hungerford(@olaservo) |
| PR | #2106 |
摘要
本 SEP 提议放宽对 inputSchema、outputSchema 和 structuredContent 的限制,以更好地支持 JSON Schema 2020-12。具体而言:
inputSchema:仍要求type: "object"(因为工具参数是对象),但允许使用任何其他 JSON Schema 属性,以支持功能强大的验证组合(anyOf、oneOf、allOf等)outputSchema:完全支持 JSON Schema 2020-12,因为 MCP 服务器可以返回任何有效 JSONstructuredContent:接受任何通过outputSchema验证的 JSON 值
本提案使 MCP 服务器能够利用 JSON Schema 2020-12 的表达能力,同时保持与现有实现的向后兼容性。
动机
当前 MCP 规范以不符合完整 JSON Schema 支持的方式限制工具 schema:
inputSchema 限制:目前只允许
type、properties和required字段。这会阻止使用anyOf、oneOf和allOf等组合关键字来构建复杂的对象验证模式。outputSchema 限制:尽管规范声称支持“JSON Schema”,但它同样被限制为
type: "object",并且只允许properties和required。structuredContent 限制:它被定义为
{ [key: string]: unknown }(键为字符串的对象),因此无法返回数组,而数组是常见的 API 响应模式。
现实影响
以一个返回逐小时预报的天气 API 工具为例:
[
{ "hour": "09:00", "temp": 68, "conditions": "sunny" },
{ "hour": "10:00", "temp": 72, "conditions": "partly cloudy" },
{ "hour": "11:00", "temp": 75, "conditions": "cloudy" }
]目前,这种自然的数组响应是不可能实现的,因为 structuredContent 必须是对象。开发者不得不用不必要的容器对象包装数组:
{
"forecasts": [
{ "hour": "09:00", "temp": 68, "conditions": "sunny" },
...
]
}这种人为限制会:
- 给响应增加不必要的嵌套
- 与常见 REST API 模式冲突
- 阻止对数组响应直接进行 schema 验证
Schema 组合用例
当前的 inputSchema 限制会阻止合理的 schema 模式。采用本 SEP 后,工具可以将组合关键字与 type: "object" 一同使用:
{
"type": "object",
"oneOf": [
{ "properties": { "id": { "type": "string" } }, "required": ["id"] },
{ "properties": { "name": { "type": "string" } }, "required": ["name"] }
]
}这种模式让工具可以接受基于 ID 或基于名称的查询。这是一种常见的 API 设计,但当前并不受支持,因为 schema 只允许 type、properties 和 required 字段。
规范
1. 放宽 inputSchema
当前定义:
inputSchema: {
type: "object";
properties?: { [key: string]: object };
required?: string[];
};提议的定义:
inputSchema: {
$schema?: string;
type: "object";
[key: string]: unknown;
};inputSchema 字段保留 type: "object" 要求(因为工具参数始终是对象),但现在接受任何其他 JSON Schema 属性。这将支持:
- 组合关键字:
anyOf、oneOf、allOf、not - 条件 schema:
if/then/else - 引用 schema:
$ref、$defs - 任何其他有效的 JSON Schema 2020-12 关键字
2. 放宽 outputSchema
当前定义:
outputSchema?: {
type: "object";
properties?: { [key: string]: object };
required?: string[];
};提议的定义:
outputSchema?: {
$schema?: string;
[key: string]: unknown;
};outputSchema 字段接受任何有效的 JSON Schema 2020-12 对象,因此可以使用验证数组、原始值或复杂组合的 schema。与 inputSchema 不同,它不要求 type: "object",因为工具输出可以是任何有效 JSON。
3. 放宽 structuredContent
当前定义:
structuredContent?: { [key: string]: unknown };提议的定义:
structuredContent?: unknown;structuredContent 字段接受任何符合工具 outputSchema 的有效 JSON 值,包括:
- 对象:
{ "key": "value" } - 数组:
[1, 2, 3]或[{ "id": "abc" }, { "id": "xyz" }] - 原始值:
"string"、42、true、null
4. 文档更新
更新 docs/specification/draft/server/tools.mdx:
- 删除
structuredContent“以 JSON 对象形式返回”的表述 - 明确
structuredContent可以是任何符合outputSchema的 JSON 值 - 增加演示数组响应的示例
5. 示例
返回对象数组的工具:
{
"name": "list_users",
"description": "List all users in the system",
"inputSchema": {
"type": "object",
"properties": {
"limit": { "type": "integer", "minimum": 1, "maximum": 100 }
}
},
"outputSchema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"email": { "type": "string", "format": "email" }
},
"required": ["id", "name"]
}
}
}响应:
{
"content": [
{
"type": "text",
"text": "Found 2 users: Alice (u1, [email protected]) and Bob (u2, [email protected])."
}
],
"structuredContent": [
{ "id": "u1", "name": "Alice", "email": "[email protected]" },
{ "id": "u2", "name": "Bob", "email": "[email protected]" }
]
}使用组合 schema 的工具:
{
"name": "find_resource",
"description": "Find a resource by ID or name",
"inputSchema": {
"type": "object",
"oneOf": [
{
"properties": { "id": { "type": "string", "format": "uuid" } },
"required": ["id"]
},
{
"properties": { "name": { "type": "string", "minLength": 1 } },
"required": ["name"]
}
]
}
}理由
为什么不只是允许数组?
虽然可以简单地扩展 structuredContent 以允许数组,但这并不能完整解决问题。根本原因在于,schema 类型被人为限制为 type: "object"。允许使用任何有效的 JSON Schema 后,我们能够:
- 发挥 JSON Schema 2020-12 的全部能力
- 与规范声称支持 JSON Schema 的表述保持一致
- 提供一致且有原则的方法,而不是零散修补
为什么不要求包装对象?
我们曾考虑要求将数组包装在对象中(例如 { "items": [...] }),但否决了这一方案,因为:
- 它给响应增加了不必要的复杂性
- 它与常见 API 设计模式冲突
- 它阻止对实际响应结构直接进行 schema 验证
- JSON Schema 已经能优雅地处理数组验证
现实中的 API 模式
许多生产 API 直接返回数组:
- GitHub Events API:返回事件对象数组
- AccuWeather Search API:返回地点匹配项数组
- REST 集合端点:标准的
GET /users返回[{...}, {...}]
强制使用包装对象,会给将现有 API 与 MCP 集成的开发者带来阻力。通用 JSON Schema 验证库应该无需针对 MCP 定制便可工作。
与 JSON Schema 2020-12 对齐
JSON Schema 2020-12 为 schema 组合与验证提供了强大的功能。通过移除人为限制,MCP 能够与行业标准保持一致(OpenAPI 3.1 使用 JSON Schema 2020-12),并使开发者可以利用已有的 JSON Schema 知识和工具。
SDK 生态证据
当前限制带来的阻力并非理论问题。FastMCP 是最流行的 MCP Python SDK 之一,它为此实现了大量变通方案:
显式错误消息承认了此项限制:
raise ValueError( f"Output schemas must represent object types due to MCP spec limitations." )自动包装基础设施增加了复杂性:
_WrappedResultdataclass 包装非对象返回值- 自定义
x-fastmcp-wrap-result扩展支持客户端解包 - SDK 和客户端都需要配套的包装/解包逻辑
这些变通方案已经导致实际 bug:
- Issue #2455:不含
type: object的$refschema 导致服务器上的所有工具失效 - Issue #2421:意外的
{"result": ...}包装令用户困惑
- Issue #2455:不含
这表明当前限制确实给生态系统带来了阻力,而 SEP-2106 将消除这些问题。
OpenAPI 先例
OpenAPI 规范经历过类似演进。OpenAPI 3.0 使用 JSON Schema 的“扩展子集”,并施加了自定义限制(例如必须使用 nullable: true,而不是允许将 "null" 作为类型)。
OpenAPI 3.1 做出战略决策,全面与 JSON Schema 2020-12 对齐,并接受了破坏性变更,以消除这些阻力。结果是:工具兼容性更好,生态系统的困惑更少。
| OpenAPI 的问题 | MCP 中的对应问题 |
|---|---|
type 必须是字符串,不能是数组 | inputSchema 只允许特定字段 |
| 无法使用标准的 null 处理方式 | schema 中不能使用 oneOf/anyOf |
自定义 nullable 关键字 | structuredContent 只能是对象 |
| 导致工具困惑 | 导致 SDK 使用变通方案 |
MCP 可以借鉴 OpenAPI 的经验,而不必在数年间重复相同的演进过程。
向后兼容性
此项变更在线上格式层面向后兼容,但根据版本不匹配的方向存在不同细节。
兼容性矩阵
| 新客户端(SEP 后) | 旧客户端(SEP 前) | |
|---|---|---|
| 新服务器(SEP 后) | 完全兼容。 | 仅当服务器返回对象类型的 structuredContent 时兼容。structuredContent 中的数组或原始值可能导致故障。 |
| 旧服务器(SEP 前) | 完全兼容。现有的仅对象 schema 仍然有效。 | 无变化。 |
这种兼容性是不对称的:利用数组或原始值 structuredContent(或在 inputSchema 中使用组合关键字)的新服务器,不能假定旧客户端会接受响应。按先前线上格式编写的旧客户端可能拒绝并非 JSON 对象的 structuredContent,或无法验证包含 type/properties/required 之外关键字的 inputSchema。
为了保持与旧客户端的互操作性,使用数组或原始值 structuredContent 的服务器还 MUST 发出一个包含序列化 JSON 的 TextContent 块(工具规范已建议如此)。不理解非对象 structuredContent 的客户端可以回退到文本内容。
TypeScript / SDK 迁移
将 structuredContent 字段类型从 { [key: string]: unknown } 扩展为 unknown,对强类型消费者来说是源码破坏性变更,即使线上格式没有变化。如下代码:
const result = await client.callTool({ name: "get_weather", arguments: { ... } });
const temp = result.structuredContent?.temperature; // previously compiled (type: unknown)
const city = result.structuredContent?.["city"] as string; // previously compiled
在变更后将不再通过类型检查,因为 TypeScript 禁止在未使用收窄守卫的情况下访问 unknown 的属性:
const sc = result.structuredContent;
if (sc && typeof sc === "object" && !Array.isArray(sc)) {
const temp = (sc as Record<string, unknown>).temperature;
}这项破坏是有意为之,因为只要工具返回非对象,之前的类型就是错误的。不过,SDK 维护者 SHOULD:
- 在 SDK 发布说明中记录迁移方式。
- 在符合易用性要求时提供类型化辅助工具(例如基于工具
outputSchema的泛型),使消费者无需手写收窄守卫。
迁移路径
- 服务器:无需迁移即可继续按原方式工作。若要使用数组或原始值
structuredContent,还应发出一个序列化的TextContent作为回退。 - 客户端:旧客户端仍可与仅使用对象的服务器配合工作。若要使用新的灵活性,应接受
structuredContent中的任何 JSON 值,并在存在outputSchema时据此进行验证。 - SDK:更新生成的类型以反映新 schema(
structuredContent使用unknown,inputSchema/outputSchema采用开放式定义),并在发布说明中指出源码破坏性类型变更。
安全影响
JSON Schema 验证已经处理类型检查、值约束和必填字段验证,实现方 MUST 继续根据声明的 schema 验证所有输入和输出。允许使用完整的 JSON Schema 2020-12 词汇表,会带来两个需要明确指导的领域。
$ref 解引用(SSRF 和 Fetch-DoS)
JSON Schema 2020-12 允许 $ref 指向绝对 URI,而不仅限于指向同一文档的 JSON Pointer。如果某个简单实现通过发出 HTTP 请求来解析遇到的每个 $ref,攻击者就能获得一种服务器端请求伪造/获取放大手段:恶意工具定义可能导致主机获取任意 URL,其中包括内部元数据端点,或旨在耗尽资源的大型载荷。
为缓解此问题:
- 实现方 MUST NOT 自动解引用解析为网络 URI 的
$ref值(即任何不是同一文档 JSON Pointer〔例如#/$defs/Foo〕或内部$anchor的引用)。 - 此处的“自动”是指“在正常验证或 schema 处理过程中,无需操作方明确执行便进行解引用”。实现方 MAY 提供一种选择启用的模式来获取非本地
$ref,但该模式 MUST 默认禁用,并且 SHOULD 强制执行主机允许列表(或至少拒绝环回、链路本地和私有网络地址)、设置超时和大小限制,并记录被解引用的 URI。 - 因外部
$ref无法解析而验证失败的 schema,SHOULD 被拒绝,而不应被静默视为宽松 schema。
组合关键字的资源使用
组合关键字(anyOf、oneOf、allOf、if/then/else)和 $defs 能构建富有表达力的 schema,但病态组合的验证成本可能很高。实现方 SHOULD 施加合理边界,例如最大 schema 深度、子 schema 总数上限或单次验证的时间预算,防止恶意工具定义对验证器形成 CPU DoS 向量。
参考实现
TypeScript SDK
一个演示放宽类型限制的参考实现:
- 分支:olaservo/typescript-sdk@sep-834-v1x
- npm:
@olaservo/[email protected] - 主要变更:
inputSchema:保留type: "object",但允许任何其他 JSON Schema 属性(例如oneOf/anyOf组合)outputSchema:任何有效的 JSON Schema 对象(数组、原始值、对象、组合)structuredContent:任何 JSON 值(对象、数组或原始值)- 更新 McpServer 高级 API,以支持数组和原始值 outputSchema
Everything Server 演示工具
在 everything 服务器中新增了三个演示 SEP-2106 能力的工具:
- 分支:olaservo/servers@sep-834-json-schema-2020-12
- npm:
@olaservo/[email protected] - 工具:
get-weather-forecast:直接在structuredContent中返回逐小时预报的原始数组- 与 SEP-2106“动机”一节中的示例完全一致
outputSchema:z.array(HourlyForecastSchema)——根级数组类型structuredContent:[{hour, temp, conditions}, ...]——直接数组
find-by-id-or-name:演示灵活的输入模式(接受id或name)get-count:直接在structuredContent中返回原始数字(不包装在对象中)outputSchema:z.number()——根级原始值类型structuredContent:42——直接原始值
相关链接
- 原始 PR:https://github.com/modelcontextprotocol/modelcontextprotocol/pull/881
- 相关 issue:https://github.com/modelcontextprotocol/modelcontextprotocol/issues/834
outputSchema类型限制不一致:https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1906- TypeScript SDK schema 类型:https://github.com/modelcontextprotocol/typescript-sdk/issues/1149
- SEP-2200(阐明工具结果内容和模型可见性):https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2200
实现指导
SDK 实现需要:
- 更新
inputSchema类型,保留type: "object",但允许任何其他 JSON Schema 属性 - 更新
outputSchema类型,允许任何有效的 JSON Schema(移除type: "object"限制) - 更新
structuredContent类型,接受任何有效的 JSON 值 - 相应地更新 JSON Schema 定义
致谢
本提案建立在 GitHub issue #834 的讨论之上,并吸收了 MCP 社区的反馈。