1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize, Default)]
9#[serde(rename_all = "camelCase")]
10pub struct McpUiResourceCsp {
11 #[serde(skip_serializing_if = "Option::is_none")]
12 pub connect_domains: Option<Vec<String>>,
13 #[serde(skip_serializing_if = "Option::is_none")]
14 pub resource_domains: Option<Vec<String>>,
15 #[serde(skip_serializing_if = "Option::is_none")]
16 pub frame_domains: Option<Vec<String>>,
17 #[serde(skip_serializing_if = "Option::is_none")]
18 pub base_uri_domains: Option<Vec<String>>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
22pub struct PermissionGrant {}
23
24#[derive(Debug, Clone, Serialize, Deserialize, Default)]
25#[serde(rename_all = "camelCase")]
26pub struct McpUiPermissions {
27 #[serde(skip_serializing_if = "Option::is_none")]
28 pub camera: Option<PermissionGrant>,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub microphone: Option<PermissionGrant>,
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub geolocation: Option<PermissionGrant>,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub clipboard_write: Option<PermissionGrant>,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, Default)]
38pub struct McpAppsRenderOptions {
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub domain: Option<String>,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub prefers_border: Option<bool>,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub csp: Option<McpUiResourceCsp>,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub permissions: Option<McpUiPermissions>,
47}
48
49fn is_allowed_domain(domain: &str) -> bool {
50 domain.starts_with("https://")
51 || domain.starts_with("http://localhost")
52 || domain.starts_with("http://127.0.0.1")
53}
54
55fn validate_domain_list(domains: Option<&Vec<String>>, field: &str) -> Result<(), String> {
56 let Some(domains) = domains else {
57 return Ok(());
58 };
59 for domain in domains {
60 if !is_allowed_domain(domain) {
61 return Err(format!(
62 "Invalid mcp_apps option '{}': unsupported domain '{}'",
63 field, domain
64 ));
65 }
66 }
67 Ok(())
68}
69
70pub fn validate_mcp_apps_render_options(options: &McpAppsRenderOptions) -> Result<(), String> {
71 if let Some(domain) = options.domain.as_deref() {
72 if !is_allowed_domain(domain) {
73 return Err(format!(
74 "Invalid mcp_apps option 'domain': unsupported domain '{}'",
75 domain
76 ));
77 }
78 }
79 if let Some(csp) = &options.csp {
80 validate_domain_list(csp.connect_domains.as_ref(), "csp.connect_domains")?;
81 validate_domain_list(csp.resource_domains.as_ref(), "csp.resource_domains")?;
82 validate_domain_list(csp.frame_domains.as_ref(), "csp.frame_domains")?;
83 validate_domain_list(csp.base_uri_domains.as_ref(), "csp.base_uri_domains")?;
84 }
85 Ok(())
86}