1use indexmap::IndexMap;
24use serde_json::Value;
25
26use rmcp::model::{
27 Annotated, AudioContent as RmcpAudioContent, Content as RmcpContent,
28 ImageContent as RmcpImageContent, Meta, RawAudioContent, RawContent, RawEmbeddedResource,
29 RawImageContent, RawResource, RawTextContent, ResourceContents as RmcpResourceContents,
30};
31
32use crate::agent::completions::message::{
33 File, ImageUrl, InputAudio, RichContent, RichContentPart, VideoUrl,
34};
35use crate::mcp::shared::{
36 BlobResourceContents, ResourceContents, ResourceContentsUnion, TextResourceContents,
37};
38use crate::mcp::tool::{
39 AudioContent, ContentBlock, EmbeddedResource, ImageContent, ImageUrlNotDataUrl, ResourceLink,
40 TextContent,
41};
42
43fn sdk_meta_to_rmcp(meta: Option<IndexMap<String, Value>>) -> Option<Meta> {
48 meta.map(|m| {
49 let mut map = serde_json::Map::with_capacity(m.len());
50 for (k, v) in m {
51 map.insert(k, v);
52 }
53 Meta(map)
54 })
55}
56
57fn rmcp_meta_to_sdk(meta: Option<Meta>) -> Option<IndexMap<String, Value>> {
60 meta.map(|Meta(map)| map.into_iter().collect::<IndexMap<String, Value>>())
61}
62
63impl From<ContentBlock> for RmcpContent {
66 fn from(block: ContentBlock) -> Self {
67 let raw = match block {
68 ContentBlock::Text(t) => RawContent::Text(RawTextContent {
69 text: t.text,
70 meta: sdk_meta_to_rmcp(t._meta),
71 }),
72 ContentBlock::Image(i) => RawContent::Image(RawImageContent {
73 data: i.data,
74 mime_type: i.mime_type,
75 meta: sdk_meta_to_rmcp(i._meta),
76 }),
77 ContentBlock::Audio(a) => RawContent::Audio(RawAudioContent {
79 data: a.data,
80 mime_type: a.mime_type,
81 }),
82 ContentBlock::EmbeddedResource(er) => RawContent::Resource(RawEmbeddedResource {
83 resource: er.resource.into(),
84 meta: sdk_meta_to_rmcp(er._meta),
85 }),
86 ContentBlock::ResourceLink(rl) => RawContent::ResourceLink(RawResource {
88 uri: rl.uri,
89 name: rl.name,
90 title: rl.title,
91 description: rl.description,
92 mime_type: rl.mime_type,
93 size: None,
94 icons: None,
95 meta: sdk_meta_to_rmcp(rl._meta),
96 }),
97 };
98 Annotated {
99 raw,
100 annotations: None,
101 }
102 }
103}
104
105impl From<RmcpContent> for ContentBlock {
106 fn from(content: RmcpContent) -> Self {
107 match content.raw {
108 RawContent::Text(t) => ContentBlock::Text(TextContent {
109 text: t.text,
110 annotations: None,
111 _meta: rmcp_meta_to_sdk(t.meta),
112 }),
113 RawContent::Image(i) => ContentBlock::Image(ImageContent {
114 data: i.data,
115 mime_type: i.mime_type,
116 annotations: None,
117 _meta: rmcp_meta_to_sdk(i.meta),
118 }),
119 RawContent::Audio(a) => ContentBlock::Audio(AudioContent {
120 data: a.data,
121 mime_type: a.mime_type,
122 annotations: None,
123 _meta: None,
124 }),
125 RawContent::Resource(er) => ContentBlock::EmbeddedResource(EmbeddedResource {
126 resource: er.resource.into(),
127 annotations: None,
128 _meta: rmcp_meta_to_sdk(er.meta),
129 }),
130 RawContent::ResourceLink(rl) => ContentBlock::ResourceLink(ResourceLink {
131 name: rl.name,
132 uri: rl.uri,
133 title: rl.title,
134 description: rl.description,
135 mime_type: rl.mime_type,
136 icons: None,
137 annotations: None,
138 _meta: rmcp_meta_to_sdk(rl.meta),
139 }),
140 }
141 }
142}
143
144impl From<ImageContent> for RmcpImageContent {
145 fn from(i: ImageContent) -> Self {
146 Annotated {
147 raw: RawImageContent {
148 data: i.data,
149 mime_type: i.mime_type,
150 meta: sdk_meta_to_rmcp(i._meta),
151 },
152 annotations: None,
153 }
154 }
155}
156
157impl From<RmcpImageContent> for ImageContent {
158 fn from(i: RmcpImageContent) -> Self {
159 ImageContent {
160 data: i.raw.data,
161 mime_type: i.raw.mime_type,
162 annotations: None,
163 _meta: rmcp_meta_to_sdk(i.raw.meta),
164 }
165 }
166}
167
168impl From<AudioContent> for RmcpAudioContent {
169 fn from(a: AudioContent) -> Self {
170 Annotated {
172 raw: RawAudioContent {
173 data: a.data,
174 mime_type: a.mime_type,
175 },
176 annotations: None,
177 }
178 }
179}
180
181impl From<RmcpAudioContent> for AudioContent {
182 fn from(a: RmcpAudioContent) -> Self {
183 AudioContent {
184 data: a.raw.data,
185 mime_type: a.raw.mime_type,
186 annotations: None,
187 _meta: None,
188 }
189 }
190}
191
192impl From<ResourceContentsUnion> for RmcpResourceContents {
193 fn from(rcu: ResourceContentsUnion) -> Self {
194 match rcu {
195 ResourceContentsUnion::Text(t) => RmcpResourceContents::TextResourceContents {
196 uri: t.base.uri,
197 mime_type: t.base.mime_type,
198 text: t.text,
199 meta: sdk_meta_to_rmcp(t.base._meta),
200 },
201 ResourceContentsUnion::Blob(b) => RmcpResourceContents::BlobResourceContents {
202 uri: b.base.uri,
203 mime_type: b.base.mime_type,
204 blob: b.blob,
205 meta: sdk_meta_to_rmcp(b.base._meta),
206 },
207 }
208 }
209}
210
211impl From<RmcpResourceContents> for ResourceContentsUnion {
212 fn from(rc: RmcpResourceContents) -> Self {
213 match rc {
214 RmcpResourceContents::TextResourceContents {
215 uri,
216 mime_type,
217 text,
218 meta,
219 } => ResourceContentsUnion::Text(TextResourceContents {
220 base: ResourceContents {
221 uri,
222 mime_type,
223 _meta: rmcp_meta_to_sdk(meta),
224 },
225 text,
226 }),
227 RmcpResourceContents::BlobResourceContents {
228 uri,
229 mime_type,
230 blob,
231 meta,
232 } => ResourceContentsUnion::Blob(BlobResourceContents {
233 base: ResourceContents {
234 uri,
235 mime_type,
236 _meta: rmcp_meta_to_sdk(meta),
237 },
238 blob,
239 }),
240 }
241 }
242}
243
244impl TryFrom<ImageUrl> for RmcpImageContent {
248 type Error = ImageUrlNotDataUrl;
249 fn try_from(v: ImageUrl) -> Result<Self, Self::Error> {
250 Ok(ImageContent::try_from(v)?.into())
251 }
252}
253
254impl From<InputAudio> for RmcpAudioContent {
256 fn from(v: InputAudio) -> Self {
257 AudioContent::from(v).into()
258 }
259}
260
261impl From<RichContentPart> for RmcpContent {
263 fn from(v: RichContentPart) -> Self {
264 ContentBlock::from(v).into()
265 }
266}
267
268impl From<ImageUrl> for RmcpContent {
270 fn from(v: ImageUrl) -> Self {
271 ContentBlock::from(v).into()
272 }
273}
274
275impl From<InputAudio> for RmcpContent {
277 fn from(v: InputAudio) -> Self {
278 ContentBlock::from(v).into()
279 }
280}
281
282impl From<VideoUrl> for RmcpContent {
284 fn from(v: VideoUrl) -> Self {
285 ContentBlock::from(v).into()
286 }
287}
288
289impl From<File> for RmcpContent {
291 fn from(v: File) -> Self {
292 ContentBlock::from(v).into()
293 }
294}
295
296impl From<RichContent> for Vec<RmcpContent> {
298 fn from(v: RichContent) -> Self {
299 Vec::<ContentBlock>::from(v)
300 .into_iter()
301 .map(RmcpContent::from)
302 .collect()
303 }
304}
305
306impl From<RmcpResourceContents> for RichContentPart {
308 fn from(v: RmcpResourceContents) -> Self {
309 ResourceContentsUnion::from(v).into()
310 }
311}
312
313impl From<RmcpContent> for RichContentPart {
315 fn from(v: RmcpContent) -> Self {
316 ContentBlock::from(v).into()
317 }
318}
319
320impl From<Vec<RmcpContent>> for RichContent {
322 fn from(v: Vec<RmcpContent>) -> Self {
323 v.into_iter()
324 .map(ContentBlock::from)
325 .collect::<Vec<ContentBlock>>()
326 .into()
327 }
328}
329
330impl From<RmcpImageContent> for ImageUrl {
332 fn from(v: RmcpImageContent) -> Self {
333 ImageContent::from(v).into()
334 }
335}
336
337impl From<RmcpAudioContent> for InputAudio {
339 fn from(v: RmcpAudioContent) -> Self {
340 AudioContent::from(v).into()
341 }
342}
343
344#[cfg(test)]
347mod tests {
348 use super::*;
349
350 fn meta() -> Option<IndexMap<String, Value>> {
351 let mut m = IndexMap::new();
352 m.insert("k".to_string(), Value::String("v".to_string()));
353 Some(m)
354 }
355
356 #[test]
357 fn content_block_text_roundtrips_through_rmcp() {
358 let cb = ContentBlock::Text(TextContent {
359 text: "hi".to_string(),
360 annotations: None,
361 _meta: meta(),
362 });
363 let back: ContentBlock = RmcpContent::from(cb).into();
364 match back {
365 ContentBlock::Text(t) => {
366 assert_eq!(t.text, "hi");
367 assert_eq!(t._meta, meta());
368 }
369 other => panic!("expected Text, got {other:?}"),
370 }
371 }
372
373 #[test]
374 fn image_content_roundtrips_through_rmcp() {
375 let ic = ImageContent {
376 data: "AAAA".to_string(),
377 mime_type: "image/png".to_string(),
378 annotations: None,
379 _meta: meta(),
380 };
381 let back: ImageContent = RmcpImageContent::from(ic).into();
382 assert_eq!(back.data, "AAAA");
383 assert_eq!(back.mime_type, "image/png");
384 assert_eq!(back._meta, meta());
385 }
386
387 #[test]
388 fn audio_content_roundtrips_through_rmcp_meta_lost() {
389 let ac = AudioContent {
390 data: "BBBB".to_string(),
391 mime_type: "audio/mpeg".to_string(),
392 annotations: None,
393 _meta: meta(),
394 };
395 let back: AudioContent = RmcpAudioContent::from(ac).into();
396 assert_eq!(back.data, "BBBB");
397 assert_eq!(back.mime_type, "audio/mpeg");
398 assert_eq!(back._meta, None);
400 }
401
402 #[test]
403 fn resource_contents_text_roundtrips_through_rmcp() {
404 let rcu = ResourceContentsUnion::Text(TextResourceContents {
405 base: ResourceContents {
406 uri: "str:///x".to_string(),
407 mime_type: Some("text/plain".to_string()),
408 _meta: meta(),
409 },
410 text: "body".to_string(),
411 });
412 let back: ResourceContentsUnion = RmcpResourceContents::from(rcu).into();
413 match back {
414 ResourceContentsUnion::Text(t) => {
415 assert_eq!(t.base.uri, "str:///x");
416 assert_eq!(t.base.mime_type.as_deref(), Some("text/plain"));
417 assert_eq!(t.text, "body");
418 assert_eq!(t.base._meta, meta());
419 }
420 other => panic!("expected Text, got {other:?}"),
421 }
422 }
423}