news 2026/10/4 4:30:33

Warp 中修复 MCP 工具调用整数参数被序列化为浮点:schema 驱动的客户端强制转换(Integer Coercion)深度解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Warp 中修复 MCP 工具调用整数参数被序列化为浮点:schema 驱动的客户端强制转换(Integer Coercion)深度解析
  • 桌面应用
  • 开发者工具
  • 人工智能
  • AI 应用
  • AI Agent
  • 代码智能体

【免费下载链接】warp

Warp is an agentic development environment, born out of the terminal.

项目地址:https://gitcode.com/GitHub_Trending/wa/warp
点击查看免费下载

本文基于开源仓库GitHub_Trending/wa/warp中的设计文档 specs/APP-4105/TECH.md(配套产品文档见 specs/APP-4105/PRODUCT.md)撰写,并结合仓库内已落地的源码与测试用例,完整剖析“MCP 工具调用中整数参数被写成5.0导致严格 MCP 服务器拒绝调用”这一经典跨进程类型丢失问题,以及 Warp 采用的、完全客户端侧、由工具自身 JSON Schema 驱动的修复方案。读完本文,你将理解structpb往返过程中的精度丢失链路、serde_json的Number表示差异、如何利用工具input_schema的"type": "integer"关键字在调度前完成安全强制转换,以及该方案在派发(dispatch)与渲染(render)两条路径上的落地点和全部边界行为。

一、问题:整数参数为何在 wire 上变成5.0

Warp 与 MCP 服务器之间的工具调用参数,需要先从 warp-server 经 protobuf 传输到 Warp 客户端。这段链路的类型丢失是问题的根源:

  1. LLM 生成工具调用的原始 JSON 字符串,例如{"line": 5},在 warp-server 侧通过json.Unmarshal解析后,被包装进google.protobuf.Struct(即structpb)随ClientAction下发。
  2. Struct的NumberValue字段将所有数值统一存储为float64,原始 JSON 中整数(5)与浮点(5.5)的区别在这一步被彻底抹掉。
  3. Rust 客户端把structpb还原为serde_json::Value时,得到的是Number内部的f64(5.0);后续无论由serde_json::Number::from_f64构造,还是借助 ryu 格式化器序列化,f64(5.0)都会输出为"5.0"而非"5"。
  4. 采用严格类型解析的 MCP 服务器(例如 GoLand 的 JVM 系服务器、启用 Pydantic 严格模式的 Python 服务器、用i64反序列化的 Rust 服务器)在解析整数字段时会直接报错,典型错误信息如:
Failed to parse literal '5.0' as an int value

设计文档中明确指出,此时工具调用虽然已被派发,但 MCP 服务器返回解析错误,对用户表现为“工具完全不可用”——即使 LLM 生成的参数语义完全正确。PRODUCT.md 中以 GoLand MCP 服务器的get_symbol_info(携带line/column整数参数)为例复现了该场景。

二、修复思路:以工具自身 input_schema 为基准的客户端强制转换

修复方案的核心设计原则是完全收编在客户端(warp-internal)内部,不涉及任何 proto、server 或协调式部署变更。具体做法是:在通过rmcp派发工具调用之前,先查找到该工具缓存的input_schema,凡是 schema 中声明为"type": "integer"的属性,若参数值为整数形态的f64(即小数部分为 0),则将其重写为i64。

关键前提(TECH.md 原文)在于:rmcp::model::Tool.input_schema的类型是Arc<JsonObject>,其中JsonObject = serde_json::Map<String, Value>,schema 本身就是原生的serde_json值;而 JSON Schema 规范中的"type"关键字是普通 JSON 字符串,因此判断字段是"integer"还是"number",本质上就是一次字符串比较。rmcp 并没有为通用工具input_schema暴露类型化枚举(它确实在elicitation_schema.rs中提供了类型化的const_string标记,但那只作用于 MCP elicitation 请求而非工具调用),所以用字面量字符串"integer"比较是符合规范习语的做法。

修复目标(来自 PRODUCT.md)非常收敛:

  • 整数型参数(如line: 5)能被要求字面量5的严格服务器接受;
  • 浮点型参数(如temperature: 0.7)完全不受影响;
  • 混合型或纯字符串参数不受影响;
  • 不改变任何服务端行为与 LLM prompt。

三、两处消费点:派发路径与渲染路径

在引入修复前,AIAgentActionType::CallMCPTool的input在客户端存在两个消费方,且都没有任何 schema 感知的校正:

  • 派发路径(call_mcp_tool.rs 中的CallMCPToolExecutor::execute):解构 action 后把input转成serde_json::Map,再经CallToolRequestParam交给rmcp。整数字段在这里是f64(5.0)。
  • 渲染路径(block.rs 中CallMCPTool的 match 分支):通过format!("MCP Tool: {name} ({input})")构造 block 详情字符串并交给handle_mcp_tool_stream_update。serde_json::Value的Display实现对整数形态的f64输出为5.0,导致 blocklist UI 中展示的参数形态与 MCP 服务器实际收到的 wire 内容一致地“失真”。

修复后的两条路径各自独立执行同一套强制转换:派发前与渲染展示前都对参数做一次 schema 驱动的 coercion,使 UI 展示的字面量形态与真正发送给 MCP 服务器的字面量形态保持完全一致(渲染出5而非5.0)。

四、源码级实现拆解

4.1 新增 schema 查询辅助方法tool_input_schema

在 app/src/ai/mcp/templatable_manager.rs 中,TemplatableMCPServerManager新增了公开方法tool_input_schema,用于跨活动 MCP 服务器按工具名查找input_schema:

/// Returns the JSON Schema `input_schema` for a named tool across active MCP servers. /// /// If `installation_id` is `Some`, only that server is considered; otherwise, the /// first active server providing a matching tool name wins (matching the existing /// `server_with_tool_name` lookup behavior). pub fn tool_input_schema( &self, installation_id: Option<Uuid>, tool_name: &str, ) -> Option<std::sync::Arc<rmcp::model::JsonObject>> { let mut candidates: Box<dyn Iterator<Item = &TemplatableMCPServerInfo>> = if let Some(uuid) = installation_id { Box::new(self.active_servers.get(&uuid).into_iter()) } else { Box::new(self.active_servers.values()) }; candidates.find_map(|server| server.tool_input_schema(tool_name)) }

注意其查找语义与既有的 server_with_tool_name 保持一致:若提供installation_id则只在指定服务器内查找;否则取第一个提供匹配工具名的活动服务器。该辅助方法紧邻既有方法tools_for_server(返回某服务器缓存的全部rmcp::model::Tool)而存在,schema 数据源同样是TemplatableMCPServerInfo中缓存的工具列表。由于tool_input_schema返回的是Arc克隆,调用方无需持有 manager 的借用即可安全使用。

4.2 核心强制转换函数coerce_integer_args

设计文档给出的是一个只处理顶层properties的纯函数版本:

/// Coerces float-valued entries in `args` to integers for fields declared /// as `"type": "integer"` in the tool's JSON Schema `input_schema`. /// /// Only top-level properties are handled. Nested objects, arrays, and /// JSON Schema combinators (`anyOf`/`oneOf`/`$ref`) are left unchanged. fn coerce_integer_args( args: &mut serde_json::Map<String, serde_json::Value>, input_schema: &serde_json::Map<String, serde_json::Value>, ) { let Some(properties) = input_schema .get("properties") .and_then(|p| p.as_object()) else { return; }; for (key, prop_def) in properties { let is_integer = prop_def.get("type").and_then(|t| t.as_str()) == Some("integer"); if !is_integer { continue; } if let Some(serde_json::Value::Number(n)) = args.get_mut(key) { if let Some(f) = n.as_f64() { if f.fract() == 0.0 { if let Ok(i) = i64::try_from(f as i128) { *n = serde_json::Number::from(i); } } } } } }

而当前仓库中实际落地的实现(call_mcp_tool.rs)在保持“纯函数、可单测”的前提下做了显著增强:它把顶层与嵌套层统一为一次递归遍历(coerce_value_against_schema),并将入口包装成对整个Value的操作,使得顶层oneOf/anyOf/allOf与additionalProperties的处理方式同深层一致。实际实现的关键点:

  • schema_declares_integer:既支持"type": "integer"字符串形式,也支持可空形式的"type": ["integer", "null"]数组;
  • coerce_number_to_int:对已经是i64/u64的值直接跳过;仅当f.fract() == 0.0且i64::try_from(f as i128)成功时才重写为整数,溢出时静默跳过;
  • 递归遍历:对oneOf、anyOf、allOf三个组合关键字全部遍历(而非只走第一个匹配分支),对properties逐键应用对应子 schema,对additionalProperties的 object 形式应用兜底 schema,对数组的items分别支持“对象 schema 应用到每个元素”和“schema 数组(tuple 校验)按位置配对”两种形态;$ref由于需要根 schema 解析,明确跳过。

文档中原本将“嵌套/数组 coercion”列为延后的 follow-up,但仓库现状表明:当出现真实用例(测试注释提到 issue #10596,一个filters数组中oneOf分支声明毫秒时间戳为integer的场景)后,递归实现已经落地。因此阅读源码时应以实际实现为准。

4.3 派发路径的接入(dispatch site)

在 call_mcp_tool.rs 的CallMCPToolExecutor::execute中,原有arguments解构从不可变改为可变,并在完成templatable_peer查找、异步派发之前插入 coercion:

let serde_json::Value::Object(mut arguments) = input.clone() else { return ActionExecution::Sync(AIAgentActionResultType::CallMCPTool( CallMCPToolResult::Error("MCP server tool input not an object".to_owned()), )); }; // Prefer the templatable server over the legacy server if both exist. let templatable_mcp_manager = TemplatableMCPServerManager::as_ref(ctx); // Coerce whole-number f64 args to i64 for fields declared as `"type": "integer"` // in the tool's input schema. ... Without coercion, the ryu formatter serializes // whole-number f64 as "5.0", which strict MCP servers (e.g. GoLand) reject. if let Some(schema) = templatable_mcp_manager.tool_input_schema(*server_id, name.as_str()) { coerce_integer_args(&mut arguments, &schema); }

随后保持原有的服务器查找逻辑不变(server_with_installation_id_and_tool_name与server_with_tool_name二选一),通过reconnecting_peer.call_tool(CallToolRequestParams::new(...).with_arguments(arguments), ...)完成派发。coercion 失败(schema 查询返回None)时直接跳过,行为与修复前完全一致,不引入任何回归路径。

4.4 渲染路径的接入(block detail)

在 app/src/ai/blocklist/block.rs 的CallMCPTool渲染分支中,构建command_text之前执行与派发相同的 coercion,使 UI 显示的MCP Tool: name ({...})字面量与真正发送到 wire 的内容一致:

AIAgentActionType::CallMCPTool { server_id, name, input } => { // Coerce the display value the same way dispatch does, so the // rendered MCP tool call detail shows `5` instead of `5.0`. let display_input = match input { serde_json::Value::Object(map) => { let mut map = map.clone(); if let Some(schema) = crate::ai::mcp::TemplatableMCPServerManager::as_ref(ctx) .tool_input_schema(*server_id, name.as_str()) { crate::ai::blocklist::action_model::coerce_integer_args(&mut map, &schema); } serde_json::Value::Object(map) } other => other.clone(), }; let command_text = if display_input.is_null() { format!("MCP Tool: {name}") } else { format!("MCP Tool: {name} ({display_input})") }; self.handle_mcp_tool_stream_update(action_id, name, &command_text, display_input, *server_id, ctx); }

为了让渲染路径复用同一份逻辑,coerce_integer_args在 call_mcp_tool.rs 中被声明为pub(crate),并经 action_model/execute.rs 以pub(crate) use call_mcp_tool::coerce_integer_args;重新导出,避免 coercion 逻辑重复实现。

两条调用点刻意保持独立:若渲染时 schema 查询失败(例如 MCP 服务器恰好断开),展示侧独立回退到未 coerc 的原始输入,不影响派发;反之亦然。设计文档提到,未来若有第三个action.input消费方,可考虑 Option 2(就地修改输出模型中缓存 action)以收敛该逻辑,但代价是改变持久化语义,因此在消费方超过两个之前推迟。

五、行为边界与单元测试验证

设计文档在“Unit tests”一节给出了强制转换的完整行为约定表(该表是验收的行为基线,必须原样遵守):

用例输入参数Schema"type"预期结果
整数字段 + 整数形态浮点{"line": 5.0}integer{"line": 5}
整数字段 + 非整数浮点{"line": 5.5}integer不变(5.5)
数字字段 + 整数形态浮点{"temp": 1.0}number不变(1.0)
字符串字段{"name": "foo"}string不变
schema 无properties键{"x": 1.0}—不变
多个混合字段{"line": 5.0, "file": "f.go"}line: integer, file: string{"line": 5, "file": "f.go"}
溢出(> i64::MAX){"n": 1e20}integer不变(跳过 coercion)

仓库中实际的测试文件 call_mcp_tool_tests.rs 将上述基线扩展为 17 个用例,覆盖了落地递归实现的所有分支:

  • 顶层整数字段、非整数形态浮点、number/无properties/无type键三种不变场景(no_coercion_when_not_typed_as_integer);
  • 嵌套对象内整数字段(nested_object_integer_is_coerced);
  • 数组items为对象 schema(array_items_integer_is_coerced)与 tuple 式 schema 数组(tuple_style_items_coerces_positional_schemas);
  • 可空类型数组形式"type": ["integer", "null"](nullable_integer_type_array_is_coerced),其中null值保持不变;
  • oneOf(属性级与根级)、anyOf、allOf组合关键字的遍历(one_of_branch_with_integer_is_coerced、any_of_at_property_level_is_coerced、all_of_with_integer_branch_is_coerced、root_level_one_of_branch_with_integer_is_coerced);
  • additionalProperties兜底 schema(属性级与根级,additional_properties_schema_is_applied、root_level_additional_properties_is_applied);
  • 同一层级同时出现多个组合关键字的遍历(multiple_combinators_at_same_level_are_all_traversed、one_of_with_multiple_branches_all_visited);
  • 负数整数形态浮点(negative_whole_float_is_coerced,-42.0→-42);
  • 已是整数的值保持不变(already_integer_value_is_unchanged)。

所有测试通过serde_json::to_string断言序列化结果为"5"(而非"5.0"),并用as_i64()确认 round-trip 后是i64。由于派发与渲染共用同一 helper,设计文档明确不需要额外的渲染层单测。

六、端到端流程

设计文档用一张时序图完整刻画了修复前后的完整链路(此处按原文转述为文本时序):

LLM → warp-server: 工具调用 JSON "{\"line\": 5}" warp-server → warp-server: json.Unmarshal → structpb.NewStruct warp-server → 客户端: ClientAction { args: Struct{line: NumberValue(5.0)} } 客户端 → 客户端: prost_to_serde_json → {line: Number(5.0)} (f64) 客户端 → 客户端: tool_input_schema(name) → Schema with line: integer 客户端 → 客户端: coerce_integer_args → {line: Number(5)} (i64) 客户端 → MCP Server: call_tool({"line": 5}) via rmcp MCP Server → 客户端: CallToolResult

其中“prost_to_serde_json 得到f64”对应类型丢失点,而“schema 查询 + coercion”是修复新增的两步。整个过程不经过任何 proto 变更或服务端协调。

七、风险与缓解措施

设计文档针对该方案可能的边界情况给出了逐项风险评估(下表继承自原文):

风险缓解措施
工具 schema 未缓存(调用中断线)tool_input_schema返回None,跳过 coercion,以既有的5.0参数继续调用——与现状行为一致
i64::try_from对超过i64::MAX的值溢出静默跳过 coercion,保留f64值;严格服务器会拒绝,与现状失败模式相同,无回归
schema 声明integer但 LLM 生成了非整数浮点(如5.5)保持不变。静默截断比让服务器明确拒绝更糟
超过 2⁵³ 的整数精度丢失(如大型 snowflake ID)该丢失在上游structpb往返中已经发生,客户端无法恢复;PRODUCT.md 中明确为非目标,唯一修复途径是未来在 proto 层传递原始 JSON 字符串
与“期望整数字段为5.0”的 MCP 工具冲突按 JSON Schema 规范不可能:"integer"明确表示“无小数部分的数字”,严格服务器要求字面量5,宽松服务器两种都接受
渲染与派发出现漂移(一方 schema 查询成功、另一方失败)可接受:失败侧回退到原始值,最坏情况是“UI 展示的就是实际发送的”,诚实但略有损耗
未来新增action.input消费方忘记 coercion若出现第三个消费方,重构为共享 helper 或就地修改 canonical action(Option 2),作为后续跟踪项而非阻塞项

八、验证方式

设计文档给出的验证与回归路径分三层:

  1. 单元测试:覆盖上文的coerce_integer_args行为表(派发与渲染共用同一 helper,测试一次即可)。
  2. 手动验证:将 GoLand MCP 服务器接入 Warp,调用get_symbol_info并传入合法的 file/line/column,确认返回符号查询结果而非Failed to parse literal '5.0'解析错误;同时验证number型参数(如temperature: 0.7)照常工作、无整数参数的 schema 不受影响、schema 查找失败时仍能照常派发。
  3. 回归:运行cargo nextest run --no-fail-fast --workspace确认无 MCP 相关失败;通过./script/presubmit(fmt + clippy + tests)完成提交前检查。

九、后续演进方向

设计文档在 Follow-ups 中明确了三类后续工作,其中部分已在当前仓库落地:

  • 嵌套 / 数组 coercion:原设计文档将递归扩展列为“出现具体用例后再做”的延后项;当前仓库已实现递归遍历(含组合关键字、additionalProperties、tupleitems、可空类型数组),并有对应测试佐证。
  • 长期 proto 级修复:根因在于 warp-server 侧structpb.NumberValue的有损编码。在CallMCPToolproto 中增加raw_args_json字符串字段可以完整保留精度(含超过 2⁵³ 的整数)并彻底消除这类问题,但不在本 PR 范围内,单独跟踪。
  • 调试日志:考虑在 coercion 实际生效时输出log::debug!,以辅助诊断 MCP 集成问题。

小结

Warp 对 MCP 整数参数问题的修复,是“跨进程类型丢失”问题的教科书式客户端补救:它不改协议、不动服务器,而是利用 MCP 工具自身携带的 JSON Schema 元数据,在派发与展示两个消费点统一执行幂等、单调、绝不截断的强制转换。其设计文档(TECH.md)对边界行为的严谨定义(number不动、非整数不动、溢出不动、schema 缺失不动)与仓库中 17 个单元测试 的完整覆盖,共同保证了该修复在不引入任何回归的前提下,让严格类型 MCP 服务器得以正常工作,也让 blocklist UI 中的工具调用详情第一次做到了与 wire 字面量完全一致。

  • 桌面应用
  • 开发者工具
  • 人工智能
  • AI 应用
  • AI Agent
  • 代码智能体

【免费下载链接】warp

Warp is an agentic development environment, born out of the terminal.

项目地址:https://gitcode.com/GitHub_Trending/wa/warp
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/4 4:30:20

AGV/RGV工业调度系统开发:A*算法的产线级改造与多车协同实战

1. 项目概述&#xff1a;这不是写个“小车动起来”的Demo&#xff0c;而是构建工业级调度系统的起点AGV、RGV车辆控制调度系统开发——光看标题&#xff0c;很多人第一反应是“不就是让小车按路径走&#xff1f;用个A算法画条线&#xff0c;再发几个串口指令不就完事了&#xf…

作者头像 李华
网站建设 2026/10/4 4:29:14

FPGA图像通路实战:OV5640与VGA硬件协同原理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 4:28:03

洛谷P14924宝石项链:倍增+动态规划解环形取段问题

最近带一个备考GESP八级的学生&#xff0c;刷到洛谷P14924这道“宝石项链”时&#xff0c;他第一反应是“这不就是个环形字符串问题吗”&#xff0c;然后一头扎进最小表示法和区间DP里出不来。我瞄了一眼题面里的数据范围和操作方式&#xff0c;直接跟他说&#xff1a;别绕了&a…

作者头像 李华
网站建设 2026/10/4 4:27:49

C# AES加密解密实战:字符串与文件加密完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 4:26:31

Windows 上 Redis 后台启动的四种方案与配置实践

1. 为什么要在 Windows 上后台运行 Redis1.1 先搞懂 Redis 在本地开发里的角色Redis 是我见过最“低调”的基础组件。它不会像数据库那样有一堆表结构让你设计&#xff0c;也不会像消息队列那样需要专门部署一套管理系统&#xff0c;但它几乎出现在所有后端系统的核心链路上&am…

作者头像 李华
网站建设 2026/10/4 4:25:34

Java台球游戏开发实战:从工程结构到碰撞物理与性能优化

简介&#xff1a;这是一份基于Java开发的台球游戏源码&#xff0c;面向具备Java基础、希望入门游戏开发或课程设计的学习者&#xff0c;可用于理解桌面小游戏从界面到逻辑的完整实现。压缩包共427个文件&#xff0c;约2.22MB&#xff0c;以194个png图片资源、168个class编译文件…

作者头像 李华