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 ) -> Pin<Box<dyn Future<Output = Result<ResourceContents, McpError>> + Send + 'a>>
20 + Send
21 + Sync,
22>;
23
24pub struct RegisteredResource {
26 pub resource: Resource,
28 pub handler: BoxedResourceFn,
30}
31
32pub struct RegisteredTemplate {
34 pub template: ResourceTemplate,
36 pub handler: BoxedResourceFn,
38}
39
40pub struct ResourceService {
45 resources: HashMap<String, RegisteredResource>,
47 templates: HashMap<String, RegisteredTemplate>,
49}
50
51impl Default for ResourceService {
52 fn default() -> Self {
53 Self::new()
54 }
55}
56
57impl ResourceService {
58 pub fn new() -> Self {
60 Self {
61 resources: HashMap::new(),
62 templates: HashMap::new(),
63 }
64 }
65
66 pub fn register<F, Fut>(&mut self, resource: Resource, handler: F)
68 where
69 F: Fn(&str, &Context<'_>) -> Fut + Send + Sync + 'static,
70 Fut: Future<Output = Result<ResourceContents, McpError>> + Send + 'static,
71 {
72 let uri = resource.uri.clone();
73 let boxed: BoxedResourceFn = Box::new(move |u, ctx| Box::pin(handler(u, ctx)));
74 self.resources.insert(
75 uri,
76 RegisteredResource {
77 resource,
78 handler: boxed,
79 },
80 );
81 }
82
83 pub fn register_template<F, Fut>(&mut self, template: ResourceTemplate, handler: F)
85 where
86 F: Fn(&str, &Context<'_>) -> Fut + Send + Sync + 'static,
87 Fut: Future<Output = Result<ResourceContents, McpError>> + Send + 'static,
88 {
89 let pattern = template.uri_template.clone();
90 let boxed: BoxedResourceFn = Box::new(move |u, ctx| Box::pin(handler(u, ctx)));
91 self.templates.insert(
92 pattern,
93 RegisteredTemplate {
94 template,
95 handler: boxed,
96 },
97 );
98 }
99
100 pub fn get(&self, uri: &str) -> Option<&RegisteredResource> {
102 self.resources.get(uri)
103 }
104
105 pub fn list(&self) -> Vec<&Resource> {
107 self.resources.values().map(|r| &r.resource).collect()
108 }
109
110 pub fn list_templates(&self) -> Vec<&ResourceTemplate> {
112 self.templates.values().map(|r| &r.template).collect()
113 }
114
115 pub async fn read(&self, uri: &str, ctx: &Context<'_>) -> Result<ResourceContents, McpError> {
119 if let Some(registered) = self.resources.get(uri) {
121 return (registered.handler)(uri, ctx).await;
122 }
123
124 for registered in self.templates.values() {
126 if Self::matches_template(®istered.template.uri_template, uri) {
127 return (registered.handler)(uri, ctx).await;
128 }
129 }
130
131 Err(McpError::invalid_params(
132 "resources/read",
133 format!("Unknown resource: {uri}"),
134 ))
135 }
136
137 fn matches_template(template: &str, uri: &str) -> bool {
142 if template.contains('{') {
145 let prefix = template.split('{').next().unwrap_or("");
146 uri.starts_with(prefix)
147 } else {
148 template == uri
149 }
150 }
151
152 pub fn len(&self) -> usize {
154 self.resources.len()
155 }
156
157 pub fn template_count(&self) -> usize {
159 self.templates.len()
160 }
161
162 pub fn is_empty(&self) -> bool {
164 self.resources.is_empty() && self.templates.is_empty()
165 }
166}
167
168impl ResourceHandler for ResourceService {
169 async fn list_resources(&self, _ctx: &Context<'_>) -> Result<Vec<Resource>, McpError> {
170 Ok(self.list().into_iter().cloned().collect())
171 }
172
173 async fn read_resource(
174 &self,
175 uri: &str,
176 ctx: &Context<'_>,
177 ) -> Result<Vec<ResourceContents>, McpError> {
178 Ok(vec![self.read(uri, ctx).await?])
179 }
180}
181
182pub struct ResourceBuilder {
184 uri: String,
185 name: String,
186 description: Option<String>,
187 mime_type: Option<String>,
188}
189
190impl ResourceBuilder {
191 pub fn new(uri: impl Into<String>, name: impl Into<String>) -> Self {
193 Self {
194 uri: uri.into(),
195 name: name.into(),
196 description: None,
197 mime_type: None,
198 }
199 }
200
201 pub fn description(mut self, desc: impl Into<String>) -> Self {
203 self.description = Some(desc.into());
204 self
205 }
206
207 pub fn mime_type(mut self, mime: impl Into<String>) -> Self {
209 self.mime_type = Some(mime.into());
210 self
211 }
212
213 pub fn build(self) -> Resource {
215 Resource {
216 uri: self.uri,
217 name: self.name,
218 description: self.description,
219 mime_type: self.mime_type,
220 size: None,
221 annotations: None,
222 }
223 }
224}
225
226pub struct ResourceTemplateBuilder {
228 uri_template: String,
229 name: String,
230 description: Option<String>,
231 mime_type: Option<String>,
232}
233
234impl ResourceTemplateBuilder {
235 pub fn new(uri_template: impl Into<String>, name: impl Into<String>) -> Self {
237 Self {
238 uri_template: uri_template.into(),
239 name: name.into(),
240 description: None,
241 mime_type: None,
242 }
243 }
244
245 pub fn description(mut self, desc: impl Into<String>) -> Self {
247 self.description = Some(desc.into());
248 self
249 }
250
251 pub fn mime_type(mut self, mime: impl Into<String>) -> Self {
253 self.mime_type = Some(mime.into());
254 self
255 }
256
257 pub fn build(self) -> ResourceTemplate {
259 ResourceTemplate {
260 uri_template: self.uri_template,
261 name: self.name,
262 description: self.description,
263 mime_type: self.mime_type,
264 annotations: None,
265 }
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn test_resource_builder() {
275 let resource = ResourceBuilder::new("file:///test.txt", "Test File")
276 .description("A test file")
277 .mime_type("text/plain")
278 .build();
279
280 assert_eq!(resource.uri, "file:///test.txt");
281 assert_eq!(resource.name, "Test File");
282 assert_eq!(resource.description.as_deref(), Some("A test file"));
283 assert_eq!(resource.mime_type.as_deref(), Some("text/plain"));
284 }
285
286 #[test]
287 fn test_template_builder() {
288 let template = ResourceTemplateBuilder::new(
289 "myserver://data/{id}",
290 "Data Item",
291 )
292 .description("Access data by ID")
293 .mime_type("application/json")
294 .build();
295
296 assert_eq!(template.uri_template, "myserver://data/{id}");
297 assert_eq!(template.name, "Data Item");
298 }
299
300 #[test]
301 fn test_template_matching() {
302 assert!(ResourceService::matches_template(
303 "myserver://data/{id}",
304 "myserver://data/123"
305 ));
306 assert!(ResourceService::matches_template(
307 "file:///config.json",
308 "file:///config.json"
309 ));
310 assert!(!ResourceService::matches_template(
311 "file:///other.json",
312 "file:///config.json"
313 ));
314 }
315}