diff --git a/crates/rmcp/src/model.rs b/crates/rmcp/src/model.rs index 7ee93d82..6a140987 100644 --- a/crates/rmcp/src/model.rs +++ b/crates/rmcp/src/model.rs @@ -3814,8 +3814,7 @@ pub struct CallToolResult { // 2. Requires at least one known field to be present, so that `CallToolResult` doesn't // greedily match arbitrary JSON objects when used inside `#[serde(untagged)]` enums // (e.g. `ServerResult`), which would shadow `CustomResult`. -// 3. Rejects `resultType: "input_required"` so untagged `ServerResult` -// decoding selects `InputRequiredResult` and preserves input requests and state. +// 3. Rejects non-`complete` result types so other `ServerResult` variants can match. impl<'de> Deserialize<'de> for CallToolResult { fn deserialize(deserializer: D) -> Result where @@ -3838,10 +3837,10 @@ impl<'de> Deserialize<'de> for CallToolResult { if helper .result_type .as_ref() - .is_some_and(ResultType::is_input_required) + .is_some_and(|result_type| !result_type.is_complete()) { return Err(serde::de::Error::custom( - "CallToolResult cannot use resultType \"input_required\"", + "CallToolResult requires resultType to be \"complete\" when present", )); } diff --git a/crates/rmcp/tests/test_result_type_wire.rs b/crates/rmcp/tests/test_result_type_wire.rs index d5f20153..a22933e3 100644 --- a/crates/rmcp/tests/test_result_type_wire.rs +++ b/crates/rmcp/tests/test_result_type_wire.rs @@ -86,6 +86,68 @@ fn legacy_call_tool_result_round_trips_without_result_type() { assert_eq!(reserialized, legacy); } +#[test] +fn call_tool_result_should_reject_non_complete_result_type() { + let input_required = json!({ + "resultType": "input_required", + "_meta": { + "example.com/replica": "r1", + }, + }); + + assert!(serde_json::from_value::(input_required).is_err()); +} + +#[test] +fn server_result_should_preserve_input_required_result_when_meta_present() { + let input_required = json!({ + "resultType": "input_required", + "requestState": "sealed", + "_meta": { + "example.com/replica": "r1", + }, + }); + + let result = + serde_json::from_value::(input_required).expect("deserialize ServerResult"); + + match result { + ServerResult::InputRequiredResult(result) => { + assert_eq!( + (result.request_state.as_deref(), result.meta.is_some()), + (Some("sealed"), true) + ); + } + _ => panic!("expected InputRequiredResult"), + } +} + +#[test] +fn server_result_should_accept_legacy_call_tool_result_when_only_meta_present() { + let legacy = json!({ + "_meta": { + "example.com/replica": "r1", + }, + }); + + let result = + serde_json::from_value::(legacy).expect("deserialize legacy CallToolResult"); + + match result { + ServerResult::CallToolResult(result) => { + assert_eq!( + ( + result.result_type, + result.content.is_empty(), + result.meta.is_some() + ), + (None, true, true) + ); + } + _ => panic!("expected CallToolResult"), + } +} + #[test] fn strip_removes_complete_result_type() { let mut result =