1const COMPLETE: &str = "complete";
54
55use protocol_droid_schema::wire::draft as wire;
56
57use super::{AUDIO_TYPE, IMAGE_TYPE, META_KEY, RESOURCE_LINK_TYPE, RESOURCE_TYPE, TEXT_TYPE};
58use crate::domain::ir::Codec;
59use crate::domain::ir::lower::{LowerError, TranslationReport, lower_tool_result};
60use crate::domain::ir::{
61 Annotations, AudioContent, Content, EmbeddedResource, ExtensionValue, Extensions, ImageContent,
62 JsonObject, ResourceIcon, ResourceLinkContent, Role, TextContent, ToolResult,
63};
64use crate::domain::{
65 CapabilitySet, DegradePolicy, FeatureProfile, Negotiation, ProtocolVersion, TransportKind,
66 resolve,
67};
68
69fn lift_annotations(annotations: &wire::Annotations) -> Annotations {
70 Annotations {
71 audience: annotations
72 .audience
73 .iter()
74 .flatten()
75 .map(|role| match role {
76 wire::Role::ASSISTANT => Role::Assistant,
77 wire::Role::USER => Role::User,
78 })
79 .collect(),
80 priority: annotations.priority,
81 last_modified: annotations.last_modified.clone(),
82 extensions: Extensions::from_json_object(annotations.extra.clone()),
83 }
84}
85
86fn lower_annotations(annotations: Option<&Annotations>) -> Option<wire::Annotations> {
87 let annotations = annotations?;
88 let extra = annotations.extensions.to_json_object();
89 if annotations.audience.is_empty()
90 && annotations.priority.is_none()
91 && annotations.last_modified.is_none()
92 && extra.is_empty()
93 {
94 return None;
95 }
96 Some(wire::Annotations {
97 audience: (!annotations.audience.is_empty()).then(|| {
98 annotations
99 .audience
100 .iter()
101 .map(|role| match role {
102 Role::Assistant => wire::Role::ASSISTANT,
103 Role::User => wire::Role::USER,
104 })
105 .collect()
106 }),
107 last_modified: annotations.last_modified.clone(),
108 priority: annotations.priority,
109 extra,
110 })
111}
112
113const RAW_STRUCTURED_KEY: &str = "structuredContent";
122
123fn take_meta(extensions: &Extensions) -> Option<serde_json::Value> {
130 let mut extensions = extensions.clone();
131 extensions
132 .take(META_KEY)
133 .map(|value| serde_json::Value::from(&value))
134}
135
136#[derive(Debug, Clone, Copy)]
138pub struct Draft;
139
140impl Codec for Draft {
141 type Wire = wire::CallToolResult;
142
143 fn version(&self) -> ProtocolVersion {
144 ProtocolVersion::V2026_07_28
145 }
146
147 fn capabilities(&self) -> FeatureProfile {
148 let caps = CapabilitySet::all_for(self.version());
149 resolve(Negotiation {
150 version: self.version(),
151 client_caps: caps,
152 server_caps: caps,
153 transport: TransportKind::Stdio,
154 })
155 }
156
157 fn lift(&self, wire: &Self::Wire) -> ToolResult {
158 let mut extensions = Extensions::from_json_object(wire.extra.clone());
159 if let Some(meta) = &wire.meta {
160 extensions.insert(META_KEY, ExtensionValue::from(meta_to_value(meta)));
161 }
162 let structured_content = match &wire.structured_content {
167 Some(value) if value.is_object() => JsonObject::new(value.to_string()).ok(),
168 Some(value) => {
169 extensions.insert(RAW_STRUCTURED_KEY, ExtensionValue::from(value.clone()));
170 None
171 }
172 None => None,
173 };
174 ToolResult {
175 content: wire.content.iter().map(lift_block).collect(),
176 structured_content,
177 is_error: wire.is_error.unwrap_or(false),
178 result_type: Some(wire.result_type.clone()),
179 extensions,
180 }
181 }
182
183 fn lower(
184 &self,
185 value: ToolResult,
186 policy: DegradePolicy,
187 report: &mut TranslationReport,
188 ) -> Result<Self::Wire, LowerError> {
189 let (value, lowered) =
190 lower_tool_result(value, &self.capabilities(), self.version(), policy)?;
191 report.absorb(&lowered);
192 let result_type = value
198 .result_type
199 .clone()
200 .unwrap_or_else(|| COMPLETE.to_owned());
201 let structured_content = match value.extensions.get(RAW_STRUCTURED_KEY) {
202 Some(raw) => Some(serde_json::Value::from(raw)),
203 None => value
204 .structured_content
205 .as_ref()
206 .and_then(|object| serde_json::from_str(object.as_str()).ok()),
207 };
208 Ok(wire::CallToolResult {
209 meta: lower_meta(value.extensions.get(META_KEY), ProtocolVersion::V2026_07_28)?,
210 content: value
211 .content
212 .iter()
213 .map(lower_block)
214 .collect::<Result<Vec<_>, _>>()?,
215 is_error: value.is_error.then_some(true),
216 result_type,
217 structured_content,
218 extra: value.extensions.to_json_object(),
219 })
220 }
221}
222
223fn lift_icon(icon: &wire::Icon) -> ResourceIcon {
227 ResourceIcon {
228 src: icon.src.clone(),
229 mime_type: icon.mime_type.clone(),
230 sizes: icon.sizes.clone().unwrap_or_default(),
235 theme: icon.theme.clone(),
236 extensions: Extensions::from_json_object(icon.extra.clone()),
237 }
238}
239
240fn lower_icon(icon: &ResourceIcon) -> wire::Icon {
247 wire::Icon {
248 src: icon.src.clone(),
249 mime_type: icon.mime_type.clone(),
250 sizes: (!icon.sizes.is_empty()).then(|| icon.sizes.clone()),
251 theme: icon.theme.clone(),
252 extra: icon.extensions.to_json_object(),
253 }
254}
255
256fn meta_to_value(meta: &wire::ResultMetaObject) -> serde_json::Value {
269 let mut map = meta.extra.clone();
270 if let Some(info) = &meta.io_modelcontextprotocol_server_info {
271 let mut object = info.extra.clone();
272 object.insert(
273 "name".to_owned(),
274 serde_json::Value::String(info.name.clone()),
275 );
276 object.insert(
277 "version".to_owned(),
278 serde_json::Value::String(info.version.clone()),
279 );
280 for (key, value) in [
281 ("title", &info.title),
282 ("description", &info.description),
283 ("websiteUrl", &info.website_url),
284 ] {
285 if let Some(value) = value {
286 object.insert(key.to_owned(), serde_json::Value::String(value.clone()));
287 }
288 }
289 if let Some(icons) = &info.icons {
290 object.insert(
291 "icons".to_owned(),
292 serde_json::Value::Array(icons.iter().map(icon_to_value).collect()),
293 );
294 }
295 map.insert(
296 SERVER_INFO_KEY.to_owned(),
297 serde_json::Value::Object(object),
298 );
299 }
300 serde_json::Value::Object(map)
301}
302
303fn icon_to_value(icon: &wire::Icon) -> serde_json::Value {
306 let mut map = icon.extra.clone();
307 map.insert(
308 "src".to_owned(),
309 serde_json::Value::String(icon.src.clone()),
310 );
311 if let Some(mime_type) = &icon.mime_type {
312 map.insert(
313 "mimeType".to_owned(),
314 serde_json::Value::String(mime_type.clone()),
315 );
316 }
317 if let Some(sizes) = &icon.sizes {
318 map.insert(
319 "sizes".to_owned(),
320 serde_json::Value::Array(
321 sizes
322 .iter()
323 .map(|s| serde_json::Value::String(s.clone()))
324 .collect(),
325 ),
326 );
327 }
328 if let Some(theme) = &icon.theme {
329 map.insert("theme".to_owned(), theme.clone());
330 }
331 serde_json::Value::Object(map)
332}
333
334fn lower_meta(
341 value: Option<&ExtensionValue>,
342 target: ProtocolVersion,
343) -> Result<Option<wire::ResultMetaObject>, LowerError> {
344 let Some(raw) = value else {
345 return Ok(None);
346 };
347 let as_value = serde_json::Value::from(raw);
348 let object = as_value
349 .as_object()
350 .ok_or_else(|| LowerError::FieldNotRepresentable {
351 field: "_meta".to_owned(),
352 expected: "an object",
353 detail: "the result carried `_meta` that is not an object".to_owned(),
354 target,
355 })?;
356 let result: wire::ResultMetaObject =
357 serde_json::from_value(serde_json::Value::Object(object.clone())).map_err(|error| {
358 LowerError::FieldNotRepresentable {
359 field: "_meta".to_owned(),
360 expected: "a result meta object",
361 detail: error.to_string(),
362 target,
363 }
364 })?;
365 let _ = object;
366 Ok(Some(result))
367}
368
369const SERVER_INFO_KEY: &str = "io.modelcontextprotocol/serverInfo";
372
373fn lift_block(block: &wire::ContentBlock) -> Content {
374 match block {
375 wire::ContentBlock::Member0(text) => {
376 let mut extensions = Extensions::from_json_object(text.extra.clone());
377 if let Some(meta) = &text.meta {
378 extensions.insert(META_KEY, ExtensionValue::from(meta.clone()));
379 }
380 Content::Text(TextContent {
381 text: text.text.clone(),
382 annotations: text.annotations.as_ref().map(lift_annotations),
383 extensions,
384 })
385 }
386 wire::ContentBlock::Member1(image) => {
387 let mut extensions = Extensions::from_json_object(image.extra.clone());
388 if let Some(meta) = &image.meta {
389 extensions.insert(META_KEY, ExtensionValue::from(meta.clone()));
390 }
391 Content::Image(ImageContent {
392 data: image.data.clone(),
393 mime_type: image.mime_type.clone(),
394 annotations: image.annotations.as_ref().map(lift_annotations),
395 extensions,
396 })
397 }
398 wire::ContentBlock::Member2(audio) => {
399 let mut extensions = Extensions::from_json_object(audio.extra.clone());
400 if let Some(meta) = &audio.meta {
401 extensions.insert(META_KEY, ExtensionValue::from(meta.clone()));
402 }
403 Content::Audio(AudioContent {
404 data: audio.data.clone(),
405 mime_type: audio.mime_type.clone(),
406 annotations: audio.annotations.as_ref().map(lift_annotations),
407 extensions,
408 })
409 }
410 wire::ContentBlock::Member3(link) => {
411 let mut extensions = Extensions::from_json_object(link.extra.clone());
412 if let Some(meta) = &link.meta {
413 extensions.insert(META_KEY, ExtensionValue::from(meta.clone()));
414 }
415 Content::ResourceLink(ResourceLinkContent {
416 uri: link.uri.clone(),
417 name: link.name.clone(),
418 description: link.description.clone(),
419 mime_type: link.mime_type.clone(),
420 size: link.size.and_then(|size| u64::try_from(size).ok()),
424 icons: link.icons.iter().flatten().map(lift_icon).collect(),
425 title: link.title.clone(),
426 annotations: link.annotations.as_ref().map(lift_annotations),
427 extensions,
428 })
429 }
430 wire::ContentBlock::Member4(resource) => {
431 let wire::EmbeddedResource {
432 meta,
433 annotations,
434 resource,
435 extra,
436 ..
437 } = resource;
438 let (uri, mime_type, text, blob) = match resource {
439 wire::EmbeddedResourceResourceChoice::Member0(text) => (
440 text.uri.clone(),
441 text.mime_type.clone(),
442 Some(text.text.clone()),
443 None,
444 ),
445 wire::EmbeddedResourceResourceChoice::Member1(blob) => (
446 blob.uri.clone(),
447 blob.mime_type.clone(),
448 None,
449 Some(blob.blob.clone()),
450 ),
451 };
452 let mut extensions = Extensions::from_json_object(extra.clone());
453 if let Some(meta) = meta {
454 extensions.insert(META_KEY, ExtensionValue::from(meta.clone()));
455 }
456 Content::EmbeddedResource(EmbeddedResource {
457 uri,
458 mime_type,
459 text,
460 blob,
461 annotations: annotations.as_ref().map(lift_annotations),
462 extensions,
463 })
464 }
465 }
466}
467
468fn lower_block(block: &Content) -> Result<wire::ContentBlock, LowerError> {
469 let lowered = match block {
470 Content::Text(text) => wire::ContentBlock::Member0(wire::TextContent {
471 meta: take_meta(&text.extensions),
472 annotations: lower_annotations(text.annotations.as_ref()),
473 text: text.text.clone(),
474 r#type: TEXT_TYPE.to_owned(),
475 extra: text.extensions.to_json_object(),
476 }),
477 Content::Image(image) => wire::ContentBlock::Member1(wire::ImageContent {
478 meta: take_meta(&image.extensions),
479 annotations: lower_annotations(image.annotations.as_ref()),
480 data: image.data.clone(),
481 mime_type: image.mime_type.clone(),
482 r#type: IMAGE_TYPE.to_owned(),
483 extra: image.extensions.to_json_object(),
484 }),
485 Content::Audio(audio) => wire::ContentBlock::Member2(wire::AudioContent {
486 meta: take_meta(&audio.extensions),
487 annotations: lower_annotations(audio.annotations.as_ref()),
488 data: audio.data.clone(),
489 mime_type: audio.mime_type.clone(),
490 r#type: AUDIO_TYPE.to_owned(),
491 extra: audio.extensions.to_json_object(),
492 }),
493 Content::ResourceLink(link) => {
494 let size = link.size.map_or(Ok(None), |size| {
495 i64::try_from(size)
496 .map(Some)
497 .map_err(|_| LowerError::FieldNotRepresentable {
498 field: "size".to_owned(),
499 expected: "a non-negative size",
500 detail: format!("{size} does not fit the i64 this revision uses"),
501 target: ProtocolVersion::V2026_07_28,
502 })
503 })?;
504 wire::ContentBlock::Member3(wire::ResourceLink {
505 meta: take_meta(&link.extensions),
506 annotations: lower_annotations(link.annotations.as_ref()),
507 description: link.description.clone(),
508 mime_type: link.mime_type.clone(),
509 name: link.name.clone(),
510 size,
511 title: link.title.clone(),
512 r#type: RESOURCE_LINK_TYPE.to_owned(),
513 uri: link.uri.clone(),
514 icons: (!link.icons.is_empty())
515 .then(|| link.icons.iter().map(lower_icon).collect()),
516 extra: link.extensions.to_json_object(),
517 })
518 }
519 Content::EmbeddedResource(resource) => {
520 let contents = if let Some(text) = &resource.text {
521 wire::EmbeddedResourceResourceChoice::Member0(wire::TextResourceContents {
522 meta: None,
523 mime_type: resource.mime_type.clone(),
524 text: text.clone(),
525 uri: resource.uri.clone(),
526 extra: serde_json::Map::new(),
527 })
528 } else if let Some(blob) = &resource.blob {
529 wire::EmbeddedResourceResourceChoice::Member1(wire::BlobResourceContents {
530 meta: None,
531 blob: blob.clone(),
532 mime_type: resource.mime_type.clone(),
533 uri: resource.uri.clone(),
534 extra: serde_json::Map::new(),
535 })
536 } else {
537 return Err(LowerError::FieldNotRepresentable {
538 field: "resource".to_owned(),
539 expected: "a text or blob body",
540 detail: "the resource has neither `text` nor `blob`".to_owned(),
541 target: ProtocolVersion::V2026_07_28,
542 });
543 };
544 wire::ContentBlock::Member4(wire::EmbeddedResource {
545 meta: take_meta(&resource.extensions),
546 annotations: lower_annotations(resource.annotations.as_ref()),
547 resource: contents,
548 r#type: RESOURCE_TYPE.to_owned(),
549 extra: resource.extensions.to_json_object(),
550 })
551 }
552 Content::Unknown(unknown) => {
553 return Err(LowerError::FieldNotRepresentable {
554 field: unknown.type_name.clone(),
555 expected: "a draft content block",
556 detail: "this build does not model the block, and this revision has no \
557 member that could carry it"
558 .to_owned(),
559 target: ProtocolVersion::V2026_07_28,
560 });
561 }
562 };
563 Ok(lowered)
564}
565
566#[cfg(test)]
567mod tests {
568 type TestError = Box<dyn std::error::Error>;
574
575 use super::Draft;
576 use crate::domain::DegradePolicy;
577 use crate::domain::ir::Codec;
578 use crate::domain::ir::lower::TranslationReport;
579 use crate::domain::ir::{Content, ResourceIcon, ResourceLinkContent, ToolResult};
580
581 fn round_trip(json: &str) -> Result<serde_json::Value, TestError> {
582 let codec = Draft;
583 let wire: super::wire::CallToolResult = serde_json::from_str(json)?;
584 let mut report = TranslationReport::new(codec.version());
585 let lowered = codec.lower(codec.lift(&wire), DegradePolicy::strict(), &mut report)?;
586 Ok(serde_json::to_value(&lowered)?)
587 }
588
589 #[test]
594 fn a_resource_link_is_the_fourth_arm() -> Result<(), TestError> {
595 let value = round_trip(
596 r#"{"content":[{"type":"resource_link","uri":"file:///a","name":"a"}],"resultType":"complete"}"#,
597 )?;
598 let content = value
599 .get("content")
600 .and_then(|c| c.as_array())
601 .and_then(|a| a.first())
602 .ok_or("the result has no blocks")?;
603 assert_eq!(
604 content.get("type").and_then(|t| t.as_str()),
605 Some("resource_link")
606 );
607 Ok(())
608 }
609
610 #[test]
614 fn an_audio_block_is_not_returned_as_an_image() -> Result<(), TestError> {
615 let value = round_trip(
616 r#"{"content":[{"type":"audio","data":"AAA","mimeType":"audio/wav"}],"resultType":"complete"}"#,
617 )?;
618 let content = value
619 .get("content")
620 .and_then(|c| c.as_array())
621 .and_then(|a| a.first())
622 .ok_or("the result has no blocks")?;
623 assert_eq!(content.get("type").and_then(|t| t.as_str()), Some("audio"));
624 Ok(())
625 }
626
627 #[test]
631 fn every_icon_member_survives_the_crossing() -> Result<(), TestError> {
632 let value = round_trip(
633 r#"{"content":[{"type":"resource_link","uri":"file:///a","name":"a",
634 "icons":[{"src":"file:///i.png","mimeType":"image/png",
635 "sizes":["48x48","any"],"theme":"dark"},
636 {"src":"file:///j.svg"}]}],"resultType":"complete"}"#,
637 )?;
638 let icons = value
639 .get("content")
640 .and_then(|c| c.as_array())
641 .and_then(|a| a.first())
642 .and_then(|b| b.get("icons"))
643 .and_then(|i| i.as_array())
644 .ok_or("the fixture has no blocks where the test expects one")?;
645 assert_eq!(icons.len(), 2);
646 assert_eq!(
647 icons
648 .first()
649 .ok_or("the fixture has no icon there")?
650 .get("src")
651 .and_then(|s| s.as_str()),
652 Some("file:///i.png")
653 );
654 assert_eq!(
655 icons
656 .first()
657 .ok_or("the fixture has no icon there")?
658 .get("mimeType")
659 .and_then(|s| s.as_str()),
660 Some("image/png")
661 );
662 assert_eq!(
663 icons
664 .first()
665 .ok_or("the fixture has no icon there")?
666 .get("sizes")
667 .and_then(|s| s.as_array())
668 .map(Vec::len),
669 Some(2)
670 );
671 assert_eq!(
672 icons
673 .first()
674 .ok_or("the fixture has no icon there")?
675 .get("theme")
676 .and_then(|t| t.as_str()),
677 Some("dark")
678 );
679 assert_eq!(
681 icons
682 .get(1)
683 .ok_or("the fixture has no icon there")?
684 .get("src")
685 .and_then(|s| s.as_str()),
686 Some("file:///j.svg")
687 );
688 assert!(
689 icons
690 .get(1)
691 .ok_or("the fixture has no icon there")?
692 .get("sizes")
693 .is_none(),
694 "an absent list is not an empty one"
695 );
696 Ok(())
697 }
698
699 #[test]
703 fn an_empty_icon_list_is_not_written_out() -> Result<(), TestError> {
704 let value = round_trip(
705 r#"{"content":[{"type":"resource_link","uri":"file:///a","name":"a",
706 "icons":[]}],"resultType":"complete"}"#,
707 )?;
708 let block = value
709 .get("content")
710 .and_then(|c| c.as_array())
711 .and_then(|a| a.first())
712 .ok_or("the result has no blocks")?;
713 assert!(
714 block.get("icons").is_none(),
715 "got {}",
716 block.get("icons").unwrap_or(&serde_json::Value::Null)
717 );
718 Ok(())
719 }
720
721 #[test]
725 fn an_unmodelled_icon_member_is_returned_whole() -> Result<(), TestError> {
726 let value = round_trip(
727 r#"{"content":[{"type":"resource_link","uri":"file:///a","name":"a",
728 "icons":[{"src":"file:///i.png","unheardOf":true}]}],"resultType":"complete"}"#,
729 )?;
730 let icon = value
731 .get("content")
732 .and_then(|c| c.as_array())
733 .and_then(|a| a.first())
734 .and_then(|b| b.get("icons"))
735 .and_then(|i| i.as_array())
736 .and_then(|a| a.first())
737 .ok_or("the result has no blocks")?;
738 assert_eq!(
739 icon.get("unheardOf").and_then(serde_json::Value::as_bool),
740 Some(true)
741 );
742 Ok(())
743 }
744
745 #[test]
749 fn a_hand_built_icon_reaches_the_wire() -> Result<(), TestError> {
750 let codec = Draft;
751 let mut ir = ToolResult::new(vec![Content::ResourceLink(ResourceLinkContent {
752 icons: vec![ResourceIcon {
753 src: "file:///i.png".to_owned(),
754 mime_type: Some("image/svg+xml".to_owned()),
755 sizes: vec!["96x96".to_owned()],
756 ..ResourceIcon::new("")
757 }],
758 ..ResourceLinkContent::new("file:///a", "a")
759 })]);
760 ir.result_type = Some("complete".to_owned());
761 let mut report = TranslationReport::new(codec.version());
762 let wire = codec.lower(ir, DegradePolicy::strict(), &mut report)?;
763 let value = serde_json::to_value(&wire)?;
764 let icon = value
765 .get("content")
766 .and_then(|c| c.as_array())
767 .and_then(|a| a.first())
768 .and_then(|b| b.get("icons"))
769 .and_then(|i| i.as_array())
770 .and_then(|a| a.first())
771 .ok_or("the result has no blocks")?;
772 assert_eq!(
773 icon.get("mimeType").and_then(|s| s.as_str()),
774 Some("image/svg+xml")
775 );
776 assert_eq!(
777 icon.get("sizes").and_then(|s| s.as_array()).map(Vec::len),
778 Some(1)
779 );
780 Ok(())
781 }
782
783 #[test]
785 fn the_result_type_survives_the_crossing() -> Result<(), TestError> {
786 let value = round_trip(
787 r#"{"content":[{"type":"text","text":"hi"}],"resultType":"input_required"}"#,
788 )?;
789 assert_eq!(
790 value.get("resultType").and_then(|t| t.as_str()),
791 Some("input_required")
792 );
793 Ok(())
794 }
795
796 #[test]
806 fn an_absent_result_type_reads_as_complete() -> Result<(), TestError> {
807 let codec = Draft;
808 let ir = ToolResult::new(vec![Content::Text(crate::domain::ir::TextContent::new(
809 "hi",
810 ))]);
811 let mut report = TranslationReport::new(codec.version());
812 let wire = codec.lower(ir, DegradePolicy::strict(), &mut report)?;
813 assert_eq!(wire.result_type, "complete");
814 Ok(())
815 }
816
817 #[test]
820 fn a_stated_result_type_is_not_overwritten() -> Result<(), TestError> {
821 let value = round_trip(
822 r#"{"content":[{"type":"text","text":"hi"}],
823 "resultType":"input_required"}"#,
824 )?;
825 assert_eq!(
826 value.get("resultType").and_then(|t| t.as_str()),
827 Some("input_required")
828 );
829 Ok(())
830 }
831
832 #[test]
836 fn a_typed_result_meta_survives_the_crossing() -> Result<(), TestError> {
837 let value = round_trip(
838 r#"{"content":[{"type":"text","text":"hi"}],"resultType":"complete",
839 "_meta":{"io.modelcontextprotocol/serverInfo":{"name":"srv","version":"1"},
840 "unheardOf":true}}"#,
841 )?;
842 let meta = value.get("_meta").ok_or("the fixture carries no `_meta`")?;
843 let info = meta
844 .get("io.modelcontextprotocol/serverInfo")
845 .ok_or("the fixture carries no `serverInfo`")?;
846 assert_eq!(info.get("name").and_then(|n| n.as_str()), Some("srv"));
847 assert_eq!(info.get("version").and_then(|v| v.as_str()), Some("1"));
848 assert_eq!(
849 meta.get("unheardOf").and_then(serde_json::Value::as_bool),
850 Some(true)
851 );
852 Ok(())
853 }
854}