SEP-2106:工具的 `inputSchema` 与 `outputSchema` 符合 JSON Schema 2020-12

SEP-2106:工具的 `inputSchema` 与 `outputSchema` 符合 JSON Schema 2020-12

社区译文;来源: MCP 29affa6b

本 SEP 已达到 Final 状态,并作为已接受设计的历史记录保留。 定稿后对协议所做的更改不会反映在此处。权威要求请参阅当前规范 及其变更日志。
字段值
SEP2106
标题工具的 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 服务器可以返回任何有效 JSON
  • structuredContent:接受任何通过 outputSchema 验证的 JSON 值

本提案使 MCP 服务器能够利用 JSON Schema 2020-12 的表达能力,同时保持与现有实现的向后兼容性。

动机

当前 MCP 规范以不符合完整 JSON Schema 支持的方式限制工具 schema:

  1. inputSchema 限制:目前只允许 type、properties 和 required 字段。这会阻止使用 anyOf、oneOf 和 allOf 等组合关键字来构建复杂的对象验证模式。

  2. outputSchema 限制:尽管规范声称支持“JSON Schema”,但它同样被限制为 type: "object",并且只允许 properties 和 required。

  3. 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 后,我们能够:

  1. 发挥 JSON Schema 2020-12 的全部能力
  2. 与规范声称支持 JSON Schema 的表述保持一致
  3. 提供一致且有原则的方法,而不是零散修补

为什么不要求包装对象?

我们曾考虑要求将数组包装在对象中(例如 { "items": [...] }),但否决了这一方案,因为:

  1. 它给响应增加了不必要的复杂性
  2. 它与常见 API 设计模式冲突
  3. 它阻止对实际响应结构直接进行 schema 验证
  4. 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 之一,它为此实现了大量变通方案:

  1. 显式错误消息承认了此项限制:

    raise ValueError(
        f"Output schemas must represent object types due to MCP spec limitations."
    )
  2. 自动包装基础设施增加了复杂性:

    • _WrappedResult dataclass 包装非对象返回值
    • 自定义 x-fastmcp-wrap-result 扩展支持客户端解包
    • SDK 和客户端都需要配套的包装/解包逻辑
  3. 这些变通方案已经导致实际 bug:

    • Issue #2455:不含 type: object 的 $ref schema 导致服务器上的所有工具失效
    • Issue #2421:意外的 {"result": ...} 包装令用户困惑

这表明当前限制确实给生态系统带来了阻力,而 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 实现需要:

  1. 更新 inputSchema 类型,保留 type: "object",但允许任何其他 JSON Schema 属性
  2. 更新 outputSchema 类型,允许任何有效的 JSON Schema(移除 type: "object" 限制)
  3. 更新 structuredContent 类型,接受任何有效的 JSON 值
  4. 相应地更新 JSON Schema 定义

致谢

本提案建立在 GitHub issue #834 的讨论之上,并吸收了 MCP 社区的反馈。