Repository navigation
Float fields fail to deserialize when serde_json's arbitrary_precision is enabled elsewhere in the build #1299
Description
When any crate in a downstream build enables serde_json/arbitrary_precision, Cargo feature unification turns it on for rmcp as well, and every f32/f64 field that rmcp reads through serde's buffered path fails on a decimal with invalid type: map, expected f32. Integers still parse, so only fractional values are affected. Because ServerResult and ServerNotification are untagged unions ending in a catch-all, nothing reports the error: a tools/call result silently becomes CustomResult and a fractional notifications/progress becomes CustomNotification.
This is serde's known limitation with that feature (serde-rs/json#721, serde-rs/serde#1183): serde replays a buffered decimal as serde_json's private number map, which a plain float field cannot read. rmcp cannot keep the feature out of a downstream build, and the case is real: the OpenAI Codex CLI gets it from starlark 0.14.2 (unconditionally) and from one of its own crates, and as a result every tool call to a server that sets a fractional annotations.priority fails there (openai/codex#38979).
Reproduction
[dependencies]
rmcp = { version = "=3.4.1", default-features = false, features = ["client"] }
serde_json = "1"
[features]
ap = ["serde_json/arbitrary_precision"]use rmcp::model::{JsonRpcMessage, ServerResult};
use rmcp::service::{RoleClient, RxJsonRpcMessage};
let line = r#"{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"ok","annotations":{"priority":0.6}}]}}"#;
let msg: RxJsonRpcMessage<RoleClient> = serde_json::from_str(line).unwrap();
let JsonRpcMessage::Response(resp) = msg else { unreachable!() };
println!("{}", matches!(resp.result, ServerResult::CallToolResult(_)));| Input | without ap |
with ap |
|---|---|---|
tools/call result, priority: 0.6 |
CallToolResult |
CustomResult |
tools/call result, priority: 1.0 |
CallToolResult |
CustomResult |
tools/call result, priority: 1 |
CallToolResult |
CallToolResult |
CallToolResult parsed directly, priority: 0.6 |
Ok |
Err(invalid type: map, expected f32) |
notifications/progress, progress: 0.5 |
ProgressNotification |
CustomNotification |
The same happens on 3.2.0. It also reaches rmcp talking to itself: an f64 progress of 1 serializes as 1.0, so in such a build an rmcp client drops the progress notifications an rmcp server sends. Running the crate's own tests with the feature on, test_request_timeout_progress (two tests) and stateless_server_first_request_handler_can_send_notifications fail on main for that reason.
Affected fields (main at 6e47ca3)
Annotations.priority(model/annotated.rs)ProgressNotificationParam.progress,total(model.rs)CreateMessageRequestParams.temperature(model.rs)ModelPreferences.cost_priority,speed_priority,intelligence_priority(model.rs)NumberSchema.minimum,maximum,default(model/elicitation_schema.rs)
Proposal
Deserialize these fields through serde_json::Number, which understands both representations, with pub(crate) helpers in model/serde_impl.rs and #[serde(deserialize_with = ...)] (plus default on the Option fields). Serialization, the generated schemas and the public API do not change, and without the feature the decoded values are the same as today, since serde_json already reads an f32 field as an f64 and casts it. With the feature on, every row above decodes as CallToolResult or ProgressNotification, including spellings such as 0.60 and 6e-1, 0.6 decodes to exactly 0.6f32, and a string priority is still rejected.
I am opening a pull request with this change right after this issue, covering all ten fields. The regression tests need a build where serde_json has the feature, so it enables it for rmcp's tests through a dev-dependency; the pull request describes the trade-off and the alternative (a small crate outside the workspace with a CI step of its own), and I am happy to switch if you prefer that.
Activity
Record that rmcp merged the float fix Codex was waiting on (#998)
State where the server stands on rate limiting tool invocations and o…