A server that attaches _meta to an MRTR InputRequiredResult produces a response
the client silently misinterprets: call_tool_once returns
CallToolResponse::Complete(CallToolResult { content: [], .. }) instead of
CallToolResponse::InputRequired(..). The inputRequests and requestState fields are dropped.
There is no error — the dialogue simply appears to have finished with empty content.
Cause
ServerResult is #[serde(untagged)] and lists CallToolResult before
InputRequiredResult (src/model.rs, ts_union! { export type ServerResult = ... }).
CallToolResult's hand-written Deserialize (src/model.rs, ~line 3800) guards against
greedy matching by requiring at least one of content, structuredContent, isError, or
_meta to be present — but it never checks resultType. An InputRequiredResult with _meta
satisfies that guard, so the earlier variant wins and the later, correct variant is never tried.
The same class of bug was anticipated for TaskAckResult; the union even carries a comment about ordering:
// TaskAckResult must come after CallToolResult/InputRequiredResult in this
// untagged union: it only carries `resultType`, so it would otherwise
// shadow any result that includes `resultType: "complete"`.
CallToolResult shadowing InputRequiredResult is the mirror image of that, and it is not guarded.
Reproduction
Version: rmcp 3.0.0
let mut meta = MetaObject::new();
meta.insert("example.com/replica".to_owned(), "r1".into());
let result = InputRequiredResult::new(Some(requests), Some("sealed".to_owned()))
.with_meta(meta);
let json = serde_json::to_value(&result).unwrap();
// Direct parse: fine.
serde_json::from_value::<InputRequiredResult>(json.clone()).unwrap();
// Through the union: wrong variant, fields silently dropped.
assert!(matches!(
serde_json::from_value::<ServerResult>(json).unwrap(),
ServerResult::InputRequiredResult(_)
)); // fails — it is CallToolResult
Removing .with_meta(..) makes it parse correctly, which is what makes this so easy to
ship by accident: it works until the first time a server adds metadata.
Impact
InputRequiredResult::with_meta is public API and InputRequiredResult.meta is a public
field, so this is the documented way to attach metadata to an intermediate MRTR result.
Any server that uses it breaks every rmcp client, and the failure is silent: the client
sees a completed tool call with no content rather than a protocol error.
Suggested fix
Add a resultType check to CallToolResult::deserialize, symmetric to the one
InputRequiredResult already has — reject when resultType is present and is not
"complete". That keeps the lenient behaviour for pre-2026-07-28 servers (absent
resultType still means complete) while stopping the variant from shadowing.
A server that attaches
_metato an MRTRInputRequiredResultproduces a responsethe client silently misinterprets:
call_tool_oncereturnsCallToolResponse::Complete(CallToolResult { content: [], .. })instead ofCallToolResponse::InputRequired(..). TheinputRequestsandrequestStatefields are dropped.There is no error — the dialogue simply appears to have finished with empty content.
Cause
ServerResultis#[serde(untagged)]and listsCallToolResultbeforeInputRequiredResult(src/model.rs,ts_union! { export type ServerResult = ... }).CallToolResult's hand-writtenDeserialize(src/model.rs, ~line 3800) guards againstgreedy matching by requiring at least one of
content,structuredContent,isError, or_metato be present — but it never checksresultType. AnInputRequiredResultwith_metasatisfies that guard, so the earlier variant wins and the later, correct variant is never tried.
The same class of bug was anticipated for
TaskAckResult; the union even carries a comment about ordering:CallToolResultshadowingInputRequiredResultis the mirror image of that, and it is not guarded.Reproduction
Version:
rmcp 3.0.0Removing
.with_meta(..)makes it parse correctly, which is what makes this so easy toship by accident: it works until the first time a server adds metadata.
Impact
InputRequiredResult::with_metais public API andInputRequiredResult.metais a publicfield, so this is the documented way to attach metadata to an intermediate MRTR result.
Any server that uses it breaks every rmcp client, and the failure is silent: the client
sees a completed tool call with no content rather than a protocol error.
Suggested fix
Add a
resultTypecheck toCallToolResult::deserialize, symmetric to the oneInputRequiredResultalready has — reject whenresultTypeis present and is not"complete". That keeps the lenient behaviour for pre-2026-07-28 servers (absentresultTypestill means complete) while stopping the variant from shadowing.