fix: default CallToolResult content to empty vec on missing field (#752)

This commit is contained in:
Dale Seo 2026-03-13 16:02:20 -04:00 • committed by GitHub
commit 44dfcf5550
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 42 additions and 33 deletions

View file

@ -2697,6 +2697,7 @@ pub type ElicitationCompletionNotification =
#[non_exhaustive] #[non_exhaustive]
pub struct CallToolResult { pub struct CallToolResult {
/// The content returned by the tool (text, images, etc.) /// The content returned by the tool (text, images, etc.)
#[serde(default)]
pub content: Vec<Content>, pub content: Vec<Content>,
/// An optional JSON object that represents the structured result of the tool call /// An optional JSON object that represents the structured result of the tool call
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@ -3247,16 +3248,16 @@ ts_union!(
| ListResourcesResult | ListResourcesResult
| ListResourceTemplatesResult | ListResourceTemplatesResult
| ReadResourceResult | ReadResourceResult
| CallToolResult
| ListToolsResult | ListToolsResult
| CreateElicitationResult | CreateElicitationResult
| EmptyResult
| CreateTaskResult | CreateTaskResult
| ListTasksResult | ListTasksResult
| GetTaskResult | GetTaskResult
| CancelTaskResult | CancelTaskResult
| CustomResult | CallToolResult
| GetTaskPayloadResult | GetTaskPayloadResult
| EmptyResult
| CustomResult
; ;
); );

View file

@ -388,6 +388,7 @@
"content": { "content": {
"description": "The content returned by the tool (text, images, etc.)", "description": "The content returned by the tool (text, images, etc.)",
"type": "array", "type": "array",
"default": [],
"items": { "items": {
"$ref": "#/definitions/Annotated" "$ref": "#/definitions/Annotated"
} }
@ -402,10 +403,7 @@
"structuredContent": { "structuredContent": {
"description": "An optional JSON object that represents the structured result of the tool call" "description": "An optional JSON object that represents the structured result of the tool call"
} }
}, }
"required": [
"content"
]
}, },
"CancelTaskResult": { "CancelTaskResult": {
"description": "Response to a `tasks/cancel` request.\n\nPer spec, `CancelTaskResult = allOf[Result, Task]` — same shape as `GetTaskResult`.", "description": "Response to a `tasks/cancel` request.\n\nPer spec, `CancelTaskResult = allOf[Result, Task]` — same shape as `GetTaskResult`.",
@ -2813,18 +2811,12 @@
{ {
"$ref": "#/definitions/ReadResourceResult" "$ref": "#/definitions/ReadResourceResult"
}, },
{
"$ref": "#/definitions/CallToolResult"
},
{ {
"$ref": "#/definitions/ListToolsResult" "$ref": "#/definitions/ListToolsResult"
}, },
{ {
"$ref": "#/definitions/CreateElicitationResult" "$ref": "#/definitions/CreateElicitationResult"
}, },
{
"$ref": "#/definitions/EmptyObject"
},
{ {
"$ref": "#/definitions/CreateTaskResult" "$ref": "#/definitions/CreateTaskResult"
}, },
@ -2838,10 +2830,16 @@
"$ref": "#/definitions/CancelTaskResult" "$ref": "#/definitions/CancelTaskResult"
}, },
{ {
"$ref": "#/definitions/CustomResult" "$ref": "#/definitions/CallToolResult"
}, },
{ {
"$ref": "#/definitions/GetTaskPayloadResult" "$ref": "#/definitions/GetTaskPayloadResult"
},
{
"$ref": "#/definitions/EmptyObject"
},
{
"$ref": "#/definitions/CustomResult"
} }
] ]
}, },

View file

@ -388,6 +388,7 @@
"content": { "content": {
"description": "The content returned by the tool (text, images, etc.)", "description": "The content returned by the tool (text, images, etc.)",
"type": "array", "type": "array",
"default": [],
"items": { "items": {
"$ref": "#/definitions/Annotated" "$ref": "#/definitions/Annotated"
} }
@ -402,10 +403,7 @@
"structuredContent": { "structuredContent": {
"description": "An optional JSON object that represents the structured result of the tool call" "description": "An optional JSON object that represents the structured result of the tool call"
} }
}, }
"required": [
"content"
]
}, },
"CancelTaskResult": { "CancelTaskResult": {
"description": "Response to a `tasks/cancel` request.\n\nPer spec, `CancelTaskResult = allOf[Result, Task]` — same shape as `GetTaskResult`.", "description": "Response to a `tasks/cancel` request.\n\nPer spec, `CancelTaskResult = allOf[Result, Task]` — same shape as `GetTaskResult`.",
@ -2813,18 +2811,12 @@
{ {
"$ref": "#/definitions/ReadResourceResult" "$ref": "#/definitions/ReadResourceResult"
}, },
{
"$ref": "#/definitions/CallToolResult"
},
{ {
"$ref": "#/definitions/ListToolsResult" "$ref": "#/definitions/ListToolsResult"
}, },
{ {
"$ref": "#/definitions/CreateElicitationResult" "$ref": "#/definitions/CreateElicitationResult"
}, },
{
"$ref": "#/definitions/EmptyObject"
},
{ {
"$ref": "#/definitions/CreateTaskResult" "$ref": "#/definitions/CreateTaskResult"
}, },
@ -2838,10 +2830,16 @@
"$ref": "#/definitions/CancelTaskResult" "$ref": "#/definitions/CancelTaskResult"
}, },
{ {
"$ref": "#/definitions/CustomResult" "$ref": "#/definitions/CallToolResult"
}, },
{ {
"$ref": "#/definitions/GetTaskPayloadResult" "$ref": "#/definitions/GetTaskPayloadResult"
},
{
"$ref": "#/definitions/EmptyObject"
},
{
"$ref": "#/definitions/CustomResult"
} }
] ]
}, },

View file

@ -298,18 +298,20 @@ async fn test_empty_content_array_with_is_error() {
assert_eq!(result.is_error, Some(false)); assert_eq!(result.is_error, Some(false));
} }
#[tokio::test] #[test]
async fn test_missing_content_is_rejected() { fn test_missing_content_defaults_to_empty() {
let raw = json!({ "isError": false }); let raw = json!({ "isError": false });
let result: Result<CallToolResult, _> = serde_json::from_value(raw); let result: CallToolResult = serde_json::from_value(raw).unwrap();
assert!(result.is_err()); assert!(result.content.is_empty());
assert_eq!(result.is_error, Some(false));
} }
#[tokio::test] #[test]
async fn test_missing_content_with_structured_content_is_rejected() { fn test_missing_content_with_structured_content_deserializes() {
let raw = json!({ "structuredContent": {"key": "value"}, "isError": false }); let raw = json!({ "structuredContent": {"key": "value"}, "isError": false });
let result: Result<CallToolResult, _> = serde_json::from_value(raw); let result: CallToolResult = serde_json::from_value(raw).unwrap();
assert!(result.is_err()); assert!(result.content.is_empty());
assert_eq!(result.structured_content.unwrap()["key"], "value");
} }
#[tokio::test] #[tokio::test]
@ -333,3 +335,13 @@ async fn test_empty_content_roundtrip() {
let deserialized: CallToolResult = serde_json::from_value(v).unwrap(); let deserialized: CallToolResult = serde_json::from_value(v).unwrap();
assert_eq!(deserialized, result); assert_eq!(deserialized, result);
} }
#[test]
fn test_call_tool_result_deserialize_without_content() {
let json = json!({
"structuredContent": {"message": "Hello"}
});
let result: CallToolResult = serde_json::from_value(json).unwrap();
assert!(result.content.is_empty());
assert!(result.structured_content.is_some());
}