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        )
20            -> Pin<Box<dyn Future<Output = Result<ResourceContents, McpError>> + Send + 'a>>
21        + Send
22        + Sync,
23>;
24
25/// A registered resource with metadata and handler.
26pub struct RegisteredResource {
27    /// Resource metadata.
28    pub resource: Resource,
29    /// Handler function for reading.
30    pub handler: BoxedResourceFn,
31}
32
33/// A registered resource template.
34pub struct RegisteredTemplate {
35    /// Template metadata.
36    pub template: ResourceTemplate,
37    /// Handler function for reading with URI parameters.
38    pub handler: BoxedResourceFn,
39}
40
41/// Service for managing resources.
42///
43/// This provides a registry for static resources and dynamic
44/// resource templates.
45pub struct ResourceService {
46    /// Static resources by URI.
47    resources: HashMap<String, RegisteredResource>,
48    /// Resource templates by URI pattern.
49    templates: HashMap<String, RegisteredTemplate>,
50}
51
52impl Default for ResourceService {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58impl ResourceService {
59    /// Create a new empty resource service.
60    #[must_use]
61    pub fn new() -> Self {
62        Self {
63            resources: HashMap::new(),
64            templates: HashMap::new(),
65        }
66    }
67
68    /// Register a static resource.
69    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    /// Register a resource template.
86    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    /// Get a static resource by URI.
103    #[must_use]
104    pub fn get(&self, uri: &str) -> Option<&RegisteredResource> {
105        self.resources.get(uri)
106    }
107
108    /// List all static resources.
109    #[must_use]
110    pub fn list(&self) -> Vec<&Resource> {
111        self.resources.values().map(|r| &r.resource).collect()
112    }
113
114    /// List all resource templates.
115    #[must_use]
116    pub fn list_templates(&self) -> Vec<&ResourceTemplate> {
117        self.templates.values().map(|r| &r.template).collect()
118    }
119
120    /// Read a resource by URI.
121    ///
122    /// This will first try static resources, then templates.
123    pub async fn read(&self, uri: &str, ctx: &Context<'_>) -> Result<ResourceContents, McpError> {
124        // Try static resources first
125        if let Some(registered) = self.resources.get(uri) {
126            return (registered.handler)(uri, ctx).await;
127        }
128
129        // Try templates
130        for registered in self.templates.values() {
131            if Self::matches_template(&registered.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    /// Check if a URI matches a template pattern.
143    ///
144    /// Simple implementation supporting `{param}` placeholders.
145    /// Uses prefix matching for templates with parameters.
146    fn matches_template(template: &str, uri: &str) -> bool {
147        // For now, do a simple prefix match
148        // A full implementation would use proper URI template matching (RFC 6570)
149        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    /// Get the number of registered resources.
158    #[must_use]
159    pub fn len(&self) -> usize {
160        self.resources.len()
161    }
162
163    /// Get the number of registered templates.
164    #[must_use]
165    pub fn template_count(&self) -> usize {
166        self.templates.len()
167    }
168
169    /// Check if the service has no resources.
170    #[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
190/// Builder for creating resources with a fluent API.
191pub struct ResourceBuilder {
192    uri: String,
193    name: String,
194    description: Option<String>,
195    mime_type: Option<String>,
196}
197
198impl ResourceBuilder {
199    /// Create a new resource builder.
200    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    /// Set the resource description.
210    pub fn description(mut self, desc: impl Into<String>) -> Self {
211        self.description = Some(desc.into());
212        self
213    }
214
215    /// Set the MIME type.
216    pub fn mime_type(mut self, mime: impl Into<String>) -> Self {
217        self.mime_type = Some(mime.into());
218        self
219    }
220
221    /// Build the resource.
222    #[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
235/// Builder for creating resource templates.
236pub struct ResourceTemplateBuilder {
237    uri_template: String,
238    name: String,
239    description: Option<String>,
240    mime_type: Option<String>,
241}
242
243impl ResourceTemplateBuilder {
244    /// Create a new template builder.
245    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    /// Set the template description.
255    pub fn description(mut self, desc: impl Into<String>) -> Self {
256        self.description = Some(desc.into());
257        self
258    }
259
260    /// Set the MIME type.
261    pub fn mime_type(mut self, mime: impl Into<String>) -> Self {
262        self.mime_type = Some(mime.into());
263        self
264    }
265
266    /// Build the template.
267    #[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}