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