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 description: self.description,
228 mime_type: self.mime_type,
229 size: None,
230 annotations: None,
231 }
232 }
233}
234
235pub struct ResourceTemplateBuilder {
237 uri_template: String,
238 name: String,
239 description: Option<String>,
240 mime_type: Option<String>,
241}
242
243impl ResourceTemplateBuilder {
244 pub fn new(uri_template: impl Into<String>, name: impl Into<String>) -> Self {
246 Self {
247 uri_template: uri_template.into(),
248 name: name.into(),
249 description: None,
250 mime_type: None,
251 }
252 }
253
254 pub fn description(mut self, desc: impl Into<String>) -> Self {
256 self.description = Some(desc.into());
257 self
258 }
259
260 pub fn mime_type(mut self, mime: impl Into<String>) -> Self {
262 self.mime_type = Some(mime.into());
263 self
264 }
265
266 #[must_use]
268 pub fn build(self) -> ResourceTemplate {
269 ResourceTemplate {
270 uri_template: self.uri_template,
271 name: self.name,
272 description: self.description,
273 mime_type: self.mime_type,
274 annotations: None,
275 }
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 #[test]
284 fn test_resource_builder() {
285 let resource = ResourceBuilder::new("file:///test.txt", "Test File")
286 .description("A test file")
287 .mime_type("text/plain")
288 .build();
289
290 assert_eq!(resource.uri, "file:///test.txt");
291 assert_eq!(resource.name, "Test File");
292 assert_eq!(resource.description.as_deref(), Some("A test file"));
293 assert_eq!(resource.mime_type.as_deref(), Some("text/plain"));
294 }
295
296 #[test]
297 fn test_template_builder() {
298 let template = ResourceTemplateBuilder::new("myserver://data/{id}", "Data Item")
299 .description("Access data by ID")
300 .mime_type("application/json")
301 .build();
302
303 assert_eq!(template.uri_template, "myserver://data/{id}");
304 assert_eq!(template.name, "Data Item");
305 }
306
307 #[test]
308 fn test_template_matching() {
309 assert!(ResourceService::matches_template(
310 "myserver://data/{id}",
311 "myserver://data/123"
312 ));
313 assert!(ResourceService::matches_template(
314 "file:///config.json",
315 "file:///config.json"
316 ));
317 assert!(!ResourceService::matches_template(
318 "file:///other.json",
319 "file:///config.json"
320 ));
321 }
322}