diff --git a/crates/rmcp/src/model.rs b/crates/rmcp/src/model.rs index a93f98d1..be951fef 100644 --- a/crates/rmcp/src/model.rs +++ b/crates/rmcp/src/model.rs @@ -2196,7 +2196,7 @@ pub type ElicitationCompletionNotification = /// /// Contains the content returned by the tool execution and an optional /// flag indicating whether the operation resulted in an error. -#[derive(Debug, Serialize, Clone, PartialEq)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct CallToolResult { @@ -2310,45 +2310,6 @@ impl CallToolResult { } } -// Custom deserialize implementation to validate mutual exclusivity -impl<'de> Deserialize<'de> for CallToolResult { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - struct CallToolResultHelper { - #[serde(skip_serializing_if = "Option::is_none")] - content: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - structured_content: Option, - #[serde(skip_serializing_if = "Option::is_none")] - is_error: Option, - /// Accept `_meta` during deserialization - #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - meta: Option, - } - - let helper = CallToolResultHelper::deserialize(deserializer)?; - let result = CallToolResult { - content: helper.content.unwrap_or_default(), - structured_content: helper.structured_content, - is_error: helper.is_error, - meta: helper.meta, - }; - - // Validate mutual exclusivity - if result.content.is_empty() && result.structured_content.is_none() { - return Err(serde::de::Error::custom( - "CallToolResult must have either content or structured_content", - )); - } - - Ok(result) - } -} - const_string!(ListToolsRequestMethod = "tools/list"); /// Request to list all available tools from a server pub type ListToolsRequest = RequestOptionalParam; diff --git a/crates/rmcp/tests/test_structured_output.rs b/crates/rmcp/tests/test_structured_output.rs index cb9a11b9..0edb8bce 100644 --- a/crates/rmcp/tests/test_structured_output.rs +++ b/crates/rmcp/tests/test_structured_output.rs @@ -2,7 +2,7 @@ use rmcp::{ Json, ServerHandler, handler::server::{router::tool::ToolRouter, tool::IntoCallToolResult, wrapper::Parameters}, - model::{CallToolResult, Content, Tool}, + model::{CallToolResult, Content, ServerResult, Tool}, tool, tool_handler, tool_router, }; use schemars::JsonSchema; @@ -280,3 +280,61 @@ async fn test_output_schema_requires_structured_content() { assert!(call_result.structured_content.is_some()); assert!(!call_result.content.is_empty()); } + +#[tokio::test] +async fn test_empty_content_array_deserializes() { + let raw = json!({ "content": [] }); + let result: CallToolResult = serde_json::from_value(raw).unwrap(); + assert!(result.content.is_empty()); + assert!(result.structured_content.is_none()); + assert!(result.is_error.is_none()); +} + +#[tokio::test] +async fn test_empty_content_array_with_is_error() { + let raw = json!({ "content": [], "isError": false }); + 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_is_rejected() { + let raw = json!({ "isError": false }); + let result: Result = serde_json::from_value(raw); + assert!(result.is_err()); +} + +#[tokio::test] +async fn test_missing_content_with_structured_content_is_rejected() { + let raw = json!({ "structuredContent": {"key": "value"}, "isError": false }); + let result: Result = serde_json::from_value(raw); + assert!(result.is_err()); +} + +#[tokio::test] +async fn test_empty_content_deserializes_as_call_tool_result_variant() { + let raw = json!({ "content": [] }); + let result: ServerResult = serde_json::from_value(raw).unwrap(); + match result { + ServerResult::CallToolResult(call_result) => { + assert!(call_result.content.is_empty()); + assert!(call_result.structured_content.is_none()); + } + other => panic!("Expected CallToolResult, got {:?}", other), + } +} + +#[tokio::test] +async fn test_empty_content_roundtrip() { + let result = CallToolResult { + content: vec![], + structured_content: None, + is_error: Some(false), + meta: None, + }; + let v = serde_json::to_value(&result).unwrap(); + assert_eq!(v["content"], json!([])); + let deserialized: CallToolResult = serde_json::from_value(v).unwrap(); + assert_eq!(deserialized, result); +}