Skip to main content

tower_mcp/
resource.rs

1//! Resource definition and builder API
2//!
3//! Provides ergonomic ways to define MCP resources:
4//!
5//! 1. **Builder pattern** - Fluent API for defining resources
6//! 2. **Trait-based** - Implement `McpResource` for full control
7//! 3. **Resource templates** - Parameterized resources using URI templates (RFC 6570)
8//!
9//! # Resource Templates
10//!
11//! Resource templates allow servers to expose parameterized resources using URI templates.
12//! When a client requests `resources/read` with a URI matching a template, the server
13//! extracts the variables and passes them to the handler.
14//!
15//! ```rust
16//! use tower_mcp::resource::ResourceTemplateBuilder;
17//! use tower_mcp::protocol::{ReadResourceResult, ResourceContent};
18//! use std::collections::HashMap;
19//!
20//! let template = ResourceTemplateBuilder::new("file:///{path}")
21//!     .name("Project Files")
22//!     .description("Access files in the project directory")
23//!     .handler(|uri: String, vars: HashMap<String, String>| async move {
24//!         let path = vars.get("path").unwrap_or(&String::new()).clone();
25//!         Ok(ReadResourceResult {
26//!             contents: vec![ResourceContent {
27//!                 uri,
28//!                 mime_type: Some("text/plain".to_string()),
29//!                 text: Some(format!("Contents of {}", path)),
30//!                 blob: None,
31//!             }],
32//!         })
33//!     });
34//! ```
35
36use std::collections::HashMap;
37use std::future::Future;
38use std::pin::Pin;
39use std::sync::Arc;
40
41use crate::error::Result;
42use crate::protocol::{
43    ReadResourceResult, ResourceContent, ResourceDefinition, ResourceTemplateDefinition, ToolIcon,
44};
45
46/// A boxed future for resource handlers
47pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
48
49/// Resource handler trait - the core abstraction for resource reading
50pub trait ResourceHandler: Send + Sync {
51    /// Read the resource contents
52    fn read(&self) -> BoxFuture<'_, Result<ReadResourceResult>>;
53}
54
55/// A complete resource definition with handler
56pub struct Resource {
57    pub uri: String,
58    pub name: String,
59    pub title: Option<String>,
60    pub description: Option<String>,
61    pub mime_type: Option<String>,
62    pub icons: Option<Vec<ToolIcon>>,
63    pub size: Option<u64>,
64    handler: Arc<dyn ResourceHandler>,
65}
66
67impl Clone for Resource {
68    fn clone(&self) -> Self {
69        Self {
70            uri: self.uri.clone(),
71            name: self.name.clone(),
72            title: self.title.clone(),
73            description: self.description.clone(),
74            mime_type: self.mime_type.clone(),
75            icons: self.icons.clone(),
76            size: self.size,
77            handler: self.handler.clone(),
78        }
79    }
80}
81
82impl std::fmt::Debug for Resource {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        f.debug_struct("Resource")
85            .field("uri", &self.uri)
86            .field("name", &self.name)
87            .field("title", &self.title)
88            .field("description", &self.description)
89            .field("mime_type", &self.mime_type)
90            .field("icons", &self.icons)
91            .field("size", &self.size)
92            .finish_non_exhaustive()
93    }
94}
95
96impl Resource {
97    /// Create a new resource builder
98    pub fn builder(uri: impl Into<String>) -> ResourceBuilder {
99        ResourceBuilder::new(uri)
100    }
101
102    /// Get the resource definition for resources/list
103    pub fn definition(&self) -> ResourceDefinition {
104        ResourceDefinition {
105            uri: self.uri.clone(),
106            name: self.name.clone(),
107            title: self.title.clone(),
108            description: self.description.clone(),
109            mime_type: self.mime_type.clone(),
110            icons: self.icons.clone(),
111            size: self.size,
112        }
113    }
114
115    /// Read the resource
116    pub fn read(&self) -> BoxFuture<'_, Result<ReadResourceResult>> {
117        self.handler.read()
118    }
119}
120
121// =============================================================================
122// Builder API
123// =============================================================================
124
125/// Builder for creating resources with a fluent API
126///
127/// # Example
128///
129/// ```rust
130/// use tower_mcp::resource::ResourceBuilder;
131/// use tower_mcp::protocol::{ReadResourceResult, ResourceContent};
132///
133/// let resource = ResourceBuilder::new("file:///config.json")
134///     .name("Configuration")
135///     .description("Application configuration file")
136///     .mime_type("application/json")
137///     .handler(|| async {
138///         Ok(ReadResourceResult {
139///             contents: vec![ResourceContent {
140///                 uri: "file:///config.json".to_string(),
141///                 mime_type: Some("application/json".to_string()),
142///                 text: Some(r#"{"setting": "value"}"#.to_string()),
143///                 blob: None,
144///             }],
145///         })
146///     });
147///
148/// assert_eq!(resource.uri, "file:///config.json");
149/// ```
150pub struct ResourceBuilder {
151    uri: String,
152    name: Option<String>,
153    title: Option<String>,
154    description: Option<String>,
155    mime_type: Option<String>,
156    icons: Option<Vec<ToolIcon>>,
157    size: Option<u64>,
158}
159
160impl ResourceBuilder {
161    pub fn new(uri: impl Into<String>) -> Self {
162        Self {
163            uri: uri.into(),
164            name: None,
165            title: None,
166            description: None,
167            mime_type: None,
168            icons: None,
169            size: None,
170        }
171    }
172
173    /// Set the resource name (human-readable)
174    pub fn name(mut self, name: impl Into<String>) -> Self {
175        self.name = Some(name.into());
176        self
177    }
178
179    /// Set a human-readable title for the resource
180    pub fn title(mut self, title: impl Into<String>) -> Self {
181        self.title = Some(title.into());
182        self
183    }
184
185    /// Set the resource description
186    pub fn description(mut self, description: impl Into<String>) -> Self {
187        self.description = Some(description.into());
188        self
189    }
190
191    /// Set the MIME type of the resource
192    pub fn mime_type(mut self, mime_type: impl Into<String>) -> Self {
193        self.mime_type = Some(mime_type.into());
194        self
195    }
196
197    /// Add an icon for the resource
198    pub fn icon(mut self, src: impl Into<String>) -> Self {
199        self.icons.get_or_insert_with(Vec::new).push(ToolIcon {
200            src: src.into(),
201            mime_type: None,
202            sizes: None,
203        });
204        self
205    }
206
207    /// Add an icon with metadata
208    pub fn icon_with_meta(
209        mut self,
210        src: impl Into<String>,
211        mime_type: Option<String>,
212        sizes: Option<Vec<String>>,
213    ) -> Self {
214        self.icons.get_or_insert_with(Vec::new).push(ToolIcon {
215            src: src.into(),
216            mime_type,
217            sizes,
218        });
219        self
220    }
221
222    /// Set the size of the resource in bytes
223    pub fn size(mut self, size: u64) -> Self {
224        self.size = Some(size);
225        self
226    }
227
228    /// Set the handler function for reading the resource
229    pub fn handler<F, Fut>(self, handler: F) -> Resource
230    where
231        F: Fn() -> Fut + Send + Sync + 'static,
232        Fut: Future<Output = Result<ReadResourceResult>> + Send + 'static,
233    {
234        // Default name to URI if not specified
235        let name = self.name.unwrap_or_else(|| self.uri.clone());
236
237        Resource {
238            uri: self.uri.clone(),
239            name,
240            title: self.title,
241            description: self.description,
242            mime_type: self.mime_type,
243            icons: self.icons,
244            size: self.size,
245            handler: Arc::new(FnHandler { handler }),
246        }
247    }
248
249    /// Create a static text resource (convenience method)
250    pub fn text(self, content: impl Into<String>) -> Resource {
251        let uri = self.uri.clone();
252        let content = content.into();
253        let mime_type = self.mime_type.clone();
254
255        self.handler(move || {
256            let uri = uri.clone();
257            let content = content.clone();
258            let mime_type = mime_type.clone();
259            async move {
260                Ok(ReadResourceResult {
261                    contents: vec![ResourceContent {
262                        uri,
263                        mime_type,
264                        text: Some(content),
265                        blob: None,
266                    }],
267                })
268            }
269        })
270    }
271
272    /// Create a static JSON resource (convenience method)
273    pub fn json(mut self, value: serde_json::Value) -> Resource {
274        let uri = self.uri.clone();
275        self.mime_type = Some("application/json".to_string());
276        let text = serde_json::to_string_pretty(&value).unwrap_or_else(|_| "{}".to_string());
277
278        self.handler(move || {
279            let uri = uri.clone();
280            let text = text.clone();
281            async move {
282                Ok(ReadResourceResult {
283                    contents: vec![ResourceContent {
284                        uri,
285                        mime_type: Some("application/json".to_string()),
286                        text: Some(text),
287                        blob: None,
288                    }],
289                })
290            }
291        })
292    }
293}
294
295// =============================================================================
296// Handler implementations
297// =============================================================================
298
299/// Handler wrapping a function
300struct FnHandler<F> {
301    handler: F,
302}
303
304impl<F, Fut> ResourceHandler for FnHandler<F>
305where
306    F: Fn() -> Fut + Send + Sync + 'static,
307    Fut: Future<Output = Result<ReadResourceResult>> + Send + 'static,
308{
309    fn read(&self) -> BoxFuture<'_, Result<ReadResourceResult>> {
310        Box::pin((self.handler)())
311    }
312}
313
314// =============================================================================
315// Trait-based resource definition
316// =============================================================================
317
318/// Trait for defining resources with full control
319///
320/// Implement this trait when you need more control than the builder provides,
321/// or when you want to define resources as standalone types.
322///
323/// # Example
324///
325/// ```rust
326/// use tower_mcp::resource::McpResource;
327/// use tower_mcp::protocol::{ReadResourceResult, ResourceContent};
328/// use tower_mcp::error::Result;
329///
330/// struct ConfigResource {
331///     config: String,
332/// }
333///
334/// impl McpResource for ConfigResource {
335///     const URI: &'static str = "file:///config.json";
336///     const NAME: &'static str = "Configuration";
337///     const DESCRIPTION: Option<&'static str> = Some("Application configuration");
338///     const MIME_TYPE: Option<&'static str> = Some("application/json");
339///
340///     async fn read(&self) -> Result<ReadResourceResult> {
341///         Ok(ReadResourceResult {
342///             contents: vec![ResourceContent {
343///                 uri: Self::URI.to_string(),
344///                 mime_type: Self::MIME_TYPE.map(|s| s.to_string()),
345///                 text: Some(self.config.clone()),
346///                 blob: None,
347///             }],
348///         })
349///     }
350/// }
351///
352/// let resource = ConfigResource { config: "{}".to_string() }.into_resource();
353/// assert_eq!(resource.uri, "file:///config.json");
354/// ```
355pub trait McpResource: Send + Sync + 'static {
356    const URI: &'static str;
357    const NAME: &'static str;
358    const DESCRIPTION: Option<&'static str> = None;
359    const MIME_TYPE: Option<&'static str> = None;
360
361    fn read(&self) -> impl Future<Output = Result<ReadResourceResult>> + Send;
362
363    /// Convert to a Resource instance
364    fn into_resource(self) -> Resource
365    where
366        Self: Sized,
367    {
368        let resource = Arc::new(self);
369        Resource {
370            uri: Self::URI.to_string(),
371            name: Self::NAME.to_string(),
372            title: None,
373            description: Self::DESCRIPTION.map(|s| s.to_string()),
374            mime_type: Self::MIME_TYPE.map(|s| s.to_string()),
375            icons: None,
376            size: None,
377            handler: Arc::new(McpResourceHandler { resource }),
378        }
379    }
380}
381
382/// Wrapper to make McpResource implement ResourceHandler
383struct McpResourceHandler<T: McpResource> {
384    resource: Arc<T>,
385}
386
387impl<T: McpResource> ResourceHandler for McpResourceHandler<T> {
388    fn read(&self) -> BoxFuture<'_, Result<ReadResourceResult>> {
389        let resource = self.resource.clone();
390        Box::pin(async move { resource.read().await })
391    }
392}
393
394// =============================================================================
395// Resource Templates
396// =============================================================================
397
398/// Handler trait for resource templates
399///
400/// Unlike [`ResourceHandler`], template handlers receive the extracted
401/// URI variables as a parameter.
402pub trait ResourceTemplateHandler: Send + Sync {
403    /// Read a resource with the given URI variables extracted from the template
404    fn read(
405        &self,
406        uri: &str,
407        variables: HashMap<String, String>,
408    ) -> BoxFuture<'_, Result<ReadResourceResult>>;
409}
410
411/// A parameterized resource template
412///
413/// Resource templates use URI template syntax (RFC 6570) to match multiple URIs
414/// and extract variable values. This allows servers to expose dynamic resources
415/// like file systems or database records.
416///
417/// # Example
418///
419/// ```rust
420/// use tower_mcp::resource::ResourceTemplateBuilder;
421/// use tower_mcp::protocol::{ReadResourceResult, ResourceContent};
422/// use std::collections::HashMap;
423///
424/// let template = ResourceTemplateBuilder::new("file:///{path}")
425///     .name("Project Files")
426///     .handler(|uri: String, vars: HashMap<String, String>| async move {
427///         let path = vars.get("path").unwrap_or(&String::new()).clone();
428///         Ok(ReadResourceResult {
429///             contents: vec![ResourceContent {
430///                 uri,
431///                 mime_type: Some("text/plain".to_string()),
432///                 text: Some(format!("Contents of {}", path)),
433///                 blob: None,
434///             }],
435///         })
436///     });
437/// ```
438pub struct ResourceTemplate {
439    /// The URI template pattern (e.g., `file:///{path}`)
440    pub uri_template: String,
441    /// Human-readable name
442    pub name: String,
443    /// Human-readable title for display purposes
444    pub title: Option<String>,
445    /// Optional description
446    pub description: Option<String>,
447    /// Optional MIME type hint
448    pub mime_type: Option<String>,
449    /// Optional icons for display in user interfaces
450    pub icons: Option<Vec<ToolIcon>>,
451    /// Compiled regex for matching URIs
452    pattern: regex::Regex,
453    /// Variable names in order of appearance
454    variables: Vec<String>,
455    /// Handler for reading matched resources
456    handler: Arc<dyn ResourceTemplateHandler>,
457}
458
459impl Clone for ResourceTemplate {
460    fn clone(&self) -> Self {
461        Self {
462            uri_template: self.uri_template.clone(),
463            name: self.name.clone(),
464            title: self.title.clone(),
465            description: self.description.clone(),
466            mime_type: self.mime_type.clone(),
467            icons: self.icons.clone(),
468            pattern: self.pattern.clone(),
469            variables: self.variables.clone(),
470            handler: self.handler.clone(),
471        }
472    }
473}
474
475impl std::fmt::Debug for ResourceTemplate {
476    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
477        f.debug_struct("ResourceTemplate")
478            .field("uri_template", &self.uri_template)
479            .field("name", &self.name)
480            .field("title", &self.title)
481            .field("description", &self.description)
482            .field("mime_type", &self.mime_type)
483            .field("icons", &self.icons)
484            .field("variables", &self.variables)
485            .finish_non_exhaustive()
486    }
487}
488
489impl ResourceTemplate {
490    /// Create a new resource template builder
491    pub fn builder(uri_template: impl Into<String>) -> ResourceTemplateBuilder {
492        ResourceTemplateBuilder::new(uri_template)
493    }
494
495    /// Get the template definition for resources/templates/list
496    pub fn definition(&self) -> ResourceTemplateDefinition {
497        ResourceTemplateDefinition {
498            uri_template: self.uri_template.clone(),
499            name: self.name.clone(),
500            title: self.title.clone(),
501            description: self.description.clone(),
502            mime_type: self.mime_type.clone(),
503            icons: self.icons.clone(),
504        }
505    }
506
507    /// Check if a URI matches this template and extract variables
508    ///
509    /// Returns `Some(HashMap)` with extracted variables if the URI matches,
510    /// `None` if it doesn't match.
511    pub fn match_uri(&self, uri: &str) -> Option<HashMap<String, String>> {
512        self.pattern.captures(uri).map(|caps| {
513            self.variables
514                .iter()
515                .enumerate()
516                .filter_map(|(i, name)| {
517                    caps.get(i + 1)
518                        .map(|m| (name.clone(), m.as_str().to_string()))
519                })
520                .collect()
521        })
522    }
523
524    /// Read a resource at the given URI using this template's handler
525    ///
526    /// # Arguments
527    ///
528    /// * `uri` - The actual URI being read
529    /// * `variables` - Variables extracted from matching the URI against the template
530    pub fn read(
531        &self,
532        uri: &str,
533        variables: HashMap<String, String>,
534    ) -> BoxFuture<'_, Result<ReadResourceResult>> {
535        self.handler.read(uri, variables)
536    }
537}
538
539/// Builder for creating resource templates
540///
541/// # Example
542///
543/// ```rust
544/// use tower_mcp::resource::ResourceTemplateBuilder;
545/// use tower_mcp::protocol::{ReadResourceResult, ResourceContent};
546/// use std::collections::HashMap;
547///
548/// let template = ResourceTemplateBuilder::new("db://users/{id}")
549///     .name("User Records")
550///     .description("Access user records by ID")
551///     .handler(|uri: String, vars: HashMap<String, String>| async move {
552///         let id = vars.get("id").unwrap();
553///         Ok(ReadResourceResult {
554///             contents: vec![ResourceContent {
555///                 uri,
556///                 mime_type: Some("application/json".to_string()),
557///                 text: Some(format!(r#"{{"id": "{}"}}"#, id)),
558///                 blob: None,
559///             }],
560///         })
561///     });
562/// ```
563pub struct ResourceTemplateBuilder {
564    uri_template: String,
565    name: Option<String>,
566    title: Option<String>,
567    description: Option<String>,
568    mime_type: Option<String>,
569    icons: Option<Vec<ToolIcon>>,
570}
571
572impl ResourceTemplateBuilder {
573    /// Create a new builder with the given URI template
574    ///
575    /// # URI Template Syntax
576    ///
577    /// Templates use RFC 6570 Level 1 syntax with simple variable expansion:
578    /// - `{varname}` - Matches any non-slash characters
579    ///
580    /// # Examples
581    ///
582    /// - `file:///{path}` - Matches `file:///README.md`
583    /// - `db://users/{id}` - Matches `db://users/123`
584    /// - `api://v1/{resource}/{id}` - Matches `api://v1/posts/456`
585    pub fn new(uri_template: impl Into<String>) -> Self {
586        Self {
587            uri_template: uri_template.into(),
588            name: None,
589            title: None,
590            description: None,
591            mime_type: None,
592            icons: None,
593        }
594    }
595
596    /// Set the human-readable name for this template
597    pub fn name(mut self, name: impl Into<String>) -> Self {
598        self.name = Some(name.into());
599        self
600    }
601
602    /// Set a human-readable title for the template
603    pub fn title(mut self, title: impl Into<String>) -> Self {
604        self.title = Some(title.into());
605        self
606    }
607
608    /// Set the description for this template
609    pub fn description(mut self, description: impl Into<String>) -> Self {
610        self.description = Some(description.into());
611        self
612    }
613
614    /// Set the MIME type hint for resources from this template
615    pub fn mime_type(mut self, mime_type: impl Into<String>) -> Self {
616        self.mime_type = Some(mime_type.into());
617        self
618    }
619
620    /// Add an icon for the template
621    pub fn icon(mut self, src: impl Into<String>) -> Self {
622        self.icons.get_or_insert_with(Vec::new).push(ToolIcon {
623            src: src.into(),
624            mime_type: None,
625            sizes: None,
626        });
627        self
628    }
629
630    /// Add an icon with metadata
631    pub fn icon_with_meta(
632        mut self,
633        src: impl Into<String>,
634        mime_type: Option<String>,
635        sizes: Option<Vec<String>>,
636    ) -> Self {
637        self.icons.get_or_insert_with(Vec::new).push(ToolIcon {
638            src: src.into(),
639            mime_type,
640            sizes,
641        });
642        self
643    }
644
645    /// Set the handler function for reading template resources
646    ///
647    /// The handler receives:
648    /// - `uri`: The full URI being read
649    /// - `variables`: A map of variable names to their values extracted from the URI
650    pub fn handler<F, Fut>(self, handler: F) -> ResourceTemplate
651    where
652        F: Fn(String, HashMap<String, String>) -> Fut + Send + Sync + 'static,
653        Fut: Future<Output = Result<ReadResourceResult>> + Send + 'static,
654    {
655        let (pattern, variables) = compile_uri_template(&self.uri_template);
656        let name = self.name.unwrap_or_else(|| self.uri_template.clone());
657
658        ResourceTemplate {
659            uri_template: self.uri_template,
660            name,
661            title: self.title,
662            description: self.description,
663            mime_type: self.mime_type,
664            icons: self.icons,
665            pattern,
666            variables,
667            handler: Arc::new(FnTemplateHandler { handler }),
668        }
669    }
670}
671
672/// Handler wrapping a function for templates
673struct FnTemplateHandler<F> {
674    handler: F,
675}
676
677impl<F, Fut> ResourceTemplateHandler for FnTemplateHandler<F>
678where
679    F: Fn(String, HashMap<String, String>) -> Fut + Send + Sync + 'static,
680    Fut: Future<Output = Result<ReadResourceResult>> + Send + 'static,
681{
682    fn read(
683        &self,
684        uri: &str,
685        variables: HashMap<String, String>,
686    ) -> BoxFuture<'_, Result<ReadResourceResult>> {
687        let uri = uri.to_string();
688        Box::pin((self.handler)(uri, variables))
689    }
690}
691
692/// Compile a URI template into a regex pattern and extract variable names
693///
694/// Supports RFC 6570 Level 1 (simple expansion):
695/// - `{var}` matches any characters except `/`
696/// - `{+var}` matches any characters including `/` (reserved expansion)
697///
698/// Returns the compiled regex and a list of variable names in order.
699fn compile_uri_template(template: &str) -> (regex::Regex, Vec<String>) {
700    let mut pattern = String::from("^");
701    let mut variables = Vec::new();
702
703    let mut chars = template.chars().peekable();
704    while let Some(c) = chars.next() {
705        if c == '{' {
706            // Check for + prefix (reserved expansion)
707            let is_reserved = chars.peek() == Some(&'+');
708            if is_reserved {
709                chars.next();
710            }
711
712            // Collect variable name
713            let var_name: String = chars.by_ref().take_while(|&c| c != '}').collect();
714            variables.push(var_name);
715
716            // Choose pattern based on expansion type
717            if is_reserved {
718                // Reserved expansion - match anything
719                pattern.push_str("(.+)");
720            } else {
721                // Simple expansion - match non-slash characters
722                pattern.push_str("([^/]+)");
723            }
724        } else {
725            // Escape regex special characters
726            match c {
727                '.' | '+' | '*' | '?' | '^' | '$' | '(' | ')' | '[' | ']' | '{' | '}' | '|'
728                | '\\' => {
729                    pattern.push('\\');
730                    pattern.push(c);
731                }
732                _ => pattern.push(c),
733            }
734        }
735    }
736
737    pattern.push('$');
738
739    // Compile the regex - panic if template is malformed
740    let regex = regex::Regex::new(&pattern)
741        .unwrap_or_else(|e| panic!("Invalid URI template '{}': {}", template, e));
742
743    (regex, variables)
744}
745
746#[cfg(test)]
747mod tests {
748    use super::*;
749
750    #[tokio::test]
751    async fn test_builder_resource() {
752        let resource = ResourceBuilder::new("file:///test.txt")
753            .name("Test File")
754            .description("A test file")
755            .text("Hello, World!");
756
757        assert_eq!(resource.uri, "file:///test.txt");
758        assert_eq!(resource.name, "Test File");
759        assert_eq!(resource.description.as_deref(), Some("A test file"));
760
761        let result = resource.read().await.unwrap();
762        assert_eq!(result.contents.len(), 1);
763        assert_eq!(result.contents[0].text.as_deref(), Some("Hello, World!"));
764    }
765
766    #[tokio::test]
767    async fn test_json_resource() {
768        let resource = ResourceBuilder::new("file:///config.json")
769            .name("Config")
770            .json(serde_json::json!({"key": "value"}));
771
772        assert_eq!(resource.mime_type.as_deref(), Some("application/json"));
773
774        let result = resource.read().await.unwrap();
775        assert!(result.contents[0].text.as_ref().unwrap().contains("key"));
776    }
777
778    #[tokio::test]
779    async fn test_handler_resource() {
780        let resource = ResourceBuilder::new("memory://counter")
781            .name("Counter")
782            .handler(|| async {
783                Ok(ReadResourceResult {
784                    contents: vec![ResourceContent {
785                        uri: "memory://counter".to_string(),
786                        mime_type: Some("text/plain".to_string()),
787                        text: Some("42".to_string()),
788                        blob: None,
789                    }],
790                })
791            });
792
793        let result = resource.read().await.unwrap();
794        assert_eq!(result.contents[0].text.as_deref(), Some("42"));
795    }
796
797    #[tokio::test]
798    async fn test_trait_resource() {
799        struct TestResource;
800
801        impl McpResource for TestResource {
802            const URI: &'static str = "test://resource";
803            const NAME: &'static str = "Test";
804            const DESCRIPTION: Option<&'static str> = Some("A test resource");
805            const MIME_TYPE: Option<&'static str> = Some("text/plain");
806
807            async fn read(&self) -> Result<ReadResourceResult> {
808                Ok(ReadResourceResult {
809                    contents: vec![ResourceContent {
810                        uri: Self::URI.to_string(),
811                        mime_type: Self::MIME_TYPE.map(|s| s.to_string()),
812                        text: Some("test content".to_string()),
813                        blob: None,
814                    }],
815                })
816            }
817        }
818
819        let resource = TestResource.into_resource();
820        assert_eq!(resource.uri, "test://resource");
821        assert_eq!(resource.name, "Test");
822
823        let result = resource.read().await.unwrap();
824        assert_eq!(result.contents[0].text.as_deref(), Some("test content"));
825    }
826
827    #[test]
828    fn test_resource_definition() {
829        let resource = ResourceBuilder::new("file:///test.txt")
830            .name("Test")
831            .description("Description")
832            .mime_type("text/plain")
833            .text("content");
834
835        let def = resource.definition();
836        assert_eq!(def.uri, "file:///test.txt");
837        assert_eq!(def.name, "Test");
838        assert_eq!(def.description.as_deref(), Some("Description"));
839        assert_eq!(def.mime_type.as_deref(), Some("text/plain"));
840    }
841
842    // =========================================================================
843    // Resource Template Tests
844    // =========================================================================
845
846    #[test]
847    fn test_compile_uri_template_simple() {
848        let (regex, vars) = compile_uri_template("file:///{path}");
849        assert_eq!(vars, vec!["path"]);
850        assert!(regex.is_match("file:///README.md"));
851        assert!(!regex.is_match("file:///foo/bar")); // no slashes in simple expansion
852    }
853
854    #[test]
855    fn test_compile_uri_template_multiple_vars() {
856        let (regex, vars) = compile_uri_template("api://v1/{resource}/{id}");
857        assert_eq!(vars, vec!["resource", "id"]);
858        assert!(regex.is_match("api://v1/users/123"));
859        assert!(regex.is_match("api://v1/posts/abc"));
860        assert!(!regex.is_match("api://v1/users")); // missing id
861    }
862
863    #[test]
864    fn test_compile_uri_template_reserved_expansion() {
865        let (regex, vars) = compile_uri_template("file:///{+path}");
866        assert_eq!(vars, vec!["path"]);
867        assert!(regex.is_match("file:///README.md"));
868        assert!(regex.is_match("file:///foo/bar/baz.txt")); // slashes allowed
869    }
870
871    #[test]
872    fn test_compile_uri_template_special_chars() {
873        let (regex, vars) = compile_uri_template("http://example.com/api?query={q}");
874        assert_eq!(vars, vec!["q"]);
875        assert!(regex.is_match("http://example.com/api?query=hello"));
876    }
877
878    #[test]
879    fn test_resource_template_match_uri() {
880        let template = ResourceTemplateBuilder::new("db://users/{id}")
881            .name("User Records")
882            .handler(|uri: String, vars: HashMap<String, String>| async move {
883                Ok(ReadResourceResult {
884                    contents: vec![ResourceContent {
885                        uri,
886                        mime_type: None,
887                        text: Some(format!("User {}", vars.get("id").unwrap())),
888                        blob: None,
889                    }],
890                })
891            });
892
893        // Test matching
894        let vars = template.match_uri("db://users/123").unwrap();
895        assert_eq!(vars.get("id"), Some(&"123".to_string()));
896
897        // Test non-matching
898        assert!(template.match_uri("db://posts/123").is_none());
899        assert!(template.match_uri("db://users").is_none());
900    }
901
902    #[test]
903    fn test_resource_template_match_multiple_vars() {
904        let template = ResourceTemplateBuilder::new("api://{version}/{resource}/{id}")
905            .name("API Resources")
906            .handler(|uri: String, _vars: HashMap<String, String>| async move {
907                Ok(ReadResourceResult {
908                    contents: vec![ResourceContent {
909                        uri,
910                        mime_type: None,
911                        text: None,
912                        blob: None,
913                    }],
914                })
915            });
916
917        let vars = template.match_uri("api://v2/users/abc-123").unwrap();
918        assert_eq!(vars.get("version"), Some(&"v2".to_string()));
919        assert_eq!(vars.get("resource"), Some(&"users".to_string()));
920        assert_eq!(vars.get("id"), Some(&"abc-123".to_string()));
921    }
922
923    #[tokio::test]
924    async fn test_resource_template_read() {
925        let template = ResourceTemplateBuilder::new("file:///{path}")
926            .name("Files")
927            .mime_type("text/plain")
928            .handler(|uri: String, vars: HashMap<String, String>| async move {
929                let path = vars.get("path").unwrap().clone();
930                Ok(ReadResourceResult {
931                    contents: vec![ResourceContent {
932                        uri,
933                        mime_type: Some("text/plain".to_string()),
934                        text: Some(format!("Contents of {}", path)),
935                        blob: None,
936                    }],
937                })
938            });
939
940        let vars = template.match_uri("file:///README.md").unwrap();
941        let result = template.read("file:///README.md", vars).await.unwrap();
942
943        assert_eq!(result.contents.len(), 1);
944        assert_eq!(result.contents[0].uri, "file:///README.md");
945        assert_eq!(
946            result.contents[0].text.as_deref(),
947            Some("Contents of README.md")
948        );
949    }
950
951    #[test]
952    fn test_resource_template_definition() {
953        let template = ResourceTemplateBuilder::new("db://records/{id}")
954            .name("Database Records")
955            .description("Access database records by ID")
956            .mime_type("application/json")
957            .handler(|uri: String, _vars: HashMap<String, String>| async move {
958                Ok(ReadResourceResult {
959                    contents: vec![ResourceContent {
960                        uri,
961                        mime_type: None,
962                        text: None,
963                        blob: None,
964                    }],
965                })
966            });
967
968        let def = template.definition();
969        assert_eq!(def.uri_template, "db://records/{id}");
970        assert_eq!(def.name, "Database Records");
971        assert_eq!(
972            def.description.as_deref(),
973            Some("Access database records by ID")
974        );
975        assert_eq!(def.mime_type.as_deref(), Some("application/json"));
976    }
977
978    #[test]
979    fn test_resource_template_reserved_path() {
980        let template = ResourceTemplateBuilder::new("file:///{+path}")
981            .name("Files with subpaths")
982            .handler(|uri: String, _vars: HashMap<String, String>| async move {
983                Ok(ReadResourceResult {
984                    contents: vec![ResourceContent {
985                        uri,
986                        mime_type: None,
987                        text: None,
988                        blob: None,
989                    }],
990                })
991            });
992
993        // Reserved expansion should match slashes
994        let vars = template.match_uri("file:///src/lib/utils.rs").unwrap();
995        assert_eq!(vars.get("path"), Some(&"src/lib/utils.rs".to_string()));
996    }
997}