fix(rmcp): allow both content and structured content (#359)

Co-authored-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
Alex Hancock 2025-08-07 11:19:46 -04:00 • committed by GitHub
commit 2ee6106fc3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 11 additions and 14 deletions

View file

@ -1181,8 +1181,6 @@ pub type RootsListChangedNotification = NotificationNoParam<RootsListChangedNoti
/// ///
/// Contains the content returned by the tool execution and an optional /// Contains the content returned by the tool execution and an optional
/// flag indicating whether the operation resulted in an error. /// flag indicating whether the operation resulted in an error.
///
/// Note: `content` and `structured_content` are mutually exclusive - exactly one must be provided.
#[derive(Debug, Serialize, Clone, PartialEq)] #[derive(Debug, Serialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
@ -1262,10 +1260,9 @@ impl CallToolResult {
} }
} }
/// Validate that content and structured_content are mutually exclusive /// Validate that content or structured content is provided
pub fn validate(&self) -> Result<(), &'static str> { pub fn validate(&self) -> Result<(), &'static str> {
match (&self.content, &self.structured_content) { match (&self.content, &self.structured_content) {
(Some(_), Some(_)) => Err("content and structured_content are mutually exclusive"),
(None, None) => Err("either content or structured_content must be provided"), (None, None) => Err("either content or structured_content must be provided"),
_ => Ok(()), _ => Ok(()),
} }

View file

@ -299,7 +299,7 @@
} }
}, },
"CallToolResult": { "CallToolResult": {
"description": "The result of a tool call operation.\n\nContains the content returned by the tool execution and an optional\nflag indicating whether the operation resulted in an error.\n\nNote: `content` and `structured_content` are mutually exclusive - exactly one must be provided.", "description": "The result of a tool call operation.\n\nContains the content returned by the tool execution and an optional\nflag indicating whether the operation resulted in an error.",
"type": "object", "type": "object",
"properties": { "properties": {
"content": { "content": {

View file

@ -299,7 +299,7 @@
} }
}, },
"CallToolResult": { "CallToolResult": {
"description": "The result of a tool call operation.\n\nContains the content returned by the tool execution and an optional\nflag indicating whether the operation resulted in an error.\n\nNote: `content` and `structured_content` are mutually exclusive - exactly one must be provided.", "description": "The result of a tool call operation.\n\nContains the content returned by the tool execution and an optional\nflag indicating whether the operation resulted in an error.",
"type": "object", "type": "object",
"properties": { "properties": {
"content": { "content": {
@ -1336,7 +1336,7 @@
{ {
"type": "object", "type": "object",
"properties": { "properties": {
"mime_type": { "mimeType": {
"type": [ "type": [
"string", "string",
"null" "null"
@ -1360,7 +1360,7 @@
"blob": { "blob": {
"type": "string" "type": "string"
}, },
"mime_type": { "mimeType": {
"type": [ "type": [
"string", "string",
"null" "null"

View file

@ -146,7 +146,7 @@ async fn test_structured_error_in_call_result() {
#[tokio::test] #[tokio::test]
async fn test_mutual_exclusivity_validation() { async fn test_mutual_exclusivity_validation() {
// Test that content and structured_content are mutually exclusive // Test that content and structured_content can both be passed separately
let content_result = CallToolResult::success(vec![Content::text("Hello")]); let content_result = CallToolResult::success(vec![Content::text("Hello")]);
let structured_result = CallToolResult::structured(json!({"message": "Hello"})); let structured_result = CallToolResult::structured(json!({"message": "Hello"}));
@ -154,15 +154,15 @@ async fn test_mutual_exclusivity_validation() {
assert!(content_result.validate().is_ok()); assert!(content_result.validate().is_ok());
assert!(structured_result.validate().is_ok()); assert!(structured_result.validate().is_ok());
// Try to create an invalid result with both fields // Try to create a result with both fields
let invalid_json = json!({ let json_with_both = json!({
"content": [{"type": "text", "text": "Hello"}], "content": [{"type": "text", "text": "Hello"}],
"structuredContent": {"message": "Hello"} "structuredContent": {"message": "Hello"}
}); });
// The deserialization itself should fail due to validation // The deserialization itself should not fail
let deserialized: Result<CallToolResult, _> = serde_json::from_value(invalid_json); let deserialized: Result<CallToolResult, _> = serde_json::from_value(json_with_both);
assert!(deserialized.is_err()); assert!(deserialized.is_ok());
} }
#[tokio::test] #[tokio::test]