Skip to content

Commit 95e640e

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

2 files changed

Lines changed: 133 additions & 0 deletions

File tree

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: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,3 +535,107 @@ 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 = content
595+
.try_into()
596+
.map_err(|e: &str| anyhow::anyhow!(e))?;
597+
assert!(sampling_content.as_text().is_some());
598+
assert_eq!(sampling_content.as_text().unwrap().text, "Hello");
599+
600+
let content = Content::image("base64data", "image/png");
601+
let sampling_content: SamplingMessageContent = content
602+
.try_into()
603+
.map_err(|e: &str| anyhow::anyhow!(e))?;
604+
assert!(matches!(sampling_content, SamplingMessageContent::Image(_)));
605+
606+
Ok(())
607+
}
608+
609+
#[tokio::test]
610+
async fn test_content_to_sampling_content_conversion() -> Result<()> {
611+
use std::convert::TryInto;
612+
613+
let content = Content::text("Hello");
614+
let sampling_content: SamplingContent<SamplingMessageContent> = content
615+
.try_into()
616+
.map_err(|e: &str| anyhow::anyhow!(e))?;
617+
assert_eq!(sampling_content.len(), 1);
618+
assert!(sampling_content.first().unwrap().as_text().is_some());
619+
620+
Ok(())
621+
}
622+
623+
#[tokio::test]
624+
async fn test_content_conversion_unsupported_variants() {
625+
use rmcp::model::ResourceContents;
626+
use std::convert::TryInto;
627+
628+
let resource_content = Content::resource(ResourceContents::TextResourceContents {
629+
uri: "file:///test.txt".to_string(),
630+
mime_type: Some("text/plain".to_string()),
631+
text: "test".to_string(),
632+
meta: None,
633+
});
634+
635+
let result: Result<SamplingMessageContent, _> = resource_content.try_into();
636+
assert!(result.is_err());
637+
assert_eq!(
638+
result.unwrap_err(),
639+
"Resource content is not supported in sampling messages"
640+
);
641+
}

0 commit comments

Comments
 (0)