Skip to main content

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            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
238/// Builder for creating resource templates.
239pub struct ResourceTemplateBuilder {
240    uri_template: String,
241    name: String,
242    description: Option<String>,
243    mime_type: Option<String>,
244}
245
246impl ResourceTemplateBuilder {
247    /// Create a new template builder.
248    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    /// Set the template description.
258    pub fn description(mut self, desc: impl Into<String>) -> Self {
259        self.description = Some(desc.into());
260        self
261    }
262
263    /// Set the MIME type.
264    pub fn mime_type(mut self, mime: impl Into<String>) -> Self {
265        self.mime_type = Some(mime.into());
266        self
267    }
268
269    /// Build the template.
270    #[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}