AI 技术
#MCP#JSON Schema#工具调用#Agent#参数校验

MCP 工具参数类型强制转换:为什么模型输出的字符串数字会让服务端校验失败

Agent 调用带数值与布尔参数的 MCP 工具时,模型常把 5 写成 "5"、把 true 写成 "true",服务端按 inputSchema 严格校验就会直接拒绝。本文用一个库存查询工具贯穿全文,梳理类型声明、序列化路径、隐式转换歧义与枚举大小写问题,并对比客户端预校验与服务端宽松解析的取舍。

一个被拒绝的库存查询

假设我们给 Agent 接了一个 MCP 服务器,暴露一个 query_inventory 工具:查某个仓库里某个 SKU 的库存,参数包括仓库编号 warehouse_id(字符串)、SKU(字符串)、是否包含在途库存 include_in_transit(布尔)、以及返回条数上限 limit(整数)。

用户问“上海仓 A-1024 还有多少货,顺便看看在途的,最多列 20 条”。模型很顺畅地拼出了调用:warehouse_id: "SH"、sku: "A-1024"、include_in_transit: "true"、limit: "20"。客户端把这段参数原样塞进 tools/call 的 arguments,服务端用 inputSchema 校验,发现 limit 是字符串而不是整数,include_in_transit 是字符串而不是布尔,直接返回参数错误。

模型没有“说错话”,它只是把参数写成了 JSON 里的字符串。这类失败在 Agent 接工具的场景里很常见,而且往往看起来像“工具坏了”,实际是类型边界没对齐。

类型声明如何变成服务端校验

MCP 规范里,每个工具用 name、description、inputSchema 等字段描述自己。inputSchema 是一段 JSON Schema,声明参数的预期类型。资料 1 给出的 get_weather 示例中,location 被声明为 {"type": "string"},并列入 required。资料 3 的 calculate_sum 示例则把 a、b 声明为 {"type": "number"}。

JSON Schema 的 type 关键字是校验的基础。资料 2 明确说明:type 取单个字符串时,实例数据只有匹配该类型才有效;type 也可以写成字符串数组,此时匹配其中任意一个类型即有效。同一份资料给出的例子很直接:schema 为 {"type": "number"} 时,42 和 42.0 合规,"42" 不合规。

这意味着,服务端只要按 inputSchema 做严格校验,字符串 "20" 就不会被当作整数 20 接受。JSON Schema 把 JSON 的基本类型分成 array、boolean、null、数值类型、object、string 等,资料 2 还指出 JSON 没有单独区分整数和浮点数——integer 与 number 的差别由校验实现处理。对服务端来说,"20" 是 string,20 是 number,两者在类型层面没有交集。

这里有一个容易被忽略的层次:MCP 规范要求服务器验证所有工具输入(资料 4 的安全考虑部分),但没有规定必须用哪种校验库、是否允许类型强制转换。校验的严格程度是服务端实现的选择,不是协议强制的行为。

模型为什么把数字和布尔写成字符串

要理解这个现象,得看参数从模型到服务端的路径。模型生成的是 token 序列,工具调用参数通常以文本形式产生,再被客户端解析成 JSON。当模型“想”表达数字 20 时,它输出的可能是字符 2 和 0,客户端在组装 arguments 时如果按字符串处理,就会得到 "20"。

布尔值同理。模型可能输出 "true" 而不是 true,因为引号内的 true 和裸 true 在文本层面只差两个引号,而模型在生成时未必区分这两种表示。

还有一层来自 schema 本身。如果工具描述里写“limit:返回条数,默认 20”,模型看到的是自然语言,不是类型约束。它可能把 limit 理解成一个“可填可不填的说明性字段”,于是填成字符串。工具描述没有强调类型时,模型缺少纠正信号。

枚举值的大小写也会出问题。假设 include_in_transit 不是布尔,而是枚举 ["true", "false"],模型输出 "True" 或 "TRUE",服务端按枚举精确匹配就会失败。JSON Schema 的 enum 默认是精确匹配,资料 2 没有给出大小写不敏感的选项,校验实现通常按字面比较。

一次调用的完整数据流

把上面的场景走一遍,能看清失败发生在哪一步。

flowchart TD
    A[用户提问: 上海仓 A-1024 库存] --> B[模型生成工具调用意图]
    B --> C[模型输出参数文本]
    C --> D[客户端解析为 arguments]
    D --> E{客户端是否预校验}
    E -->|否| F[发送 tools/call]
    E -->|是| G{类型是否匹配}
    G -->|不匹配| H[客户端拦截并回问模型]
    G -->|匹配| F
    F --> I[服务端按 inputSchema 校验]
    I --> J{严格校验是否通过}
    J -->|否| K[返回参数错误]
    J -->|是| L[执行工具并返回结果]
    K --> M[模型看到错误并尝试修正]
    M --> B

关键转折在 E 和 J 两个判断点。客户端预校验能在本地拦下类型错误,避免一次网络往返;服务端校验是最后一道防线,但它只能拒绝,不能替模型“猜”意图。

资料 4 的错误处理部分把工具错误分成两类:协议错误(如未知工具、无效参数)和工具执行错误(isError: true)。类型不匹配通常落在协议错误一侧,返回的是 JSON-RPC 错误对象。模型看到错误后可能重试,但如果它不理解“类型”这个概念,重试往往还是同样的字符串。

严格校验、隐式转换与枚举匹配的取舍

服务端面对 "20" 时有三种处理方式,各有代价。

处理方式行为优点风险
严格校验类型不匹配直接拒绝语义清晰,不会误读参数模型反复失败,需要重试或人工介入
隐式转换尝试把字符串转成数字或布尔提高一次调用成功率转换规则不统一,"1" 可能被当成 true 或 1
客户端预校验发送前在本地检查类型减少无效请求,反馈更快客户端需要维护一份校验逻辑,可能与服务端不一致

隐式转换的歧义最值得警惕。字符串 "1" 转布尔时,有的实现按“非空即真”处理,得到 true;有的按数字解析,得到 1 再转 true;还有的只认 "true" 和 "false"。同一个参数在不同实现下可能得到不同结果。如果这个参数控制的是“是否包含在途库存”,误判会让返回结果多出一批在途数据,用户看到的是错误的库存数字。

枚举大小写同理。"True" 和 "true" 在人类看来一样,在校验器看来是两个不同的字符串。服务端如果做大小写归一化,就得明确哪些枚举值允许归一化;如果全部归一化,又可能掩盖模型对枚举语义的误解。

客户端预校验的代价是双份维护。客户端从 tools/list 拿到 inputSchema,理论上可以复用同一份 schema 做校验。但客户端和服务端可能用不同的校验库,对 integer 与 number、format 等关键字的处理不完全一致。资料 2 提到,format 默认只是注解,不参与校验;校验实现可以选择把它当断言。如果客户端开启而服务端关闭,两边行为就会分叉。

把校验点放在哪里

一个可落地的分工是:客户端做“能提前发现”的校验,服务端做“必须守住”的校验。

客户端在发送 tools/call 前,用 inputSchema 检查参数类型。发现 limit 是字符串时,不直接发送,而是把错误信息回给模型,让它重新生成参数。这样模型能在同一轮对话里修正,用户感知不到失败。

服务端仍然要独立校验。客户端可能被绕过,或者用了旧版本的 schema。服务端按 inputSchema 严格校验,拒绝类型不符的请求,并在错误信息里写清楚哪个字段、期望什么类型、实际收到什么类型。错误信息越具体,模型修正的概率越高。

对于确实需要宽松处理的参数,可以在 schema 层面放宽,而不是在校验器里偷偷转换。例如把 limit 声明为 {"type": ["integer", "string"]},再在工具实现里显式解析。这样类型转换是工具逻辑的一部分,有明确的代码位置,而不是藏在校验器的默认行为里。

枚举参数可以在工具描述里写明大小写要求,并在实现里做归一化。归一化逻辑要可测试,不能依赖“应该没问题”的假设。

需要观察的信号与失败模式

线上跑起来后,有几类指标能帮你判断问题出在哪。

参数校验失败率按字段统计。如果 limit 的失败率明显高于其他字段,说明模型对这个字段的类型理解有偏差,可能需要改描述或加示例。

重试次数分布。一次调用失败后模型重试几次才成功,重试次数高说明错误信息不够明确,或者模型没有能力从错误中恢复。

客户端预校验拦截率。如果拦截率很高,说明模型输出字符串数字是常态,客户端预校验确实在起作用;如果拦截率低但服务端失败率高,说明两边校验逻辑不一致。

枚举值分布。统计 include_in_transit 实际收到的值,如果出现 "True"、"TRUE"、"1" 等变体,说明模型在枚举上不稳定,需要收紧描述或做归一化。

失败模式里还有一类容易被忽略:模型把可选参数填成了空字符串。limit 声明为可选,模型没想好填什么,就填了 ""。严格校验会拒绝,隐式转换可能把空字符串当成 0 或默认值。空字符串的语义在 JSON Schema 里是合法的 string,但在工具逻辑里通常没有意义。工具实现需要显式处理这种情况。

边界与未解决的问题

类型强制转换的问题不会因为一次修复就消失。模型版本更新、工具描述修改、客户端校验库升级,都可能让原本能通过的调用重新失败。

一个尚未有统一答案的问题是:错误信息应该多详细。写得太少,模型无法修正;写得太多,可能暴露服务端内部结构。MCP 规范没有规定错误信息的格式,这部分留给实现者判断。

另一个问题是多客户端场景。同一个 MCP 服务器可能被多个客户端调用,各客户端的预校验能力不同。服务端不能假设客户端会做校验,也不能假设客户端不做校验。服务端的严格校验是唯一稳定的底线。

如果你的工具参数里有数值或布尔,先检查 inputSchema 是否声明了正确类型,再检查工具描述是否给了模型足够的类型提示。客户端预校验和服务端严格校验各司其职,中间不要插入没有文档的隐式转换。

资料来源

  1. Model Context Protocol Specification - Tools
  2. JSON Schema - Type-specific keywords
  3. 工具 | MCP 中文文档
  4. 工具 – MCP 中文站(Model Context Protocol 中文)