1use std::collections::BTreeMap;
6use std::fmt;
7
8use crate::common_types::{
9 AbsoluteUri, Annotations, ContentBlock, Implementation, JsonInteger, OpenMetadata, RawIcon,
10 SamplingContentBlock,
11};
12use crate::extensions::MCP_APPS_HTML_MIME_TYPE;
13use crate::messages::{FinalCallToolResult, FinalCoreResult};
14use crate::result::{MAX_RESULT_CONTAINER_MEMBERS, MAX_RESULT_ENCODED_BYTES};
15use base64::Engine as _;
16use serde::de::Error as _;
17use serde::{Deserialize, Serialize};
18
19pub const PROTOCOL_VERSION: &str = "2024-11-05";
21
22#[derive(Debug, Clone, Default, Serialize, Deserialize)]
24pub struct ServerCapabilities {
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub tools: Option<ToolsCapability>,
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub resources: Option<ResourcesCapability>,
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub prompts: Option<PromptsCapability>,
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub logging: Option<LoggingCapability>,
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub completions: Option<CompletionsCapability>,
40 #[serde(skip_serializing_if = "Option::is_none")]
42 pub tasks: Option<TasksCapability>,
43}
44
45#[derive(Debug, Clone, Default, Serialize, Deserialize)]
47pub struct CompletionsCapability {}
48
49#[derive(Debug, Clone, Default, Serialize, Deserialize)]
51pub struct ToolsCapability {
52 #[serde(
54 default,
55 rename = "listChanged",
56 skip_serializing_if = "std::ops::Not::not"
57 )]
58 pub list_changed: bool,
59}
60
61#[derive(Debug, Clone, Default, Serialize, Deserialize)]
63pub struct ResourcesCapability {
64 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
66 pub subscribe: bool,
67 #[serde(
69 default,
70 rename = "listChanged",
71 skip_serializing_if = "std::ops::Not::not"
72 )]
73 pub list_changed: bool,
74}
75
76#[derive(Debug, Clone, Default, Serialize, Deserialize)]
78pub struct PromptsCapability {
79 #[serde(
81 default,
82 rename = "listChanged",
83 skip_serializing_if = "std::ops::Not::not"
84 )]
85 pub list_changed: bool,
86}
87
88#[derive(Debug, Clone, Default, Serialize, Deserialize)]
90pub struct LoggingCapability {}
91
92#[derive(Debug, Clone, Default, Serialize, Deserialize)]
94pub struct ClientCapabilities {
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub sampling: Option<SamplingCapability>,
98 #[serde(skip_serializing_if = "Option::is_none")]
100 pub elicitation: Option<ElicitationCapability>,
101 #[serde(skip_serializing_if = "Option::is_none")]
103 pub roots: Option<RootsCapability>,
104}
105
106#[derive(Debug, Clone, Default, Serialize, Deserialize)]
108pub struct SamplingCapability {}
109
110#[derive(Debug, Clone, Default, Serialize, Deserialize)]
112pub struct FormElicitationCapability {}
113
114#[derive(Debug, Clone, Default, Serialize, Deserialize)]
116pub struct UrlElicitationCapability {}
117
118#[derive(Debug, Clone, Default, Serialize, Deserialize)]
122pub struct ElicitationCapability {
123 #[serde(skip_serializing_if = "Option::is_none")]
125 pub form: Option<FormElicitationCapability>,
126 #[serde(skip_serializing_if = "Option::is_none")]
128 pub url: Option<UrlElicitationCapability>,
129}
130
131impl ElicitationCapability {
132 #[must_use]
134 pub fn form() -> Self {
135 Self {
136 form: Some(FormElicitationCapability {}),
137 url: None,
138 }
139 }
140
141 #[must_use]
143 pub fn url() -> Self {
144 Self {
145 form: None,
146 url: Some(UrlElicitationCapability {}),
147 }
148 }
149
150 #[must_use]
152 pub fn both() -> Self {
153 Self {
154 form: Some(FormElicitationCapability {}),
155 url: Some(UrlElicitationCapability {}),
156 }
157 }
158
159 #[must_use]
161 pub fn supports_form(&self) -> bool {
162 self.form.is_some()
163 }
164
165 #[must_use]
167 pub fn supports_url(&self) -> bool {
168 self.url.is_some()
169 }
170}
171
172#[derive(Debug, Clone, Default, Serialize, Deserialize)]
174pub struct RootsCapability {
175 #[serde(
177 rename = "listChanged",
178 default,
179 skip_serializing_if = "std::ops::Not::not"
180 )]
181 pub list_changed: bool,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
189pub struct Root {
190 pub uri: String,
192 #[serde(skip_serializing_if = "Option::is_none")]
194 pub name: Option<String>,
195}
196
197impl Root {
198 #[must_use]
200 pub fn new(uri: impl Into<String>) -> Self {
201 Self {
202 uri: uri.into(),
203 name: None,
204 }
205 }
206
207 #[must_use]
209 pub fn with_name(uri: impl Into<String>, name: impl Into<String>) -> Self {
210 Self {
211 uri: uri.into(),
212 name: Some(name.into()),
213 }
214 }
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct ServerInfo {
220 pub name: String,
222 pub version: String,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct ClientInfo {
229 pub name: String,
231 pub version: String,
233}
234
235impl ClientInfo {
236 #[must_use]
241 pub fn to_implementation(&self) -> Implementation {
242 let name = if self.name.is_empty() {
243 "unknown"
244 } else {
245 self.name.as_str()
246 };
247 let version = if self.version.is_empty() {
248 "0"
249 } else {
250 self.version.as_str()
251 };
252 Implementation::try_new(name, version).expect("the fallback client identity is nonempty")
253 }
254}
255
256#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
265#[serde(rename_all = "camelCase", deny_unknown_fields)]
266pub struct Icon {
267 #[serde(skip_serializing_if = "Option::is_none")]
273 pub src: Option<String>,
274
275 #[serde(skip_serializing_if = "Option::is_none")]
277 pub mime_type: Option<String>,
278
279 #[serde(skip_serializing_if = "Option::is_none")]
281 pub sizes: Option<String>,
282}
283
284impl Icon {
285 #[must_use]
287 pub fn new(src: impl Into<String>) -> Self {
288 Self {
289 src: Some(src.into()),
290 mime_type: None,
291 sizes: None,
292 }
293 }
294
295 #[must_use]
297 pub fn with_mime_type(src: impl Into<String>, mime_type: impl Into<String>) -> Self {
298 Self {
299 src: Some(src.into()),
300 mime_type: Some(mime_type.into()),
301 sizes: None,
302 }
303 }
304
305 #[must_use]
307 pub fn full(
308 src: impl Into<String>,
309 mime_type: impl Into<String>,
310 sizes: impl Into<String>,
311 ) -> Self {
312 Self {
313 src: Some(src.into()),
314 mime_type: Some(mime_type.into()),
315 sizes: Some(sizes.into()),
316 }
317 }
318
319 #[must_use]
321 pub fn has_src(&self) -> bool {
322 self.src.is_some()
323 }
324
325 #[must_use]
327 pub fn is_data_uri(&self) -> bool {
328 self.src.as_ref().is_some_and(|s| s.starts_with("data:"))
329 }
330}
331
332#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
341pub struct ToolAnnotations {
342 #[serde(rename = "destructiveHint", skip_serializing_if = "Option::is_none")]
346 pub destructive: Option<bool>,
347 #[serde(rename = "idempotentHint", skip_serializing_if = "Option::is_none")]
351 pub idempotent: Option<bool>,
352 #[serde(rename = "readOnlyHint", skip_serializing_if = "Option::is_none")]
356 pub read_only: Option<bool>,
357 #[serde(rename = "openWorldHint", skip_serializing_if = "Option::is_none")]
361 pub open_world_hint: Option<bool>,
362}
363
364impl ToolAnnotations {
365 #[must_use]
367 pub fn new() -> Self {
368 Self::default()
369 }
370
371 #[must_use]
373 pub fn destructive(mut self, value: bool) -> Self {
374 self.destructive = Some(value);
375 self
376 }
377
378 #[must_use]
380 pub fn idempotent(mut self, value: bool) -> Self {
381 self.idempotent = Some(value);
382 self
383 }
384
385 #[must_use]
387 pub fn read_only(mut self, value: bool) -> Self {
388 self.read_only = Some(value);
389 self
390 }
391
392 #[must_use]
397 pub fn open_world_hint(mut self, value: bool) -> Self {
398 self.open_world_hint = Some(value);
399 self
400 }
401
402 #[must_use]
404 pub fn is_empty(&self) -> bool {
405 self.destructive.is_none()
406 && self.idempotent.is_none()
407 && self.read_only.is_none()
408 && self.open_world_hint.is_none()
409 }
410}
411
412#[derive(Debug, Clone, Serialize, Deserialize)]
414pub struct Tool {
415 pub name: String,
417 #[serde(skip_serializing_if = "Option::is_none")]
419 pub description: Option<String>,
420 #[serde(rename = "inputSchema")]
422 pub input_schema: serde_json::Value,
423 #[serde(rename = "outputSchema", skip_serializing_if = "Option::is_none")]
425 pub output_schema: Option<serde_json::Value>,
426 #[serde(skip_serializing_if = "Option::is_none")]
428 pub icon: Option<Icon>,
429 #[serde(skip_serializing_if = "Option::is_none")]
431 pub version: Option<String>,
432 #[serde(default, skip_serializing_if = "Vec::is_empty")]
434 pub tags: Vec<String>,
435 #[serde(skip_serializing_if = "Option::is_none")]
437 pub annotations: Option<ToolAnnotations>,
438}
439
440#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
442pub struct Resource {
443 pub uri: String,
445 pub name: String,
447 #[serde(skip_serializing_if = "Option::is_none")]
449 pub description: Option<String>,
450 #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
452 pub mime_type: Option<String>,
453 #[serde(skip_serializing_if = "Option::is_none")]
455 pub icon: Option<Icon>,
456 #[serde(skip_serializing_if = "Option::is_none")]
458 pub version: Option<String>,
459 #[serde(default, skip_serializing_if = "Vec::is_empty")]
461 pub tags: Vec<String>,
462}
463
464#[derive(Debug, Clone, Serialize, Deserialize)]
466pub struct ResourceTemplate {
467 #[serde(
469 rename = "uriTemplate",
470 serialize_with = "serialize_resource_uri_template",
471 deserialize_with = "deserialize_resource_uri_template"
472 )]
473 pub uri_template: String,
474 pub name: String,
476 #[serde(skip_serializing_if = "Option::is_none")]
478 pub description: Option<String>,
479 #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
481 pub mime_type: Option<String>,
482 #[serde(skip_serializing_if = "Option::is_none")]
484 pub icon: Option<Icon>,
485 #[serde(skip_serializing_if = "Option::is_none")]
487 pub version: Option<String>,
488 #[serde(default, skip_serializing_if = "Vec::is_empty")]
490 pub tags: Vec<String>,
491}
492
493fn deserialize_resource_uri_template<'de, D>(deserializer: D) -> Result<String, D::Error>
494where
495 D: serde::Deserializer<'de>,
496{
497 let value = String::deserialize(deserializer)?;
498 crate::UriTemplate::parse(&value)
499 .map(|_| value)
500 .map_err(D::Error::custom)
501}
502
503fn serialize_resource_uri_template<S>(value: &String, serializer: S) -> Result<S::Ok, S::Error>
504where
505 S: serde::Serializer,
506{
507 crate::UriTemplate::parse(value).map_err(serde::ser::Error::custom)?;
508 serializer.serialize_str(value)
509}
510
511#[derive(Debug, Clone, Serialize, Deserialize)]
513pub struct Prompt {
514 pub name: String,
516 #[serde(skip_serializing_if = "Option::is_none")]
518 pub description: Option<String>,
519 #[serde(default, skip_serializing_if = "Vec::is_empty")]
521 pub arguments: Vec<PromptArgument>,
522 #[serde(skip_serializing_if = "Option::is_none")]
524 pub icon: Option<Icon>,
525 #[serde(skip_serializing_if = "Option::is_none")]
527 pub version: Option<String>,
528 #[serde(default, skip_serializing_if = "Vec::is_empty")]
530 pub tags: Vec<String>,
531}
532
533#[derive(Debug, Clone, Serialize, Deserialize)]
535pub struct PromptArgument {
536 pub name: String,
538 #[serde(skip_serializing_if = "Option::is_none")]
540 pub description: Option<String>,
541 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
543 pub required: bool,
544}
545
546#[derive(Debug, Clone, Serialize, Deserialize)]
557#[serde(deny_unknown_fields)]
558pub struct FinalBaseMetadata {
559 pub name: String,
561 #[serde(default, skip_serializing_if = "Option::is_none")]
563 pub title: Option<String>,
564}
565
566#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
568#[serde(deny_unknown_fields)]
569pub struct FinalToolAnnotations {
570 #[serde(default, skip_serializing_if = "Option::is_none")]
572 pub title: Option<String>,
573 #[serde(
575 rename = "destructiveHint",
576 default,
577 skip_serializing_if = "Option::is_none"
578 )]
579 pub destructive: Option<bool>,
580 #[serde(
582 rename = "idempotentHint",
583 default,
584 skip_serializing_if = "Option::is_none"
585 )]
586 pub idempotent: Option<bool>,
587 #[serde(
589 rename = "readOnlyHint",
590 default,
591 skip_serializing_if = "Option::is_none"
592 )]
593 pub read_only: Option<bool>,
594 #[serde(
596 rename = "openWorldHint",
597 default,
598 skip_serializing_if = "Option::is_none"
599 )]
600 pub open_world_hint: Option<bool>,
601}
602
603pub const MCP_APPS_UI_METADATA_KEY: &str = "ui";
609pub const MCP_APPS_DEPRECATED_RESOURCE_URI_METADATA_KEY: &str = "ui/resourceUri";
611pub const MAX_MCP_APPS_UI_METADATA_MEMBERS: usize = 2;
613pub const MAX_MCP_APPS_TOOL_VISIBILITY_ENTRIES: usize = 128;
615pub const MAX_MCP_APPS_CSP_DOMAINS_PER_DIRECTIVE: usize = 128;
617pub const MAX_MCP_APPS_CSP_DOMAIN_BYTES: usize = 2_048;
619
620#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
622#[serde(rename_all = "lowercase")]
623pub enum McpAppsToolVisibility {
624 Model,
626 App,
628}
629
630#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
632#[serde(rename_all = "lowercase")]
633pub enum McpAppsDisplayMode {
634 Inline,
636 Fullscreen,
638 Pip,
640}
641
642#[derive(Clone, Debug, Eq, PartialEq)]
648pub struct McpAppsToolMetadata {
649 pub resource_uri: Option<AbsoluteUri>,
651 pub visibility: Option<Vec<McpAppsToolVisibility>>,
653}
654
655impl McpAppsToolMetadata {
656 pub fn try_new(
658 resource_uri: Option<AbsoluteUri>,
659 visibility: Option<Vec<McpAppsToolVisibility>>,
660 ) -> Result<Self, McpAppsMetadataError> {
661 if resource_uri
662 .as_ref()
663 .is_some_and(|resource_uri| !resource_uri.as_str().starts_with("ui://"))
664 {
665 return Err(McpAppsMetadataError::ResourceUriMustUseUiPrefix);
666 }
667 if visibility
668 .as_ref()
669 .is_some_and(|visibility| visibility.len() > MAX_MCP_APPS_TOOL_VISIBILITY_ENTRIES)
670 {
671 return Err(McpAppsMetadataError::TooManyToolVisibilityEntries);
672 }
673 Ok(Self {
674 resource_uri,
675 visibility,
676 })
677 }
678
679 #[must_use]
682 pub fn effective_visibility(&self) -> &[McpAppsToolVisibility] {
683 const DEFAULT_VISIBILITY: [McpAppsToolVisibility; 2] =
684 [McpAppsToolVisibility::Model, McpAppsToolVisibility::App];
685 self.visibility.as_deref().unwrap_or(&DEFAULT_VISIBILITY)
686 }
687
688 pub fn to_open_metadata(&self) -> Result<OpenMetadata, McpAppsMetadataError> {
691 let value =
692 serde_json::to_value(self).map_err(|_| McpAppsMetadataError::InvalidToolMetadata)?;
693 OpenMetadata::try_from_entries([(MCP_APPS_UI_METADATA_KEY.to_owned(), value)])
694 .map_err(|_| McpAppsMetadataError::InvalidToolMetadata)
695 }
696
697 pub fn merge_into(
699 &self,
700 metadata: &OpenMetadata,
701 ) -> Result<OpenMetadata, McpAppsMetadataError> {
702 reject_deprecated_mcp_apps_metadata(metadata)?;
703 if metadata.entries().contains_key(MCP_APPS_UI_METADATA_KEY) {
704 return Err(McpAppsMetadataError::UiMetadataAlreadyPresent);
705 }
706 let mut entries = metadata.entries().clone();
707 entries.insert(
708 MCP_APPS_UI_METADATA_KEY.to_owned(),
709 serde_json::to_value(self).map_err(|_| McpAppsMetadataError::InvalidToolMetadata)?,
710 );
711 OpenMetadata::try_from_entries(entries)
712 .map_err(|_| McpAppsMetadataError::InvalidToolMetadata)
713 }
714}
715
716#[derive(Serialize, Deserialize)]
717#[serde(rename_all = "camelCase", deny_unknown_fields)]
718struct McpAppsToolMetadataWire {
719 #[serde(default, skip_serializing_if = "Option::is_none")]
720 resource_uri: Option<AbsoluteUri>,
721 #[serde(default, skip_serializing_if = "Option::is_none")]
722 visibility: Option<Vec<McpAppsToolVisibility>>,
723}
724
725impl Serialize for McpAppsToolMetadata {
726 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
727 where
728 S: serde::Serializer,
729 {
730 Self::try_new(self.resource_uri.clone(), self.visibility.clone())
731 .map_err(serde::ser::Error::custom)?;
732 McpAppsToolMetadataWire {
733 resource_uri: self.resource_uri.clone(),
734 visibility: self.visibility.clone(),
735 }
736 .serialize(serializer)
737 }
738}
739
740impl<'de> Deserialize<'de> for McpAppsToolMetadata {
741 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
742 where
743 D: serde::Deserializer<'de>,
744 {
745 let wire = McpAppsToolMetadataWire::deserialize(deserializer)?;
746 Self::try_new(wire.resource_uri, wire.visibility).map_err(serde::de::Error::custom)
747 }
748}
749
750#[derive(Clone, Debug, Default, Eq, PartialEq)]
755pub struct McpAppsResourceCsp {
756 pub connect_domains: Option<Vec<String>>,
758 pub resource_domains: Option<Vec<String>>,
760 pub frame_domains: Option<Vec<String>>,
762 pub base_uri_domains: Option<Vec<String>>,
764}
765
766impl McpAppsResourceCsp {
767 pub fn try_new(
769 connect_domains: Option<Vec<String>>,
770 resource_domains: Option<Vec<String>>,
771 frame_domains: Option<Vec<String>>,
772 base_uri_domains: Option<Vec<String>>,
773 ) -> Result<Self, McpAppsMetadataError> {
774 for domains in [
775 connect_domains.as_deref(),
776 resource_domains.as_deref(),
777 frame_domains.as_deref(),
778 base_uri_domains.as_deref(),
779 ] {
780 validate_mcp_apps_domains(domains)?;
781 }
782 Ok(Self {
783 connect_domains,
784 resource_domains,
785 frame_domains,
786 base_uri_domains,
787 })
788 }
789}
790
791#[derive(Serialize, Deserialize)]
792#[serde(deny_unknown_fields)]
793struct McpAppsResourceCspWire {
794 #[serde(
795 rename = "connectDomains",
796 default,
797 skip_serializing_if = "Option::is_none"
798 )]
799 connect: Option<Vec<String>>,
800 #[serde(
801 rename = "resourceDomains",
802 default,
803 skip_serializing_if = "Option::is_none"
804 )]
805 resources: Option<Vec<String>>,
806 #[serde(
807 rename = "frameDomains",
808 default,
809 skip_serializing_if = "Option::is_none"
810 )]
811 frames: Option<Vec<String>>,
812 #[serde(
813 rename = "baseUriDomains",
814 default,
815 skip_serializing_if = "Option::is_none"
816 )]
817 base_uris: Option<Vec<String>>,
818}
819
820impl Serialize for McpAppsResourceCsp {
821 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
822 where
823 S: serde::Serializer,
824 {
825 Self::try_new(
826 self.connect_domains.clone(),
827 self.resource_domains.clone(),
828 self.frame_domains.clone(),
829 self.base_uri_domains.clone(),
830 )
831 .map_err(serde::ser::Error::custom)?;
832 McpAppsResourceCspWire {
833 connect: self.connect_domains.clone(),
834 resources: self.resource_domains.clone(),
835 frames: self.frame_domains.clone(),
836 base_uris: self.base_uri_domains.clone(),
837 }
838 .serialize(serializer)
839 }
840}
841
842impl<'de> Deserialize<'de> for McpAppsResourceCsp {
843 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
844 where
845 D: serde::Deserializer<'de>,
846 {
847 let wire = McpAppsResourceCspWire::deserialize(deserializer)?;
848 Self::try_new(wire.connect, wire.resources, wire.frames, wire.base_uris)
849 .map_err(serde::de::Error::custom)
850 }
851}
852
853#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
855#[serde(deny_unknown_fields)]
856pub struct McpAppsResourcePermission {}
857
858#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
863#[serde(rename_all = "camelCase", deny_unknown_fields)]
864pub struct McpAppsResourcePermissions {
865 #[serde(default, skip_serializing_if = "Option::is_none")]
867 pub camera: Option<McpAppsResourcePermission>,
868 #[serde(default, skip_serializing_if = "Option::is_none")]
870 pub microphone: Option<McpAppsResourcePermission>,
871 #[serde(default, skip_serializing_if = "Option::is_none")]
873 pub geolocation: Option<McpAppsResourcePermission>,
874 #[serde(default, skip_serializing_if = "Option::is_none")]
876 pub clipboard_write: Option<McpAppsResourcePermission>,
877}
878
879#[derive(Clone, Debug, Default, Eq, PartialEq)]
881pub struct McpAppsResourceMetadata {
882 pub csp: Option<McpAppsResourceCsp>,
884 pub permissions: Option<McpAppsResourcePermissions>,
886 pub domain: Option<String>,
888 pub prefers_border: Option<bool>,
890}
891
892impl McpAppsResourceMetadata {
893 #[must_use]
895 pub const fn new(prefers_border: Option<bool>) -> Self {
896 Self {
897 csp: None,
898 permissions: None,
899 domain: None,
900 prefers_border,
901 }
902 }
903
904 pub fn try_new(
907 csp: Option<McpAppsResourceCsp>,
908 permissions: Option<McpAppsResourcePermissions>,
909 domain: Option<String>,
910 prefers_border: Option<bool>,
911 ) -> Result<Self, McpAppsMetadataError> {
912 if domain
913 .as_deref()
914 .is_some_and(|domain| domain.is_empty() || domain.len() > MAX_MCP_APPS_CSP_DOMAIN_BYTES)
915 {
916 return Err(McpAppsMetadataError::InvalidDomain);
917 }
918 Ok(Self {
919 csp,
920 permissions,
921 domain,
922 prefers_border,
923 })
924 }
925
926 pub fn to_open_metadata(&self) -> Result<OpenMetadata, McpAppsMetadataError> {
929 let value = serde_json::to_value(self)
930 .map_err(|_| McpAppsMetadataError::InvalidResourceMetadata)?;
931 OpenMetadata::try_from_entries([(MCP_APPS_UI_METADATA_KEY.to_owned(), value)])
932 .map_err(|_| McpAppsMetadataError::InvalidResourceMetadata)
933 }
934
935 pub fn merge_into(
937 &self,
938 metadata: &OpenMetadata,
939 ) -> Result<OpenMetadata, McpAppsMetadataError> {
940 reject_deprecated_mcp_apps_metadata(metadata)?;
941 if metadata.entries().contains_key(MCP_APPS_UI_METADATA_KEY) {
942 return Err(McpAppsMetadataError::UiMetadataAlreadyPresent);
943 }
944 let mut entries = metadata.entries().clone();
945 entries.insert(
946 MCP_APPS_UI_METADATA_KEY.to_owned(),
947 serde_json::to_value(self)
948 .map_err(|_| McpAppsMetadataError::InvalidResourceMetadata)?,
949 );
950 OpenMetadata::try_from_entries(entries)
951 .map_err(|_| McpAppsMetadataError::InvalidResourceMetadata)
952 }
953}
954
955#[derive(Serialize, Deserialize)]
956#[serde(rename_all = "camelCase", deny_unknown_fields)]
957struct McpAppsResourceMetadataWire {
958 #[serde(default, skip_serializing_if = "Option::is_none")]
959 csp: Option<McpAppsResourceCsp>,
960 #[serde(default, skip_serializing_if = "Option::is_none")]
961 permissions: Option<McpAppsResourcePermissions>,
962 #[serde(default, skip_serializing_if = "Option::is_none")]
963 domain: Option<String>,
964 #[serde(default, skip_serializing_if = "Option::is_none")]
965 prefers_border: Option<bool>,
966}
967
968impl Serialize for McpAppsResourceMetadata {
969 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
970 where
971 S: serde::Serializer,
972 {
973 McpAppsResourceMetadataWire {
974 csp: self.csp.clone(),
975 permissions: self.permissions.clone(),
976 domain: self.domain.clone(),
977 prefers_border: self.prefers_border,
978 }
979 .serialize(serializer)
980 }
981}
982
983impl<'de> Deserialize<'de> for McpAppsResourceMetadata {
984 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
985 where
986 D: serde::Deserializer<'de>,
987 {
988 let wire = McpAppsResourceMetadataWire::deserialize(deserializer)?;
989 Self::try_new(wire.csp, wire.permissions, wire.domain, wire.prefers_border)
990 .map_err(serde::de::Error::custom)
991 }
992}
993
994#[derive(Clone, Debug, Eq, PartialEq)]
997pub struct McpAppsResourceBinding {
998 pub resource_uri: AbsoluteUri,
1000 pub visibility: Vec<McpAppsToolVisibility>,
1002}
1003
1004impl McpAppsResourceBinding {
1005 pub fn from_tool(tool: &FinalTool) -> Result<Option<Self>, McpAppsMetadataError> {
1007 let Some(metadata) = tool.mcp_apps_metadata()? else {
1008 return Ok(None);
1009 };
1010 let visibility = metadata.effective_visibility().to_vec();
1011 let Some(resource_uri) = metadata.resource_uri else {
1012 return Ok(None);
1013 };
1014 Ok(Some(Self {
1015 resource_uri,
1016 visibility,
1017 }))
1018 }
1019
1020 pub fn validate_resource(
1023 &self,
1024 resource: &FinalResource,
1025 ) -> Result<(), McpAppsResourceBindingError> {
1026 let _ = resource
1027 .mcp_apps_metadata()
1028 .map_err(McpAppsResourceBindingError::Metadata)?;
1029 if resource.uri != self.resource_uri {
1030 return Err(McpAppsResourceBindingError::UriMismatch);
1031 }
1032 if resource.mime_type.as_deref() != Some(MCP_APPS_HTML_MIME_TYPE) {
1033 return Err(McpAppsResourceBindingError::HtmlMimeTypeRequired);
1034 }
1035 Ok(())
1036 }
1037}
1038
1039#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1042pub enum McpAppsViewLifecycle {
1043 New,
1045 InitializeInFlight,
1047 AwaitingInitialized,
1049 Active,
1051 Closing,
1053 Closed,
1055}
1056
1057impl Default for McpAppsViewLifecycle {
1058 fn default() -> Self {
1059 Self::New
1060 }
1061}
1062
1063impl McpAppsViewLifecycle {
1064 pub fn begin_initialize(&mut self) -> Result<(), McpAppsLifecycleError> {
1066 self.transition(Self::New, Self::InitializeInFlight, "initialize")
1067 }
1068
1069 pub fn initialization_succeeded(&mut self) -> Result<(), McpAppsLifecycleError> {
1071 self.transition(
1072 Self::InitializeInFlight,
1073 Self::AwaitingInitialized,
1074 "initialize response",
1075 )
1076 }
1077
1078 pub fn initialization_failed(&mut self) -> Result<(), McpAppsLifecycleError> {
1080 self.transition(Self::InitializeInFlight, Self::New, "initialize rollback")
1081 }
1082
1083 pub fn admit_initialized(&mut self) -> Result<(), McpAppsLifecycleError> {
1085 self.transition(
1086 Self::AwaitingInitialized,
1087 Self::Active,
1088 "initialized notification",
1089 )
1090 }
1091
1092 pub fn begin_closing(&mut self) -> Result<(), McpAppsLifecycleError> {
1094 match *self {
1095 Self::New | Self::InitializeInFlight | Self::AwaitingInitialized | Self::Active => {
1096 *self = Self::Closing;
1097 Ok(())
1098 }
1099 Self::Closing | Self::Closed => Err(McpAppsLifecycleError::InvalidTransition {
1100 from: *self,
1101 operation: "begin closing",
1102 }),
1103 }
1104 }
1105
1106 pub fn finish_closing(&mut self) -> Result<(), McpAppsLifecycleError> {
1108 self.transition(Self::Closing, Self::Closed, "finish closing")
1109 }
1110
1111 #[must_use]
1113 pub const fn permits_application_traffic(self) -> bool {
1114 matches!(self, Self::Active)
1115 }
1116
1117 fn transition(
1118 &mut self,
1119 expected: Self,
1120 next: Self,
1121 operation: &'static str,
1122 ) -> Result<(), McpAppsLifecycleError> {
1123 if *self != expected {
1124 return Err(McpAppsLifecycleError::InvalidTransition {
1125 from: *self,
1126 operation,
1127 });
1128 }
1129 *self = next;
1130 Ok(())
1131 }
1132}
1133
1134#[derive(Clone, Debug, PartialEq)]
1140pub struct McpAppsToolResult {
1141 pub content: Vec<ContentBlock>,
1143 pub is_error: bool,
1145 pub structured_content: Option<serde_json::Value>,
1147}
1148
1149impl McpAppsToolResult {
1150 pub fn try_new(
1152 content: Vec<ContentBlock>,
1153 is_error: bool,
1154 structured_content: Option<serde_json::Value>,
1155 ) -> Result<Self, McpAppsResultProjectionError> {
1156 if content.len() > MAX_RESULT_CONTAINER_MEMBERS {
1157 return Err(McpAppsResultProjectionError::ResultTooLarge);
1158 }
1159 let result = Self {
1160 content,
1161 is_error,
1162 structured_content,
1163 };
1164 let encoded = serde_json::to_vec(&McpAppsToolResultWire::from(&result))
1165 .map_err(|_| McpAppsResultProjectionError::ResultTooLarge)?;
1166 if encoded.len() > MAX_RESULT_ENCODED_BYTES {
1167 return Err(McpAppsResultProjectionError::ResultTooLarge);
1168 }
1169 Ok(result)
1170 }
1171
1172 pub fn from_final_call_tool_result(
1174 result: &FinalCallToolResult,
1175 ) -> Result<Self, McpAppsResultProjectionError> {
1176 Self::try_new(
1177 result.content.clone(),
1178 result.is_error,
1179 result.structured_content.clone(),
1180 )
1181 }
1182}
1183
1184#[derive(Serialize, Deserialize)]
1185#[serde(rename_all = "camelCase", deny_unknown_fields)]
1186struct McpAppsToolResultWire {
1187 content: Vec<ContentBlock>,
1188 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1189 is_error: bool,
1190 #[serde(default, skip_serializing_if = "Option::is_none")]
1191 #[serde(deserialize_with = "deserialize_apps_present_json_value")]
1192 structured_content: Option<serde_json::Value>,
1193}
1194
1195fn deserialize_apps_present_json_value<'de, D>(
1196 deserializer: D,
1197) -> Result<Option<serde_json::Value>, D::Error>
1198where
1199 D: serde::Deserializer<'de>,
1200{
1201 serde_json::Value::deserialize(deserializer).map(Some)
1202}
1203
1204impl From<&McpAppsToolResult> for McpAppsToolResultWire {
1205 fn from(result: &McpAppsToolResult) -> Self {
1206 Self {
1207 content: result.content.clone(),
1208 is_error: result.is_error,
1209 structured_content: result.structured_content.clone(),
1210 }
1211 }
1212}
1213
1214impl Serialize for McpAppsToolResult {
1215 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1216 where
1217 S: serde::Serializer,
1218 {
1219 Self::try_new(
1220 self.content.clone(),
1221 self.is_error,
1222 self.structured_content.clone(),
1223 )
1224 .map_err(serde::ser::Error::custom)?;
1225 McpAppsToolResultWire::from(self).serialize(serializer)
1226 }
1227}
1228
1229impl<'de> Deserialize<'de> for McpAppsToolResult {
1230 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1231 where
1232 D: serde::Deserializer<'de>,
1233 {
1234 let wire = McpAppsToolResultWire::deserialize(deserializer)?;
1235 Self::try_new(wire.content, wire.is_error, wire.structured_content)
1236 .map_err(serde::de::Error::custom)
1237 }
1238}
1239
1240pub fn project_final_core_tools_call_result(
1243 result: &FinalCoreResult,
1244) -> Result<McpAppsToolResult, McpAppsResultProjectionError> {
1245 match result {
1246 FinalCoreResult::ToolsCall { result, .. } => {
1247 McpAppsToolResult::from_final_call_tool_result(&result.payload)
1248 }
1249 #[cfg(feature = "tasks")]
1250 FinalCoreResult::ToolsCallTask { .. } => {
1251 Err(McpAppsResultProjectionError::TasksUnsupported)
1252 }
1253 FinalCoreResult::ToolsCallInputRequired { .. } => {
1254 Err(McpAppsResultProjectionError::MrtrUnsupported)
1255 }
1256 _ => Err(McpAppsResultProjectionError::NotToolsCall),
1257 }
1258}
1259
1260#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1262pub enum McpAppsMetadataError {
1263 DeprecatedFlatResourceUri,
1265 UiMetadataMustBeObject,
1267 InvalidToolMetadata,
1269 InvalidResourceMetadata,
1271 ResourceUriMustUseUiPrefix,
1273 TooManyToolVisibilityEntries,
1275 TooManyCspDomains,
1277 InvalidCspDomain,
1279 InvalidDomain,
1281 UiMetadataAlreadyPresent,
1283}
1284
1285impl fmt::Display for McpAppsMetadataError {
1286 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1287 match self {
1288 Self::DeprecatedFlatResourceUri => formatter.write_str(
1289 "deprecated flat MCP Apps metadata key ui/resourceUri is forbidden; use _meta.ui.resourceUri",
1290 ),
1291 Self::UiMetadataMustBeObject => {
1292 formatter.write_str("MCP Apps _meta.ui must be an object")
1293 }
1294 Self::InvalidToolMetadata => {
1295 formatter.write_str("MCP Apps tool _meta.ui does not satisfy its closed schema")
1296 }
1297 Self::InvalidResourceMetadata => formatter
1298 .write_str("MCP Apps resource _meta.ui does not satisfy its closed schema"),
1299 Self::ResourceUriMustUseUiPrefix => {
1300 formatter.write_str("MCP Apps resourceUri must start with ui://")
1301 }
1302 Self::TooManyToolVisibilityEntries => {
1303 formatter.write_str("MCP Apps tool visibility exceeds its entry limit")
1304 }
1305 Self::TooManyCspDomains => {
1306 formatter.write_str("MCP Apps CSP directive exceeds its origin limit")
1307 }
1308 Self::InvalidCspDomain => {
1309 formatter.write_str("MCP Apps CSP origin is empty or exceeds its byte limit")
1310 }
1311 Self::InvalidDomain => {
1312 formatter.write_str("MCP Apps domain is empty or exceeds its byte limit")
1313 }
1314 Self::UiMetadataAlreadyPresent => {
1315 formatter.write_str("MCP Apps _meta already contains a ui member")
1316 }
1317 }
1318 }
1319}
1320
1321impl std::error::Error for McpAppsMetadataError {}
1322
1323#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1325pub enum McpAppsResourceBindingError {
1326 UriMismatch,
1328 HtmlMimeTypeRequired,
1330 Metadata(McpAppsMetadataError),
1332}
1333
1334impl fmt::Display for McpAppsResourceBindingError {
1335 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1336 match self {
1337 Self::UriMismatch => {
1338 formatter.write_str("MCP Apps resource URI differs from tool binding")
1339 }
1340 Self::HtmlMimeTypeRequired => {
1341 formatter.write_str("MCP Apps bound resource must use text/html;profile=mcp-app")
1342 }
1343 Self::Metadata(error) => write!(formatter, "MCP Apps resource metadata: {error}"),
1344 }
1345 }
1346}
1347
1348impl std::error::Error for McpAppsResourceBindingError {}
1349
1350#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1352pub enum McpAppsLifecycleError {
1353 InvalidTransition {
1355 from: McpAppsViewLifecycle,
1357 operation: &'static str,
1359 },
1360}
1361
1362impl fmt::Display for McpAppsLifecycleError {
1363 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1364 match self {
1365 Self::InvalidTransition { from, operation } => {
1366 write!(
1367 formatter,
1368 "MCP Apps lifecycle cannot {operation} from {from:?}"
1369 )
1370 }
1371 }
1372 }
1373}
1374
1375impl std::error::Error for McpAppsLifecycleError {}
1376
1377#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1379pub enum McpAppsResultProjectionError {
1380 ResultTooLarge,
1382 TasksUnsupported,
1384 MrtrUnsupported,
1386 NotToolsCall,
1388}
1389
1390impl fmt::Display for McpAppsResultProjectionError {
1391 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1392 match self {
1393 Self::ResultTooLarge => {
1394 formatter.write_str("MCP Apps result exceeds final result bounds")
1395 }
1396 Self::TasksUnsupported => {
1397 formatter.write_str("MCP Apps does not project Tasks-backed tool results")
1398 }
1399 Self::MrtrUnsupported => {
1400 formatter.write_str("MCP Apps does not project MRTR input-required tool results")
1401 }
1402 Self::NotToolsCall => {
1403 formatter.write_str("MCP Apps projects only final tools/call results")
1404 }
1405 }
1406 }
1407}
1408
1409impl std::error::Error for McpAppsResultProjectionError {}
1410
1411fn validate_mcp_apps_domains(domains: Option<&[String]>) -> Result<(), McpAppsMetadataError> {
1412 let Some(domains) = domains else {
1413 return Ok(());
1414 };
1415 if domains.len() > MAX_MCP_APPS_CSP_DOMAINS_PER_DIRECTIVE {
1416 return Err(McpAppsMetadataError::TooManyCspDomains);
1417 }
1418 if domains
1419 .iter()
1420 .any(|domain| domain.is_empty() || domain.len() > MAX_MCP_APPS_CSP_DOMAIN_BYTES)
1421 {
1422 return Err(McpAppsMetadataError::InvalidCspDomain);
1423 }
1424 Ok(())
1425}
1426
1427fn reject_deprecated_mcp_apps_metadata(
1428 metadata: &OpenMetadata,
1429) -> Result<(), McpAppsMetadataError> {
1430 if metadata
1431 .entries()
1432 .contains_key(MCP_APPS_DEPRECATED_RESOURCE_URI_METADATA_KEY)
1433 {
1434 Err(McpAppsMetadataError::DeprecatedFlatResourceUri)
1435 } else {
1436 Ok(())
1437 }
1438}
1439
1440fn parse_mcp_apps_tool_metadata(
1441 metadata: &OpenMetadata,
1442) -> Result<Option<McpAppsToolMetadata>, McpAppsMetadataError> {
1443 reject_deprecated_mcp_apps_metadata(metadata)?;
1444 let Some(value) = metadata.entries().get(MCP_APPS_UI_METADATA_KEY) else {
1445 return Ok(None);
1446 };
1447 if !value.is_object() {
1448 return Err(McpAppsMetadataError::UiMetadataMustBeObject);
1449 }
1450 let metadata = serde_json::from_value(value.clone())
1451 .map_err(|_| McpAppsMetadataError::InvalidToolMetadata)?;
1452 Ok(Some(metadata))
1453}
1454
1455fn parse_mcp_apps_resource_metadata(
1456 metadata: &OpenMetadata,
1457) -> Result<Option<McpAppsResourceMetadata>, McpAppsMetadataError> {
1458 reject_deprecated_mcp_apps_metadata(metadata)?;
1459 let Some(value) = metadata.entries().get(MCP_APPS_UI_METADATA_KEY) else {
1460 return Ok(None);
1461 };
1462 if !value.is_object() {
1463 return Err(McpAppsMetadataError::UiMetadataMustBeObject);
1464 }
1465 let metadata = serde_json::from_value(value.clone())
1466 .map_err(|_| McpAppsMetadataError::InvalidResourceMetadata)?;
1467 Ok(Some(metadata))
1468}
1469
1470fn serialize_final_tool_metadata<S>(
1471 metadata: &Option<OpenMetadata>,
1472 serializer: S,
1473) -> Result<S::Ok, S::Error>
1474where
1475 S: serde::Serializer,
1476{
1477 if let Some(metadata) = metadata {
1478 parse_mcp_apps_tool_metadata(metadata).map_err(serde::ser::Error::custom)?;
1479 }
1480 metadata.serialize(serializer)
1481}
1482
1483fn deserialize_final_tool_metadata<'de, D>(
1484 deserializer: D,
1485) -> Result<Option<OpenMetadata>, D::Error>
1486where
1487 D: serde::Deserializer<'de>,
1488{
1489 let metadata = Option::<OpenMetadata>::deserialize(deserializer)?;
1490 if let Some(metadata) = &metadata {
1491 parse_mcp_apps_tool_metadata(metadata).map_err(serde::de::Error::custom)?;
1492 }
1493 Ok(metadata)
1494}
1495
1496fn serialize_final_resource_metadata<S>(
1497 metadata: &Option<OpenMetadata>,
1498 serializer: S,
1499) -> Result<S::Ok, S::Error>
1500where
1501 S: serde::Serializer,
1502{
1503 if let Some(metadata) = metadata {
1504 parse_mcp_apps_resource_metadata(metadata).map_err(serde::ser::Error::custom)?;
1505 }
1506 metadata.serialize(serializer)
1507}
1508
1509fn deserialize_final_resource_metadata<'de, D>(
1510 deserializer: D,
1511) -> Result<Option<OpenMetadata>, D::Error>
1512where
1513 D: serde::Deserializer<'de>,
1514{
1515 let metadata = Option::<OpenMetadata>::deserialize(deserializer)?;
1516 if let Some(metadata) = &metadata {
1517 parse_mcp_apps_resource_metadata(metadata).map_err(serde::de::Error::custom)?;
1518 }
1519 Ok(metadata)
1520}
1521
1522#[derive(Debug, Clone, Serialize, Deserialize)]
1524#[serde(deny_unknown_fields)]
1525pub struct FinalTool {
1526 pub name: String,
1528 #[serde(default, skip_serializing_if = "Option::is_none")]
1530 pub title: Option<String>,
1531 #[serde(default, skip_serializing_if = "Option::is_none")]
1533 pub description: Option<String>,
1534 #[serde(default, skip_serializing_if = "Option::is_none")]
1536 pub icons: Option<Vec<RawIcon>>,
1537 #[serde(
1539 rename = "inputSchema",
1540 deserialize_with = "deserialize_final_tool_input_schema"
1541 )]
1542 pub input_schema: serde_json::Value,
1543 #[serde(
1545 rename = "outputSchema",
1546 default,
1547 skip_serializing_if = "Option::is_none",
1548 deserialize_with = "deserialize_optional_final_json_object"
1549 )]
1550 pub output_schema: Option<serde_json::Value>,
1551 #[serde(default, skip_serializing_if = "Option::is_none")]
1553 pub annotations: Option<FinalToolAnnotations>,
1554 #[serde(
1556 rename = "_meta",
1557 default,
1558 skip_serializing_if = "Option::is_none",
1559 serialize_with = "serialize_final_tool_metadata",
1560 deserialize_with = "deserialize_final_tool_metadata"
1561 )]
1562 pub meta: Option<OpenMetadata>,
1563}
1564
1565impl FinalTool {
1566 pub fn mcp_apps_metadata(&self) -> Result<Option<McpAppsToolMetadata>, McpAppsMetadataError> {
1568 self.meta
1569 .as_ref()
1570 .map_or(Ok(None), parse_mcp_apps_tool_metadata)
1571 }
1572
1573 pub fn mcp_apps_resource_binding(
1575 &self,
1576 ) -> Result<Option<McpAppsResourceBinding>, McpAppsMetadataError> {
1577 McpAppsResourceBinding::from_tool(self)
1578 }
1579}
1580
1581#[derive(Debug, Clone, Serialize, Deserialize)]
1583#[serde(deny_unknown_fields)]
1584pub struct FinalResource {
1585 pub uri: AbsoluteUri,
1587 pub name: String,
1589 #[serde(default, skip_serializing_if = "Option::is_none")]
1591 pub title: Option<String>,
1592 #[serde(default, skip_serializing_if = "Option::is_none")]
1594 pub description: Option<String>,
1595 #[serde(default, skip_serializing_if = "Option::is_none")]
1597 pub icons: Option<Vec<RawIcon>>,
1598 #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")]
1600 pub mime_type: Option<String>,
1601 #[serde(default, skip_serializing_if = "Option::is_none")]
1603 pub size: Option<JsonInteger>,
1604 #[serde(default, skip_serializing_if = "Option::is_none")]
1606 pub annotations: Option<Annotations>,
1607 #[serde(
1609 rename = "_meta",
1610 default,
1611 skip_serializing_if = "Option::is_none",
1612 serialize_with = "serialize_final_resource_metadata",
1613 deserialize_with = "deserialize_final_resource_metadata"
1614 )]
1615 pub meta: Option<OpenMetadata>,
1616}
1617
1618impl FinalResource {
1619 pub fn mcp_apps_metadata(
1622 &self,
1623 ) -> Result<Option<McpAppsResourceMetadata>, McpAppsMetadataError> {
1624 self.meta
1625 .as_ref()
1626 .map_or(Ok(None), parse_mcp_apps_resource_metadata)
1627 }
1628}
1629
1630#[derive(Debug, Clone, Serialize, Deserialize)]
1632#[serde(deny_unknown_fields)]
1633pub struct FinalResourceTemplate {
1634 #[serde(
1636 rename = "uriTemplate",
1637 serialize_with = "serialize_final_resource_uri_template",
1638 deserialize_with = "deserialize_final_resource_uri_template"
1639 )]
1640 pub uri_template: String,
1641 pub name: String,
1643 #[serde(default, skip_serializing_if = "Option::is_none")]
1645 pub title: Option<String>,
1646 #[serde(default, skip_serializing_if = "Option::is_none")]
1648 pub description: Option<String>,
1649 #[serde(default, skip_serializing_if = "Option::is_none")]
1651 pub icons: Option<Vec<RawIcon>>,
1652 #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")]
1654 pub mime_type: Option<String>,
1655 #[serde(default, skip_serializing_if = "Option::is_none")]
1657 pub annotations: Option<Annotations>,
1658 #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
1660 pub meta: Option<OpenMetadata>,
1661}
1662
1663fn deserialize_final_resource_uri_template<'de, D>(deserializer: D) -> Result<String, D::Error>
1664where
1665 D: serde::Deserializer<'de>,
1666{
1667 let value = String::deserialize(deserializer)?;
1668 crate::UriTemplate::parse(&value)
1669 .map(|_| value)
1670 .map_err(D::Error::custom)
1671}
1672
1673fn serialize_final_resource_uri_template<S>(
1674 value: &String,
1675 serializer: S,
1676) -> Result<S::Ok, S::Error>
1677where
1678 S: serde::Serializer,
1679{
1680 crate::UriTemplate::parse(value).map_err(serde::ser::Error::custom)?;
1681 serializer.serialize_str(value)
1682}
1683
1684#[derive(Debug, Clone, Serialize, Deserialize)]
1686#[serde(deny_unknown_fields)]
1687pub struct FinalPromptArgument {
1688 pub name: String,
1690 #[serde(default, skip_serializing_if = "Option::is_none")]
1692 pub title: Option<String>,
1693 #[serde(default, skip_serializing_if = "Option::is_none")]
1695 pub description: Option<String>,
1696 #[serde(default, skip_serializing_if = "Option::is_none")]
1698 pub required: Option<bool>,
1699}
1700
1701#[derive(Debug, Clone, Serialize, Deserialize)]
1703#[serde(deny_unknown_fields)]
1704pub struct FinalPrompt {
1705 pub name: String,
1707 #[serde(default, skip_serializing_if = "Option::is_none")]
1709 pub title: Option<String>,
1710 #[serde(default, skip_serializing_if = "Option::is_none")]
1712 pub description: Option<String>,
1713 #[serde(default, skip_serializing_if = "Option::is_none")]
1715 pub icons: Option<Vec<RawIcon>>,
1716 #[serde(default, skip_serializing_if = "Option::is_none")]
1718 pub arguments: Option<Vec<FinalPromptArgument>>,
1719 #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
1721 pub meta: Option<OpenMetadata>,
1722}
1723
1724fn deserialize_final_tool_input_schema<'de, D>(
1725 deserializer: D,
1726) -> Result<serde_json::Value, D::Error>
1727where
1728 D: serde::Deserializer<'de>,
1729{
1730 let value = serde_json::Value::deserialize(deserializer)?;
1731 let Some(object) = value.as_object() else {
1732 return Err(D::Error::custom("final tool inputSchema must be an object"));
1733 };
1734 if object.get("type").and_then(serde_json::Value::as_str) != Some("object") {
1735 return Err(D::Error::custom(
1736 "final tool inputSchema must declare type object",
1737 ));
1738 }
1739 Ok(value)
1740}
1741
1742fn deserialize_optional_final_json_object<'de, D>(
1743 deserializer: D,
1744) -> Result<Option<serde_json::Value>, D::Error>
1745where
1746 D: serde::Deserializer<'de>,
1747{
1748 let value = serde_json::Value::deserialize(deserializer)?;
1749 if !value.is_object() {
1750 return Err(D::Error::custom(
1751 "final tool outputSchema must be an object",
1752 ));
1753 }
1754 Ok(Some(value))
1755}
1756
1757pub type LegacyMetadata = BTreeMap<String, serde_json::Value>;
1763
1764#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1771#[serde(tag = "type", rename_all = "lowercase")]
1772pub enum LegacyContent {
1773 Text {
1775 text: String,
1777 #[serde(default, skip_serializing_if = "Option::is_none")]
1779 annotations: Option<Annotations>,
1780 #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
1782 additional: BTreeMap<String, serde_json::Value>,
1783 },
1784 Image {
1786 data: String,
1788 #[serde(rename = "mimeType")]
1790 mime_type: String,
1791 #[serde(default, skip_serializing_if = "Option::is_none")]
1793 annotations: Option<Annotations>,
1794 #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
1796 additional: BTreeMap<String, serde_json::Value>,
1797 },
1798 Resource {
1800 resource: LegacyResourceContent,
1802 #[serde(default, skip_serializing_if = "Option::is_none")]
1804 annotations: Option<Annotations>,
1805 #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
1807 additional: BTreeMap<String, serde_json::Value>,
1808 },
1809}
1810
1811#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1813#[serde(untagged)]
1814pub enum LegacyResourceContent {
1815 Text {
1817 uri: String,
1819 text: String,
1821 #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")]
1823 mime_type: Option<String>,
1824 #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
1826 additional: BTreeMap<String, serde_json::Value>,
1827 },
1828 Blob {
1830 uri: String,
1832 blob: String,
1834 #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")]
1836 mime_type: Option<String>,
1837 #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
1839 additional: BTreeMap<String, serde_json::Value>,
1840 },
1841}
1842
1843#[derive(Debug, Clone, Serialize, Deserialize)]
1845#[serde(tag = "type", rename_all = "lowercase", deny_unknown_fields)]
1846pub enum Content {
1847 Text {
1849 text: String,
1851 },
1852 Image {
1854 data: String,
1856 #[serde(rename = "mimeType")]
1858 mime_type: String,
1859 },
1860 Audio {
1862 data: String,
1864 #[serde(rename = "mimeType")]
1866 mime_type: String,
1867 },
1868 Resource {
1870 resource: ResourceContent,
1872 },
1873}
1874
1875impl Content {
1876 #[must_use]
1878 pub fn text(text: impl Into<String>) -> Self {
1879 Self::Text { text: text.into() }
1880 }
1881
1882 #[must_use]
1884 pub fn image_base64(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
1885 Self::Image {
1886 data: data.into(),
1887 mime_type: mime_type.into(),
1888 }
1889 }
1890
1891 #[must_use]
1893 pub fn image_bytes(bytes: impl AsRef<[u8]>, mime_type: impl Into<String>) -> Self {
1894 let data = base64::engine::general_purpose::STANDARD.encode(bytes.as_ref());
1895 Self::image_base64(data, mime_type)
1896 }
1897
1898 #[must_use]
1900 pub fn audio_base64(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
1901 Self::Audio {
1902 data: data.into(),
1903 mime_type: mime_type.into(),
1904 }
1905 }
1906
1907 #[must_use]
1909 pub fn audio_bytes(bytes: impl AsRef<[u8]>, mime_type: impl Into<String>) -> Self {
1910 let data = base64::engine::general_purpose::STANDARD.encode(bytes.as_ref());
1911 Self::audio_base64(data, mime_type)
1912 }
1913
1914 #[must_use]
1916 pub fn resource_text(
1917 uri: impl Into<String>,
1918 mime_type: Option<String>,
1919 text: impl Into<String>,
1920 ) -> Self {
1921 Self::Resource {
1922 resource: ResourceContent {
1923 uri: uri.into(),
1924 mime_type,
1925 text: Some(text.into()),
1926 blob: None,
1927 },
1928 }
1929 }
1930
1931 #[must_use]
1933 pub fn resource_blob_base64(
1934 uri: impl Into<String>,
1935 mime_type: Option<String>,
1936 blob: impl Into<String>,
1937 ) -> Self {
1938 Self::Resource {
1939 resource: ResourceContent {
1940 uri: uri.into(),
1941 mime_type,
1942 text: None,
1943 blob: Some(blob.into()),
1944 },
1945 }
1946 }
1947
1948 #[must_use]
1950 pub fn resource_blob_bytes(
1951 uri: impl Into<String>,
1952 mime_type: Option<String>,
1953 bytes: impl AsRef<[u8]>,
1954 ) -> Self {
1955 let blob = base64::engine::general_purpose::STANDARD.encode(bytes.as_ref());
1956 Self::resource_blob_base64(uri, mime_type, blob)
1957 }
1958}
1959
1960#[derive(Debug, Clone, Serialize, Deserialize)]
1962#[serde(deny_unknown_fields)]
1963pub struct ResourceContent {
1964 pub uri: String,
1966 #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
1968 pub mime_type: Option<String>,
1969 #[serde(skip_serializing_if = "Option::is_none")]
1971 pub text: Option<String>,
1972 #[serde(skip_serializing_if = "Option::is_none")]
1974 pub blob: Option<String>,
1975}
1976
1977#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1979#[serde(rename_all = "lowercase")]
1980pub enum Role {
1981 User,
1983 Assistant,
1985}
1986
1987#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1989pub struct LegacyPromptMessage {
1990 pub role: Role,
1992 pub content: LegacyContent,
1994 #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
1996 pub additional: BTreeMap<String, serde_json::Value>,
1997}
1998
1999#[derive(Debug, Clone, Serialize, Deserialize)]
2001pub struct PromptMessage {
2002 pub role: Role,
2004 pub content: Content,
2006}
2007
2008#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2016pub struct TaskId(pub String);
2017
2018impl TaskId {
2019 #[must_use]
2021 pub fn new() -> Self {
2022 use std::time::{SystemTime, UNIX_EPOCH};
2023 let timestamp = SystemTime::now()
2024 .duration_since(UNIX_EPOCH)
2025 .unwrap_or_default()
2026 .as_nanos();
2027 Self(format!("task-{timestamp:x}"))
2028 }
2029
2030 #[must_use]
2032 pub fn from_string(s: impl Into<String>) -> Self {
2033 Self(s.into())
2034 }
2035
2036 #[must_use]
2038 pub fn as_str(&self) -> &str {
2039 &self.0
2040 }
2041}
2042
2043impl Default for TaskId {
2044 fn default() -> Self {
2045 Self::new()
2046 }
2047}
2048
2049impl std::fmt::Display for TaskId {
2050 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2051 write!(f, "{}", self.0)
2052 }
2053}
2054
2055impl From<String> for TaskId {
2056 fn from(s: String) -> Self {
2057 Self(s)
2058 }
2059}
2060
2061impl From<&str> for TaskId {
2062 fn from(s: &str) -> Self {
2063 Self(s.to_owned())
2064 }
2065}
2066
2067#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2069#[serde(rename_all = "lowercase")]
2070pub enum TaskStatus {
2071 Pending,
2073 Running,
2075 Completed,
2077 Failed,
2079 Cancelled,
2081}
2082
2083impl TaskStatus {
2084 #[must_use]
2086 pub fn is_terminal(&self) -> bool {
2087 matches!(
2088 self,
2089 TaskStatus::Completed | TaskStatus::Failed | TaskStatus::Cancelled
2090 )
2091 }
2092
2093 #[must_use]
2095 pub fn is_active(&self) -> bool {
2096 matches!(self, TaskStatus::Pending | TaskStatus::Running)
2097 }
2098}
2099
2100#[derive(Debug, Clone, Serialize, Deserialize)]
2102pub struct TaskInfo {
2103 pub id: TaskId,
2105 #[serde(rename = "taskType")]
2107 pub task_type: String,
2108 pub status: TaskStatus,
2110 #[serde(skip_serializing_if = "Option::is_none")]
2112 pub progress: Option<f64>,
2113 #[serde(skip_serializing_if = "Option::is_none")]
2115 pub message: Option<String>,
2116 #[serde(rename = "createdAt")]
2118 pub created_at: String,
2119 #[serde(rename = "startedAt", skip_serializing_if = "Option::is_none")]
2121 pub started_at: Option<String>,
2122 #[serde(rename = "completedAt", skip_serializing_if = "Option::is_none")]
2124 pub completed_at: Option<String>,
2125 #[serde(skip_serializing_if = "Option::is_none")]
2127 pub error: Option<String>,
2128}
2129
2130#[derive(Debug, Clone, Serialize, Deserialize)]
2132pub struct TaskResult {
2133 pub id: TaskId,
2135 pub success: bool,
2137 #[serde(skip_serializing_if = "Option::is_none")]
2139 pub data: Option<serde_json::Value>,
2140 #[serde(skip_serializing_if = "Option::is_none")]
2142 pub error: Option<String>,
2143}
2144
2145#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2147pub struct TasksCapability {
2148 #[serde(
2150 default,
2151 rename = "listChanged",
2152 skip_serializing_if = "std::ops::Not::not"
2153 )]
2154 pub list_changed: bool,
2155}
2156
2157#[derive(Debug, Clone, Serialize, Deserialize)]
2165#[serde(tag = "type", rename_all = "lowercase")]
2166pub enum SamplingContent {
2167 Text {
2169 text: String,
2171 },
2172 Image {
2174 data: String,
2176 #[serde(rename = "mimeType")]
2178 mime_type: String,
2179 },
2180}
2181
2182#[derive(Debug, Clone, Serialize, Deserialize)]
2184pub struct SamplingMessage {
2185 pub role: Role,
2187 pub content: SamplingContent,
2189}
2190
2191impl SamplingMessage {
2192 #[must_use]
2194 pub fn user(text: impl Into<String>) -> Self {
2195 Self {
2196 role: Role::User,
2197 content: SamplingContent::Text { text: text.into() },
2198 }
2199 }
2200
2201 #[must_use]
2203 pub fn assistant(text: impl Into<String>) -> Self {
2204 Self {
2205 role: Role::Assistant,
2206 content: SamplingContent::Text { text: text.into() },
2207 }
2208 }
2209}
2210
2211pub type FinalSamplingMessageContentBlock = SamplingContentBlock;
2213
2214#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2217#[serde(untagged)]
2218pub enum FinalSamplingMessageContent {
2219 Block(FinalSamplingMessageContentBlock),
2221 Blocks(Vec<FinalSamplingMessageContentBlock>),
2223}
2224
2225#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2227#[serde(deny_unknown_fields)]
2228pub struct FinalSamplingMessage {
2229 pub role: Role,
2231 pub content: FinalSamplingMessageContent,
2233 #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
2235 pub meta: Option<OpenMetadata>,
2236}
2237
2238#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2240#[serde(rename_all = "lowercase")]
2241pub enum FinalToolChoiceMode {
2242 Auto,
2244 Required,
2246 None,
2248}
2249
2250#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2252#[serde(deny_unknown_fields)]
2253pub struct FinalToolChoice {
2254 #[serde(default, skip_serializing_if = "Option::is_none")]
2256 pub mode: Option<FinalToolChoiceMode>,
2257}
2258
2259#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2261pub struct ModelPreferences {
2262 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2264 pub hints: Vec<ModelHint>,
2265 #[serde(rename = "costPriority", skip_serializing_if = "Option::is_none")]
2267 pub cost_priority: Option<f64>,
2268 #[serde(rename = "speedPriority", skip_serializing_if = "Option::is_none")]
2270 pub speed_priority: Option<f64>,
2271 #[serde(
2273 rename = "intelligencePriority",
2274 skip_serializing_if = "Option::is_none"
2275 )]
2276 pub intelligence_priority: Option<f64>,
2277}
2278
2279#[derive(Debug, Clone, Serialize, Deserialize)]
2281pub struct ModelHint {
2282 #[serde(skip_serializing_if = "Option::is_none")]
2284 pub name: Option<String>,
2285}
2286
2287#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
2289#[serde(rename_all = "camelCase")]
2290pub enum StopReason {
2291 #[default]
2293 EndTurn,
2294 StopSequence,
2296 MaxTokens,
2298}
2299
2300#[cfg(test)]
2305mod tests {
2306 use super::*;
2307 use serde_json::json;
2308
2309 #[test]
2314 fn server_capabilities_default_serialization() {
2315 let caps = ServerCapabilities::default();
2316 let value = serde_json::to_value(&caps).expect("serialize");
2317 assert_eq!(value, json!({}));
2319 }
2320
2321 #[test]
2322 fn server_capabilities_full_serialization() {
2323 let caps = ServerCapabilities {
2324 tools: Some(ToolsCapability { list_changed: true }),
2325 resources: Some(ResourcesCapability {
2326 subscribe: true,
2327 list_changed: true,
2328 }),
2329 prompts: Some(PromptsCapability { list_changed: true }),
2330 logging: Some(LoggingCapability {}),
2331 completions: Some(CompletionsCapability {}),
2332 tasks: Some(TasksCapability { list_changed: true }),
2333 };
2334 let value = serde_json::to_value(&caps).expect("serialize");
2335 assert_eq!(value["tools"]["listChanged"], true);
2336 assert_eq!(value["resources"]["subscribe"], true);
2337 assert_eq!(value["resources"]["listChanged"], true);
2338 assert_eq!(value["prompts"]["listChanged"], true);
2339 assert!(value.get("logging").is_some());
2340 assert!(value.get("completions").is_some());
2341 assert_eq!(value["tasks"]["listChanged"], true);
2342 }
2343
2344 #[test]
2345 fn server_capabilities_partial_serialization() {
2346 let caps = ServerCapabilities {
2347 tools: Some(ToolsCapability::default()),
2348 ..Default::default()
2349 };
2350 let value = serde_json::to_value(&caps).expect("serialize");
2351 assert!(value.get("tools").is_some());
2352 assert!(value.get("resources").is_none());
2353 assert!(value.get("prompts").is_none());
2354 assert!(value.get("logging").is_none());
2355 assert!(value.get("tasks").is_none());
2356 }
2357
2358 #[test]
2359 fn server_capabilities_round_trip() {
2360 let caps = ServerCapabilities {
2361 tools: Some(ToolsCapability { list_changed: true }),
2362 resources: Some(ResourcesCapability {
2363 subscribe: false,
2364 list_changed: true,
2365 }),
2366 prompts: None,
2367 logging: Some(LoggingCapability {}),
2368 completions: None,
2369 tasks: None,
2370 };
2371 let json_str = serde_json::to_string(&caps).expect("serialize");
2372 let deserialized: ServerCapabilities =
2373 serde_json::from_str(&json_str).expect("deserialize");
2374 assert!(deserialized.tools.is_some());
2375 assert!(deserialized.tools.as_ref().unwrap().list_changed);
2376 assert!(deserialized.resources.is_some());
2377 assert!(!deserialized.resources.as_ref().unwrap().subscribe);
2378 assert!(deserialized.prompts.is_none());
2379 assert!(deserialized.logging.is_some());
2380 assert!(deserialized.tasks.is_none());
2381 }
2382
2383 #[test]
2388 fn tools_capability_default_omits_false() {
2389 let cap = ToolsCapability::default();
2390 let value = serde_json::to_value(&cap).expect("serialize");
2391 assert!(value.get("listChanged").is_none());
2393 }
2394
2395 #[test]
2396 fn tools_capability_list_changed() {
2397 let cap = ToolsCapability { list_changed: true };
2398 let value = serde_json::to_value(&cap).expect("serialize");
2399 assert_eq!(value["listChanged"], true);
2400 }
2401
2402 #[test]
2407 fn resources_capability_default() {
2408 let cap = ResourcesCapability::default();
2409 let value = serde_json::to_value(&cap).expect("serialize");
2410 assert!(value.get("subscribe").is_none());
2411 assert!(value.get("listChanged").is_none());
2412 }
2413
2414 #[test]
2415 fn resources_capability_full() {
2416 let cap = ResourcesCapability {
2417 subscribe: true,
2418 list_changed: true,
2419 };
2420 let value = serde_json::to_value(&cap).expect("serialize");
2421 assert_eq!(value["subscribe"], true);
2422 assert_eq!(value["listChanged"], true);
2423 }
2424
2425 #[test]
2430 fn client_capabilities_default_serialization() {
2431 let caps = ClientCapabilities::default();
2432 let value = serde_json::to_value(&caps).expect("serialize");
2433 assert_eq!(value, json!({}));
2434 }
2435
2436 #[test]
2437 fn client_capabilities_full_serialization() {
2438 let caps = ClientCapabilities {
2439 sampling: Some(SamplingCapability {}),
2440 elicitation: Some(ElicitationCapability::both()),
2441 roots: Some(RootsCapability { list_changed: true }),
2442 };
2443 let value = serde_json::to_value(&caps).expect("serialize");
2444 assert!(value.get("sampling").is_some());
2445 assert!(value.get("elicitation").is_some());
2446 assert_eq!(value["roots"]["listChanged"], true);
2447 }
2448
2449 #[test]
2450 fn client_capabilities_round_trip() {
2451 let caps = ClientCapabilities {
2452 sampling: Some(SamplingCapability {}),
2453 elicitation: None,
2454 roots: Some(RootsCapability {
2455 list_changed: false,
2456 }),
2457 };
2458 let json_str = serde_json::to_string(&caps).expect("serialize");
2459 let deserialized: ClientCapabilities =
2460 serde_json::from_str(&json_str).expect("deserialize");
2461 assert!(deserialized.sampling.is_some());
2462 assert!(deserialized.elicitation.is_none());
2463 assert!(deserialized.roots.is_some());
2464 }
2465
2466 #[test]
2471 fn elicitation_capability_form_only() {
2472 let cap = ElicitationCapability::form();
2473 assert!(cap.supports_form());
2474 assert!(!cap.supports_url());
2475 let value = serde_json::to_value(&cap).expect("serialize");
2476 assert!(value.get("form").is_some());
2477 assert!(value.get("url").is_none());
2478 }
2479
2480 #[test]
2481 fn elicitation_capability_url_only() {
2482 let cap = ElicitationCapability::url();
2483 assert!(!cap.supports_form());
2484 assert!(cap.supports_url());
2485 }
2486
2487 #[test]
2488 fn elicitation_capability_both() {
2489 let cap = ElicitationCapability::both();
2490 assert!(cap.supports_form());
2491 assert!(cap.supports_url());
2492 }
2493
2494 #[test]
2499 fn server_info_serialization() {
2500 let info = ServerInfo {
2501 name: "test-server".to_string(),
2502 version: "1.0.0".to_string(),
2503 };
2504 let value = serde_json::to_value(&info).expect("serialize");
2505 assert_eq!(value["name"], "test-server");
2506 assert_eq!(value["version"], "1.0.0");
2507 }
2508
2509 #[test]
2510 fn client_info_serialization() {
2511 let info = ClientInfo {
2512 name: "test-client".to_string(),
2513 version: "0.1.0".to_string(),
2514 };
2515 let value = serde_json::to_value(&info).expect("serialize");
2516 assert_eq!(value["name"], "test-client");
2517 assert_eq!(value["version"], "0.1.0");
2518 }
2519
2520 #[test]
2525 fn icon_new() {
2526 let icon = Icon::new("https://example.com/icon.png");
2527 assert!(icon.has_src());
2528 assert!(!icon.is_data_uri());
2529 assert!(icon.mime_type.is_none());
2530 assert!(icon.sizes.is_none());
2531 }
2532
2533 #[test]
2534 fn icon_with_mime_type() {
2535 let icon = Icon::with_mime_type("https://example.com/icon.png", "image/png");
2536 assert!(icon.has_src());
2537 assert_eq!(icon.mime_type, Some("image/png".to_string()));
2538 }
2539
2540 #[test]
2541 fn icon_full() {
2542 let icon = Icon::full("https://example.com/icon.png", "image/png", "32x32");
2543 assert_eq!(icon.src, Some("https://example.com/icon.png".to_string()));
2544 assert_eq!(icon.mime_type, Some("image/png".to_string()));
2545 assert_eq!(icon.sizes, Some("32x32".to_string()));
2546 }
2547
2548 #[test]
2549 fn icon_data_uri() {
2550 let icon = Icon::new("data:image/png;base64,iVBORw0KGgo=");
2551 assert!(icon.is_data_uri());
2552 }
2553
2554 #[test]
2555 fn icon_default_no_src() {
2556 let icon = Icon::default();
2557 assert!(!icon.has_src());
2558 assert!(!icon.is_data_uri());
2559 }
2560
2561 #[test]
2562 fn icon_serialization() {
2563 let icon = Icon::full(
2564 "https://example.com/icon.svg",
2565 "image/svg+xml",
2566 "16x16 32x32",
2567 );
2568 let value = serde_json::to_value(&icon).expect("serialize");
2569 assert_eq!(value["src"], "https://example.com/icon.svg");
2570 assert_eq!(value["mimeType"], "image/svg+xml");
2571 assert_eq!(value["sizes"], "16x16 32x32");
2572 }
2573
2574 #[test]
2575 fn icon_serialization_omits_none_fields() {
2576 let icon = Icon::new("https://example.com/icon.png");
2577 let value = serde_json::to_value(&icon).expect("serialize");
2578 assert!(value.get("src").is_some());
2579 assert!(value.get("mimeType").is_none());
2580 assert!(value.get("sizes").is_none());
2581 }
2582
2583 #[test]
2584 fn icon_equality() {
2585 let a = Icon::new("https://example.com/icon.png");
2586 let b = Icon::new("https://example.com/icon.png");
2587 let c = Icon::new("https://example.com/other.png");
2588 assert_eq!(a, b);
2589 assert_ne!(a, c);
2590 }
2591
2592 #[test]
2593 fn legacy_icon_rejects_final_theme_without_mutating_accepted_wire() {
2594 let accepted = json!({
2595 "src": "https://example.com/icon.svg",
2596 "mimeType": "image/svg+xml",
2597 "sizes": "48x48"
2598 });
2599 let legacy: Icon = serde_json::from_value(accepted.clone()).expect("legacy icon");
2600 let baseline = accepted.clone();
2601 let mut planted = accepted.clone();
2602 planted["theme"] = json!("dark");
2603 assert!(
2604 serde_json::from_value::<Icon>(planted).is_err(),
2605 "the final-only theme field must not be silently discarded by the legacy icon"
2606 );
2607 assert_eq!(
2608 accepted, baseline,
2609 "the rejected one-field final addition cannot mutate accepted legacy wire state"
2610 );
2611 assert_eq!(serde_json::to_value(legacy).expect("legacy wire"), accepted);
2612 }
2613
2614 #[test]
2619 fn content_text_serialization() {
2620 let content = Content::Text {
2621 text: "Hello, world!".to_string(),
2622 };
2623 let value = serde_json::to_value(&content).expect("serialize");
2624 assert_eq!(value["type"], "text");
2625 assert_eq!(value["text"], "Hello, world!");
2626 }
2627
2628 #[test]
2629 fn content_image_serialization() {
2630 let content = Content::Image {
2631 data: "iVBORw0KGgo=".to_string(),
2632 mime_type: "image/png".to_string(),
2633 };
2634 let value = serde_json::to_value(&content).expect("serialize");
2635 assert_eq!(value["type"], "image");
2636 assert_eq!(value["data"], "iVBORw0KGgo=");
2637 assert_eq!(value["mimeType"], "image/png");
2638 }
2639
2640 #[test]
2641 fn content_audio_serialization() {
2642 let content = Content::Audio {
2643 data: "UklGRg==".to_string(),
2644 mime_type: "audio/wav".to_string(),
2645 };
2646 let value = serde_json::to_value(&content).expect("serialize");
2647 assert_eq!(value["type"], "audio");
2648 assert_eq!(value["data"], "UklGRg==");
2649 assert_eq!(value["mimeType"], "audio/wav");
2650 }
2651
2652 #[test]
2653 fn content_resource_serialization() {
2654 let content = Content::Resource {
2655 resource: ResourceContent {
2656 uri: "file://config.json".to_string(),
2657 mime_type: Some("application/json".to_string()),
2658 text: Some("{\"key\": \"value\"}".to_string()),
2659 blob: None,
2660 },
2661 };
2662 let value = serde_json::to_value(&content).expect("serialize");
2663 assert_eq!(value["type"], "resource");
2664 assert_eq!(value["resource"]["uri"], "file://config.json");
2665 assert_eq!(value["resource"]["mimeType"], "application/json");
2666 assert_eq!(value["resource"]["text"], "{\"key\": \"value\"}");
2667 assert!(value["resource"].get("blob").is_none());
2668 }
2669
2670 #[test]
2671 fn content_text_deserialization() {
2672 let json = json!({"type": "text", "text": "Hello!"});
2673 let content: Content = serde_json::from_value(json).expect("deserialize");
2674 let text = match content {
2675 Content::Text { text } => Some(text),
2676 _ => None,
2677 };
2678 assert_eq!(text.as_deref(), Some("Hello!"));
2679 }
2680
2681 #[test]
2682 fn content_image_deserialization() {
2683 let json = json!({"type": "image", "data": "abc123", "mimeType": "image/jpeg"});
2684 let content: Content = serde_json::from_value(json).expect("deserialize");
2685 let (data, mime_type) = match content {
2686 Content::Image { data, mime_type } => (Some(data), Some(mime_type)),
2687 _ => (None, None),
2688 };
2689 assert_eq!(data.as_deref(), Some("abc123"));
2690 assert_eq!(mime_type.as_deref(), Some("image/jpeg"));
2691 }
2692
2693 #[test]
2694 fn content_audio_deserialization() {
2695 let json = json!({"type": "audio", "data": "abc123", "mimeType": "audio/mpeg"});
2696 let content: Content = serde_json::from_value(json).expect("deserialize");
2697 let (data, mime_type) = match content {
2698 Content::Audio { data, mime_type } => (Some(data), Some(mime_type)),
2699 _ => (None, None),
2700 };
2701 assert_eq!(data.as_deref(), Some("abc123"));
2702 assert_eq!(mime_type.as_deref(), Some("audio/mpeg"));
2703 }
2704
2705 #[test]
2706 fn legacy_content_rejects_final_metadata_without_mutating_accepted_wire() {
2707 let accepted = json!({"type": "text", "text": "legacy text"});
2708 let legacy: Content = serde_json::from_value(accepted.clone()).expect("legacy content");
2709 let baseline = accepted.clone();
2710 let mut planted = accepted.clone();
2711 planted["_meta"] = json!({"com.example/renderHint": true});
2712 assert!(
2713 serde_json::from_value::<Content>(planted).is_err(),
2714 "the final-only content metadata must not be silently discarded by legacy content"
2715 );
2716 assert_eq!(
2717 accepted, baseline,
2718 "the rejected one-field final metadata addition cannot mutate accepted legacy wire state"
2719 );
2720 assert_eq!(serde_json::to_value(legacy).expect("legacy wire"), accepted);
2721 }
2722
2723 #[test]
2728 fn resource_content_text_serialization() {
2729 let rc = ResourceContent {
2730 uri: "file://readme.md".to_string(),
2731 mime_type: Some("text/markdown".to_string()),
2732 text: Some("# Hello".to_string()),
2733 blob: None,
2734 };
2735 let value = serde_json::to_value(&rc).expect("serialize");
2736 assert_eq!(value["uri"], "file://readme.md");
2737 assert_eq!(value["mimeType"], "text/markdown");
2738 assert_eq!(value["text"], "# Hello");
2739 assert!(value.get("blob").is_none());
2740 }
2741
2742 #[test]
2743 fn resource_content_blob_serialization() {
2744 let rc = ResourceContent {
2745 uri: "file://image.png".to_string(),
2746 mime_type: Some("image/png".to_string()),
2747 text: None,
2748 blob: Some("base64data".to_string()),
2749 };
2750 let value = serde_json::to_value(&rc).expect("serialize");
2751 assert_eq!(value["uri"], "file://image.png");
2752 assert!(value.get("text").is_none());
2753 assert_eq!(value["blob"], "base64data");
2754 }
2755
2756 #[test]
2757 fn resource_content_minimal() {
2758 let rc = ResourceContent {
2759 uri: "file://test".to_string(),
2760 mime_type: None,
2761 text: None,
2762 blob: None,
2763 };
2764 let value = serde_json::to_value(&rc).expect("serialize");
2765 assert_eq!(value["uri"], "file://test");
2766 assert!(value.get("mimeType").is_none());
2767 assert!(value.get("text").is_none());
2768 assert!(value.get("blob").is_none());
2769 }
2770
2771 #[test]
2776 fn role_serialization() {
2777 assert_eq!(serde_json::to_value(Role::User).unwrap(), "user");
2778 assert_eq!(serde_json::to_value(Role::Assistant).unwrap(), "assistant");
2779 }
2780
2781 #[test]
2782 fn role_deserialization() {
2783 let user: Role = serde_json::from_value(json!("user")).expect("deserialize");
2784 assert_eq!(user, Role::User);
2785 let assistant: Role = serde_json::from_value(json!("assistant")).expect("deserialize");
2786 assert_eq!(assistant, Role::Assistant);
2787 }
2788
2789 #[test]
2794 fn prompt_message_serialization() {
2795 let msg = PromptMessage {
2796 role: Role::User,
2797 content: Content::Text {
2798 text: "Tell me a joke".to_string(),
2799 },
2800 };
2801 let value = serde_json::to_value(&msg).expect("serialize");
2802 assert_eq!(value["role"], "user");
2803 assert_eq!(value["content"]["type"], "text");
2804 assert_eq!(value["content"]["text"], "Tell me a joke");
2805 }
2806
2807 #[test]
2808 fn prompt_message_assistant() {
2809 let msg = PromptMessage {
2810 role: Role::Assistant,
2811 content: Content::Text {
2812 text: "Here's a joke...".to_string(),
2813 },
2814 };
2815 let value = serde_json::to_value(&msg).expect("serialize");
2816 assert_eq!(value["role"], "assistant");
2817 }
2818
2819 #[test]
2824 fn prompt_argument_required() {
2825 let arg = PromptArgument {
2826 name: "language".to_string(),
2827 description: Some("Target language".to_string()),
2828 required: true,
2829 };
2830 let value = serde_json::to_value(&arg).expect("serialize");
2831 assert_eq!(value["name"], "language");
2832 assert_eq!(value["description"], "Target language");
2833 assert_eq!(value["required"], true);
2834 }
2835
2836 #[test]
2837 fn prompt_argument_optional_omits_false() {
2838 let arg = PromptArgument {
2839 name: "style".to_string(),
2840 description: None,
2841 required: false,
2842 };
2843 let value = serde_json::to_value(&arg).expect("serialize");
2844 assert_eq!(value["name"], "style");
2845 assert!(value.get("description").is_none());
2846 assert!(value.get("required").is_none());
2848 }
2849
2850 #[test]
2851 fn prompt_argument_deserialization_defaults() {
2852 let json = json!({"name": "arg1"});
2853 let arg: PromptArgument = serde_json::from_value(json).expect("deserialize");
2854 assert_eq!(arg.name, "arg1");
2855 assert!(arg.description.is_none());
2856 assert!(!arg.required);
2857 }
2858
2859 #[test]
2864 fn tool_minimal_serialization() {
2865 let tool = Tool {
2866 name: "add".to_string(),
2867 description: None,
2868 input_schema: json!({"type": "object"}),
2869 output_schema: None,
2870 icon: None,
2871 version: None,
2872 tags: vec![],
2873 annotations: None,
2874 };
2875 let value = serde_json::to_value(&tool).expect("serialize");
2876 assert_eq!(value["name"], "add");
2877 assert_eq!(value["inputSchema"]["type"], "object");
2878 assert!(value.get("description").is_none());
2879 assert!(value.get("outputSchema").is_none());
2880 assert!(value.get("icon").is_none());
2881 assert!(value.get("version").is_none());
2882 assert!(value.get("tags").is_none());
2883 assert!(value.get("annotations").is_none());
2884 }
2885
2886 #[test]
2887 fn tool_full_serialization() {
2888 let tool = Tool {
2889 name: "compute".to_string(),
2890 description: Some("Runs a computation".to_string()),
2891 input_schema: json!({
2892 "type": "object",
2893 "properties": { "x": { "type": "number" } },
2894 "required": ["x"]
2895 }),
2896 output_schema: Some(json!({"type": "number"})),
2897 icon: Some(Icon::new("https://example.com/icon.png")),
2898 version: Some("2.1.0".to_string()),
2899 tags: vec!["math".to_string(), "compute".to_string()],
2900 annotations: Some(ToolAnnotations::new().read_only(true).idempotent(true)),
2901 };
2902 let value = serde_json::to_value(&tool).expect("serialize");
2903 assert_eq!(value["name"], "compute");
2904 assert_eq!(value["description"], "Runs a computation");
2905 assert!(value["inputSchema"]["properties"]["x"].is_object());
2906 assert_eq!(value["outputSchema"]["type"], "number");
2907 assert_eq!(value["icon"]["src"], "https://example.com/icon.png");
2908 assert_eq!(value["version"], "2.1.0");
2909 assert_eq!(value["tags"], json!(["math", "compute"]));
2910 assert_eq!(value["annotations"]["readOnlyHint"], true);
2911 assert_eq!(value["annotations"]["idempotentHint"], true);
2912 }
2913
2914 #[test]
2915 fn tool_round_trip() {
2916 let json = json!({
2917 "name": "greet",
2918 "description": "Greets the user",
2919 "inputSchema": {"type": "object", "properties": {"name": {"type": "string"}}},
2920 "outputSchema": {"type": "string"},
2921 "version": "1.0.0",
2922 "tags": ["greeting"],
2923 "annotations": {"readOnlyHint": true}
2924 });
2925 let tool: Tool = serde_json::from_value(json.clone()).expect("deserialize");
2926 assert_eq!(tool.name, "greet");
2927 assert_eq!(tool.version, Some("1.0.0".to_string()));
2928 assert_eq!(tool.tags, vec!["greeting"]);
2929 assert!(tool.annotations.as_ref().unwrap().read_only.unwrap());
2930 let re_serialized = serde_json::to_value(&tool).expect("re-serialize");
2931 assert_eq!(re_serialized["name"], json["name"]);
2932 }
2933
2934 #[test]
2939 fn resource_minimal_serialization() {
2940 let resource = Resource {
2941 uri: "file://test.txt".to_string(),
2942 name: "Test File".to_string(),
2943 description: None,
2944 mime_type: None,
2945 icon: None,
2946 version: None,
2947 tags: vec![],
2948 };
2949 let value = serde_json::to_value(&resource).expect("serialize");
2950 assert_eq!(value["uri"], "file://test.txt");
2951 assert_eq!(value["name"], "Test File");
2952 assert!(value.get("description").is_none());
2953 assert!(value.get("mimeType").is_none());
2954 }
2955
2956 #[test]
2957 fn resource_full_round_trip() {
2958 let json = json!({
2959 "uri": "file://config.json",
2960 "name": "Config",
2961 "description": "Application configuration",
2962 "mimeType": "application/json",
2963 "version": "3.0.0",
2964 "tags": ["config", "json"]
2965 });
2966 let resource: Resource = serde_json::from_value(json).expect("deserialize");
2967 assert_eq!(resource.uri, "file://config.json");
2968 assert_eq!(resource.mime_type, Some("application/json".to_string()));
2969 assert_eq!(resource.tags, vec!["config", "json"]);
2970 }
2971
2972 #[test]
2977 fn resource_template_serialization() {
2978 let template = ResourceTemplate {
2979 uri_template: "file://{path}".to_string(),
2980 name: "File Reader".to_string(),
2981 description: Some("Read any file".to_string()),
2982 mime_type: Some("text/plain".to_string()),
2983 icon: None,
2984 version: None,
2985 tags: vec![],
2986 };
2987 let value = serde_json::to_value(&template).expect("serialize");
2988 assert_eq!(value["uriTemplate"], "file://{path}");
2989 assert_eq!(value["name"], "File Reader");
2990 assert_eq!(value["description"], "Read any file");
2991 assert_eq!(value["mimeType"], "text/plain");
2992 }
2993
2994 #[test]
2995 fn resource_template_peer_admission_rejects_malformed_and_oversized_templates() {
2996 let accepted_wire = json!({
2997 "uriTemplate": "file://{path}",
2998 "name": "File Reader"
2999 });
3000 let accepted: ResourceTemplate = serde_json::from_value(accepted_wire.clone())
3001 .expect("a legal exact-2024 resource template remains admissible");
3002 assert_eq!(
3003 serde_json::to_value(accepted).expect("admitted template serializes"),
3004 accepted_wire
3005 );
3006
3007 let mut malformed = accepted_wire.clone();
3008 malformed["uriTemplate"] = json!("file://{path");
3009 assert!(
3010 serde_json::from_value::<ResourceTemplate>(malformed).is_err(),
3011 "changing only the closing brace rejects malformed peer input"
3012 );
3013
3014 let mut oversized = accepted_wire;
3015 oversized["uriTemplate"] = json!(format!(
3016 "mcp://{}",
3017 "x".repeat(crate::MAX_URI_TEMPLATE_BYTES)
3018 ));
3019 assert!(
3020 serde_json::from_value::<ResourceTemplate>(oversized).is_err(),
3021 "changing only the template length beyond the protocol bound rejects peer input"
3022 );
3023 }
3024
3025 #[test]
3030 fn prompt_with_arguments() {
3031 let prompt = Prompt {
3032 name: "translate".to_string(),
3033 description: Some("Translate text".to_string()),
3034 arguments: vec![
3035 PromptArgument {
3036 name: "text".to_string(),
3037 description: Some("Text to translate".to_string()),
3038 required: true,
3039 },
3040 PromptArgument {
3041 name: "language".to_string(),
3042 description: Some("Target language".to_string()),
3043 required: true,
3044 },
3045 PromptArgument {
3046 name: "style".to_string(),
3047 description: None,
3048 required: false,
3049 },
3050 ],
3051 icon: None,
3052 version: None,
3053 tags: vec![],
3054 };
3055 let value = serde_json::to_value(&prompt).expect("serialize");
3056 assert_eq!(value["name"], "translate");
3057 let args = value["arguments"].as_array().expect("arguments array");
3058 assert_eq!(args.len(), 3);
3059 assert_eq!(args[0]["name"], "text");
3060 assert_eq!(args[0]["required"], true);
3061 assert_eq!(args[2]["name"], "style");
3062 assert!(args[2].get("required").is_none());
3064 }
3065
3066 #[test]
3067 fn prompt_empty_arguments_omitted() {
3068 let prompt = Prompt {
3069 name: "simple".to_string(),
3070 description: None,
3071 arguments: vec![],
3072 icon: None,
3073 version: None,
3074 tags: vec![],
3075 };
3076 let value = serde_json::to_value(&prompt).expect("serialize");
3077 assert!(value.get("arguments").is_none());
3078 }
3079
3080 #[test]
3085 fn task_id_new_has_prefix() {
3086 let id = TaskId::new();
3087 assert!(id.as_str().starts_with("task-"));
3088 }
3089
3090 #[test]
3091 fn task_id_from_string() {
3092 let id = TaskId::from_string("task-abc123");
3093 assert_eq!(id.as_str(), "task-abc123");
3094 }
3095
3096 #[test]
3097 fn task_id_display() {
3098 let id = TaskId::from_string("task-xyz");
3099 assert_eq!(format!("{id}"), "task-xyz");
3100 }
3101
3102 #[test]
3103 fn task_id_from_impls() {
3104 let from_string: TaskId = "my-task".to_string().into();
3105 assert_eq!(from_string.as_str(), "my-task");
3106
3107 let from_str: TaskId = "another-task".into();
3108 assert_eq!(from_str.as_str(), "another-task");
3109 }
3110
3111 #[test]
3112 fn task_id_serialization() {
3113 let id = TaskId::from_string("task-1");
3114 let value = serde_json::to_value(&id).expect("serialize");
3115 assert_eq!(value, "task-1");
3116
3117 let deserialized: TaskId = serde_json::from_value(json!("task-2")).expect("deserialize");
3118 assert_eq!(deserialized.as_str(), "task-2");
3119 }
3120
3121 #[test]
3122 fn task_id_equality() {
3123 let a = TaskId::from_string("task-1");
3124 let b = TaskId::from_string("task-1");
3125 let c = TaskId::from_string("task-2");
3126 assert_eq!(a, b);
3127 assert_ne!(a, c);
3128 }
3129
3130 #[test]
3135 fn task_status_is_terminal() {
3136 assert!(TaskStatus::Completed.is_terminal());
3137 assert!(TaskStatus::Failed.is_terminal());
3138 assert!(TaskStatus::Cancelled.is_terminal());
3139 assert!(!TaskStatus::Pending.is_terminal());
3140 assert!(!TaskStatus::Running.is_terminal());
3141 }
3142
3143 #[test]
3144 fn task_status_is_active() {
3145 assert!(TaskStatus::Pending.is_active());
3146 assert!(TaskStatus::Running.is_active());
3147 assert!(!TaskStatus::Completed.is_active());
3148 assert!(!TaskStatus::Failed.is_active());
3149 assert!(!TaskStatus::Cancelled.is_active());
3150 }
3151
3152 #[test]
3153 fn task_status_serialization() {
3154 assert_eq!(
3155 serde_json::to_value(TaskStatus::Pending).unwrap(),
3156 "pending"
3157 );
3158 assert_eq!(
3159 serde_json::to_value(TaskStatus::Running).unwrap(),
3160 "running"
3161 );
3162 assert_eq!(
3163 serde_json::to_value(TaskStatus::Completed).unwrap(),
3164 "completed"
3165 );
3166 assert_eq!(serde_json::to_value(TaskStatus::Failed).unwrap(), "failed");
3167 assert_eq!(
3168 serde_json::to_value(TaskStatus::Cancelled).unwrap(),
3169 "cancelled"
3170 );
3171 }
3172
3173 #[test]
3174 fn task_status_deserialization() {
3175 assert_eq!(
3176 serde_json::from_value::<TaskStatus>(json!("pending")).unwrap(),
3177 TaskStatus::Pending
3178 );
3179 assert_eq!(
3180 serde_json::from_value::<TaskStatus>(json!("running")).unwrap(),
3181 TaskStatus::Running
3182 );
3183 assert_eq!(
3184 serde_json::from_value::<TaskStatus>(json!("completed")).unwrap(),
3185 TaskStatus::Completed
3186 );
3187 assert_eq!(
3188 serde_json::from_value::<TaskStatus>(json!("failed")).unwrap(),
3189 TaskStatus::Failed
3190 );
3191 assert_eq!(
3192 serde_json::from_value::<TaskStatus>(json!("cancelled")).unwrap(),
3193 TaskStatus::Cancelled
3194 );
3195 }
3196
3197 #[test]
3202 fn task_info_serialization() {
3203 let info = TaskInfo {
3204 id: TaskId::from_string("task-1"),
3205 task_type: "compute".to_string(),
3206 status: TaskStatus::Running,
3207 progress: Some(0.5),
3208 message: Some("Processing...".to_string()),
3209 created_at: "2026-01-28T00:00:00Z".to_string(),
3210 started_at: Some("2026-01-28T00:01:00Z".to_string()),
3211 completed_at: None,
3212 error: None,
3213 };
3214 let value = serde_json::to_value(&info).expect("serialize");
3215 assert_eq!(value["id"], "task-1");
3216 assert_eq!(value["taskType"], "compute");
3217 assert_eq!(value["status"], "running");
3218 assert_eq!(value["progress"], 0.5);
3219 assert_eq!(value["message"], "Processing...");
3220 assert_eq!(value["createdAt"], "2026-01-28T00:00:00Z");
3221 assert_eq!(value["startedAt"], "2026-01-28T00:01:00Z");
3222 assert!(value.get("completedAt").is_none());
3223 assert!(value.get("error").is_none());
3224 }
3225
3226 #[test]
3227 fn task_info_minimal() {
3228 let json = json!({
3229 "id": "task-2",
3230 "taskType": "demo",
3231 "status": "pending",
3232 "createdAt": "2026-01-28T00:00:00Z"
3233 });
3234 let info: TaskInfo = serde_json::from_value(json).expect("deserialize");
3235 assert_eq!(info.id.as_str(), "task-2");
3236 assert_eq!(info.status, TaskStatus::Pending);
3237 assert!(info.progress.is_none());
3238 assert!(info.message.is_none());
3239 }
3240
3241 #[test]
3246 fn task_result_success() {
3247 let result = TaskResult {
3248 id: TaskId::from_string("task-1"),
3249 success: true,
3250 data: Some(json!({"value": 42})),
3251 error: None,
3252 };
3253 let value = serde_json::to_value(&result).expect("serialize");
3254 assert_eq!(value["id"], "task-1");
3255 assert_eq!(value["success"], true);
3256 assert_eq!(value["data"]["value"], 42);
3257 assert!(value.get("error").is_none());
3258 }
3259
3260 #[test]
3261 fn task_result_failure() {
3262 let result = TaskResult {
3263 id: TaskId::from_string("task-2"),
3264 success: false,
3265 data: None,
3266 error: Some("computation failed".to_string()),
3267 };
3268 let value = serde_json::to_value(&result).expect("serialize");
3269 assert_eq!(value["success"], false);
3270 assert!(value.get("data").is_none());
3271 assert_eq!(value["error"], "computation failed");
3272 }
3273
3274 #[test]
3279 fn sampling_content_text_serialization() {
3280 let content = SamplingContent::Text {
3281 text: "Hello".to_string(),
3282 };
3283 let value = serde_json::to_value(&content).expect("serialize");
3284 assert_eq!(value["type"], "text");
3285 assert_eq!(value["text"], "Hello");
3286 }
3287
3288 #[test]
3289 fn sampling_content_image_serialization() {
3290 let content = SamplingContent::Image {
3291 data: "base64data".to_string(),
3292 mime_type: "image/png".to_string(),
3293 };
3294 let value = serde_json::to_value(&content).expect("serialize");
3295 assert_eq!(value["type"], "image");
3296 assert_eq!(value["data"], "base64data");
3297 assert_eq!(value["mimeType"], "image/png");
3298 }
3299
3300 #[test]
3305 fn sampling_message_user_constructor() {
3306 let msg = SamplingMessage::user("Hello!");
3307 let value = serde_json::to_value(&msg).expect("serialize");
3308 assert_eq!(value["role"], "user");
3309 assert_eq!(value["content"]["type"], "text");
3310 assert_eq!(value["content"]["text"], "Hello!");
3311 }
3312
3313 #[test]
3314 fn sampling_message_assistant_constructor() {
3315 let msg = SamplingMessage::assistant("Hi there!");
3316 let value = serde_json::to_value(&msg).expect("serialize");
3317 assert_eq!(value["role"], "assistant");
3318 assert_eq!(value["content"]["text"], "Hi there!");
3319 }
3320
3321 #[test]
3326 fn model_preferences_default() {
3327 let prefs = ModelPreferences::default();
3328 let value = serde_json::to_value(&prefs).expect("serialize");
3329 assert!(value.get("hints").is_none());
3331 assert!(value.get("costPriority").is_none());
3332 assert!(value.get("speedPriority").is_none());
3333 assert!(value.get("intelligencePriority").is_none());
3334 }
3335
3336 #[test]
3337 fn model_preferences_full() {
3338 let prefs = ModelPreferences {
3339 hints: vec![ModelHint {
3340 name: Some("claude-3".to_string()),
3341 }],
3342 cost_priority: Some(0.3),
3343 speed_priority: Some(0.5),
3344 intelligence_priority: Some(0.9),
3345 };
3346 let value = serde_json::to_value(&prefs).expect("serialize");
3347 assert_eq!(value["hints"][0]["name"], "claude-3");
3348 assert_eq!(value["costPriority"], 0.3);
3349 assert_eq!(value["speedPriority"], 0.5);
3350 assert_eq!(value["intelligencePriority"], 0.9);
3351 }
3352
3353 #[test]
3358 fn stop_reason_serialization() {
3359 assert_eq!(
3360 serde_json::to_value(StopReason::EndTurn).unwrap(),
3361 "endTurn"
3362 );
3363 assert_eq!(
3364 serde_json::to_value(StopReason::StopSequence).unwrap(),
3365 "stopSequence"
3366 );
3367 assert_eq!(
3368 serde_json::to_value(StopReason::MaxTokens).unwrap(),
3369 "maxTokens"
3370 );
3371 }
3372
3373 #[test]
3374 fn stop_reason_deserialization() {
3375 assert_eq!(
3376 serde_json::from_value::<StopReason>(json!("endTurn")).unwrap(),
3377 StopReason::EndTurn
3378 );
3379 assert_eq!(
3380 serde_json::from_value::<StopReason>(json!("stopSequence")).unwrap(),
3381 StopReason::StopSequence
3382 );
3383 assert_eq!(
3384 serde_json::from_value::<StopReason>(json!("maxTokens")).unwrap(),
3385 StopReason::MaxTokens
3386 );
3387 }
3388
3389 #[test]
3390 fn stop_reason_default() {
3391 assert_eq!(StopReason::default(), StopReason::EndTurn);
3392 }
3393
3394 #[test]
3399 fn protocol_version_value() {
3400 assert_eq!(PROTOCOL_VERSION, "2024-11-05");
3401 }
3402
3403 #[test]
3408 fn tool_annotations_default_is_empty() {
3409 let ann = ToolAnnotations::new();
3410 assert!(ann.is_empty());
3411 }
3412
3413 #[test]
3414 fn tool_annotations_builder_chain() {
3415 let ann = ToolAnnotations::new()
3416 .read_only(true)
3417 .idempotent(true)
3418 .destructive(false)
3419 .open_world_hint(false);
3420
3421 assert_eq!(ann.read_only, Some(true));
3422 assert_eq!(ann.idempotent, Some(true));
3423 assert_eq!(ann.destructive, Some(false));
3424 assert_eq!(ann.open_world_hint, Some(false));
3425 assert!(!ann.is_empty());
3426 }
3427
3428 #[test]
3429 fn tool_annotations_single_field_not_empty() {
3430 assert!(!ToolAnnotations::new().destructive(true).is_empty());
3431 assert!(!ToolAnnotations::new().idempotent(false).is_empty());
3432 assert!(!ToolAnnotations::new().read_only(true).is_empty());
3433 assert!(!ToolAnnotations::new().open_world_hint(true).is_empty());
3434 }
3435
3436 #[test]
3437 fn tool_annotations_serialization_skips_none() {
3438 let ann = ToolAnnotations::new().read_only(true);
3439 let value = serde_json::to_value(&ann).expect("serialize");
3440 assert_eq!(value["readOnlyHint"], true);
3441 assert!(value.get("destructiveHint").is_none());
3442 assert!(value.get("idempotentHint").is_none());
3443 assert!(value.get("openWorldHint").is_none());
3444 }
3445
3446 #[test]
3447 fn tool_annotations_round_trip() {
3448 let ann = ToolAnnotations::new()
3449 .destructive(true)
3450 .idempotent(false)
3451 .open_world_hint(true);
3452 let json_str = serde_json::to_string(&ann).expect("serialize");
3453 let deserialized: ToolAnnotations = serde_json::from_str(&json_str).expect("deserialize");
3454 assert_eq!(ann, deserialized);
3455 }
3456
3457 #[test]
3462 fn icon_is_data_uri_with_data_prefix() {
3463 let icon = Icon {
3464 src: Some("data:image/png;base64,iVBOR".to_string()),
3465 mime_type: None,
3466 sizes: None,
3467 };
3468 assert!(icon.is_data_uri());
3469 }
3470
3471 #[test]
3472 fn icon_is_data_uri_without_data_prefix() {
3473 let icon = Icon {
3474 src: Some("https://example.com/icon.png".to_string()),
3475 mime_type: None,
3476 sizes: None,
3477 };
3478 assert!(!icon.is_data_uri());
3479 }
3480
3481 #[test]
3482 fn icon_is_data_uri_no_src() {
3483 let icon = Icon {
3484 src: None,
3485 mime_type: None,
3486 sizes: None,
3487 };
3488 assert!(!icon.is_data_uri());
3489 }
3490
3491 #[test]
3492 fn icon_is_data_uri_empty_string() {
3493 let icon = Icon {
3494 src: Some(String::new()),
3495 mime_type: None,
3496 sizes: None,
3497 };
3498 assert!(!icon.is_data_uri());
3499 }
3500
3501 #[test]
3506 fn content_image_bytes_encodes_base64() {
3507 let bytes: &[u8] = &[0x89, 0x50, 0x4E, 0x47]; let content = Content::image_bytes(bytes, "image/png");
3509 match &content {
3510 Content::Image { data, mime_type } => {
3511 assert_eq!(mime_type, "image/png");
3512 let decoded = base64::engine::general_purpose::STANDARD
3514 .decode(data)
3515 .expect("valid base64");
3516 assert_eq!(decoded, bytes);
3517 }
3518 _ => panic!("expected Image content"),
3519 }
3520 }
3521
3522 #[test]
3523 fn content_image_bytes_empty() {
3524 let content = Content::image_bytes(&[] as &[u8], "image/png");
3525 match &content {
3526 Content::Image { data, .. } => {
3527 let decoded = base64::engine::general_purpose::STANDARD
3528 .decode(data)
3529 .expect("valid base64");
3530 assert!(decoded.is_empty());
3531 }
3532 _ => panic!("expected Image content"),
3533 }
3534 }
3535
3536 #[test]
3537 fn content_audio_bytes_encodes_base64() {
3538 let bytes: &[u8] = &[0x52, 0x49, 0x46, 0x46]; let content = Content::audio_bytes(bytes, "audio/wav");
3540 match &content {
3541 Content::Audio { data, mime_type } => {
3542 assert_eq!(mime_type, "audio/wav");
3543 let decoded = base64::engine::general_purpose::STANDARD
3544 .decode(data)
3545 .expect("valid base64");
3546 assert_eq!(decoded, bytes);
3547 }
3548 _ => panic!("expected Audio content"),
3549 }
3550 }
3551
3552 #[test]
3553 fn content_resource_blob_bytes_encodes_base64() {
3554 let bytes: &[u8] = &[0xDE, 0xAD, 0xBE, 0xEF];
3555 let content = Content::resource_blob_bytes(
3556 "blob://test",
3557 Some("application/octet-stream".into()),
3558 bytes,
3559 );
3560 match &content {
3561 Content::Resource {
3562 resource: ResourceContent { uri, blob, .. },
3563 } => {
3564 assert_eq!(uri, "blob://test");
3565 let blob_data = blob.as_ref().expect("should have blob");
3566 let decoded = base64::engine::general_purpose::STANDARD
3567 .decode(blob_data)
3568 .expect("valid base64");
3569 assert_eq!(decoded, bytes);
3570 }
3571 _ => panic!("expected Resource content"),
3572 }
3573 }
3574
3575 #[test]
3576 fn content_resource_blob_bytes_none_mime() {
3577 let content = Content::resource_blob_bytes("blob://test", None, &[1, 2, 3]);
3578 match &content {
3579 Content::Resource {
3580 resource: ResourceContent { mime_type, .. },
3581 } => {
3582 assert!(mime_type.is_none());
3583 }
3584 _ => panic!("expected Resource content"),
3585 }
3586 }
3587
3588 #[test]
3593 fn root_new_constructor() {
3594 let root = Root::new("file:///home/user/project");
3595 assert_eq!(root.uri, "file:///home/user/project");
3596 assert!(root.name.is_none());
3597 }
3598
3599 #[test]
3600 fn root_new_from_string() {
3601 let uri = String::from("file:///tmp");
3602 let root = Root::new(uri);
3603 assert_eq!(root.uri, "file:///tmp");
3604 }
3605
3606 #[test]
3607 fn root_with_name_constructor() {
3608 let root = Root::with_name("file:///workspace", "My Project");
3609 assert_eq!(root.uri, "file:///workspace");
3610 assert_eq!(root.name.as_deref(), Some("My Project"));
3611 }
3612
3613 #[test]
3614 fn root_with_name_from_strings() {
3615 let uri = String::from("file:///src");
3616 let name = String::from("Source");
3617 let root = Root::with_name(uri, name);
3618 assert_eq!(root.uri, "file:///src");
3619 assert_eq!(root.name.unwrap(), "Source");
3620 }
3621
3622 #[test]
3623 fn content_text_constructor() {
3624 let content = Content::text("hello world");
3625 match &content {
3626 Content::Text { text } => assert_eq!(text, "hello world"),
3627 _ => panic!("expected Text content"),
3628 }
3629 }
3630
3631 #[test]
3632 fn content_text_from_string() {
3633 let s = String::from("owned string");
3634 let content = Content::text(s);
3635 match &content {
3636 Content::Text { text } => assert_eq!(text, "owned string"),
3637 _ => panic!("expected Text content"),
3638 }
3639 }
3640
3641 #[test]
3642 fn content_image_base64_constructor() {
3643 let content = Content::image_base64("aGVsbG8=", "image/png");
3644 match &content {
3645 Content::Image { data, mime_type } => {
3646 assert_eq!(data, "aGVsbG8=");
3647 assert_eq!(mime_type, "image/png");
3648 }
3649 _ => panic!("expected Image content"),
3650 }
3651 }
3652
3653 #[test]
3654 fn content_audio_base64_constructor() {
3655 let content = Content::audio_base64("AAAA", "audio/mp3");
3656 match &content {
3657 Content::Audio { data, mime_type } => {
3658 assert_eq!(data, "AAAA");
3659 assert_eq!(mime_type, "audio/mp3");
3660 }
3661 _ => panic!("expected Audio content"),
3662 }
3663 }
3664
3665 #[test]
3666 fn content_resource_text_constructor() {
3667 let content = Content::resource_text(
3668 "file:///readme.md",
3669 Some("text/markdown".to_string()),
3670 "# Hello",
3671 );
3672 match &content {
3673 Content::Resource { resource } => {
3674 assert_eq!(resource.uri, "file:///readme.md");
3675 assert_eq!(resource.mime_type.as_deref(), Some("text/markdown"));
3676 assert_eq!(resource.text.as_deref(), Some("# Hello"));
3677 assert!(resource.blob.is_none());
3678 }
3679 _ => panic!("expected Resource content"),
3680 }
3681 }
3682
3683 #[test]
3684 fn content_resource_text_no_mime() {
3685 let content = Content::resource_text("file:///data.txt", None, "data");
3686 match &content {
3687 Content::Resource { resource } => {
3688 assert!(resource.mime_type.is_none());
3689 assert_eq!(resource.text.as_deref(), Some("data"));
3690 }
3691 _ => panic!("expected Resource content"),
3692 }
3693 }
3694
3695 #[test]
3696 fn content_resource_blob_base64_constructor() {
3697 let content = Content::resource_blob_base64(
3698 "file:///image.png",
3699 Some("image/png".to_string()),
3700 "iVBOR",
3701 );
3702 match &content {
3703 Content::Resource { resource } => {
3704 assert_eq!(resource.uri, "file:///image.png");
3705 assert_eq!(resource.mime_type.as_deref(), Some("image/png"));
3706 assert!(resource.text.is_none());
3707 assert_eq!(resource.blob.as_deref(), Some("iVBOR"));
3708 }
3709 _ => panic!("expected Resource content"),
3710 }
3711 }
3712
3713 #[test]
3714 fn content_resource_blob_base64_no_mime() {
3715 let content = Content::resource_blob_base64("file:///bin", None, "AQID");
3716 match &content {
3717 Content::Resource { resource } => {
3718 assert!(resource.mime_type.is_none());
3719 assert_eq!(resource.blob.as_deref(), Some("AQID"));
3720 }
3721 _ => panic!("expected Resource content"),
3722 }
3723 }
3724
3725 #[test]
3726 fn final_models_and_sampling_blocks_round_trip_without_legacy_fields() {
3727 let tool_wire = json!({
3728 "name": "weather",
3729 "title": "Weather lookup",
3730 "description": "Looks up forecast data",
3731 "icons": [{"src": "https://example.test/weather.png"}],
3732 "inputSchema": {"type": "object", "properties": {"city": {"type": "string"}}},
3733 "annotations": {"title": "Forecast", "readOnlyHint": true},
3734 "_meta": {"com.example/source": "catalog"}
3735 });
3736 let tool: FinalTool = serde_json::from_value(tool_wire.clone())
3737 .expect("final tool accepts final metadata and icons array");
3738 assert_eq!(tool.title.as_deref(), Some("Weather lookup"));
3739 assert_eq!(
3740 serde_json::to_value(&tool).expect("final tool re-encodes"),
3741 tool_wire
3742 );
3743
3744 let sampling_wire = json!({
3745 "role": "assistant",
3746 "content": [
3747 {"type": "tool_use", "id": "call-1", "name": "weather", "input": {"city": "Boston"}},
3748 {"type": "tool_result", "toolUseId": "call-1", "content": [{"type": "text", "text": "sunny"}], "structuredContent": {"temperature": 22}}
3749 ],
3750 "_meta": {"com.example/turn": 4}
3751 });
3752 let message: FinalSamplingMessage = serde_json::from_value(sampling_wire.clone())
3753 .expect("final sampling admits tool-use and tool-result blocks");
3754 assert_eq!(
3755 serde_json::to_value(message).expect("final sampling re-encodes"),
3756 sampling_wire
3757 );
3758 }
3759
3760 #[test]
3761 fn final_tool_rejects_one_legacy_icon_field_without_mutating_final_baseline() {
3762 let accepted = json!({
3763 "name": "weather",
3764 "inputSchema": {"type": "object"}
3765 });
3766 let baseline: FinalTool =
3767 serde_json::from_value(accepted.clone()).expect("final tool baseline");
3768 let mut planted = accepted.clone();
3769 planted["icon"] = json!({"src": "https://example.test/legacy.png"});
3770 assert!(
3771 serde_json::from_value::<FinalTool>(planted).is_err(),
3772 "only the legacy singular icon field changes the final model"
3773 );
3774 assert_eq!(
3775 serde_json::to_value(baseline).expect("baseline re-encodes"),
3776 accepted,
3777 "legacy-field rejection does not mutate final model state"
3778 );
3779 }
3780
3781 #[test]
3782 fn final_tool_output_schema_distinguishes_absence_from_explicit_null() {
3783 let absent_wire = json!({
3784 "name": "weather",
3785 "inputSchema": {"type": "object"}
3786 });
3787 let absent: FinalTool =
3788 serde_json::from_value(absent_wire.clone()).expect("absent outputSchema is valid");
3789 assert!(absent.output_schema.is_none());
3790 assert_eq!(
3791 serde_json::to_value(absent).expect("absent outputSchema re-encodes"),
3792 absent_wire
3793 );
3794
3795 let accepted_wire = json!({
3796 "name": "weather",
3797 "inputSchema": {"type": "object"},
3798 "outputSchema": {"type": "null"}
3799 });
3800 let accepted: FinalTool = serde_json::from_value(accepted_wire.clone())
3801 .expect("an object schema whose admitted instances are null is valid");
3802
3803 let mut planted = accepted_wire.clone();
3804 planted["outputSchema"] = serde_json::Value::Null;
3805 assert!(
3806 serde_json::from_value::<FinalTool>(planted).is_err(),
3807 "a present outputSchema must itself be an object, not JSON null"
3808 );
3809 assert_eq!(
3810 serde_json::to_value(accepted).expect("accepted outputSchema re-encodes"),
3811 accepted_wire,
3812 "rejecting the one-field null plant cannot mutate the accepted model"
3813 );
3814 }
3815
3816 #[test]
3817 fn final_resource_size_preserves_arbitrary_width_and_rejects_fractional_values() {
3818 let accepted: serde_json::Value = serde_json::from_str(
3819 r#"{"uri":"file:///data.bin","name":"data","size":922337203685477580812345678901234567890}"#,
3820 )
3821 .expect("arbitrary-width resource size wire parses");
3822 let resource: FinalResource = serde_json::from_value(accepted.clone())
3823 .expect("arbitrary-width final resource size is accepted");
3824 assert_eq!(
3825 resource.size.as_ref().map(JsonInteger::as_str),
3826 Some("922337203685477580812345678901234567890")
3827 );
3828 assert_eq!(
3829 serde_json::to_value(resource).expect("arbitrary-width final resource re-encodes"),
3830 accepted,
3831 "the exact integer resource size lexeme round-trips"
3832 );
3833
3834 let planted: serde_json::Value = serde_json::from_str(
3835 r#"{"uri":"file:///data.bin","name":"data","size":922337203685477580812345678901234567890.5}"#,
3836 )
3837 .expect("one-field fractional resource size wire parses");
3838 assert!(
3839 serde_json::from_value::<FinalResource>(planted).is_err(),
3840 "changing only the resource size to a fractional number rejects it"
3841 );
3842 }
3843
3844 #[test]
3845 fn final_resource_template_enforces_the_uri_template_schema_format() {
3846 let accepted_wire = json!({
3847 "uriTemplate": "mcp://resources/{item:3}{?cursor,labels*}",
3848 "name": "resource-template"
3849 });
3850 let accepted: FinalResourceTemplate = serde_json::from_value(accepted_wire.clone())
3851 .expect("a final RFC 6570 Level 4 resource template is admitted");
3852 assert_eq!(
3853 accepted.uri_template, "mcp://resources/{item:3}{?cursor,labels*}",
3854 "typed final decoding preserves the accepted template spelling"
3855 );
3856 assert_eq!(
3857 serde_json::to_value(accepted).expect("accepted template re-encodes"),
3858 accepted_wire,
3859 "template validation does not normalize the final wire value"
3860 );
3861
3862 let mut planted = accepted_wire;
3863 planted["uriTemplate"] = json!("mcp://resources/{item:0}");
3864 assert!(
3865 serde_json::from_value::<FinalResourceTemplate>(planted).is_err(),
3866 "changing only the prefix modifier to RFC 6570's forbidden zero rejects the template"
3867 );
3868
3869 let locally_invalid = FinalResourceTemplate {
3870 uri_template: "mcp://resources/{item:0}".to_owned(),
3871 name: "resource-template".to_owned(),
3872 title: None,
3873 description: None,
3874 icons: None,
3875 mime_type: None,
3876 annotations: None,
3877 meta: None,
3878 };
3879 assert!(
3880 serde_json::to_value(locally_invalid).is_err(),
3881 "direct construction cannot serialize a URI template rejected at peer admission"
3882 );
3883 }
3884
3885 #[test]
3886 fn apps_02_nested_tool_resource_metadata_and_result_projection_round_trip() {
3887 let tool_wire = json!({
3888 "name": "weather",
3889 "inputSchema": {"type": "object"},
3890 "_meta": {
3891 "ui": {
3892 "resourceUri": "ui://weather/dashboard",
3893 "visibility": ["model", "app"]
3894 },
3895 "com.example/catalog": "weather"
3896 }
3897 });
3898 let tool: FinalTool = serde_json::from_value(tool_wire.clone())
3899 .expect("a nested Apps tool resource binding is valid final metadata");
3900 let metadata = tool
3901 .mcp_apps_metadata()
3902 .expect("nested Apps tool metadata remains typed")
3903 .expect("the tool declares Apps metadata");
3904 assert_eq!(
3905 metadata.resource_uri.as_ref().map(AbsoluteUri::as_str),
3906 Some("ui://weather/dashboard")
3907 );
3908 assert_eq!(
3909 metadata.effective_visibility(),
3910 [McpAppsToolVisibility::Model, McpAppsToolVisibility::App]
3911 );
3912 assert_eq!(
3913 serde_json::to_value(&tool).expect("tool re-encodes exact nested metadata"),
3914 tool_wire
3915 );
3916
3917 let resource_wire = json!({
3918 "uri": "ui://weather/dashboard",
3919 "name": "weather-dashboard",
3920 "mimeType": MCP_APPS_HTML_MIME_TYPE,
3921 "_meta": {"ui": {
3922 "csp": {
3923 "connectDomains": ["https://api.weather.example"],
3924 "resourceDomains": ["https://cdn.weather.example"],
3925 "frameDomains": ["https://maps.weather.example"],
3926 "baseUriDomains": ["https://cdn.weather.example"]
3927 },
3928 "permissions": {"geolocation": {}},
3929 "domain": "weather-view.host.example",
3930 "prefersBorder": true
3931 }}
3932 });
3933 let resource: FinalResource = serde_json::from_value(resource_wire.clone())
3934 .expect("an Apps HTML resource accepts nested presentation metadata");
3935 let resource_metadata = resource
3936 .mcp_apps_metadata()
3937 .expect("resource metadata remains typed")
3938 .expect("resource declares Apps presentation");
3939 assert_eq!(resource_metadata.prefers_border, Some(true));
3940 assert_eq!(
3941 resource_metadata
3942 .csp
3943 .as_ref()
3944 .and_then(|csp| csp.connect_domains.as_ref())
3945 .map(|domains| domains.iter().map(String::as_str).collect::<Vec<_>>()),
3946 Some(vec!["https://api.weather.example"])
3947 );
3948 assert!(
3949 resource_metadata
3950 .permissions
3951 .as_ref()
3952 .and_then(|permissions| permissions.geolocation.as_ref())
3953 .is_some()
3954 );
3955 assert_eq!(
3956 resource_metadata.domain.as_deref(),
3957 Some("weather-view.host.example")
3958 );
3959 tool.mcp_apps_resource_binding()
3960 .expect("tool metadata is valid")
3961 .expect("tool has an Apps binding")
3962 .validate_resource(&resource)
3963 .expect("the exact Apps HTML catalog resource satisfies the binding");
3964 assert_eq!(
3965 serde_json::to_value(&resource).expect("resource re-encodes exact nested metadata"),
3966 resource_wire
3967 );
3968
3969 let final_result = FinalCallToolResult {
3970 content: vec![ContentBlock::text("sunny")],
3971 is_error: false,
3972 structured_content: Some(serde_json::Value::Null),
3973 };
3974 let projected = McpAppsToolResult::from_final_call_tool_result(&final_result)
3975 .expect("a complete final tools/call result projects into Apps content");
3976 let result_wire = json!({
3977 "content": [{"type": "text", "text": "sunny"}],
3978 "structuredContent": null
3979 });
3980 assert_eq!(
3981 serde_json::to_value(&projected).expect("Apps result serializes"),
3982 result_wire
3983 );
3984 assert_eq!(
3985 serde_json::from_value::<McpAppsToolResult>(result_wire)
3986 .expect("Apps result round-trips exactly"),
3987 projected
3988 );
3989 }
3990
3991 #[test]
3992 fn apps_02_rejects_one_opaque_ui_uri_in_tool_metadata_construction_and_serde() {
3993 let accepted_uri = AbsoluteUri::parse("ui://opaque").expect("authority-form UI URI");
3994 let accepted = McpAppsToolMetadata::try_new(Some(accepted_uri), None)
3995 .expect("the exact UI URI is admitted");
3996 let accepted_wire = serde_json::json!({"resourceUri": "ui://opaque"});
3997 assert_eq!(
3998 serde_json::to_value(&accepted).expect("the admitted UI URI serializes"),
3999 accepted_wire,
4000 );
4001
4002 let opaque_uri = AbsoluteUri::parse("ui:opaque").expect("opaque UI URI is absolute");
4003 assert_eq!(
4004 McpAppsToolMetadata::try_new(Some(opaque_uri.clone()), None),
4005 Err(McpAppsMetadataError::ResourceUriMustUseUiPrefix),
4006 "removing only the authority delimiter rejects constructor input"
4007 );
4008 let planted = McpAppsToolMetadata {
4009 resource_uri: Some(opaque_uri),
4010 visibility: None,
4011 };
4012 assert!(
4013 serde_json::to_value(&planted).is_err(),
4014 "direct construction cannot serialize an opaque ui: URI"
4015 );
4016
4017 let mut opaque_wire = accepted_wire.clone();
4018 opaque_wire["resourceUri"] = serde_json::json!("ui:opaque");
4019 assert!(
4020 serde_json::from_value::<McpAppsToolMetadata>(opaque_wire).is_err(),
4021 "removing only the authority delimiter rejects deserialization"
4022 );
4023 assert_eq!(
4024 serde_json::to_value(&accepted).expect("rejected variants do not mutate the baseline"),
4025 accepted_wire,
4026 );
4027 }
4028
4029 #[test]
4030 fn apps_02_rejects_only_deprecated_flat_resource_uri_metadata() {
4031 let accepted = json!({
4032 "name": "weather",
4033 "inputSchema": {"type": "object"},
4034 "_meta": {"ui": {"resourceUri": "ui://weather/dashboard"}}
4035 });
4036 let baseline: FinalTool = serde_json::from_value(accepted.clone())
4037 .expect("nested Apps resource metadata is the baseline");
4038 let mut planted = accepted.clone();
4039 let metadata = planted["_meta"]
4040 .as_object_mut()
4041 .expect("baseline metadata is an object");
4042 let nested = metadata
4043 .remove(MCP_APPS_UI_METADATA_KEY)
4044 .expect("baseline has nested Apps metadata");
4045 let resource_uri = nested["resourceUri"].clone();
4046 metadata.insert(
4047 MCP_APPS_DEPRECATED_RESOURCE_URI_METADATA_KEY.to_owned(),
4048 resource_uri,
4049 );
4050
4051 assert!(
4052 serde_json::from_value::<FinalTool>(planted).is_err(),
4053 "only replacing nested ui.resourceUri with the deprecated flat key rejects the tool"
4054 );
4055 assert_eq!(
4056 serde_json::to_value(&baseline).expect("baseline remains serializable"),
4057 accepted,
4058 "the flat-key rejection cannot mutate the accepted nested binding"
4059 );
4060 }
4061
4062 #[test]
4063 fn apps_02_rejects_one_csp_origin_beyond_the_bounded_directive_limit() {
4064 let accepted = json!({
4065 "uri": "ui://weather/dashboard",
4066 "name": "weather-dashboard",
4067 "mimeType": MCP_APPS_HTML_MIME_TYPE,
4068 "_meta": {"ui": {"csp": {
4069 "connectDomains": (0..MAX_MCP_APPS_CSP_DOMAINS_PER_DIRECTIVE)
4070 .map(|index| format!("https://{index}.weather.example"))
4071 .collect::<Vec<_>>()
4072 }}}
4073 });
4074 let baseline: FinalResource = serde_json::from_value(accepted.clone())
4075 .expect("a bounded Apps CSP declaration is valid");
4076 let mut planted = accepted.clone();
4077 planted["_meta"]["ui"]["csp"]["connectDomains"]
4078 .as_array_mut()
4079 .expect("bounded baseline has a CSP origin array")
4080 .push(json!(format!(
4081 "https://{MAX_MCP_APPS_CSP_DOMAINS_PER_DIRECTIVE}.weather.example"
4082 )));
4083
4084 assert!(
4085 serde_json::from_value::<FinalResource>(planted).is_err(),
4086 "adding only one origin beyond the CSP directive bound rejects the resource metadata"
4087 );
4088 assert_eq!(
4089 serde_json::to_value(baseline).expect("bounded baseline re-encodes"),
4090 accepted,
4091 "the one-origin rejection cannot mutate the admitted Apps metadata"
4092 );
4093 }
4094
4095 #[test]
4096 fn apps_02_direct_csp_construction_cannot_bypass_serialization_bounds() {
4097 let accepted = McpAppsResourceCsp {
4098 connect_domains: Some(
4099 (0..MAX_MCP_APPS_CSP_DOMAINS_PER_DIRECTIVE)
4100 .map(|index| format!("https://{index}.weather.example"))
4101 .collect(),
4102 ),
4103 ..McpAppsResourceCsp::default()
4104 };
4105 let accepted_wire = serde_json::to_value(&accepted)
4106 .expect("the direct CSP value at the directive bound serializes");
4107
4108 let mut planted = accepted.clone();
4109 planted
4110 .connect_domains
4111 .as_mut()
4112 .expect("the direct CSP fixture has connect domains")
4113 .push(format!(
4114 "https://{MAX_MCP_APPS_CSP_DOMAINS_PER_DIRECTIVE}.weather.example"
4115 ));
4116
4117 assert!(
4118 serde_json::to_value(&planted).is_err(),
4119 "adding only one domain beyond the bound rejects direct CSP serialization"
4120 );
4121 assert_eq!(
4122 serde_json::to_value(&accepted).expect("accepted direct CSP re-serializes"),
4123 accepted_wire,
4124 "a rejected direct CSP serialization cannot mutate the bounded baseline"
4125 );
4126 }
4127
4128 #[test]
4129 fn apps_02_preserves_duplicate_and_ordered_visibility() {
4130 let accepted = json!({
4131 "name": "weather",
4132 "inputSchema": {"type": "object"},
4133 "_meta": {"ui": {"visibility": ["app", "model", "app"]}}
4134 });
4135 let tool: FinalTool = serde_json::from_value(accepted.clone())
4136 .expect("the stable Apps visibility array permits duplicates in wire order");
4137 let metadata = tool
4138 .mcp_apps_metadata()
4139 .expect("Apps metadata remains typed")
4140 .expect("the tool declares Apps metadata");
4141 assert_eq!(
4142 metadata.effective_visibility(),
4143 [
4144 McpAppsToolVisibility::App,
4145 McpAppsToolVisibility::Model,
4146 McpAppsToolVisibility::App,
4147 ],
4148 "Apps visibility preserves the received duplicate sequence"
4149 );
4150 assert_eq!(
4151 serde_json::to_value(&tool).expect("tool re-encodes"),
4152 accepted,
4153 "Apps visibility re-encodes duplicates in their received order"
4154 );
4155 }
4156
4157 #[test]
4158 fn apps_02_bounds_tool_visibility_entries_without_changing_duplicates_or_order() {
4159 let accepted_visibility = (0..MAX_MCP_APPS_TOOL_VISIBILITY_ENTRIES)
4160 .map(|index| {
4161 if index % 2 == 0 {
4162 McpAppsToolVisibility::App
4163 } else {
4164 McpAppsToolVisibility::Model
4165 }
4166 })
4167 .collect::<Vec<_>>();
4168 let baseline = McpAppsToolMetadata::try_new(None, Some(accepted_visibility.clone()))
4169 .expect("the tool visibility entry bound is admitted");
4170 let baseline_wire =
4171 serde_json::to_value(&baseline).expect("bounded visibility metadata serializes");
4172 assert_eq!(
4173 baseline.effective_visibility(),
4174 accepted_visibility,
4175 "bounded visibility retains received duplicate entries in order"
4176 );
4177
4178 let mut planted_visibility = accepted_visibility;
4179 planted_visibility.push(McpAppsToolVisibility::App);
4180 assert_eq!(
4181 McpAppsToolMetadata::try_new(None, Some(planted_visibility.clone())),
4182 Err(McpAppsMetadataError::TooManyToolVisibilityEntries),
4183 "adding only one visibility entry beyond the bound is rejected with the typed error"
4184 );
4185 let planted = McpAppsToolMetadata {
4186 resource_uri: None,
4187 visibility: Some(planted_visibility),
4188 };
4189 assert!(
4190 serde_json::to_value(&planted).is_err(),
4191 "direct construction cannot bypass the visibility entry bound during serialization"
4192 );
4193 assert_eq!(
4194 serde_json::to_value(&baseline).expect("bounded metadata re-serializes"),
4195 baseline_wire,
4196 "rejecting the one-entry plant cannot mutate the admitted metadata"
4197 );
4198 }
4199
4200 #[test]
4201 fn apps_02_rejects_one_non_html_bound_resource_without_mutating_binding() {
4202 let tool: FinalTool = serde_json::from_value(json!({
4203 "name": "weather",
4204 "inputSchema": {"type": "object"},
4205 "_meta": {"ui": {"resourceUri": "ui://weather/dashboard"}}
4206 }))
4207 .expect("nested Apps binding is valid");
4208 let binding = tool
4209 .mcp_apps_resource_binding()
4210 .expect("tool metadata is valid")
4211 .expect("tool declares an Apps resource");
4212 let accepted = json!({
4213 "uri": "ui://weather/dashboard",
4214 "name": "weather-dashboard",
4215 "mimeType": MCP_APPS_HTML_MIME_TYPE
4216 });
4217 let baseline: FinalResource =
4218 serde_json::from_value(accepted.clone()).expect("Apps resource baseline decodes");
4219 let mut planted = accepted.clone();
4220 planted["mimeType"] = json!("text/plain");
4221
4222 assert_eq!(binding.validate_resource(&baseline), Ok(()));
4223 let incompatible: FinalResource = serde_json::from_value(planted)
4224 .expect("only MIME type changes; resource remains a final resource");
4225 assert_eq!(
4226 binding.validate_resource(&incompatible),
4227 Err(McpAppsResourceBindingError::HtmlMimeTypeRequired),
4228 "only replacing the Apps HTML MIME type rejects the bound resource"
4229 );
4230 assert_eq!(
4231 serde_json::to_value(&baseline).expect("baseline resource re-encodes"),
4232 accepted,
4233 "the invalid resource cannot mutate the admitted binding target"
4234 );
4235 }
4236
4237 #[test]
4238 fn apps_02_view_lifecycle_requires_initialization_before_activation() {
4239 assert_eq!(
4240 serde_json::to_value(McpAppsDisplayMode::Pip).expect("display mode serializes exactly"),
4241 json!("pip")
4242 );
4243 assert_eq!(
4244 serde_json::from_value::<McpAppsDisplayMode>(json!("fullscreen"))
4245 .expect("known display mode decodes"),
4246 McpAppsDisplayMode::Fullscreen
4247 );
4248 assert!(
4249 serde_json::from_value::<McpAppsDisplayMode>(json!("overlay")).is_err(),
4250 "only replacing a known display mode with an undeclared value rejects it"
4251 );
4252
4253 let mut lifecycle = McpAppsViewLifecycle::default();
4254 assert!(!lifecycle.permits_application_traffic());
4255 assert_eq!(
4256 lifecycle.admit_initialized(),
4257 Err(McpAppsLifecycleError::InvalidTransition {
4258 from: McpAppsViewLifecycle::New,
4259 operation: "initialized notification",
4260 }),
4261 "only omitting the prior initialization transition rejects early activation"
4262 );
4263 assert_eq!(lifecycle, McpAppsViewLifecycle::New);
4264
4265 lifecycle
4266 .begin_initialize()
4267 .expect("one initialization reservation is legal from New");
4268 lifecycle
4269 .initialization_succeeded()
4270 .expect("a successful initialization awaits exactly one notification");
4271 lifecycle
4272 .admit_initialized()
4273 .expect("the first initialized notification activates the View");
4274 assert!(lifecycle.permits_application_traffic());
4275 lifecycle
4276 .begin_closing()
4277 .expect("an active View can begin terminal teardown");
4278 lifecycle
4279 .finish_closing()
4280 .expect("a closing View reaches Closed exactly once");
4281 assert_eq!(lifecycle, McpAppsViewLifecycle::Closed);
4282 }
4283
4284 #[test]
4285 fn apps_02_result_projection_rejects_one_unknown_wire_member() {
4286 let accepted = json!({
4287 "content": [{"type": "text", "text": "sunny"}],
4288 "isError": true
4289 });
4290 let baseline: McpAppsToolResult = serde_json::from_value(accepted.clone())
4291 .expect("bounded complete Apps tool result is valid");
4292 let mut planted = accepted.clone();
4293 planted["task"] = json!({"taskId": "deferred"});
4294
4295 assert!(
4296 serde_json::from_value::<McpAppsToolResult>(planted).is_err(),
4297 "only adding a Tasks-shaped member rejects the complete Apps result projection"
4298 );
4299 assert_eq!(
4300 serde_json::to_value(&baseline).expect("baseline result re-encodes"),
4301 accepted,
4302 "the rejected Tasks-shaped member cannot mutate the complete Apps result"
4303 );
4304 }
4305}