1use std::collections::BTreeMap;
13
14use schemars::{JsonSchema, Schema};
15use serde::{Deserialize, Serialize};
16use serde_with::{DefaultOnError, VecSkipError, serde_as, skip_serializing_none};
17
18use super::Meta;
19use crate::{IntoOption, SkipListener};
20
21#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
37#[serde(tag = "type", rename_all = "snake_case")]
38#[schemars(extend("discriminator" = {"propertyName": "type"}))]
39#[non_exhaustive]
40pub enum ContentBlock {
41 Text(TextContent),
46 Image(ImageContent),
50 Audio(AudioContent),
54 ResourceLink(ResourceLink),
58 Resource(EmbeddedResource),
64 #[serde(untagged)]
74 Other(OtherContentBlock),
75}
76
77#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
79#[schemars(inline)]
80#[schemars(transform = other_content_block_schema)]
81#[serde(rename_all = "camelCase")]
82#[non_exhaustive]
83pub struct OtherContentBlock {
84 #[serde(rename = "type")]
90 pub type_: String,
91 #[serde(flatten)]
93 pub fields: BTreeMap<String, serde_json::Value>,
94}
95
96impl OtherContentBlock {
97 #[must_use]
99 pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
100 fields.remove("type");
101 Self {
102 type_: type_.into(),
103 fields,
104 }
105 }
106}
107
108impl<'de> Deserialize<'de> for OtherContentBlock {
109 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
110 where
111 D: serde::Deserializer<'de>,
112 {
113 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
114 let type_ = fields
115 .remove("type")
116 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
117 let serde_json::Value::String(type_) = type_ else {
118 return Err(serde::de::Error::custom("`type` must be a string"));
119 };
120
121 if is_known_content_block_type(&type_) {
122 return Err(serde::de::Error::custom(format!(
123 "known content block `{type_}` did not match its schema"
124 )));
125 }
126
127 Ok(Self { type_, fields })
128 }
129}
130
131fn is_known_content_block_type(type_: &str) -> bool {
132 matches!(
133 type_,
134 "text" | "image" | "audio" | "resource_link" | "resource"
135 )
136}
137
138fn other_content_block_schema(schema: &mut Schema) {
139 super::schema_util::reject_known_string_discriminators(
140 schema,
141 "type",
142 &["text", "image", "audio", "resource_link", "resource"],
143 );
144}
145
146#[serde_as]
148#[skip_serializing_none]
149#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
150#[non_exhaustive]
151pub struct TextContent {
152 pub text: String,
154 #[serde_as(deserialize_as = "DefaultOnError")]
156 #[schemars(extend("x-deserialize-default-on-error" = true))]
157 #[serde(default)]
158 pub annotations: Option<Annotations>,
159 #[serde_as(deserialize_as = "DefaultOnError")]
165 #[schemars(extend("x-deserialize-default-on-error" = true))]
166 #[serde(default)]
167 #[serde(rename = "_meta")]
168 pub meta: Option<Meta>,
169}
170
171impl TextContent {
172 #[must_use]
174 pub fn new(text: impl Into<String>) -> Self {
175 Self {
176 annotations: None,
177 text: text.into(),
178 meta: None,
179 }
180 }
181
182 #[must_use]
184 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
185 self.annotations = annotations.into_option();
186 self
187 }
188
189 #[must_use]
195 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
196 self.meta = meta.into_option();
197 self
198 }
199}
200
201impl<T: Into<String>> From<T> for ContentBlock {
202 fn from(value: T) -> Self {
203 Self::Text(TextContent::new(value))
204 }
205}
206
207#[serde_as]
209#[skip_serializing_none]
210#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
211#[serde(rename_all = "camelCase")]
212#[non_exhaustive]
213pub struct ImageContent {
214 #[schemars(extend("format" = "byte"))]
216 pub data: String,
217 pub mime_type: String,
219 #[serde_as(deserialize_as = "DefaultOnError")]
221 #[schemars(extend("x-deserialize-default-on-error" = true))]
222 #[schemars(url)]
223 #[serde(default)]
224 pub uri: Option<String>,
225 #[serde_as(deserialize_as = "DefaultOnError")]
227 #[schemars(extend("x-deserialize-default-on-error" = true))]
228 #[serde(default)]
229 pub annotations: Option<Annotations>,
230 #[serde_as(deserialize_as = "DefaultOnError")]
236 #[schemars(extend("x-deserialize-default-on-error" = true))]
237 #[serde(default)]
238 #[serde(rename = "_meta")]
239 pub meta: Option<Meta>,
240}
241
242impl ImageContent {
243 #[must_use]
245 pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
246 Self {
247 annotations: None,
248 data: data.into(),
249 mime_type: mime_type.into(),
250 uri: None,
251 meta: None,
252 }
253 }
254
255 #[must_use]
257 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
258 self.annotations = annotations.into_option();
259 self
260 }
261
262 #[must_use]
264 pub fn uri(mut self, uri: impl IntoOption<String>) -> Self {
265 self.uri = uri.into_option();
266 self
267 }
268
269 #[must_use]
275 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
276 self.meta = meta.into_option();
277 self
278 }
279}
280
281#[serde_as]
283#[skip_serializing_none]
284#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
285#[serde(rename_all = "camelCase")]
286#[non_exhaustive]
287pub struct AudioContent {
288 #[schemars(extend("format" = "byte"))]
290 pub data: String,
291 pub mime_type: String,
293 #[serde_as(deserialize_as = "DefaultOnError")]
295 #[schemars(extend("x-deserialize-default-on-error" = true))]
296 #[serde(default)]
297 pub annotations: Option<Annotations>,
298 #[serde_as(deserialize_as = "DefaultOnError")]
304 #[schemars(extend("x-deserialize-default-on-error" = true))]
305 #[serde(default)]
306 #[serde(rename = "_meta")]
307 pub meta: Option<Meta>,
308}
309
310impl AudioContent {
311 #[must_use]
313 pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
314 Self {
315 annotations: None,
316 data: data.into(),
317 mime_type: mime_type.into(),
318 meta: None,
319 }
320 }
321
322 #[must_use]
324 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
325 self.annotations = annotations.into_option();
326 self
327 }
328
329 #[must_use]
335 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
336 self.meta = meta.into_option();
337 self
338 }
339}
340
341#[serde_as]
343#[skip_serializing_none]
344#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
345#[non_exhaustive]
346pub struct EmbeddedResource {
347 pub resource: EmbeddedResourceResource,
349 #[serde_as(deserialize_as = "DefaultOnError")]
351 #[schemars(extend("x-deserialize-default-on-error" = true))]
352 #[serde(default)]
353 pub annotations: Option<Annotations>,
354 #[serde_as(deserialize_as = "DefaultOnError")]
360 #[schemars(extend("x-deserialize-default-on-error" = true))]
361 #[serde(default)]
362 #[serde(rename = "_meta")]
363 pub meta: Option<Meta>,
364}
365
366impl EmbeddedResource {
367 #[must_use]
369 pub fn new(resource: EmbeddedResourceResource) -> Self {
370 Self {
371 annotations: None,
372 resource,
373 meta: None,
374 }
375 }
376
377 #[must_use]
379 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
380 self.annotations = annotations.into_option();
381 self
382 }
383
384 #[must_use]
390 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
391 self.meta = meta.into_option();
392 self
393 }
394}
395
396#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
398#[serde(untagged)]
399#[non_exhaustive]
400pub enum EmbeddedResourceResource {
401 TextResourceContents(TextResourceContents),
403 BlobResourceContents(BlobResourceContents),
405}
406
407#[serde_as]
409#[skip_serializing_none]
410#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
411#[serde(rename_all = "camelCase")]
412#[non_exhaustive]
413pub struct TextResourceContents {
414 pub text: String,
416 #[schemars(url)]
418 pub uri: String,
419 #[serde_as(deserialize_as = "DefaultOnError")]
421 #[schemars(extend("x-deserialize-default-on-error" = true))]
422 #[serde(default)]
423 pub mime_type: Option<String>,
424 #[serde_as(deserialize_as = "DefaultOnError")]
430 #[schemars(extend("x-deserialize-default-on-error" = true))]
431 #[serde(default)]
432 #[serde(rename = "_meta")]
433 pub meta: Option<Meta>,
434}
435
436impl TextResourceContents {
437 #[must_use]
439 pub fn new(text: impl Into<String>, uri: impl Into<String>) -> Self {
440 Self {
441 mime_type: None,
442 text: text.into(),
443 uri: uri.into(),
444 meta: None,
445 }
446 }
447
448 #[must_use]
450 pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
451 self.mime_type = mime_type.into_option();
452 self
453 }
454
455 #[must_use]
461 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
462 self.meta = meta.into_option();
463 self
464 }
465}
466
467#[serde_as]
469#[skip_serializing_none]
470#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
471#[serde(rename_all = "camelCase")]
472#[non_exhaustive]
473pub struct BlobResourceContents {
474 #[schemars(extend("format" = "byte"))]
476 pub blob: String,
477 #[schemars(url)]
479 pub uri: String,
480 #[serde_as(deserialize_as = "DefaultOnError")]
482 #[schemars(extend("x-deserialize-default-on-error" = true))]
483 #[serde(default)]
484 pub mime_type: Option<String>,
485 #[serde_as(deserialize_as = "DefaultOnError")]
491 #[schemars(extend("x-deserialize-default-on-error" = true))]
492 #[serde(default)]
493 #[serde(rename = "_meta")]
494 pub meta: Option<Meta>,
495}
496
497impl BlobResourceContents {
498 #[must_use]
500 pub fn new(blob: impl Into<String>, uri: impl Into<String>) -> Self {
501 Self {
502 blob: blob.into(),
503 mime_type: None,
504 uri: uri.into(),
505 meta: None,
506 }
507 }
508
509 #[must_use]
511 pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
512 self.mime_type = mime_type.into_option();
513 self
514 }
515
516 #[must_use]
522 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
523 self.meta = meta.into_option();
524 self
525 }
526}
527
528#[serde_as]
530#[skip_serializing_none]
531#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
532#[serde(rename_all = "camelCase")]
533#[non_exhaustive]
534pub struct ResourceLink {
535 pub name: String,
537 #[schemars(url)]
539 pub uri: String,
540 #[serde_as(deserialize_as = "DefaultOnError")]
542 #[schemars(extend("x-deserialize-default-on-error" = true))]
543 #[serde(default)]
544 pub title: Option<String>,
545 #[serde_as(deserialize_as = "DefaultOnError")]
547 #[schemars(extend("x-deserialize-default-on-error" = true))]
548 #[serde(default)]
549 pub description: Option<String>,
550 #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
552 #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
553 #[serde(default)]
554 pub icons: Option<Vec<Icon>>,
555 #[serde_as(deserialize_as = "DefaultOnError")]
557 #[schemars(extend("x-deserialize-default-on-error" = true))]
558 #[serde(default)]
559 pub mime_type: Option<String>,
560 #[serde_as(deserialize_as = "DefaultOnError")]
562 #[schemars(extend("x-deserialize-default-on-error" = true))]
563 #[serde(default)]
564 pub size: Option<i64>,
565 #[serde_as(deserialize_as = "DefaultOnError")]
567 #[schemars(extend("x-deserialize-default-on-error" = true))]
568 #[serde(default)]
569 pub annotations: Option<Annotations>,
570 #[serde_as(deserialize_as = "DefaultOnError")]
576 #[schemars(extend("x-deserialize-default-on-error" = true))]
577 #[serde(default)]
578 #[serde(rename = "_meta")]
579 pub meta: Option<Meta>,
580}
581
582impl ResourceLink {
583 #[must_use]
585 pub fn new(name: impl Into<String>, uri: impl Into<String>) -> Self {
586 Self {
587 annotations: None,
588 description: None,
589 icons: None,
590 mime_type: None,
591 name: name.into(),
592 size: None,
593 title: None,
594 uri: uri.into(),
595 meta: None,
596 }
597 }
598
599 #[must_use]
601 pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
602 self.annotations = annotations.into_option();
603 self
604 }
605
606 #[must_use]
608 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
609 self.description = description.into_option();
610 self
611 }
612
613 #[must_use]
615 pub fn icons(mut self, icons: impl IntoOption<Vec<Icon>>) -> Self {
616 self.icons = icons.into_option();
617 self
618 }
619
620 #[must_use]
622 pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
623 self.mime_type = mime_type.into_option();
624 self
625 }
626
627 #[must_use]
629 pub fn size(mut self, size: impl IntoOption<i64>) -> Self {
630 self.size = size.into_option();
631 self
632 }
633
634 #[must_use]
636 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
637 self.title = title.into_option();
638 self
639 }
640
641 #[must_use]
647 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
648 self.meta = meta.into_option();
649 self
650 }
651}
652
653#[serde_as]
655#[skip_serializing_none]
656#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
657#[serde(rename_all = "camelCase")]
658#[non_exhaustive]
659pub struct Icon {
660 #[schemars(url)]
662 pub src: String,
663 #[serde_as(deserialize_as = "DefaultOnError")]
665 #[schemars(extend("x-deserialize-default-on-error" = true))]
666 #[serde(default)]
667 pub mime_type: Option<String>,
668 #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
670 #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
671 #[serde(default)]
672 pub sizes: Option<Vec<String>>,
673 #[serde_as(deserialize_as = "DefaultOnError")]
675 #[schemars(extend("x-deserialize-default-on-error" = true))]
676 #[serde(default)]
677 pub theme: Option<IconTheme>,
678}
679
680impl Icon {
681 #[must_use]
683 pub fn new(src: impl Into<String>) -> Self {
684 Self {
685 src: src.into(),
686 mime_type: None,
687 sizes: None,
688 theme: None,
689 }
690 }
691
692 #[must_use]
694 pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
695 self.mime_type = mime_type.into_option();
696 self
697 }
698
699 #[must_use]
701 pub fn sizes(mut self, sizes: impl IntoOption<Vec<String>>) -> Self {
702 self.sizes = sizes.into_option();
703 self
704 }
705
706 #[must_use]
708 pub fn theme(mut self, theme: impl IntoOption<IconTheme>) -> Self {
709 self.theme = theme.into_option();
710 self
711 }
712}
713
714#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
716#[serde(rename_all = "camelCase")]
717#[non_exhaustive]
718pub enum IconTheme {
719 Light,
721 Dark,
723 #[serde(untagged)]
729 Other(String),
730}
731
732#[serde_as]
734#[skip_serializing_none]
735#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, Default)]
736#[serde(rename_all = "camelCase")]
737#[non_exhaustive]
738pub struct Annotations {
739 #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
741 #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
742 #[serde(default)]
743 pub audience: Option<Vec<Role>>,
744 #[serde_as(deserialize_as = "DefaultOnError")]
748 #[schemars(extend("x-deserialize-default-on-error" = true, "format" = "date-time"))]
749 #[serde(default)]
750 pub last_modified: Option<String>,
751 #[serde_as(deserialize_as = "DefaultOnError")]
753 #[schemars(extend("x-deserialize-default-on-error" = true))]
754 #[schemars(range(min = 0, max = 1))]
755 #[serde(default)]
756 pub priority: Option<f64>,
757 #[serde_as(deserialize_as = "DefaultOnError")]
763 #[schemars(extend("x-deserialize-default-on-error" = true))]
764 #[serde(default)]
765 #[serde(rename = "_meta")]
766 pub meta: Option<Meta>,
767}
768
769impl Annotations {
770 #[must_use]
772 pub fn new() -> Self {
773 Self::default()
774 }
775
776 #[must_use]
778 pub fn audience(mut self, audience: impl IntoOption<Vec<Role>>) -> Self {
779 self.audience = audience.into_option();
780 self
781 }
782
783 #[must_use]
785 pub fn last_modified(mut self, last_modified: impl IntoOption<String>) -> Self {
786 self.last_modified = last_modified.into_option();
787 self
788 }
789
790 #[must_use]
792 pub fn priority(mut self, priority: impl IntoOption<f64>) -> Self {
793 self.priority = priority.into_option();
794 self
795 }
796
797 #[must_use]
803 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
804 self.meta = meta.into_option();
805 self
806 }
807}
808
809#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
811#[serde(rename_all = "camelCase")]
812#[non_exhaustive]
813pub enum Role {
814 Assistant,
816 User,
818 #[serde(untagged)]
824 Other(String),
825}
826
827#[cfg(test)]
828mod tests {
829 use super::*;
830
831 #[test]
832 fn test_text_content_roundtrip() {
833 let content = TextContent::new("hello world");
834 let json = serde_json::to_value(&content).unwrap();
835 let parsed: TextContent = serde_json::from_value(json).unwrap();
836 assert_eq!(content, parsed);
837 }
838
839 #[test]
840 fn test_text_content_omits_optional_fields() {
841 let content = TextContent::new("hello");
842 let json = serde_json::to_value(&content).unwrap();
843 assert!(!json.as_object().unwrap().contains_key("annotations"));
844 assert!(!json.as_object().unwrap().contains_key("meta"));
845 }
846
847 #[test]
848 fn test_text_content_meta_defaults_on_missing_or_malformed_value() {
849 let missing: TextContent = serde_json::from_value(serde_json::json!({
850 "text": "hello"
851 }))
852 .unwrap();
853 assert_eq!(missing.meta, None);
854
855 let malformed: TextContent = serde_json::from_value(serde_json::json!({
856 "text": "hello",
857 "_meta": false
858 }))
859 .unwrap();
860 assert_eq!(malformed.meta, None);
861 }
862
863 #[test]
864 fn test_text_content_from_string() {
865 let block: ContentBlock = "hello".into();
866 match block {
867 ContentBlock::Text(c) => assert_eq!(c.text, "hello"),
868 _ => panic!("Expected Text variant"),
869 }
870 }
871
872 #[test]
873 fn role_preserves_unknown_variant() {
874 let role: Role = serde_json::from_str("\"critic\"").unwrap();
875 assert_eq!(role, Role::Other("critic".to_string()));
876 assert_eq!(serde_json::to_value(&role).unwrap(), "critic");
877 }
878
879 #[test]
880 fn icon_theme_preserves_unknown_variant() {
881 let theme: IconTheme = serde_json::from_str("\"contrast\"").unwrap();
882 assert_eq!(theme, IconTheme::Other("contrast".to_string()));
883 assert_eq!(serde_json::to_value(&theme).unwrap(), "contrast");
884 }
885
886 #[test]
887 fn content_block_preserves_unknown_variant() {
888 let block: ContentBlock = serde_json::from_value(serde_json::json!({
889 "type": "_widget",
890 "title": "Status",
891 "state": {"ok": true}
892 }))
893 .unwrap();
894
895 let ContentBlock::Other(unknown) = block else {
896 panic!("expected unknown content block");
897 };
898
899 assert_eq!(unknown.type_, "_widget");
900 assert_eq!(
901 unknown.fields.get("title"),
902 Some(&serde_json::json!("Status"))
903 );
904 assert_eq!(
905 serde_json::to_value(ContentBlock::Other(unknown)).unwrap(),
906 serde_json::json!({
907 "type": "_widget",
908 "title": "Status",
909 "state": {"ok": true}
910 })
911 );
912 }
913
914 #[test]
915 fn content_block_does_not_hide_malformed_known_variant() {
916 assert!(
917 serde_json::from_value::<ContentBlock>(serde_json::json!({
918 "type": "text"
919 }))
920 .is_err()
921 );
922 }
923
924 #[test]
925 fn test_image_content_roundtrip() {
926 let content = ImageContent::new("base64data", "image/png");
927 let json = serde_json::to_value(&content).unwrap();
928 let parsed: ImageContent = serde_json::from_value(json).unwrap();
929 assert_eq!(content, parsed);
930 }
931
932 #[test]
933 fn test_image_content_omits_optional_fields() {
934 let content = ImageContent::new("data", "image/png");
935 let json = serde_json::to_value(&content).unwrap();
936 assert!(!json.as_object().unwrap().contains_key("uri"));
937 assert!(!json.as_object().unwrap().contains_key("annotations"));
938 assert!(!json.as_object().unwrap().contains_key("meta"));
939 }
940
941 #[test]
942 fn test_image_content_with_uri() {
943 let content = ImageContent::new("data", "image/png").uri("https://example.com/image.png");
944 let json = serde_json::to_value(&content).unwrap();
945 assert_eq!(json["uri"], "https://example.com/image.png");
946 }
947
948 #[test]
949 fn test_audio_content_roundtrip() {
950 let content = AudioContent::new("base64audio", "audio/mp3");
951 let json = serde_json::to_value(&content).unwrap();
952 let parsed: AudioContent = serde_json::from_value(json).unwrap();
953 assert_eq!(content, parsed);
954 }
955
956 #[test]
957 fn test_audio_content_omits_optional_fields() {
958 let content = AudioContent::new("data", "audio/mp3");
959 let json = serde_json::to_value(&content).unwrap();
960 assert!(!json.as_object().unwrap().contains_key("annotations"));
961 assert!(!json.as_object().unwrap().contains_key("meta"));
962 }
963
964 #[test]
965 fn resource_link_icons_roundtrip() {
966 let icon = Icon::new("https://example.com/icon.png")
967 .mime_type("image/png")
968 .sizes(vec!["48x48".to_string(), "any".to_string()])
969 .theme(IconTheme::Dark);
970 let link = ResourceLink::new("Example", "file:///example.txt").icons(vec![icon]);
971
972 let json = serde_json::to_value(&link).unwrap();
973 assert_eq!(json["icons"][0]["src"], "https://example.com/icon.png");
974 assert_eq!(json["icons"][0]["mimeType"], "image/png");
975 assert_eq!(json["icons"][0]["sizes"][0], "48x48");
976 assert_eq!(json["icons"][0]["theme"], "dark");
977
978 let parsed: ResourceLink = serde_json::from_value(json).unwrap();
979 assert_eq!(link, parsed);
980 }
981
982 #[test]
983 fn annotations_priority_schema_matches_mcp_bounds() {
984 let schema = schemars::schema_for!(Annotations);
985 let json = serde_json::to_value(schema).unwrap();
986
987 assert_eq!(json["properties"]["priority"]["minimum"], 0);
988 assert_eq!(json["properties"]["priority"]["maximum"], 1);
989 assert_eq!(json["properties"]["lastModified"]["format"], "date-time");
990 }
991
992 #[test]
993 fn content_schema_matches_mcp_string_formats() {
994 let image = serde_json::to_value(schemars::schema_for!(ImageContent)).unwrap();
995 assert_eq!(image["properties"]["data"]["format"], "byte");
996 assert_eq!(image["properties"]["uri"]["format"], "uri");
997
998 let audio = serde_json::to_value(schemars::schema_for!(AudioContent)).unwrap();
999 assert_eq!(audio["properties"]["data"]["format"], "byte");
1000
1001 let text_resource =
1002 serde_json::to_value(schemars::schema_for!(TextResourceContents)).unwrap();
1003 assert_eq!(text_resource["properties"]["uri"]["format"], "uri");
1004
1005 let blob_resource =
1006 serde_json::to_value(schemars::schema_for!(BlobResourceContents)).unwrap();
1007 assert_eq!(blob_resource["properties"]["blob"]["format"], "byte");
1008 assert_eq!(blob_resource["properties"]["uri"]["format"], "uri");
1009
1010 let resource_link = serde_json::to_value(schemars::schema_for!(ResourceLink)).unwrap();
1011 assert_eq!(resource_link["properties"]["uri"]["format"], "uri");
1012
1013 let icon = serde_json::to_value(schemars::schema_for!(Icon)).unwrap();
1014 assert_eq!(icon["properties"]["src"]["format"], "uri");
1015 }
1016}