深度解析)
桌面应用开发者工具人工智能AI 应用AI Agent代码智能体【免费下载链接】warpWarp 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.0Warp 与 MCP 服务器之间的工具调用参数需要先从 warp-server 经 protobuf 传输到 Warp 客户端。这段链路的类型丢失是问题的根源LLM 生成工具调用的原始 JSON 字符串例如{line: 5}在 warp-server 侧通过json.Unmarshal解析后被包装进google.protobuf.Struct即structpb随ClientAction下发。Struct的NumberValue字段将所有数值统一存储为float64原始 JSON 中整数5与浮点5.5的区别在这一步被彻底抹掉。Rust 客户端把structpb还原为serde_json::Value时得到的是Number内部的f64(5.0)后续无论由serde_json::Number::from_f64构造还是借助 ryu 格式化器序列化f64(5.0)都会输出为5.0而非5。采用严格类型解析的 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的类型是ArcJsonObject其中JsonObject serde_json::MapString, Valueschema 本身就是原生的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: OptionUuid, tool_name: str, ) - Optionstd::sync::Arcrmcp::model::JsonObject { let mut candidates: Boxdyn IteratorItem 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 tools 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::MapString, serde_json::Value, input_schema: serde_json::MapString, 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查找、异步派发之前插入 coercionlet 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 tools 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”一节给出了强制转换的完整行为约定表该表是验收的行为基线必须原样遵守用例输入参数Schematype预期结果整数字段 整数形态浮点{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为对象 schemaarray_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_coercedadditionalProperties兜底 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 actionOption 2作为后续跟踪项而非阻塞项八、验证方式设计文档给出的验证与回归路径分三层单元测试覆盖上文的coerce_integer_args行为表派发与渲染共用同一 helper测试一次即可。手动验证将 GoLand MCP 服务器接入 Warp调用get_symbol_info并传入合法的 file/line/column确认返回符号查询结果而非Failed to parse literal 5.0解析错误同时验证number型参数如temperature: 0.7照常工作、无整数参数的 schema 不受影响、schema 查找失败时仍能照常派发。回归运行cargo nextest run --no-fail-fast --workspace确认无 MCP 相关失败通过./script/presubmitfmt 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代码智能体【免费下载链接】warpWarp is an agentic development environment, born out of the terminal.项目地址https://gitcode.com/GitHub_Trending/wa/warp点击查看免费下载相关推荐MCP for Beginners 实战为 MCP 客户端接入 LLM用自然语言驱动工具调用MCP for Beginners 实战为 MCP 客户端接入 LLM用自然语言驱动工具调用 本教程来自开源课程 mcp for beginners 的第三教程文档人工智能创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考