Skip to content

Commit 73e60d7

Browse files
committed
feat: add TryFrom<Content> for backward-compatible migration
1 parent 70f5c84 commit 73e60d7

File tree

2 files changed

+130
-0
lines changed

2 files changed

+130
-0
lines changed

crates/rmcp/src/model.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1467,6 +1467,35 @@ impl From<&str> for SamplingMessageContent {
14671467
}
14681468
}
14691469

1470+
// Backward compatibility: Convert Content to SamplingMessageContent
1471+
// Note: Resource and ResourceLink variants are not supported in sampling messages
1472+
impl TryFrom<Content> for SamplingMessageContent {
1473+
type Error = &'static str;
1474+
1475+
fn try_from(content: Content) -> Result<Self, Self::Error> {
1476+
match content.raw {
1477+
RawContent::Text(text) => Ok(SamplingMessageContent::Text(text)),
1478+
RawContent::Image(image) => Ok(SamplingMessageContent::Image(image)),
1479+
RawContent::Audio(audio) => Ok(SamplingMessageContent::Audio(audio)),
1480+
RawContent::Resource(_) => {
1481+
Err("Resource content is not supported in sampling messages")
1482+
}
1483+
RawContent::ResourceLink(_) => {
1484+
Err("ResourceLink content is not supported in sampling messages")
1485+
}
1486+
}
1487+
}
1488+
}
1489+
1490+
// Backward compatibility: Convert Content to SamplingContent<SamplingMessageContent>
1491+
impl TryFrom<Content> for SamplingContent<SamplingMessageContent> {
1492+
type Error = &'static str;
1493+
1494+
fn try_from(content: Content) -> Result<Self, Self::Error> {
1495+
Ok(SamplingContent::Single(content.try_into()?))
1496+
}
1497+
}
1498+
14701499
/// Specifies how much context should be included in sampling requests.
14711500
///
14721501
/// This allows clients to control what additional context information

crates/rmcp/tests/test_sampling.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,3 +535,104 @@ async fn test_sampling_capability() -> Result<()> {
535535

536536
Ok(())
537537
}
538+
539+
#[tokio::test]
540+
async fn test_backward_compat_sampling_message_deserialization() -> Result<()> {
541+
let old_format_json = r#"{
542+
"role": "user",
543+
"content": {
544+
"type": "text",
545+
"text": "Hello, world!"
546+
}
547+
}"#;
548+
549+
let message: SamplingMessage = serde_json::from_str(old_format_json)?;
550+
assert_eq!(message.role, Role::User);
551+
let text = message.content.first().unwrap().as_text().unwrap();
552+
assert_eq!(text.text, "Hello, world!");
553+
554+
Ok(())
555+
}
556+
557+
#[tokio::test]
558+
async fn test_backward_compat_sampling_message_with_image() -> Result<()> {
559+
let old_format_json = r#"{
560+
"role": "user",
561+
"content": {
562+
"type": "image",
563+
"data": "base64data",
564+
"mimeType": "image/png"
565+
}
566+
}"#;
567+
568+
let message: SamplingMessage = serde_json::from_str(old_format_json)?;
569+
assert_eq!(message.role, Role::User);
570+
assert_eq!(message.content.len(), 1);
571+
572+
Ok(())
573+
}
574+
575+
#[tokio::test]
576+
async fn test_backward_compat_sampling_capability_empty_object() -> Result<()> {
577+
let empty_json = "{}";
578+
let cap: SamplingCapability = serde_json::from_str(empty_json)?;
579+
assert!(cap.tools.is_none());
580+
assert!(cap.context.is_none());
581+
582+
let client_cap_json = r#"{"sampling": {}}"#;
583+
let client_cap: ClientCapabilities = serde_json::from_str(client_cap_json)?;
584+
assert!(client_cap.sampling.is_some());
585+
586+
Ok(())
587+
}
588+
589+
#[tokio::test]
590+
async fn test_content_to_sampling_message_content_conversion() -> Result<()> {
591+
use std::convert::TryInto;
592+
593+
let content = Content::text("Hello");
594+
let sampling_content: SamplingMessageContent =
595+
content.try_into().map_err(|e: &str| anyhow::anyhow!(e))?;
596+
assert!(sampling_content.as_text().is_some());
597+
assert_eq!(sampling_content.as_text().unwrap().text, "Hello");
598+
599+
let content = Content::image("base64data", "image/png");
600+
let sampling_content: SamplingMessageContent =
601+
content.try_into().map_err(|e: &str| anyhow::anyhow!(e))?;
602+
assert!(matches!(sampling_content, SamplingMessageContent::Image(_)));
603+
604+
Ok(())
605+
}
606+
607+
#[tokio::test]
608+
async fn test_content_to_sampling_content_conversion() -> Result<()> {
609+
use std::convert::TryInto;
610+
611+
let content = Content::text("Hello");
612+
let sampling_content: SamplingContent<SamplingMessageContent> =
613+
content.try_into().map_err(|e: &str| anyhow::anyhow!(e))?;
614+
assert_eq!(sampling_content.len(), 1);
615+
assert!(sampling_content.first().unwrap().as_text().is_some());
616+
617+
Ok(())
618+
}
619+
620+
#[tokio::test]
621+
async fn test_content_conversion_unsupported_variants() {
622+
use rmcp::model::ResourceContents;
623+
use std::convert::TryInto;
624+
625+
let resource_content = Content::resource(ResourceContents::TextResourceContents {
626+
uri: "file:///test.txt".to_string(),
627+
mime_type: Some("text/plain".to_string()),
628+
text: "test".to_string(),
629+
meta: None,
630+
});
631+
632+
let result: Result<SamplingMessageContent, _> = resource_content.try_into();
633+
assert!(result.is_err());
634+
assert_eq!(
635+
result.unwrap_err(),
636+
"Resource content is not supported in sampling messages"
637+
);
638+
}

0 commit comments

Comments
 (0)