use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
const META_KIND: &str = "objectiveai/kind";
const META_IMAGE_DETAIL: &str = "objectiveai/image_detail";
const META_FILENAME: &str = "objectiveai/filename";
const KIND_IMAGE_URL_REMOTE: &str = "image_url_remote";
const KIND_INPUT_VIDEO_REMOTE: &str = "input_video_remote";
const KIND_FILE_URL: &str = "file_url";
const KIND_FILE_ID: &str = "file_id";
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type")]
#[schemars(rename = "mcp.tool.ContentBlock")]
pub enum ContentBlock {
#[serde(rename = "text")]
#[schemars(title = "Text")]
Text(super::TextContent),
#[serde(rename = "image")]
#[schemars(title = "Image")]
Image(super::ImageContent),
#[serde(rename = "audio")]
#[schemars(title = "Audio")]
Audio(super::AudioContent),
#[serde(rename = "resource_link")]
#[schemars(title = "ResourceLink")]
ResourceLink(super::ResourceLink),
#[serde(rename = "resource")]
#[schemars(title = "EmbeddedResource")]
EmbeddedResource(super::EmbeddedResource),
}
impl From<crate::agent::completions::message::RichContentPart>
for ContentBlock
{
fn from(part: crate::agent::completions::message::RichContentPart) -> Self {
use crate::agent::completions::message::RichContentPart;
match part {
RichContentPart::Text { text } => {
ContentBlock::Text(super::TextContent {
text,
annotations: None,
_meta: None,
})
}
RichContentPart::ImageUrl { image_url } => image_url.into(),
RichContentPart::InputAudio { input_audio } => input_audio.into(),
RichContentPart::InputVideo { video_url } => video_url.into(),
RichContentPart::VideoUrl { video_url } => video_url.into(),
RichContentPart::File { file } => file.into(),
}
}
}
impl From<crate::agent::completions::message::ImageUrl> for ContentBlock {
fn from(image_url: crate::agent::completions::message::ImageUrl) -> Self {
let detail_value = image_url
.detail
.as_ref()
.and_then(|d| serde_json::to_value(d).ok());
match super::ImageContent::try_from(image_url) {
Ok(mut ic) => {
if let Some(v) = detail_value {
let mut m = indexmap::IndexMap::new();
m.insert(META_IMAGE_DETAIL.to_string(), v);
ic._meta = Some(m);
}
ContentBlock::Image(ic)
}
Err(err) => {
let mut meta = indexmap::IndexMap::new();
meta.insert(
META_KIND.to_string(),
serde_json::Value::String(
KIND_IMAGE_URL_REMOTE.to_string(),
),
);
if let Some(v) = detail_value {
meta.insert(META_IMAGE_DETAIL.to_string(), v);
}
ContentBlock::Text(super::TextContent {
text: err.url,
annotations: None,
_meta: Some(meta),
})
}
}
}
}
impl From<crate::agent::completions::message::InputAudio> for ContentBlock {
fn from(input_audio: crate::agent::completions::message::InputAudio) -> Self {
ContentBlock::Audio(input_audio.into())
}
}
impl From<crate::agent::completions::message::VideoUrl> for ContentBlock {
fn from(video_url: crate::agent::completions::message::VideoUrl) -> Self {
if crate::data_url::parse_data_url(&video_url.url).is_some() {
ContentBlock::Text(super::TextContent {
text: video_url.url,
annotations: None,
_meta: None,
})
} else {
ContentBlock::Text(super::TextContent {
text: video_url.url,
annotations: None,
_meta: Some(single_meta(
META_KIND,
KIND_INPUT_VIDEO_REMOTE.to_string(),
)),
})
}
}
}
impl From<crate::agent::completions::message::File> for ContentBlock {
fn from(file: crate::agent::completions::message::File) -> Self {
let filename = file.filename.clone();
if let Some(blob) = file.file_data {
let body = format!("data:application/octet-stream;base64,{blob}");
let meta = filename.map(|n| single_meta(META_FILENAME, n));
ContentBlock::Text(super::TextContent {
text: body,
annotations: None,
_meta: meta,
})
} else if let Some(url) = file.file_url {
let mut m = single_meta(META_KIND, KIND_FILE_URL.to_string());
if let Some(n) = filename {
m.insert(META_FILENAME.to_string(), serde_json::Value::String(n));
}
ContentBlock::Text(super::TextContent {
text: url,
annotations: None,
_meta: Some(m),
})
} else if let Some(id) = file.file_id {
let mut m = single_meta(META_KIND, KIND_FILE_ID.to_string());
if let Some(n) = filename {
m.insert(META_FILENAME.to_string(), serde_json::Value::String(n));
}
ContentBlock::Text(super::TextContent {
text: id,
annotations: None,
_meta: Some(m),
})
} else {
ContentBlock::Text(super::TextContent {
text: String::new(),
annotations: None,
_meta: None,
})
}
}
}
fn single_meta(
key: &str,
value: String,
) -> indexmap::IndexMap<String, serde_json::Value> {
let mut m = indexmap::IndexMap::new();
m.insert(key.to_string(), serde_json::Value::String(value));
m
}
impl From<crate::agent::completions::message::RichContent>
for Vec<ContentBlock>
{
fn from(content: crate::agent::completions::message::RichContent) -> Self {
use crate::agent::completions::message::RichContent;
match content {
RichContent::Text(text) => {
vec![ContentBlock::Text(super::TextContent {
text,
annotations: None,
_meta: None,
})]
}
RichContent::Parts(parts) => {
parts.into_iter().map(Into::into).collect()
}
}
}
}
#[cfg(test)]
mod round_trip_tests {
use super::*;
use crate::agent::completions::message::{
File, ImageUrl, ImageUrlDetail, InputAudio, RichContentPart, VideoUrl,
};
fn norm(part: &mut RichContentPart) {
let _ = part;
}
fn assert_round_trips(part: RichContentPart) {
let mut expected = part.clone();
norm(&mut expected);
let block: ContentBlock = part.into();
let mut back: RichContentPart = block.into();
norm(&mut back);
assert_eq!(
expected, back,
"round-trip mismatch: expected {expected:?}, got {back:?}"
);
}
#[test]
fn rt_text_plain() {
assert_round_trips(RichContentPart::Text {
text: "hello world".into(),
});
}
#[test]
fn rt_image_url_data_url_no_detail() {
assert_round_trips(RichContentPart::ImageUrl {
image_url: ImageUrl {
url: "data:image/png;base64,iVBORw0KGgo".into(),
detail: None,
},
});
}
#[test]
fn rt_image_url_data_url_with_detail_high() {
assert_round_trips(RichContentPart::ImageUrl {
image_url: ImageUrl {
url: "data:image/png;base64,iVBORw0KGgo".into(),
detail: Some(ImageUrlDetail::High),
},
});
}
#[test]
fn rt_image_url_data_url_with_detail_low() {
assert_round_trips(RichContentPart::ImageUrl {
image_url: ImageUrl {
url: "data:image/jpeg;base64,/9j/4AAQ".into(),
detail: Some(ImageUrlDetail::Low),
},
});
}
#[test]
fn rt_image_url_remote_url_no_detail() {
assert_round_trips(RichContentPart::ImageUrl {
image_url: ImageUrl {
url: "https://example.com/a.png".into(),
detail: None,
},
});
}
#[test]
fn rt_image_url_remote_url_with_detail() {
assert_round_trips(RichContentPart::ImageUrl {
image_url: ImageUrl {
url: "https://example.com/a.png".into(),
detail: Some(ImageUrlDetail::Auto),
},
});
}
#[test]
fn rt_input_audio() {
assert_round_trips(RichContentPart::InputAudio {
input_audio: InputAudio {
data: "SUQzBAA".into(),
format: "audio/mpeg".into(),
},
});
}
#[test]
fn rt_input_video_data_url() {
assert_round_trips(RichContentPart::InputVideo {
video_url: VideoUrl {
url: "data:video/mp4;base64,AAAA".into(),
},
});
}
#[test]
fn rt_input_video_remote_url() {
assert_round_trips(RichContentPart::InputVideo {
video_url: VideoUrl {
url: "https://example.com/v.mp4".into(),
},
});
}
#[test]
fn rt_file_with_file_data_no_filename() {
assert_round_trips(RichContentPart::File {
file: File {
file_data: Some("JVBERi0".into()),
filename: None,
file_id: None,
file_url: None,
},
});
}
#[test]
fn rt_file_with_file_data_and_filename() {
assert_round_trips(RichContentPart::File {
file: File {
file_data: Some("JVBERi0".into()),
filename: Some("report.pdf".into()),
file_id: None,
file_url: None,
},
});
}
#[test]
fn rt_file_with_file_url() {
assert_round_trips(RichContentPart::File {
file: File {
file_data: None,
filename: Some("remote.bin".into()),
file_id: None,
file_url: Some("https://example.com/x.bin".into()),
},
});
}
#[test]
fn rt_file_with_file_id() {
assert_round_trips(RichContentPart::File {
file: File {
file_data: None,
filename: Some("upstream-name.txt".into()),
file_id: Some("file-abc123".into()),
file_url: None,
},
});
}
#[test]
fn rt_file_multifield_collapses_to_file_data() {
let input = RichContentPart::File {
file: File {
file_data: Some("JVBERi0".into()),
filename: Some("multi.bin".into()),
file_id: Some("ignored-id".into()),
file_url: Some("https://example.com/ignored".into()),
},
};
let block: ContentBlock = input.into();
let back: RichContentPart = block.into();
let expected = RichContentPart::File {
file: File {
file_data: Some("JVBERi0".into()),
filename: Some("multi.bin".into()),
file_id: None,
file_url: None,
},
};
assert_eq!(back, expected);
}
#[test]
fn rt_text_containing_data_url_decodes_to_media() {
let input = RichContentPart::Text {
text: "data:image/png;base64,iVBORw0KGgo".into(),
};
let block: ContentBlock = input.into();
let back: RichContentPart = block.into();
assert!(
matches!(back, RichContentPart::ImageUrl { .. }),
"expected media, got {back:?}"
);
}
}