onshape_mcp_resources/
lib.rs1use rmcp::model::{
22 AnnotateAble, Annotated, RawResource, ReadResourceResult, ResourceContents, Role,
23};
24
25pub struct ResourceEntry {
31 pub group: &'static str,
33 pub name: &'static str,
35 pub title: &'static str,
37 pub description: &'static str,
39 pub uri: &'static str,
41 pub content: &'static str,
43}
44
45#[allow(clippy::needless_raw_string_hashes)]
48mod generated {
49 use super::ResourceEntry;
50 include!(concat!(env!("OUT_DIR"), "/resources_generated.rs"));
51}
52pub use generated::RESOURCES;
53
54pub enum ResourceResult {
65 Immediate(Result<ReadResourceResult, ResourceError>),
67}
68
69#[derive(Debug)]
71pub enum ResourceError {
72 NotFound(String),
74}
75
76#[must_use]
86pub fn list_resources() -> Vec<Annotated<RawResource>> {
87 RESOURCES
88 .iter()
89 .map(|entry| {
90 RawResource::new(entry.uri, entry.name)
91 .with_title(entry.title)
92 .with_description(entry.description)
93 .with_mime_type("text/markdown")
94 .with_size(u32::try_from(entry.content.len()).unwrap_or(u32::MAX))
95 .no_annotation()
96 .with_audience(vec![Role::Assistant])
97 .with_priority(0.8)
98 })
99 .collect()
100}
101
102#[must_use]
107pub fn read_resource(uri: &str) -> ResourceResult {
108 let result = RESOURCES
109 .iter()
110 .find(|entry| entry.uri == uri)
111 .map(|entry| {
112 ReadResourceResult::new(vec![
113 ResourceContents::text(entry.content, entry.uri).with_mime_type("text/markdown"),
114 ])
115 })
116 .ok_or_else(|| ResourceError::NotFound(uri.into()));
117
118 ResourceResult::Immediate(result)
119}
120
121#[cfg(test)]
126#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn generated_resources_are_not_empty() {
132 assert!(
133 !RESOURCES.is_empty(),
134 "build.rs should generate at least one resource"
135 );
136 }
137
138 #[test]
139 fn all_entries_have_required_fields() {
140 for entry in RESOURCES {
141 assert!(!entry.group.is_empty(), "group should not be empty");
142 assert!(!entry.name.is_empty(), "name should not be empty");
143 assert!(!entry.title.is_empty(), "title should not be empty");
144 assert!(
145 !entry.description.is_empty(),
146 "description should not be empty for {}",
147 entry.name
148 );
149 assert!(!entry.uri.is_empty(), "uri should not be empty");
150 assert!(
151 !entry.content.is_empty(),
152 "content should not be empty for {}",
153 entry.name
154 );
155 }
156 }
157
158 #[test]
159 fn uri_format_matches_group_colon_name() {
160 for entry in RESOURCES {
161 let expected = format!("{}:{}", entry.group, entry.name);
162 assert_eq!(
163 entry.uri, expected,
164 "URI should be group:name for {}",
165 entry.name
166 );
167 }
168 }
169
170 #[test]
171 fn list_resources_returns_all_entries() {
172 let resources = list_resources();
173 assert_eq!(resources.len(), RESOURCES.len());
174 }
175
176 #[test]
177 fn list_resources_sets_annotations() {
178 let resources = list_resources();
179 for resource in &resources {
180 let annotations = resource
181 .annotations
182 .as_ref()
183 .expect("annotations should be set");
184 assert_eq!(
185 annotations.audience.as_deref(),
186 Some(&[Role::Assistant][..])
187 );
188 assert_eq!(annotations.priority, Some(0.8));
189 }
190 }
191
192 #[test]
193 fn list_resources_sets_mime_type() {
194 let resources = list_resources();
195 for resource in &resources {
196 assert_eq!(resource.raw.mime_type.as_deref(), Some("text/markdown"));
197 }
198 }
199
200 #[test]
201 fn read_resource_returns_content_for_valid_uri() {
202 for entry in RESOURCES {
203 let result = read_resource(entry.uri);
204 let ResourceResult::Immediate(Ok(read_result)) = result else {
205 panic!("expected Immediate(Ok(...)) for URI {}", entry.uri);
206 };
207 assert_eq!(read_result.contents.len(), 1);
208 match &read_result.contents[0] {
209 ResourceContents::TextResourceContents { uri, text, .. } => {
210 assert_eq!(uri, entry.uri);
211 assert_eq!(text, entry.content);
212 }
213 ResourceContents::BlobResourceContents { .. } => {
214 panic!("expected text content, got blob");
215 }
216 }
217 }
218 }
219
220 #[test]
221 fn read_resource_returns_error_for_unknown_uri() {
222 let result = read_resource("nonexistent:nothing");
223 let ResourceResult::Immediate(Err(ResourceError::NotFound(uri))) = result else {
224 panic!("expected Immediate(Err(NotFound(...)))");
225 };
226 assert_eq!(uri, "nonexistent:nothing");
227 }
228
229 #[test]
230 fn insights_group_exists() {
231 let has_insights = RESOURCES.iter().any(|e| e.group == "insights");
232 assert!(has_insights, "should have at least one insights resource");
233 }
234
235 #[test]
236 fn known_insight_resources_present() {
237 let names: Vec<&str> = RESOURCES
238 .iter()
239 .filter(|e| e.group == "insights")
240 .map(|e| e.name)
241 .collect();
242 assert!(
243 names.contains(&"shaded-views"),
244 "should have shaded-views insight"
245 );
246 assert!(names.contains(&"sketch"), "should have sketch insight");
247 }
248}