Skip to main content

fastmcp_protocol/
types.rs

1//! MCP protocol types.
2//!
3//! Core types used in MCP communication.
4
5use 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
19/// MCP protocol version.
20pub const PROTOCOL_VERSION: &str = "2024-11-05";
21
22/// Server capabilities advertised during initialization.
23#[derive(Debug, Clone, Default, Serialize, Deserialize)]
24pub struct ServerCapabilities {
25    /// Tool-related capabilities.
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub tools: Option<ToolsCapability>,
28    /// Resource-related capabilities.
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub resources: Option<ResourcesCapability>,
31    /// Prompt-related capabilities.
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub prompts: Option<PromptsCapability>,
34    /// Logging capability.
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub logging: Option<LoggingCapability>,
37    /// Argument-completion capability (`completion/complete`).
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub completions: Option<CompletionsCapability>,
40    /// Background tasks capability (Docket/SEP-1686).
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub tasks: Option<TasksCapability>,
43}
44
45/// Empty object advertised when `completion/complete` is installed.
46#[derive(Debug, Clone, Default, Serialize, Deserialize)]
47pub struct CompletionsCapability {}
48
49/// Tool capabilities.
50#[derive(Debug, Clone, Default, Serialize, Deserialize)]
51pub struct ToolsCapability {
52    /// Whether the server supports tool list changes.
53    #[serde(
54        default,
55        rename = "listChanged",
56        skip_serializing_if = "std::ops::Not::not"
57    )]
58    pub list_changed: bool,
59}
60
61/// Resource capabilities.
62#[derive(Debug, Clone, Default, Serialize, Deserialize)]
63pub struct ResourcesCapability {
64    /// Whether the server supports resource subscriptions.
65    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
66    pub subscribe: bool,
67    /// Whether the server supports resource list changes.
68    #[serde(
69        default,
70        rename = "listChanged",
71        skip_serializing_if = "std::ops::Not::not"
72    )]
73    pub list_changed: bool,
74}
75
76/// Prompt capabilities.
77#[derive(Debug, Clone, Default, Serialize, Deserialize)]
78pub struct PromptsCapability {
79    /// Whether the server supports prompt list changes.
80    #[serde(
81        default,
82        rename = "listChanged",
83        skip_serializing_if = "std::ops::Not::not"
84    )]
85    pub list_changed: bool,
86}
87
88/// Logging capability.
89#[derive(Debug, Clone, Default, Serialize, Deserialize)]
90pub struct LoggingCapability {}
91
92/// Client capabilities.
93#[derive(Debug, Clone, Default, Serialize, Deserialize)]
94pub struct ClientCapabilities {
95    /// Sampling capability.
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub sampling: Option<SamplingCapability>,
98    /// Elicitation capability (user input requests).
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub elicitation: Option<ElicitationCapability>,
101    /// Roots capability (filesystem roots).
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub roots: Option<RootsCapability>,
104}
105
106/// Sampling capability.
107#[derive(Debug, Clone, Default, Serialize, Deserialize)]
108pub struct SamplingCapability {}
109
110/// Capability for form mode elicitation.
111#[derive(Debug, Clone, Default, Serialize, Deserialize)]
112pub struct FormElicitationCapability {}
113
114/// Capability for URL mode elicitation.
115#[derive(Debug, Clone, Default, Serialize, Deserialize)]
116pub struct UrlElicitationCapability {}
117
118/// Elicitation capability.
119///
120/// Clients must support at least one mode (form or url).
121#[derive(Debug, Clone, Default, Serialize, Deserialize)]
122pub struct ElicitationCapability {
123    /// Present if the client supports form mode elicitation.
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub form: Option<FormElicitationCapability>,
126    /// Present if the client supports URL mode elicitation.
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub url: Option<UrlElicitationCapability>,
129}
130
131impl ElicitationCapability {
132    /// Creates a form-mode elicitation capability.
133    #[must_use]
134    pub fn form() -> Self {
135        Self {
136            form: Some(FormElicitationCapability {}),
137            url: None,
138        }
139    }
140
141    /// Creates a URL-mode elicitation capability.
142    #[must_use]
143    pub fn url() -> Self {
144        Self {
145            form: None,
146            url: Some(UrlElicitationCapability {}),
147        }
148    }
149
150    /// Creates an elicitation capability supporting both modes.
151    #[must_use]
152    pub fn both() -> Self {
153        Self {
154            form: Some(FormElicitationCapability {}),
155            url: Some(UrlElicitationCapability {}),
156        }
157    }
158
159    /// Returns true if form mode is supported.
160    #[must_use]
161    pub fn supports_form(&self) -> bool {
162        self.form.is_some()
163    }
164
165    /// Returns true if URL mode is supported.
166    #[must_use]
167    pub fn supports_url(&self) -> bool {
168        self.url.is_some()
169    }
170}
171
172/// Roots capability.
173#[derive(Debug, Clone, Default, Serialize, Deserialize)]
174pub struct RootsCapability {
175    /// Whether the client supports list changes notifications.
176    #[serde(
177        rename = "listChanged",
178        default,
179        skip_serializing_if = "std::ops::Not::not"
180    )]
181    pub list_changed: bool,
182}
183
184/// A root definition representing a filesystem location.
185///
186/// Roots define the boundaries of where servers can operate within the filesystem,
187/// allowing them to understand which directories and files they have access to.
188#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
189pub struct Root {
190    /// Unique identifier for the root. Must be a `file://` URI.
191    pub uri: String,
192    /// Optional human-readable name for display purposes.
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub name: Option<String>,
195}
196
197impl Root {
198    /// Creates a new root with the given URI.
199    #[must_use]
200    pub fn new(uri: impl Into<String>) -> Self {
201        Self {
202            uri: uri.into(),
203            name: None,
204        }
205    }
206
207    /// Creates a new root with a name.
208    #[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/// Server information.
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct ServerInfo {
220    /// Server name.
221    pub name: String,
222    /// Server version.
223    pub version: String,
224}
225
226/// Client information.
227#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct ClientInfo {
229    /// Client name.
230    pub name: String,
231    /// Client version.
232    pub version: String,
233}
234
235impl ClientInfo {
236    /// Projects this exact-2024 name/version pair into a final Implementation.
237    ///
238    /// Empty name or version is replaced with a nonempty fallback so modern
239    /// request `_meta` can always carry a typed identity object.
240    #[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// ============================================================================
257// Icon Metadata
258// ============================================================================
259
260/// Icon metadata for visual representation of components.
261///
262/// Icons provide visual representation for tools, resources, and prompts
263/// in client UIs. All fields are optional to support various use cases.
264#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
265#[serde(rename_all = "camelCase", deny_unknown_fields)]
266pub struct Icon {
267    /// URL or data URI for the icon.
268    ///
269    /// Can be:
270    /// - HTTP/HTTPS URL: `https://example.com/icon.png`
271    /// - Data URI: `data:image/png;base64,iVBORw0KGgo...`
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub src: Option<String>,
274
275    /// MIME type of the icon (e.g., "image/png", "image/svg+xml").
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub mime_type: Option<String>,
278
279    /// Size hints for the icon (e.g., "32x32", "16x16 32x32 64x64").
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub sizes: Option<String>,
282}
283
284impl Icon {
285    /// Creates a new icon with just a source URL.
286    #[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    /// Creates a new icon with source and MIME type.
296    #[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    /// Creates a new icon with all fields.
306    #[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    /// Returns true if this icon has a source.
320    #[must_use]
321    pub fn has_src(&self) -> bool {
322        self.src.is_some()
323    }
324
325    /// Returns true if the source is a data URI.
326    #[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// ============================================================================
333// Component Definitions
334// ============================================================================
335
336/// Tool annotations for additional metadata.
337///
338/// These annotations provide hints about tool behavior to help clients
339/// make informed decisions about tool usage.
340#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
341pub struct ToolAnnotations {
342    /// Whether the tool may cause destructive side effects.
343    /// True means the tool modifies external state (e.g., deleting files).
344    /// Serialized as the MCP-spec `destructiveHint` field.
345    #[serde(rename = "destructiveHint", skip_serializing_if = "Option::is_none")]
346    pub destructive: Option<bool>,
347    /// Whether the tool is idempotent (safe to retry without side effects).
348    /// True means calling the tool multiple times has the same effect as calling it once.
349    /// Serialized as the MCP-spec `idempotentHint` field.
350    #[serde(rename = "idempotentHint", skip_serializing_if = "Option::is_none")]
351    pub idempotent: Option<bool>,
352    /// Whether the tool is read-only (has no side effects).
353    /// True means the tool only reads data without modifying anything.
354    /// Serialized as the MCP-spec `readOnlyHint` field.
355    #[serde(rename = "readOnlyHint", skip_serializing_if = "Option::is_none")]
356    pub read_only: Option<bool>,
357    /// Whether the tool interacts with an "open world" of external entities.
358    /// Per the MCP spec `openWorldHint` is a boolean: `true` if the tool may reach
359    /// external/unknown systems, `false` if it operates over a closed/local domain.
360    #[serde(rename = "openWorldHint", skip_serializing_if = "Option::is_none")]
361    pub open_world_hint: Option<bool>,
362}
363
364impl ToolAnnotations {
365    /// Creates a new empty annotations struct.
366    #[must_use]
367    pub fn new() -> Self {
368        Self::default()
369    }
370
371    /// Sets the destructive annotation.
372    #[must_use]
373    pub fn destructive(mut self, value: bool) -> Self {
374        self.destructive = Some(value);
375        self
376    }
377
378    /// Sets the idempotent annotation.
379    #[must_use]
380    pub fn idempotent(mut self, value: bool) -> Self {
381        self.idempotent = Some(value);
382        self
383    }
384
385    /// Sets the read_only annotation.
386    #[must_use]
387    pub fn read_only(mut self, value: bool) -> Self {
388        self.read_only = Some(value);
389        self
390    }
391
392    /// Sets the open_world_hint annotation.
393    ///
394    /// Per the MCP spec `openWorldHint` is a boolean: `true` if the tool interacts
395    /// with an open world of external entities, `false` for a closed/local domain.
396    #[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    /// Returns true if any annotation is set.
403    #[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/// Tool definition.
413#[derive(Debug, Clone, Serialize, Deserialize)]
414pub struct Tool {
415    /// Tool name.
416    pub name: String,
417    /// Tool description.
418    #[serde(skip_serializing_if = "Option::is_none")]
419    pub description: Option<String>,
420    /// Input schema (JSON Schema).
421    #[serde(rename = "inputSchema")]
422    pub input_schema: serde_json::Value,
423    /// Output schema (JSON Schema) describing the tool's result structure.
424    #[serde(rename = "outputSchema", skip_serializing_if = "Option::is_none")]
425    pub output_schema: Option<serde_json::Value>,
426    /// Icon for visual representation.
427    #[serde(skip_serializing_if = "Option::is_none")]
428    pub icon: Option<Icon>,
429    /// Component version (semver-like string).
430    #[serde(skip_serializing_if = "Option::is_none")]
431    pub version: Option<String>,
432    /// Tags for filtering and organization.
433    #[serde(default, skip_serializing_if = "Vec::is_empty")]
434    pub tags: Vec<String>,
435    /// Tool annotations providing behavioral hints.
436    #[serde(skip_serializing_if = "Option::is_none")]
437    pub annotations: Option<ToolAnnotations>,
438}
439
440/// Resource definition.
441#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
442pub struct Resource {
443    /// Resource URI.
444    pub uri: String,
445    /// Resource name.
446    pub name: String,
447    /// Resource description.
448    #[serde(skip_serializing_if = "Option::is_none")]
449    pub description: Option<String>,
450    /// MIME type.
451    #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
452    pub mime_type: Option<String>,
453    /// Icon for visual representation.
454    #[serde(skip_serializing_if = "Option::is_none")]
455    pub icon: Option<Icon>,
456    /// Component version (semver-like string).
457    #[serde(skip_serializing_if = "Option::is_none")]
458    pub version: Option<String>,
459    /// Tags for filtering and organization.
460    #[serde(default, skip_serializing_if = "Vec::is_empty")]
461    pub tags: Vec<String>,
462}
463
464/// Resource template definition.
465#[derive(Debug, Clone, Serialize, Deserialize)]
466pub struct ResourceTemplate {
467    /// URI template (RFC 6570).
468    #[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    /// Template name.
475    pub name: String,
476    /// Template description.
477    #[serde(skip_serializing_if = "Option::is_none")]
478    pub description: Option<String>,
479    /// MIME type.
480    #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
481    pub mime_type: Option<String>,
482    /// Icon for visual representation.
483    #[serde(skip_serializing_if = "Option::is_none")]
484    pub icon: Option<Icon>,
485    /// Component version (semver-like string).
486    #[serde(skip_serializing_if = "Option::is_none")]
487    pub version: Option<String>,
488    /// Tags for filtering and organization.
489    #[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/// Prompt definition.
512#[derive(Debug, Clone, Serialize, Deserialize)]
513pub struct Prompt {
514    /// Prompt name.
515    pub name: String,
516    /// Prompt description.
517    #[serde(skip_serializing_if = "Option::is_none")]
518    pub description: Option<String>,
519    /// Prompt arguments.
520    #[serde(default, skip_serializing_if = "Vec::is_empty")]
521    pub arguments: Vec<PromptArgument>,
522    /// Icon for visual representation.
523    #[serde(skip_serializing_if = "Option::is_none")]
524    pub icon: Option<Icon>,
525    /// Component version (semver-like string).
526    #[serde(skip_serializing_if = "Option::is_none")]
527    pub version: Option<String>,
528    /// Tags for filtering and organization.
529    #[serde(default, skip_serializing_if = "Vec::is_empty")]
530    pub tags: Vec<String>,
531}
532
533/// Prompt argument definition.
534#[derive(Debug, Clone, Serialize, Deserialize)]
535pub struct PromptArgument {
536    /// Argument name.
537    pub name: String,
538    /// Argument description.
539    #[serde(skip_serializing_if = "Option::is_none")]
540    pub description: Option<String>,
541    /// Whether the argument is required.
542    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
543    pub required: bool,
544}
545
546// ============================================================================
547// Final component definitions
548// ============================================================================
549
550/// Shared final component identity fields.
551///
552/// The final protocol separates a stable programmatic `name` from an optional
553/// user-facing `title`. Legacy component definitions deliberately remain
554/// separate because their icon, version, and tag members are not final wire
555/// members.
556#[derive(Debug, Clone, Serialize, Deserialize)]
557#[serde(deny_unknown_fields)]
558pub struct FinalBaseMetadata {
559    /// Programmatic component identifier.
560    pub name: String,
561    /// Optional human-facing display title.
562    #[serde(default, skip_serializing_if = "Option::is_none")]
563    pub title: Option<String>,
564}
565
566/// Final tool annotations, including the final display-title hint.
567#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
568#[serde(deny_unknown_fields)]
569pub struct FinalToolAnnotations {
570    /// Optional display title, lower priority than the enclosing tool title.
571    #[serde(default, skip_serializing_if = "Option::is_none")]
572    pub title: Option<String>,
573    /// Whether the tool may perform destructive updates.
574    #[serde(
575        rename = "destructiveHint",
576        default,
577        skip_serializing_if = "Option::is_none"
578    )]
579    pub destructive: Option<bool>,
580    /// Whether repeated calls are idempotent.
581    #[serde(
582        rename = "idempotentHint",
583        default,
584        skip_serializing_if = "Option::is_none"
585    )]
586    pub idempotent: Option<bool>,
587    /// Whether the tool is read-only.
588    #[serde(
589        rename = "readOnlyHint",
590        default,
591        skip_serializing_if = "Option::is_none"
592    )]
593    pub read_only: Option<bool>,
594    /// Whether the tool may interact with an open world.
595    #[serde(
596        rename = "openWorldHint",
597        default,
598        skip_serializing_if = "Option::is_none"
599    )]
600    pub open_world_hint: Option<bool>,
601}
602
603// ============================================================================
604// MCP Apps metadata, lifecycle, and result projection
605// ============================================================================
606
607/// Nested `_meta` member reserved by the MCP Apps protocol.
608pub const MCP_APPS_UI_METADATA_KEY: &str = "ui";
609/// Deprecated flat metadata member that this final-only surface rejects.
610pub const MCP_APPS_DEPRECATED_RESOURCE_URI_METADATA_KEY: &str = "ui/resourceUri";
611/// Maximum members in a closed nested MCP Apps tool `ui` metadata object.
612pub const MAX_MCP_APPS_UI_METADATA_MEMBERS: usize = 2;
613/// Maximum audience entries retained by one Apps tool visibility declaration.
614pub const MAX_MCP_APPS_TOOL_VISIBILITY_ENTRIES: usize = 128;
615/// Maximum origins retained by one Apps CSP directive.
616pub const MAX_MCP_APPS_CSP_DOMAINS_PER_DIRECTIVE: usize = 128;
617/// Maximum UTF-8 bytes retained for one Apps CSP origin or host-selected domain.
618pub const MAX_MCP_APPS_CSP_DOMAIN_BYTES: usize = 2_048;
619
620/// A tool audience declared in nested MCP Apps metadata.
621#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
622#[serde(rename_all = "lowercase")]
623pub enum McpAppsToolVisibility {
624    /// The model can discover and invoke the tool.
625    Model,
626    /// The rendered App can invoke the tool through its later bridge runtime.
627    App,
628}
629
630/// A Host-selected way to present an App View.
631#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
632#[serde(rename_all = "lowercase")]
633pub enum McpAppsDisplayMode {
634    /// The View appears in normal document flow.
635    Inline,
636    /// The View occupies the host's full display surface.
637    Fullscreen,
638    /// The View is presented picture-in-picture.
639    Pip,
640}
641
642/// Closed Apps metadata attached to a final `Tool` under `_meta.ui`.
643///
644/// The resource URI is intentionally typed as an exact authority-form
645/// `ui://` URI. Security configuration belongs to resource metadata and is
646/// intentionally not part of this non-security protocol slice.
647#[derive(Clone, Debug, Eq, PartialEq)]
648pub struct McpAppsToolMetadata {
649    /// UI resource rendered when this tool is invoked, when declared.
650    pub resource_uri: Option<AbsoluteUri>,
651    /// Optional explicit audiences; absence retains the Apps default of both.
652    pub visibility: Option<Vec<McpAppsToolVisibility>>,
653}
654
655impl McpAppsToolMetadata {
656    /// Creates validated closed Apps tool metadata.
657    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    /// Returns the effective visibility without changing absent versus present
680    /// wire state.
681    #[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    /// Produces a standalone final `_meta` object containing this exact nested
689    /// Apps member.
690    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    /// Merges this typed `ui` member into existing final open metadata.
698    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/// Bounded CSP origins declared by an Apps resource.
751///
752/// These declarations remain requests to the host. They do not authorize a
753/// network connection, nested frame, or base URI without host-side policy.
754#[derive(Clone, Debug, Default, Eq, PartialEq)]
755pub struct McpAppsResourceCsp {
756    /// Origins for network requests (`connect-src`).
757    pub connect_domains: Option<Vec<String>>,
758    /// Origins for static resources (`img-src`, `script-src`, and related directives).
759    pub resource_domains: Option<Vec<String>>,
760    /// Origins allowed for nested frames (`frame-src`).
761    pub frame_domains: Option<Vec<String>>,
762    /// Origins allowed as document base URIs (`base-uri`).
763    pub base_uri_domains: Option<Vec<String>>,
764}
765
766impl McpAppsResourceCsp {
767    /// Creates a bounded CSP declaration without granting any host authority.
768    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/// An empty-object Apps sandbox permission marker.
854#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
855#[serde(deny_unknown_fields)]
856pub struct McpAppsResourcePermission {}
857
858/// Optional sandbox permissions requested by an Apps resource.
859///
860/// Presence requests a host permission; absence does not. A host may further
861/// restrict or reject every requested permission.
862#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
863#[serde(rename_all = "camelCase", deny_unknown_fields)]
864pub struct McpAppsResourcePermissions {
865    /// Camera permission marker.
866    #[serde(default, skip_serializing_if = "Option::is_none")]
867    pub camera: Option<McpAppsResourcePermission>,
868    /// Microphone permission marker.
869    #[serde(default, skip_serializing_if = "Option::is_none")]
870    pub microphone: Option<McpAppsResourcePermission>,
871    /// Geolocation permission marker.
872    #[serde(default, skip_serializing_if = "Option::is_none")]
873    pub geolocation: Option<McpAppsResourcePermission>,
874    /// Clipboard-write permission marker.
875    #[serde(default, skip_serializing_if = "Option::is_none")]
876    pub clipboard_write: Option<McpAppsResourcePermission>,
877}
878
879/// Closed Apps rendering metadata attached to a `Resource` under `_meta.ui`.
880#[derive(Clone, Debug, Default, Eq, PartialEq)]
881pub struct McpAppsResourceMetadata {
882    /// Optional CSP declarations for the rendered view.
883    pub csp: Option<McpAppsResourceCsp>,
884    /// Optional sandbox permission requests.
885    pub permissions: Option<McpAppsResourcePermissions>,
886    /// Optional host-defined dedicated view domain.
887    pub domain: Option<String>,
888    /// Whether the View prefers a visible host-provided border and background.
889    pub prefers_border: Option<bool>,
890}
891
892impl McpAppsResourceMetadata {
893    /// Creates closed non-security resource presentation metadata.
894    #[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    /// Creates bounded resource rendering metadata with all currently stable
905    /// Apps fields. The domain is retained as host-defined opaque data.
906    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    /// Produces a standalone final `_meta` object containing this exact nested
927    /// Apps member.
928    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    /// Merges this typed `ui` member into existing final open metadata.
936    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/// A validated association between a tool's nested Apps metadata and an HTML
995/// UI resource in the final catalog.
996#[derive(Clone, Debug, Eq, PartialEq)]
997pub struct McpAppsResourceBinding {
998    /// Exact authority-form `ui://` resource URI selected by the tool.
999    pub resource_uri: AbsoluteUri,
1000    /// The tool's effective Apps visibility.
1001    pub visibility: Vec<McpAppsToolVisibility>,
1002}
1003
1004impl McpAppsResourceBinding {
1005    /// Derives a binding only when the tool declares a nested Apps resource URI.
1006    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    /// Verifies that a catalog resource is the exact HTML resource selected by
1021    /// this binding. Resource presentation metadata remains optional.
1022    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/// A View lifecycle phase. This pure protocol state machine does not send or
1040/// receive any `ui/*` RPC message; a future bridge runtime owns that work.
1041#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1042pub enum McpAppsViewLifecycle {
1043    /// No initialization attempt has been admitted.
1044    New,
1045    /// One initialization request is reserved and awaiting its response.
1046    InitializeInFlight,
1047    /// Initialization succeeded and the initialized notification is due.
1048    AwaitingInitialized,
1049    /// The View may receive ordinary application traffic.
1050    Active,
1051    /// Terminal teardown has begun.
1052    Closing,
1053    /// Terminal teardown is complete.
1054    Closed,
1055}
1056
1057impl Default for McpAppsViewLifecycle {
1058    fn default() -> Self {
1059        Self::New
1060    }
1061}
1062
1063impl McpAppsViewLifecycle {
1064    /// Reserves the single legal initialization attempt.
1065    pub fn begin_initialize(&mut self) -> Result<(), McpAppsLifecycleError> {
1066        self.transition(Self::New, Self::InitializeInFlight, "initialize")
1067    }
1068
1069    /// Commits a successful initialization response.
1070    pub fn initialization_succeeded(&mut self) -> Result<(), McpAppsLifecycleError> {
1071        self.transition(
1072            Self::InitializeInFlight,
1073            Self::AwaitingInitialized,
1074            "initialize response",
1075        )
1076    }
1077
1078    /// Atomically rolls a failed initialization back before exposure.
1079    pub fn initialization_failed(&mut self) -> Result<(), McpAppsLifecycleError> {
1080        self.transition(Self::InitializeInFlight, Self::New, "initialize rollback")
1081    }
1082
1083    /// Admits the sole initialized notification and enables application traffic.
1084    pub fn admit_initialized(&mut self) -> Result<(), McpAppsLifecycleError> {
1085        self.transition(
1086            Self::AwaitingInitialized,
1087            Self::Active,
1088            "initialized notification",
1089        )
1090    }
1091
1092    /// Begins one terminal teardown from every non-terminal phase.
1093    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    /// Completes one terminal teardown.
1107    pub fn finish_closing(&mut self) -> Result<(), McpAppsLifecycleError> {
1108        self.transition(Self::Closing, Self::Closed, "finish closing")
1109    }
1110
1111    /// Returns whether ordinary Host/View application traffic is legal.
1112    #[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/// One validated Apps-side projection of a complete final `tools/call` result.
1135///
1136/// This projection intentionally accepts only the complete final result
1137/// branch. Tasks and MRTR input-required branches remain outside the Apps
1138/// bridge until an explicit composition contract is implemented.
1139#[derive(Clone, Debug, PartialEq)]
1140pub struct McpAppsToolResult {
1141    /// Complete final content projected without normalization.
1142    pub content: Vec<ContentBlock>,
1143    /// Tool-level error indicator retained exactly.
1144    pub is_error: bool,
1145    /// Optional structured output, including an explicitly present JSON null.
1146    pub structured_content: Option<serde_json::Value>,
1147}
1148
1149impl McpAppsToolResult {
1150    /// Constructs a bounded Apps result projection.
1151    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    /// Projects one fully validated final `tools/call` payload exactly once.
1173    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
1240/// Projects the complete `tools/call` branch while rejecting deferred Tasks and
1241/// MRTR branches before any Apps result is produced.
1242pub 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/// Metadata validation failures specific to the closed Apps `ui` member.
1261#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1262pub enum McpAppsMetadataError {
1263    /// The old flat `ui/resourceUri` key is forbidden on the final surface.
1264    DeprecatedFlatResourceUri,
1265    /// The nested `ui` member was not an object.
1266    UiMetadataMustBeObject,
1267    /// A tool `ui` object did not satisfy its closed schema.
1268    InvalidToolMetadata,
1269    /// A resource `ui` object did not satisfy its closed schema.
1270    InvalidResourceMetadata,
1271    /// A resource binding URI must start with the exact `ui://` prefix.
1272    ResourceUriMustUseUiPrefix,
1273    /// A tool visibility declaration carried more than its bounded number of entries.
1274    TooManyToolVisibilityEntries,
1275    /// One CSP directive carried more than its bounded number of origins.
1276    TooManyCspDomains,
1277    /// One CSP origin was empty or exceeded its bounded byte allowance.
1278    InvalidCspDomain,
1279    /// The host-defined Apps domain was empty or exceeded its bounded allowance.
1280    InvalidDomain,
1281    /// A merge would overwrite a pre-existing nested `ui` member.
1282    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/// A catalog resource did not satisfy one validated Apps binding.
1324#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1325pub enum McpAppsResourceBindingError {
1326    /// The candidate resource URI differs from the tool's exact binding URI.
1327    UriMismatch,
1328    /// The candidate resource is not an Apps HTML resource.
1329    HtmlMimeTypeRequired,
1330    /// Resource metadata was not a valid closed Apps metadata object.
1331    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/// Illegal transition in the pure Apps View lifecycle state machine.
1351#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1352pub enum McpAppsLifecycleError {
1353    /// The requested operation is not legal from the retained lifecycle phase.
1354    InvalidTransition {
1355        /// Current lifecycle phase.
1356        from: McpAppsViewLifecycle,
1357        /// Name of the rejected operation.
1358        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/// Apps result projection failures.
1378#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1379pub enum McpAppsResultProjectionError {
1380    /// The projected result exceeds the final result bounds.
1381    ResultTooLarge,
1382    /// A Tasks-backed `tools/call` result has no Apps result composition.
1383    TasksUnsupported,
1384    /// An MRTR `input_required` tools/call result has no Apps result composition.
1385    MrtrUnsupported,
1386    /// Only the final `tools/call` result family can be projected.
1387    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/// Exact final `Tool` model.
1523#[derive(Debug, Clone, Serialize, Deserialize)]
1524#[serde(deny_unknown_fields)]
1525pub struct FinalTool {
1526    /// Programmatic component identifier.
1527    pub name: String,
1528    /// Optional human-facing display title.
1529    #[serde(default, skip_serializing_if = "Option::is_none")]
1530    pub title: Option<String>,
1531    /// Optional human-readable description.
1532    #[serde(default, skip_serializing_if = "Option::is_none")]
1533    pub description: Option<String>,
1534    /// Optional sized icon collection.
1535    #[serde(default, skip_serializing_if = "Option::is_none")]
1536    pub icons: Option<Vec<RawIcon>>,
1537    /// Required JSON Schema object for tool input.
1538    #[serde(
1539        rename = "inputSchema",
1540        deserialize_with = "deserialize_final_tool_input_schema"
1541    )]
1542    pub input_schema: serde_json::Value,
1543    /// Optional JSON Schema object for structured tool output.
1544    #[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    /// Optional behavioral and display hints.
1552    #[serde(default, skip_serializing_if = "Option::is_none")]
1553    pub annotations: Option<FinalToolAnnotations>,
1554    /// Optional final metadata.
1555    #[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    /// Reads and validates the tool's optional closed `_meta.ui` Apps member.
1567    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    /// Derives the optional exact Apps resource binding declared by this tool.
1574    pub fn mcp_apps_resource_binding(
1575        &self,
1576    ) -> Result<Option<McpAppsResourceBinding>, McpAppsMetadataError> {
1577        McpAppsResourceBinding::from_tool(self)
1578    }
1579}
1580
1581/// Exact final `Resource` model.
1582#[derive(Debug, Clone, Serialize, Deserialize)]
1583#[serde(deny_unknown_fields)]
1584pub struct FinalResource {
1585    /// Exact resource URI.
1586    pub uri: AbsoluteUri,
1587    /// Programmatic resource identifier.
1588    pub name: String,
1589    /// Optional human-facing display title.
1590    #[serde(default, skip_serializing_if = "Option::is_none")]
1591    pub title: Option<String>,
1592    /// Optional human-readable description.
1593    #[serde(default, skip_serializing_if = "Option::is_none")]
1594    pub description: Option<String>,
1595    /// Optional sized icon collection.
1596    #[serde(default, skip_serializing_if = "Option::is_none")]
1597    pub icons: Option<Vec<RawIcon>>,
1598    /// Optional resource MIME type.
1599    #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")]
1600    pub mime_type: Option<String>,
1601    /// Optional raw content size.
1602    #[serde(default, skip_serializing_if = "Option::is_none")]
1603    pub size: Option<JsonInteger>,
1604    /// Optional client-facing annotations.
1605    #[serde(default, skip_serializing_if = "Option::is_none")]
1606    pub annotations: Option<Annotations>,
1607    /// Optional final metadata.
1608    #[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    /// Reads and validates the resource's optional closed `_meta.ui` Apps
1620    /// presentation member.
1621    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/// Exact final `ResourceTemplate` model.
1631#[derive(Debug, Clone, Serialize, Deserialize)]
1632#[serde(deny_unknown_fields)]
1633pub struct FinalResourceTemplate {
1634    /// RFC 6570 resource URI template.
1635    #[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    /// Programmatic template identifier.
1642    pub name: String,
1643    /// Optional human-facing display title.
1644    #[serde(default, skip_serializing_if = "Option::is_none")]
1645    pub title: Option<String>,
1646    /// Optional human-readable description.
1647    #[serde(default, skip_serializing_if = "Option::is_none")]
1648    pub description: Option<String>,
1649    /// Optional sized icon collection.
1650    #[serde(default, skip_serializing_if = "Option::is_none")]
1651    pub icons: Option<Vec<RawIcon>>,
1652    /// Optional MIME type for resources matched by the template.
1653    #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")]
1654    pub mime_type: Option<String>,
1655    /// Optional client-facing annotations.
1656    #[serde(default, skip_serializing_if = "Option::is_none")]
1657    pub annotations: Option<Annotations>,
1658    /// Optional final metadata.
1659    #[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/// Exact final prompt-argument model.
1685#[derive(Debug, Clone, Serialize, Deserialize)]
1686#[serde(deny_unknown_fields)]
1687pub struct FinalPromptArgument {
1688    /// Programmatic argument identifier.
1689    pub name: String,
1690    /// Optional human-facing display title.
1691    #[serde(default, skip_serializing_if = "Option::is_none")]
1692    pub title: Option<String>,
1693    /// Optional human-readable description.
1694    #[serde(default, skip_serializing_if = "Option::is_none")]
1695    pub description: Option<String>,
1696    /// Whether an argument is required; absence remains distinct from false.
1697    #[serde(default, skip_serializing_if = "Option::is_none")]
1698    pub required: Option<bool>,
1699}
1700
1701/// Exact final `Prompt` model.
1702#[derive(Debug, Clone, Serialize, Deserialize)]
1703#[serde(deny_unknown_fields)]
1704pub struct FinalPrompt {
1705    /// Programmatic prompt identifier.
1706    pub name: String,
1707    /// Optional human-facing display title.
1708    #[serde(default, skip_serializing_if = "Option::is_none")]
1709    pub title: Option<String>,
1710    /// Optional human-readable description.
1711    #[serde(default, skip_serializing_if = "Option::is_none")]
1712    pub description: Option<String>,
1713    /// Optional sized icon collection.
1714    #[serde(default, skip_serializing_if = "Option::is_none")]
1715    pub icons: Option<Vec<RawIcon>>,
1716    /// Optional prompt arguments.
1717    #[serde(default, skip_serializing_if = "Option::is_none")]
1718    pub arguments: Option<Vec<FinalPromptArgument>>,
1719    /// Optional final metadata.
1720    #[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
1757/// Legacy 2024 metadata, retained exactly as an open JSON object.
1758///
1759/// The 2024-11-05 schema permits arbitrary JSON members in result `_meta`
1760/// objects. This intentionally remains distinct from the final-era
1761/// [`OpenMetadata`] policy.
1762pub type LegacyMetadata = BTreeMap<String, serde_json::Value>;
1763
1764/// Exact 2024-11-05 content blocks carried in legacy results.
1765///
1766/// The legacy `Content` surface remains available to 2026 adapters. This
1767/// separate wire model preserves the annotations, `_meta`, and open members
1768/// that the checked-in 2024 schema permits, while deliberately excluding audio
1769/// from the legacy content union.
1770#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1771#[serde(tag = "type", rename_all = "lowercase")]
1772pub enum LegacyContent {
1773    /// Text content.
1774    Text {
1775        /// The text content.
1776        text: String,
1777        /// Optional presentation annotations.
1778        #[serde(default, skip_serializing_if = "Option::is_none")]
1779        annotations: Option<Annotations>,
1780        /// Other schema-allowed content members, including an open `_meta` value.
1781        #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
1782        additional: BTreeMap<String, serde_json::Value>,
1783    },
1784    /// Image content.
1785    Image {
1786        /// Base64-encoded image data.
1787        data: String,
1788        /// MIME type (e.g., `image/png`).
1789        #[serde(rename = "mimeType")]
1790        mime_type: String,
1791        /// Optional presentation annotations.
1792        #[serde(default, skip_serializing_if = "Option::is_none")]
1793        annotations: Option<Annotations>,
1794        /// Other schema-allowed content members, including an open `_meta` value.
1795        #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
1796        additional: BTreeMap<String, serde_json::Value>,
1797    },
1798    /// An embedded resource.
1799    Resource {
1800        /// Resource contents.
1801        resource: LegacyResourceContent,
1802        /// Optional presentation annotations.
1803        #[serde(default, skip_serializing_if = "Option::is_none")]
1804        annotations: Option<Annotations>,
1805        /// Other schema-allowed content members, including an open `_meta` value.
1806        #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
1807        additional: BTreeMap<String, serde_json::Value>,
1808    },
1809}
1810
1811/// Exact 2024-11-05 resource contents carried by legacy results.
1812#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1813#[serde(untagged)]
1814pub enum LegacyResourceContent {
1815    /// Text resource contents.
1816    Text {
1817        /// Resource URI.
1818        uri: String,
1819        /// Resource text.
1820        text: String,
1821        /// Optional MIME type.
1822        #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")]
1823        mime_type: Option<String>,
1824        /// Other schema-allowed resource members, including an open `_meta` value.
1825        #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
1826        additional: BTreeMap<String, serde_json::Value>,
1827    },
1828    /// Binary resource contents.
1829    Blob {
1830        /// Resource URI.
1831        uri: String,
1832        /// Base64-encoded resource data.
1833        blob: String,
1834        /// Optional MIME type.
1835        #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")]
1836        mime_type: Option<String>,
1837        /// Other schema-allowed resource members, including an open `_meta` value.
1838        #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
1839        additional: BTreeMap<String, serde_json::Value>,
1840    },
1841}
1842
1843/// Content types used by the broader server and 2026 adapter surfaces.
1844#[derive(Debug, Clone, Serialize, Deserialize)]
1845#[serde(tag = "type", rename_all = "lowercase", deny_unknown_fields)]
1846pub enum Content {
1847    /// Text content.
1848    Text {
1849        /// The text content.
1850        text: String,
1851    },
1852    /// Image content.
1853    Image {
1854        /// Base64-encoded image data.
1855        data: String,
1856        /// MIME type (e.g., "image/png").
1857        #[serde(rename = "mimeType")]
1858        mime_type: String,
1859    },
1860    /// Audio content.
1861    Audio {
1862        /// Base64-encoded audio data.
1863        data: String,
1864        /// MIME type (e.g., "audio/wav").
1865        #[serde(rename = "mimeType")]
1866        mime_type: String,
1867    },
1868    /// Resource content.
1869    Resource {
1870        /// The resource being referenced.
1871        resource: ResourceContent,
1872    },
1873}
1874
1875impl Content {
1876    /// Creates text content.
1877    #[must_use]
1878    pub fn text(text: impl Into<String>) -> Self {
1879        Self::Text { text: text.into() }
1880    }
1881
1882    /// Creates image content from base64-encoded data.
1883    #[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    /// Creates image content from raw bytes (base64-encodes internally).
1892    #[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    /// Creates audio content from base64-encoded data.
1899    #[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    /// Creates audio content from raw bytes (base64-encodes internally).
1908    #[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    /// Creates an embedded resource content with text payload.
1915    #[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    /// Creates an embedded resource content with base64 blob payload.
1932    #[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    /// Creates an embedded resource content with raw bytes payload (base64-encodes internally).
1949    #[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/// Resource content in a message.
1961#[derive(Debug, Clone, Serialize, Deserialize)]
1962#[serde(deny_unknown_fields)]
1963pub struct ResourceContent {
1964    /// Resource URI.
1965    pub uri: String,
1966    /// MIME type.
1967    #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
1968    pub mime_type: Option<String>,
1969    /// Text content (if text).
1970    #[serde(skip_serializing_if = "Option::is_none")]
1971    pub text: Option<String>,
1972    /// Binary content (if blob, base64).
1973    #[serde(skip_serializing_if = "Option::is_none")]
1974    pub blob: Option<String>,
1975}
1976
1977/// Role in prompt messages.
1978#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1979#[serde(rename_all = "lowercase")]
1980pub enum Role {
1981    /// User role.
1982    User,
1983    /// Assistant role.
1984    Assistant,
1985}
1986
1987/// Exact 2024-11-05 prompt messages carried in legacy results.
1988#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1989pub struct LegacyPromptMessage {
1990    /// Message role.
1991    pub role: Role,
1992    /// Exact legacy message content.
1993    pub content: LegacyContent,
1994    /// Other schema-allowed message members, including an open `_meta` value.
1995    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
1996    pub additional: BTreeMap<String, serde_json::Value>,
1997}
1998
1999/// A message in a prompt.
2000#[derive(Debug, Clone, Serialize, Deserialize)]
2001pub struct PromptMessage {
2002    /// Message role.
2003    pub role: Role,
2004    /// Message content.
2005    pub content: Content,
2006}
2007
2008// ============================================================================
2009// Background Tasks (Docket/SEP-1686)
2010// ============================================================================
2011
2012/// Task identifier.
2013///
2014/// Unique identifier for background tasks.
2015#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2016pub struct TaskId(pub String);
2017
2018impl TaskId {
2019    /// Creates a new random task ID.
2020    #[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    /// Creates a task ID from a string.
2031    #[must_use]
2032    pub fn from_string(s: impl Into<String>) -> Self {
2033        Self(s.into())
2034    }
2035
2036    /// Returns the task ID as a string.
2037    #[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/// Status of a background task.
2068#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2069#[serde(rename_all = "lowercase")]
2070pub enum TaskStatus {
2071    /// Task is queued but not yet started.
2072    Pending,
2073    /// Task is currently running.
2074    Running,
2075    /// Task completed successfully.
2076    Completed,
2077    /// Task failed with an error.
2078    Failed,
2079    /// Task was cancelled.
2080    Cancelled,
2081}
2082
2083impl TaskStatus {
2084    /// Returns true if the task is in a terminal state.
2085    #[must_use]
2086    pub fn is_terminal(&self) -> bool {
2087        matches!(
2088            self,
2089            TaskStatus::Completed | TaskStatus::Failed | TaskStatus::Cancelled
2090        )
2091    }
2092
2093    /// Returns true if the task is still active.
2094    #[must_use]
2095    pub fn is_active(&self) -> bool {
2096        matches!(self, TaskStatus::Pending | TaskStatus::Running)
2097    }
2098}
2099
2100/// Information about a background task.
2101#[derive(Debug, Clone, Serialize, Deserialize)]
2102pub struct TaskInfo {
2103    /// Unique task identifier.
2104    pub id: TaskId,
2105    /// Task type (identifies the kind of work).
2106    #[serde(rename = "taskType")]
2107    pub task_type: String,
2108    /// Current status.
2109    pub status: TaskStatus,
2110    /// Progress (0.0 to 1.0, if known).
2111    #[serde(skip_serializing_if = "Option::is_none")]
2112    pub progress: Option<f64>,
2113    /// Progress message.
2114    #[serde(skip_serializing_if = "Option::is_none")]
2115    pub message: Option<String>,
2116    /// Task creation timestamp (ISO 8601).
2117    #[serde(rename = "createdAt")]
2118    pub created_at: String,
2119    /// Task start timestamp (ISO 8601), if started.
2120    #[serde(rename = "startedAt", skip_serializing_if = "Option::is_none")]
2121    pub started_at: Option<String>,
2122    /// Task completion timestamp (ISO 8601), if completed.
2123    #[serde(rename = "completedAt", skip_serializing_if = "Option::is_none")]
2124    pub completed_at: Option<String>,
2125    /// Error message if failed.
2126    #[serde(skip_serializing_if = "Option::is_none")]
2127    pub error: Option<String>,
2128}
2129
2130/// Task result payload.
2131#[derive(Debug, Clone, Serialize, Deserialize)]
2132pub struct TaskResult {
2133    /// Task identifier.
2134    pub id: TaskId,
2135    /// Whether the task succeeded.
2136    pub success: bool,
2137    /// Result data (if successful).
2138    #[serde(skip_serializing_if = "Option::is_none")]
2139    pub data: Option<serde_json::Value>,
2140    /// Error message (if failed).
2141    #[serde(skip_serializing_if = "Option::is_none")]
2142    pub error: Option<String>,
2143}
2144
2145/// Task capability for server capabilities.
2146#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2147pub struct TasksCapability {
2148    /// Whether the server supports task list changes notifications.
2149    #[serde(
2150        default,
2151        rename = "listChanged",
2152        skip_serializing_if = "std::ops::Not::not"
2153    )]
2154    pub list_changed: bool,
2155}
2156
2157// ============================================================================
2158// Sampling Protocol Types
2159// ============================================================================
2160
2161/// Message content for sampling requests.
2162///
2163/// Can contain text, images, or tool-related content.
2164#[derive(Debug, Clone, Serialize, Deserialize)]
2165#[serde(tag = "type", rename_all = "lowercase")]
2166pub enum SamplingContent {
2167    /// Text content.
2168    Text {
2169        /// The text content.
2170        text: String,
2171    },
2172    /// Image content.
2173    Image {
2174        /// Base64-encoded image data.
2175        data: String,
2176        /// MIME type (e.g., "image/png").
2177        #[serde(rename = "mimeType")]
2178        mime_type: String,
2179    },
2180}
2181
2182/// A message in a sampling conversation.
2183#[derive(Debug, Clone, Serialize, Deserialize)]
2184pub struct SamplingMessage {
2185    /// Message role (user or assistant).
2186    pub role: Role,
2187    /// Message content.
2188    pub content: SamplingContent,
2189}
2190
2191impl SamplingMessage {
2192    /// Creates a new user message with text content.
2193    #[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    /// Creates a new assistant message with text content.
2202    #[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
2211/// Final sampling-specific content block.
2212pub type FinalSamplingMessageContentBlock = SamplingContentBlock;
2213
2214/// Exact final sampling-message content, preserving one block versus an array
2215/// of blocks.
2216#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2217#[serde(untagged)]
2218pub enum FinalSamplingMessageContent {
2219    /// One sampled content block.
2220    Block(FinalSamplingMessageContentBlock),
2221    /// Multiple sampled content blocks.
2222    Blocks(Vec<FinalSamplingMessageContentBlock>),
2223}
2224
2225/// A final sampling conversation message.
2226#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2227#[serde(deny_unknown_fields)]
2228pub struct FinalSamplingMessage {
2229    /// Sender role.
2230    pub role: Role,
2231    /// Exact final sampling content shape.
2232    pub content: FinalSamplingMessageContent,
2233    /// Optional metadata retained on the final wire.
2234    #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
2235    pub meta: Option<OpenMetadata>,
2236}
2237
2238/// Tool-selection mode for final sampling.
2239#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2240#[serde(rename_all = "lowercase")]
2241pub enum FinalToolChoiceMode {
2242    /// The model decides whether to use tools.
2243    Auto,
2244    /// The model must use at least one tool.
2245    Required,
2246    /// The model must not use tools.
2247    None,
2248}
2249
2250/// Tool-selection controls for final sampling.
2251#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2252#[serde(deny_unknown_fields)]
2253pub struct FinalToolChoice {
2254    /// Optional mode; absence keeps the wire default of `auto` distinct.
2255    #[serde(default, skip_serializing_if = "Option::is_none")]
2256    pub mode: Option<FinalToolChoiceMode>,
2257}
2258
2259/// Model preferences for sampling requests.
2260#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2261pub struct ModelPreferences {
2262    /// Hints for model selection (model names or patterns).
2263    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2264    pub hints: Vec<ModelHint>,
2265    /// Priority for cost (0.0 = lowest priority, 1.0 = highest).
2266    #[serde(rename = "costPriority", skip_serializing_if = "Option::is_none")]
2267    pub cost_priority: Option<f64>,
2268    /// Priority for speed (0.0 = lowest priority, 1.0 = highest).
2269    #[serde(rename = "speedPriority", skip_serializing_if = "Option::is_none")]
2270    pub speed_priority: Option<f64>,
2271    /// Priority for intelligence (0.0 = lowest priority, 1.0 = highest).
2272    #[serde(
2273        rename = "intelligencePriority",
2274        skip_serializing_if = "Option::is_none"
2275    )]
2276    pub intelligence_priority: Option<f64>,
2277}
2278
2279/// A hint for model selection.
2280#[derive(Debug, Clone, Serialize, Deserialize)]
2281pub struct ModelHint {
2282    /// Model name or pattern.
2283    #[serde(skip_serializing_if = "Option::is_none")]
2284    pub name: Option<String>,
2285}
2286
2287/// Stop reason for sampling responses.
2288#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
2289#[serde(rename_all = "camelCase")]
2290pub enum StopReason {
2291    /// End of natural turn.
2292    #[default]
2293    EndTurn,
2294    /// Hit stop sequence.
2295    StopSequence,
2296    /// Hit max tokens limit.
2297    MaxTokens,
2298}
2299
2300// ============================================================================
2301// Tests
2302// ============================================================================
2303
2304#[cfg(test)]
2305mod tests {
2306    use super::*;
2307    use serde_json::json;
2308
2309    // ========================================================================
2310    // ServerCapabilities Tests
2311    // ========================================================================
2312
2313    #[test]
2314    fn server_capabilities_default_serialization() {
2315        let caps = ServerCapabilities::default();
2316        let value = serde_json::to_value(&caps).expect("serialize");
2317        // All None fields should be omitted
2318        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    // ========================================================================
2384    // ToolsCapability Tests
2385    // ========================================================================
2386
2387    #[test]
2388    fn tools_capability_default_omits_false() {
2389        let cap = ToolsCapability::default();
2390        let value = serde_json::to_value(&cap).expect("serialize");
2391        // list_changed defaults to false and should be omitted
2392        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    // ========================================================================
2403    // ResourcesCapability Tests
2404    // ========================================================================
2405
2406    #[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    // ========================================================================
2426    // ClientCapabilities Tests
2427    // ========================================================================
2428
2429    #[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    // ========================================================================
2467    // ElicitationCapability Tests
2468    // ========================================================================
2469
2470    #[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    // ========================================================================
2495    // ServerInfo / ClientInfo Tests
2496    // ========================================================================
2497
2498    #[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    // ========================================================================
2521    // Icon Tests
2522    // ========================================================================
2523
2524    #[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    // ========================================================================
2615    // Content Tests
2616    // ========================================================================
2617
2618    #[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    // ========================================================================
2724    // ResourceContent Tests
2725    // ========================================================================
2726
2727    #[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    // ========================================================================
2772    // Role Tests
2773    // ========================================================================
2774
2775    #[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    // ========================================================================
2790    // PromptMessage Tests
2791    // ========================================================================
2792
2793    #[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    // ========================================================================
2820    // PromptArgument Tests
2821    // ========================================================================
2822
2823    #[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        // required=false should be omitted
2847        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    // ========================================================================
2860    // Tool Definition Tests
2861    // ========================================================================
2862
2863    #[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    // ========================================================================
2935    // Resource Definition Tests
2936    // ========================================================================
2937
2938    #[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    // ========================================================================
2973    // ResourceTemplate Tests
2974    // ========================================================================
2975
2976    #[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    // ========================================================================
3026    // Prompt Definition Tests
3027    // ========================================================================
3028
3029    #[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        // required=false should be omitted
3063        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    // ========================================================================
3081    // TaskId Tests
3082    // ========================================================================
3083
3084    #[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    // ========================================================================
3131    // TaskStatus Tests
3132    // ========================================================================
3133
3134    #[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    // ========================================================================
3198    // TaskInfo Tests
3199    // ========================================================================
3200
3201    #[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    // ========================================================================
3242    // TaskResult Tests
3243    // ========================================================================
3244
3245    #[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    // ========================================================================
3275    // SamplingContent Tests
3276    // ========================================================================
3277
3278    #[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    // ========================================================================
3301    // SamplingMessage Tests
3302    // ========================================================================
3303
3304    #[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    // ========================================================================
3322    // ModelPreferences Tests
3323    // ========================================================================
3324
3325    #[test]
3326    fn model_preferences_default() {
3327        let prefs = ModelPreferences::default();
3328        let value = serde_json::to_value(&prefs).expect("serialize");
3329        // All optional fields should be omitted
3330        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    // ========================================================================
3354    // StopReason Tests
3355    // ========================================================================
3356
3357    #[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    // ========================================================================
3395    // PROTOCOL_VERSION Test
3396    // ========================================================================
3397
3398    #[test]
3399    fn protocol_version_value() {
3400        assert_eq!(PROTOCOL_VERSION, "2024-11-05");
3401    }
3402
3403    // ========================================================================
3404    // ToolAnnotations Tests
3405    // ========================================================================
3406
3407    #[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    // ========================================================================
3458    // Icon Tests
3459    // ========================================================================
3460
3461    #[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    // ========================================================================
3502    // Content Binary Factory Tests
3503    // ========================================================================
3504
3505    #[test]
3506    fn content_image_bytes_encodes_base64() {
3507        let bytes: &[u8] = &[0x89, 0x50, 0x4E, 0x47]; // PNG header
3508        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                // Verify it's valid base64 that decodes back
3513                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]; // RIFF header
3539        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    // =========================================================================
3589    // Additional coverage tests (bd-1cd5)
3590    // =========================================================================
3591
3592    #[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}