Skip to content

Commit 6755d12

Browse files
committed
feat!: relax tool result structuredContent type (SEP-2106)
Re-applies #919 (reverted by #932): ToolResultContent.structured_content becomes Option<Value> so non-object structured content is accepted, matching CallToolResult and SEP-2106. BREAKING CHANGE: ToolResultContent.structured_content changes from Option<JsonObject> to Option<Value>.
1 parent e1af378 commit 6755d12

6 files changed

Lines changed: 40 additions & 30 deletions

File tree

crates/rmcp/src/model/content.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
// ToolUseContent/ToolResultContent are SEP-2577-deprecated; internal references are expected.
1111
#![expect(deprecated)]
1212
use serde::{Deserialize, Serialize};
13-
use serde_json::json;
13+
use serde_json::{Value, json};
1414

1515
use super::{Annotations, Meta, resource::ResourceContents};
1616

@@ -207,7 +207,7 @@ pub struct ToolResultContent {
207207
pub tool_use_id: String,
208208
pub content: Vec<ContentBlock>,
209209
#[serde(skip_serializing_if = "Option::is_none")]
210-
pub structured_content: Option<super::JsonObject>,
210+
pub structured_content: Option<Value>,
211211
#[serde(skip_serializing_if = "Option::is_none")]
212212
pub is_error: Option<bool>,
213213
}

crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2352,13 +2352,7 @@
23522352
"null"
23532353
]
23542354
},
2355-
"structuredContent": {
2356-
"type": [
2357-
"object",
2358-
"null"
2359-
],
2360-
"additionalProperties": true
2361-
},
2355+
"structuredContent": true,
23622356
"toolUseId": {
23632357
"type": "string"
23642358
}

crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2352,13 +2352,7 @@
23522352
"null"
23532353
]
23542354
},
2355-
"structuredContent": {
2356-
"type": [
2357-
"object",
2358-
"null"
2359-
],
2360-
"additionalProperties": true
2361-
},
2355+
"structuredContent": true,
23622356
"toolUseId": {
23632357
"type": "string"
23642358
}

crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3454,13 +3454,7 @@
34543454
"null"
34553455
]
34563456
},
3457-
"structuredContent": {
3458-
"type": [
3459-
"object",
3460-
"null"
3461-
],
3462-
"additionalProperties": true
3463-
},
3457+
"structuredContent": true,
34643458
"toolUseId": {
34653459
"type": "string"
34663460
}

crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3454,13 +3454,7 @@
34543454
"null"
34553455
]
34563456
},
3457-
"structuredContent": {
3458-
"type": [
3459-
"object",
3460-
"null"
3461-
],
3462-
"additionalProperties": true
3463-
},
3457+
"structuredContent": true,
34643458
"toolUseId": {
34653459
"type": "string"
34663460
}

crates/rmcp/tests/test_sampling.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use rmcp::{
99
model::*,
1010
service::{RequestContext, Service},
1111
};
12+
use rstest::rstest;
1213

1314
#[tokio::test]
1415
async fn test_basic_sampling_message_creation() -> Result<()> {
@@ -370,6 +371,39 @@ fn test_tool_result_content_requires_content() {
370371
assert!(err.to_string().contains("missing field `content`"));
371372
}
372373

374+
#[rstest]
375+
#[case::array(serde_json::json!([{ "city": "SF", "temp": 72 }, { "city": "NY", "temp": 65 }]))]
376+
#[case::string(serde_json::json!("sunny"))]
377+
#[case::integer(serde_json::json!(42))]
378+
#[case::float(serde_json::json!(3.14))]
379+
#[case::boolean(serde_json::json!(true))]
380+
fn tool_result_content_round_trips_non_object_structured_content(
381+
#[case] structured: serde_json::Value,
382+
) -> Result<()> {
383+
let mut tool_result = ToolResultContent::new("call_123", vec![ContentBlock::text("x")]);
384+
tool_result.structured_content = Some(structured);
385+
386+
let json = serde_json::to_string(&tool_result)?;
387+
let deserialized: ToolResultContent = serde_json::from_str(&json)?;
388+
assert_eq!(tool_result, deserialized);
389+
390+
Ok(())
391+
}
392+
393+
// `null` is a valid JSON value, but `Option<Value>` deserializes JSON `null`
394+
// as `None`, so `Some(Value::Null)` collapses to `None` on round-trip.
395+
#[test]
396+
fn tool_result_content_null_structured_content_round_trips_to_none() -> Result<()> {
397+
let mut tool_result = ToolResultContent::new("call_123", vec![ContentBlock::text("x")]);
398+
tool_result.structured_content = Some(serde_json::Value::Null);
399+
400+
let json = serde_json::to_string(&tool_result)?;
401+
let deserialized: ToolResultContent = serde_json::from_str(&json)?;
402+
assert_eq!(deserialized.structured_content, None);
403+
404+
Ok(())
405+
}
406+
373407
#[tokio::test]
374408
async fn test_sampling_message_with_tool_use() -> Result<()> {
375409
let message = SamplingMessage::assistant_tool_use(

0 commit comments

Comments
 (0)