一个被拒绝的库存查询
假设我们给 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 是否声明了正确类型,再检查工具描述是否给了模型足够的类型提示。客户端预校验和服务端严格校验各司其职,中间不要插入没有文档的隐式转换。