Skip to main content

aster/aster_apps/
resource.rs

1use serde::{Deserialize, Serialize};
2use utoipa::ToSchema;
3
4/// Content Security Policy metadata for MCP Apps
5/// Specifies allowed domains for network connections and resource loading
6#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
7#[serde(rename_all = "camelCase")]
8pub struct CspMetadata {
9    /// Domains allowed for connect-src (fetch, XHR, WebSocket)
10    #[serde(skip_serializing_if = "Option::is_none")]
11    pub connect_domains: Option<Vec<String>>,
12    /// Domains allowed for resource loading (scripts, styles, images, fonts, media)
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub resource_domains: Option<Vec<String>>,
15}
16
17/// UI-specific metadata for MCP resources
18#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
19#[serde(rename_all = "camelCase")]
20pub struct UiMetadata {
21    /// Content Security Policy configuration
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub csp: Option<CspMetadata>,
24    /// Preferred domain for the app (used for CORS)
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub domain: Option<String>,
27    /// Whether the app prefers to have a border around it
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub prefers_border: Option<bool>,
30}
31
32/// Resource metadata containing UI configuration
33#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
34#[serde(rename_all = "camelCase")]
35pub struct ResourceMetadata {
36    /// UI-specific configuration
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub ui: Option<UiMetadata>,
39}
40
41/// MCP App Resource
42/// Represents a UI resource that can be rendered in an MCP App
43#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
44#[serde(rename_all = "camelCase")]
45pub struct McpAppResource {
46    /// URI of the resource (must use ui:// scheme)
47    pub uri: String,
48    /// Human-readable name of the resource
49    pub name: String,
50    /// Optional description of what this resource does
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub description: Option<String>,
53    /// MIME type (should be "text/html;profile=mcp-app" for MCP Apps)
54    pub mime_type: String,
55    /// Text content of the resource (HTML for MCP Apps)
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub text: Option<String>,
58    /// Base64-encoded binary content (alternative to text)
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub blob: Option<String>,
61    /// Resource metadata including UI configuration
62    #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
63    pub meta: Option<ResourceMetadata>,
64}
65
66impl McpAppResource {
67    pub fn new_html(uri: String, name: String, html: String) -> Self {
68        Self {
69            uri,
70            name,
71            description: None,
72            mime_type: "text/html;profile=mcp-app".to_string(),
73            text: Some(html),
74            blob: None,
75            meta: None,
76        }
77    }
78
79    pub fn new_html_with_csp(uri: String, name: String, html: String, csp: CspMetadata) -> Self {
80        Self {
81            uri,
82            name,
83            description: None,
84            mime_type: "text/html;profile=mcp-app".to_string(),
85            text: Some(html),
86            blob: None,
87            meta: Some(ResourceMetadata {
88                ui: Some(UiMetadata {
89                    csp: Some(csp),
90                    domain: None,
91                    prefers_border: None,
92                }),
93            }),
94        }
95    }
96
97    pub fn with_description(mut self, description: String) -> Self {
98        self.description = Some(description);
99        self
100    }
101
102    pub fn with_ui_metadata(mut self, ui_metadata: UiMetadata) -> Self {
103        if let Some(meta) = &mut self.meta {
104            meta.ui = Some(ui_metadata);
105        } else {
106            self.meta = Some(ResourceMetadata {
107                ui: Some(ui_metadata),
108            });
109        }
110        self
111    }
112}