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

View file

@ -388,6 +388,7 @@
"content": {
"description": "The content returned by the tool (text, images, etc.)",
"type": "array",
"default": [],
"items": {
"$ref": "#/definitions/Annotated"
}
@ -402,10 +403,7 @@
"structuredContent": {
"description": "An optional JSON object that represents the structured result of the tool call"
}
},
"required": [
"content"
]
}
},
"CancelTaskResult": {
"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/CallToolResult"
},
{
"$ref": "#/definitions/ListToolsResult"
},
{
"$ref": "#/definitions/CreateElicitationResult"
},
{
"$ref": "#/definitions/EmptyObject"
},
{
"$ref": "#/definitions/CreateTaskResult"
},
@ -2838,10 +2830,16 @@
"$ref": "#/definitions/CancelTaskResult"
},
{
"$ref": "#/definitions/CustomResult"
"$ref": "#/definitions/CallToolResult"
},
{
"$ref": "#/definitions/GetTaskPayloadResult"
},
{
"$ref": "#/definitions/EmptyObject"
},
{
"$ref": "#/definitions/CustomResult"
}
]
},

View file

@ -388,6 +388,7 @@
"content": {
"description": "The content returned by the tool (text, images, etc.)",
"type": "array",
"default": [],
"items": {
"$ref": "#/definitions/Annotated"
}
@ -402,10 +403,7 @@
"structuredContent": {
"description": "An optional JSON object that represents the structured result of the tool call"
}
},
"required": [
"content"
]
}
},
"CancelTaskResult": {
"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/CallToolResult"
},
{
"$ref": "#/definitions/ListToolsResult"
},
{
"$ref": "#/definitions/CreateElicitationResult"
},
{
"$ref": "#/definitions/EmptyObject"
},
{
"$ref": "#/definitions/CreateTaskResult"
},
@ -2838,10 +2830,16 @@
"$ref": "#/definitions/CancelTaskResult"
},
{
"$ref": "#/definitions/CustomResult"
"$ref": "#/definitions/CallToolResult"
},
{
"$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));
}
#[tokio::test]
async fn test_missing_content_is_rejected() {
#[test]
fn test_missing_content_defaults_to_empty() {
let raw = json!({ "isError": false });
let result: Result<CallToolResult, _> = serde_json::from_value(raw);
assert!(result.is_err());
let result: CallToolResult = serde_json::from_value(raw).unwrap();
assert!(result.content.is_empty());
assert_eq!(result.is_error, Some(false));
}
#[tokio::test]
async fn test_missing_content_with_structured_content_is_rejected() {
#[test]
fn test_missing_content_with_structured_content_deserializes() {
let raw = json!({ "structuredContent": {"key": "value"}, "isError": false });
let result: Result<CallToolResult, _> = serde_json::from_value(raw);
assert!(result.is_err());
let result: CallToolResult = serde_json::from_value(raw).unwrap();
assert!(result.content.is_empty());
assert_eq!(result.structured_content.unwrap()["key"], "value");
}
#[tokio::test]
@ -333,3 +335,13 @@ async fn test_empty_content_roundtrip() {
let deserialized: CallToolResult = serde_json::from_value(v).unwrap();
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());
}