6.2 KiB
APP-4105: Fix MCP Tool Call Integer Parameter Coercion
Summary
MCP tool calls that include integer-typed parameters fail on strict MCP servers because Warp serializes those values as floats (e.g. 5.0) instead of integers (e.g. 5) in JSON. The fix coerces float-valued arguments back to integers at the client before dispatching, using the tool's own JSON Schema "type" keyword to decide which fields need coercion.
Problem
When a user invokes an MCP tool that has integer parameters (e.g. line and column in a code navigation tool), some MCP servers — including the GoLand MCP server — reject the call with a parsing error like:
Failed to parse literal '5.0' as an int value
This makes those tools completely unusable from Warp, even though the model generates semantically correct arguments. The failure is invisible to the user: the tool call is dispatched, but the MCP server returns an error.
The root cause is that the LLM's original JSON string (e.g. {"line": 5}) is parsed into a google.protobuf.Struct on the server. Struct's NumberValue stores all numbers as float64, erasing the integer/float distinction. When the Rust client re-serializes to JSON via serde_json::Number::from_f64, whole-number floats become 5.0 on the wire instead of 5. JSON Schema deserializers in strict-typed languages (Kotlin/Java, strict Python, Rust with i64) reject 5.0 for integer-typed fields.
Goals
- MCP tool calls with integer-typed parameters are accepted by strict MCP servers that require
5rather than5.0in JSON. - MCP tool calls with float-typed parameters (e.g.
temperature: 0.7) continue to work correctly. - MCP tool calls with mixed or string-only parameters are unaffected.
- No server-side or proto changes — the fix is entirely client-side.
Non-goals
- Fixing the underlying loss of type information in the
structpb.NumberValuewire format. The lossy server-side encoding remains; the fix restores the integer literal at dispatch time using the tool's schema. - Preserving full 64-bit integer precision for values above 2⁵³. Those have already been rounded by the
float64round-trip before the client sees them; this fix can restore the integer literal form but not the lost bits. In practice this is only a concern for very large IDs and timestamps; typical MCP tools use small integers. - Walking nested objects, arrays, or JSON Schema combinators (
anyOf/oneOf/$ref). Only top-level properties with"type": "integer"are coerced. No known MCP tool requires more than this today. - Changing server-side behavior or the LLM prompt.
Figma
Figma: none provided (no UI changes).
User Experience
Normal case — integer parameters
Given a tool get_symbol_info with schema:
{ "properties": { "line": { "type": "integer" }, "column": { "type": "integer" }, "filePath": { "type": "string" } } }
When the model generates {"line": 5, "column": 1, "filePath": "server.go"}, the MCP server receives:
{"line": 5, "column": 1, "filePath": "server.go"}
(integers, not 5.0/1.0).
Normal case — number (float) parameters
Given a tool with schema { "properties": { "temperature": { "type": "number" } } } and model input {"temperature": 0.7}, the MCP server receives {"temperature": 0.7} unchanged. Floats are never touched.
If the model happens to generate a whole-number value for a number-typed field (e.g. {"temperature": 1.0}), this is left as-is and serialized as a float — which is correct, since the schema declared the field as number.
Schema lookup failure
If the tool's input_schema cannot be located (server disconnected mid-call, tool name mismatch, no active server matches), the call proceeds with the arguments unchanged. The failure mode is no worse than today.
Non-integer-valued floats in integer fields
If the LLM generates a non-whole float like {"line": 5.5} for an integer-typed field, the value is left unchanged (not coerced). The MCP server will reject it as invalid — this is correct behavior; the LLM generated bad input, and silent truncation would be worse than a clear error.
MCP tool call detail in the blocklist
The rendered MCP tool call detail (shown in each AI block alongside the tool name) displays the same arguments that will actually be dispatched. An integer-typed parameter is rendered as 5, not 5.0. This keeps the UI honest about what the MCP server will receive — a user reading the block can trust that the literal form they see matches what's sent over the wire.
The same coercion step that runs at dispatch time also runs when building the block-detail display string, so render and dispatch stay in lock-step. If the schema lookup fails for either path, that path falls back to the raw (uncoerced) values independently — there is no assumption of coupling between the two.
Success Criteria
- Calling a GoLand MCP tool with integer-typed parameters (e.g.
get_symbol_infowithline: 5) succeeds and returns a result, rather than returning a parse error. - A tool with
number-typed parameters (e.g.temperature: 0.7) continues to work correctly. - A tool with no integer parameters in its schema is unaffected.
- A tool whose schema cannot be found at dispatch time still dispatches (with uncoerced arguments) rather than erroring out of the coercion path.
- The MCP tool call detail rendered in the blocklist shows integer-typed fields as integers (e.g.
5), matching what the MCP server will receive. - Existing MCP tool call tests continue to pass.
Validation
- Manual: Connect the GoLand MCP server to Warp and invoke
get_symbol_infoat a valid line/column. Confirm a valid result is returned rather than aFailed to parse literal '5.0'error. - Unit tests on the coercion helper:
integer-typed field with whole-number float value → coerced to integerinteger-typed field with non-whole float value → unchangednumber-typed field with whole-number float value → unchangedstring-typed field → unchanged- missing
propertieskey in schema → no-op - schema absent entirely → no-op
- Regression: Run existing MCP-related tests and confirm no failures.
Open Questions
- None at this time.