mcpkit_server/capability/
resources.rs

1//! Resource capability implementation.
2//!
3//! This module provides utilities for managing and serving resources
4//! in an MCP server.
5
6use 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
14/// A boxed async function for resource reading.
15pub 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
24/// A registered resource with metadata and handler.
25pub struct RegisteredResource {
26    /// Resource metadata.
27    pub resource: Resource,
28    /// Handler function for reading.
29    pub handler: BoxedResourceFn,
30}
31
32/// A registered resource template.
33pub struct RegisteredTemplate {
34    /// Template metadata.
35    pub template: ResourceTemplate,
36    /// Handler function for reading with URI parameters.
37    pub handler: BoxedResourceFn,
38}
39
40/// Service for managing resources.
41///
42/// This provides a registry for static resources and dynamic
43/// resource templates.
44pub struct ResourceService {
45    /// Static resources by URI.
46    resources: HashMap<String, RegisteredResource>,
47    /// Resource templates by URI pattern.
48    templates: HashMap<String, RegisteredTemplate>,
49}
50
51impl Default for ResourceService {
52    fn default() -> Self {
53        Self::new()
54    }
55}
56
57impl ResourceService {
58    /// Create a new empty resource service.
59    pub fn new() -> Self {
60        Self {
61            resources: HashMap::new(),
62            templates: HashMap::new(),
63        }
64    }
65
66    /// Register a static resource.
67    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    /// Register a resource template.
84    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    /// Get a static resource by URI.
101    pub fn get(&self, uri: &str) -> Option<&RegisteredResource> {
102        self.resources.get(uri)
103    }
104
105    /// List all static resources.
106    pub fn list(&self) -> Vec<&Resource> {
107        self.resources.values().map(|r| &r.resource).collect()
108    }
109
110    /// List all resource templates.
111    pub fn list_templates(&self) -> Vec<&ResourceTemplate> {
112        self.templates.values().map(|r| &r.template).collect()
113    }
114
115    /// Read a resource by URI.
116    ///
117    /// This will first try static resources, then templates.
118    pub async fn read(&self, uri: &str, ctx: &Context<'_>) -> Result<ResourceContents, McpError> {
119        // Try static resources first
120        if let Some(registered) = self.resources.get(uri) {
121            return (registered.handler)(uri, ctx).await;
122        }
123
124        // Try templates
125        for registered in self.templates.values() {
126            if Self::matches_template(&registered.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    /// Check if a URI matches a template pattern.
138    ///
139    /// Simple implementation supporting `{param}` placeholders.
140    /// Uses prefix matching for templates with parameters.
141    fn matches_template(template: &str, uri: &str) -> bool {
142        // For now, do a simple prefix match
143        // A full implementation would use proper URI template matching (RFC 6570)
144        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    /// Get the number of registered resources.
153    pub fn len(&self) -> usize {
154        self.resources.len()
155    }
156
157    /// Get the number of registered templates.
158    pub fn template_count(&self) -> usize {
159        self.templates.len()
160    }
161
162    /// Check if the service has no resources.
163    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
182/// Builder for creating resources with a fluent API.
183pub struct ResourceBuilder {
184    uri: String,
185    name: String,
186    description: Option<String>,
187    mime_type: Option<String>,
188}
189
190impl ResourceBuilder {
191    /// Create a new resource builder.
192    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    /// Set the resource description.
202    pub fn description(mut self, desc: impl Into<String>) -> Self {
203        self.description = Some(desc.into());
204        self
205    }
206
207    /// Set the MIME type.
208    pub fn mime_type(mut self, mime: impl Into<String>) -> Self {
209        self.mime_type = Some(mime.into());
210        self
211    }
212
213    /// Build the resource.
214    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
226/// Builder for creating resource templates.
227pub struct ResourceTemplateBuilder {
228    uri_template: String,
229    name: String,
230    description: Option<String>,
231    mime_type: Option<String>,
232}
233
234impl ResourceTemplateBuilder {
235    /// Create a new template builder.
236    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    /// Set the template description.
246    pub fn description(mut self, desc: impl Into<String>) -> Self {
247        self.description = Some(desc.into());
248        self
249    }
250
251    /// Set the MIME type.
252    pub fn mime_type(mut self, mime: impl Into<String>) -> Self {
253        self.mime_type = Some(mime.into());
254        self
255    }
256
257    /// Build the template.
258    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}