mcpkit_server/capability/
resources.rs1use crate::context::Context;
7use crate::handler::ResourceHandler;
8use mcpkit_core::error::McpError;
9use mcpkit_core::types::resource::{Resource, ResourceContents, ResourceTemplate};
10use std::collections::HashMap;
11use std::future::Future;
12use std::pin::Pin;
13
14pub type BoxedResourceFn = Box<
16 dyn for<'a> Fn(
17 &'a str,
18 &'a Context<'a>,
19 )
20 -> Pin<Box<dyn Future<Output = Result<ResourceContents, McpError>> + Send + 'a>>
21 + Send
22 + Sync,
23>;
24
25pub struct RegisteredResource {
27 pub resource: Resource,
29 pub handler: BoxedResourceFn,
31}
32
33pub struct RegisteredTemplate {
35 pub template: ResourceTemplate,
37 pub handler: BoxedResourceFn,
39}
40
41pub struct ResourceService {
46 resources: HashMap<String, RegisteredResource>,
48 templates: HashMap<String, RegisteredTemplate>,
50}
51
52impl Default for ResourceService {
53 fn default() -> Self {
54 Self::new()
55 }
56}
57
58impl ResourceService {
59 #[must_use]
61 pub fn new() -> Self {
62 Self {
63 resources: HashMap::new(),
64 templates: HashMap::new(),
65 }
66 }
67
68 pub fn register<F, Fut>(&mut self, resource: Resource, handler: F)
70 where
71 F: Fn(&str, &Context<'_>) -> Fut + Send + Sync + 'static,
72 Fut: Future<Output = Result<ResourceContents, McpError>> + Send + 'static,
73 {
74 let uri = resource.uri.clone();
75 let boxed: BoxedResourceFn = Box::new(move |u, ctx| Box::pin(handler(u, ctx)));
76 self.resources.insert(
77 uri,
78 RegisteredResource {
79 resource,
80 handler: boxed,
81 },
82 );
83 }
84
85 pub fn register_template<F, Fut>(&mut self, template: ResourceTemplate, handler: F)
87 where
88 F: Fn(&str, &Context<'_>) -> Fut + Send + Sync + 'static,
89 Fut: Future<Output = Result<ResourceContents, McpError>> + Send + 'static,
90 {
91 let pattern = template.uri_template.clone();
92 let boxed: BoxedResourceFn = Box::new(move |u, ctx| Box::pin(handler(u, ctx)));
93 self.templates.insert(
94 pattern,
95 RegisteredTemplate {
96 template,
97 handler: boxed,
98 },
99 );
100 }
101
102 #[must_use]
104 pub fn get(&self, uri: &str) -> Option<&RegisteredResource> {
105 self.resources.get(uri)
106 }
107
108 #[must_use]
110 pub fn list(&self) -> Vec<&Resource> {
111 self.resources.values().map(|r| &r.resource).collect()
112 }
113
114 #[must_use]
116 pub fn list_templates(&self) -> Vec<&ResourceTemplate> {
117 self.templates.values().map(|r| &r.template).collect()
118 }
119
120 pub async fn read(&self, uri: &str, ctx: &Context<'_>) -> Result<ResourceContents, McpError> {
124 if let Some(registered) = self.resources.get(uri) {
126 return (registered.handler)(uri, ctx).await;
127 }
128
129 for registered in self.templates.values() {
131 if Self::matches_template(®istered.template.uri_template, uri) {
132 return (registered.handler)(uri, ctx).await;
133 }
134 }
135
136 Err(McpError::invalid_params(
137 "resources/read",
138 format!("Unknown resource: {uri}"),
139 ))
140 }
141
142 fn matches_template(template: &str, uri: &str) -> bool {
147 if template.contains('{') {
150 let prefix = template.split('{').next().unwrap_or("");
151 uri.starts_with(prefix)
152 } else {
153 template == uri
154 }
155 }
156
157 #[must_use]
159 pub fn len(&self) -> usize {
160 self.resources.len()
161 }
162
163 #[must_use]
165 pub fn template_count(&self) -> usize {
166 self.templates.len()
167 }
168
169 #[must_use]
171 pub fn is_empty(&self) -> bool {
172 self.resources.is_empty() && self.templates.is_empty()
173 }
174}
175
176impl ResourceHandler for ResourceService {
177 async fn list_resources(&self, _ctx: &Context<'_>) -> Result<Vec<Resource>, McpError> {
178 Ok(self.list().into_iter().cloned().collect())
179 }
180
181 async fn read_resource(
182 &self,
183 uri: &str,
184 ctx: &Context<'_>,
185 ) -> Result<Vec<ResourceContents>, McpError> {
186 Ok(vec![self.read(uri, ctx).await?])
187 }
188}
189
190pub struct ResourceBuilder {
192 uri: String,
193 name: String,
194 description: Option<String>,
195 mime_type: Option<String>,
196}
197
198impl ResourceBuilder {
199 pub fn new(uri: impl Into<String>, name: impl Into<String>) -> Self {
201 Self {
202 uri: uri.into(),
203 name: name.into(),
204 description: None,
205 mime_type: None,
206 }
207 }
208
209 pub fn description(mut self, desc: impl Into<String>) -> Self {
211 self.description = Some(desc.into());
212 self
213 }
214
215 pub fn mime_type(mut self, mime: impl Into<String>) -> Self {
217 self.mime_type = Some(mime.into());
218 self
219 }
220
221 #[must_use]
223 pub fn build(self) -> Resource {
224 Resource {
225 uri: self.uri,
226 name: self.name,
227 title: None,
228 description: self.description,
229 mime_type: self.mime_type,
230 size: None,
231 icons: None,
232 annotations: None,
233 meta: None,
234 }
235 }
236}
237
238pub struct ResourceTemplateBuilder {
240 uri_template: String,
241 name: String,
242 description: Option<String>,
243 mime_type: Option<String>,
244}
245
246impl ResourceTemplateBuilder {
247 pub fn new(uri_template: impl Into<String>, name: impl Into<String>) -> Self {
249 Self {
250 uri_template: uri_template.into(),
251 name: name.into(),
252 description: None,
253 mime_type: None,
254 }
255 }
256
257 pub fn description(mut self, desc: impl Into<String>) -> Self {
259 self.description = Some(desc.into());
260 self
261 }
262
263 pub fn mime_type(mut self, mime: impl Into<String>) -> Self {
265 self.mime_type = Some(mime.into());
266 self
267 }
268
269 #[must_use]
271 pub fn build(self) -> ResourceTemplate {
272 ResourceTemplate {
273 uri_template: self.uri_template,
274 name: self.name,
275 title: None,
276 description: self.description,
277 mime_type: self.mime_type,
278 icons: None,
279 annotations: None,
280 meta: None,
281 }
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn test_resource_builder() {
291 let resource = ResourceBuilder::new("file:///test.txt", "Test File")
292 .description("A test file")
293 .mime_type("text/plain")
294 .build();
295
296 assert_eq!(resource.uri, "file:///test.txt");
297 assert_eq!(resource.name, "Test File");
298 assert_eq!(resource.description.as_deref(), Some("A test file"));
299 assert_eq!(resource.mime_type.as_deref(), Some("text/plain"));
300 }
301
302 #[test]
303 fn test_template_builder() {
304 let template = ResourceTemplateBuilder::new("myserver://data/{id}", "Data Item")
305 .description("Access data by ID")
306 .mime_type("application/json")
307 .build();
308
309 assert_eq!(template.uri_template, "myserver://data/{id}");
310 assert_eq!(template.name, "Data Item");
311 }
312
313 #[test]
314 fn test_template_matching() {
315 assert!(ResourceService::matches_template(
316 "myserver://data/{id}",
317 "myserver://data/123"
318 ));
319 assert!(ResourceService::matches_template(
320 "file:///config.json",
321 "file:///config.json"
322 ));
323 assert!(!ResourceService::matches_template(
324 "file:///other.json",
325 "file:///config.json"
326 ));
327 }
328}