Skip to main content

component_shape_mcp/
metadata.rs

1use super::*;
2
3/// Static icon metadata for an MCP tool definition.
4#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5pub struct McpToolIcon {
6    src: &'static str,
7    mime_type: Option<&'static str>,
8    sizes: &'static [&'static str],
9    theme: Option<McpIconTheme>,
10}
11
12impl McpToolIcon {
13    /// Create icon metadata from an icon resource URI or data URI.
14    pub const fn new(src: &'static str) -> Self {
15        Self {
16            src,
17            mime_type: None,
18            sizes: &[],
19            theme: None,
20        }
21    }
22
23    /// Override the icon MIME type.
24    pub const fn with_mime_type(mut self, mime_type: &'static str) -> Self {
25        self.mime_type = Some(mime_type);
26        self
27    }
28
29    /// Declare supported icon sizes such as `"48x48"` or `"any"`.
30    pub const fn with_sizes(mut self, sizes: &'static [&'static str]) -> Self {
31        self.sizes = sizes;
32        self
33    }
34
35    /// Declare the icon's intended theme.
36    pub const fn with_theme(mut self, theme: McpIconTheme) -> Self {
37        self.theme = Some(theme);
38        self
39    }
40
41    /// Returns the icon source URI.
42    pub const fn src(self) -> &'static str {
43        self.src
44    }
45
46    /// Returns the icon MIME type.
47    pub const fn mime_type(self) -> Option<&'static str> {
48        self.mime_type
49    }
50
51    /// Returns declared icon sizes.
52    pub const fn sizes(self) -> &'static [&'static str] {
53        self.sizes
54    }
55
56    /// Returns the icon theme.
57    pub const fn theme(self) -> Option<McpIconTheme> {
58        self.theme
59    }
60
61    fn into_definition_icon(self) -> McpIcon {
62        let mut icon = McpIcon::new(self.src);
63        if let Some(mime_type) = self.mime_type {
64            icon = icon.with_mime_type(mime_type);
65        }
66        if !self.sizes.is_empty() {
67            icon = icon.with_sizes(self.sizes.iter().map(|size| (*size).to_string()).collect());
68        }
69        if let Some(theme) = self.theme {
70            icon = icon.with_theme(theme);
71        }
72        icon
73    }
74
75    fn validate(self) -> Result<(), McpToolError> {
76        validate_required_metadata_text("icon src", self.src)?;
77        if let Some(mime_type) = self.mime_type {
78            validate_required_metadata_text("icon mime_type", mime_type)?;
79        }
80        for size in self.sizes {
81            validate_required_metadata_text("icon size", size)?;
82        }
83        Ok(())
84    }
85}
86
87/// Optional application-facing metadata for a generated MCP tool.
88#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
89pub struct McpToolMetadata {
90    name: Option<&'static str>,
91    title: Option<&'static str>,
92    description: Option<&'static str>,
93    read_only_hint: Option<bool>,
94    destructive_hint: Option<bool>,
95    idempotent_hint: Option<bool>,
96    open_world_hint: Option<bool>,
97    icons: &'static [McpToolIcon],
98}
99
100impl McpToolMetadata {
101    /// Create empty tool metadata.
102    pub const fn new() -> Self {
103        Self {
104            name: None,
105            title: None,
106            description: None,
107            read_only_hint: None,
108            destructive_hint: None,
109            idempotent_hint: None,
110            open_world_hint: None,
111            icons: &[],
112        }
113    }
114
115    /// Override the generated tool name.
116    pub const fn with_name(mut self, name: &'static str) -> Self {
117        self.name = Some(name);
118        self
119    }
120
121    /// Set the human-readable tool title.
122    pub const fn with_title(mut self, title: &'static str) -> Self {
123        self.title = Some(title);
124        self
125    }
126
127    /// Set the human-readable tool description.
128    pub const fn with_description(mut self, description: &'static str) -> Self {
129        self.description = Some(description);
130        self
131    }
132
133    /// Set the MCP read-only hint.
134    pub const fn with_read_only_hint(mut self, read_only: bool) -> Self {
135        self.read_only_hint = Some(read_only);
136        self
137    }
138
139    /// Set the MCP destructive hint.
140    pub const fn with_destructive_hint(mut self, destructive: bool) -> Self {
141        self.destructive_hint = Some(destructive);
142        self
143    }
144
145    /// Set the MCP idempotent hint.
146    pub const fn with_idempotent_hint(mut self, idempotent: bool) -> Self {
147        self.idempotent_hint = Some(idempotent);
148        self
149    }
150
151    /// Set the MCP open-world hint.
152    pub const fn with_open_world_hint(mut self, open_world: bool) -> Self {
153        self.open_world_hint = Some(open_world);
154        self
155    }
156
157    /// Set static icon metadata for the generated tool definition.
158    pub const fn with_icons(mut self, icons: &'static [McpToolIcon]) -> Self {
159        self.icons = icons;
160        self
161    }
162
163    /// Returns the explicit tool name override.
164    pub const fn name(self) -> Option<&'static str> {
165        self.name
166    }
167
168    /// Returns the human-readable tool title.
169    pub const fn title(self) -> Option<&'static str> {
170        self.title
171    }
172
173    /// Returns the human-readable tool description.
174    pub const fn description(self) -> Option<&'static str> {
175        self.description
176    }
177
178    /// Returns the MCP read-only hint.
179    pub const fn read_only_hint(self) -> Option<bool> {
180        self.read_only_hint
181    }
182
183    /// Returns the MCP destructive hint.
184    pub const fn destructive_hint(self) -> Option<bool> {
185        self.destructive_hint
186    }
187
188    /// Returns the MCP idempotent hint.
189    pub const fn idempotent_hint(self) -> Option<bool> {
190        self.idempotent_hint
191    }
192
193    /// Returns the MCP open-world hint.
194    pub const fn open_world_hint(self) -> Option<bool> {
195        self.open_world_hint
196    }
197
198    /// Returns static icon metadata.
199    pub const fn icons(self) -> &'static [McpToolIcon] {
200        self.icons
201    }
202
203    /// Converts metadata hints into MCP tool annotations.
204    pub fn tool_annotations(self) -> Option<McpToolAnnotations> {
205        if self.read_only_hint.is_none()
206            && self.destructive_hint.is_none()
207            && self.idempotent_hint.is_none()
208            && self.open_world_hint.is_none()
209        {
210            return None;
211        }
212
213        Some(McpToolAnnotations::from_raw(
214            self.title.map(str::to_string),
215            self.read_only_hint,
216            self.destructive_hint,
217            self.idempotent_hint,
218            self.open_world_hint,
219        ))
220    }
221
222    /// Converts static icon metadata into MCP icon definitions.
223    pub fn tool_icons(self) -> Option<Vec<McpIcon>> {
224        (!self.icons.is_empty()).then(|| {
225            self.icons
226                .iter()
227                .map(|icon| icon.into_definition_icon())
228                .collect()
229        })
230    }
231
232    /// Validate generated tool metadata.
233    ///
234    /// # Errors
235    ///
236    /// Returns [`McpToolError`] when hints conflict or text/icon metadata is
237    /// invalid.
238    pub fn validate(self) -> Result<(), McpToolError> {
239        validate_tool_annotation_hints(self.read_only_hint, self.destructive_hint)?;
240        if let Some(name) = self.name {
241            validate_tool_name(name)?;
242        }
243        if let Some(title) = self.title {
244            validate_tool_metadata_text("title", title)?;
245        }
246        if let Some(description) = self.description {
247            validate_tool_metadata_text("description", description)?;
248        }
249        for icon in self.icons {
250            icon.validate()?;
251        }
252        Ok(())
253    }
254}