fix: disambiguate input-required results (#1103)

This commit is contained in:
Dale Seo 2026-08-03 12:54:59 -04:00 • committed by GitHub
commit 684ff5a51e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 65 additions and 4 deletions

View file

@ -3814,8 +3814,7 @@ pub struct CallToolResult {
// 2. Requires at least one known field to be present, so that `CallToolResult` doesn't // 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 // greedily match arbitrary JSON objects when used inside `#[serde(untagged)]` enums
// (e.g. `ServerResult`), which would shadow `CustomResult`. // (e.g. `ServerResult`), which would shadow `CustomResult`.
// 3. Rejects `resultType: "input_required"` so untagged `ServerResult` // 3. Rejects non-`complete` result types so other `ServerResult` variants can match.
// decoding selects `InputRequiredResult` and preserves input requests and state.
impl<'de> Deserialize<'de> for CallToolResult { impl<'de> Deserialize<'de> for CallToolResult {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where where
@ -3838,10 +3837,10 @@ impl<'de> Deserialize<'de> for CallToolResult {
if helper if helper
.result_type .result_type
.as_ref() .as_ref()
.is_some_and(ResultType::is_input_required) .is_some_and(|result_type| !result_type.is_complete())
{ {
return Err(serde::de::Error::custom( return Err(serde::de::Error::custom(
"CallToolResult cannot use resultType \"input_required\"", "CallToolResult requires resultType to be \"complete\" when present",
)); ));
} }

View file

@ -86,6 +86,68 @@ fn legacy_call_tool_result_round_trips_without_result_type() {
assert_eq!(reserialized, legacy); 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::<CallToolResult>(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::<ServerResult>(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::<ServerResult>(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] #[test]
fn strip_removes_complete_result_type() { fn strip_removes_complete_result_type() {
let mut result = let mut result =