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//! ## Per-Resource Middleware
10//!
11//! Resources are implemented as Tower services internally, enabling middleware
12//! composition via the `.layer()` method:
13//!
14//! ```rust
15//! use std::time::Duration;
16//! use tower::timeout::TimeoutLayer;
17//! use tower_mcp::resource::ResourceBuilder;
18//! use tower_mcp::protocol::{ReadResourceResult, ResourceContent};
19//!
20//! let resource = ResourceBuilder::new("file:///large-file.txt")
21//!     .name("Large File")
22//!     .description("A large file that may take time to read")
23//!     .handler(|| async {
24//!         // Simulate slow read
25//!         Ok(ReadResourceResult {
26//!             contents: vec![ResourceContent {
27//!                 uri: "file:///large-file.txt".to_string(),
28//!                 mime_type: Some("text/plain".to_string()),
29//!                 text: Some("content".to_string()),
30//!                 blob: None,
31//!             }],
32//!         })
33//!     })
34//!     .layer(TimeoutLayer::new(Duration::from_secs(30)))
35//!     .build();
36//! ```
37//!
38//! # Resource Templates
39//!
40//! Resource templates allow servers to expose parameterized resources using URI templates.
41//! When a client requests `resources/read` with a URI matching a template, the server
42//! extracts the variables and passes them to the handler.
43//!
44//! ```rust
45//! use tower_mcp::resource::ResourceTemplateBuilder;
46//! use tower_mcp::protocol::{ReadResourceResult, ResourceContent};
47//! use std::collections::HashMap;
48//!
49//! let template = ResourceTemplateBuilder::new("file:///{path}")
50//!     .name("Project Files")
51//!     .description("Access files in the project directory")
52//!     .handler(|uri: String, vars: HashMap<String, String>| async move {
53//!         let path = vars.get("path").unwrap_or(&String::new()).clone();
54//!         Ok(ReadResourceResult {
55//!             contents: vec![ResourceContent {
56//!                 uri,
57//!                 mime_type: Some("text/plain".to_string()),
58//!                 text: Some(format!("Contents of {}", path)),
59//!                 blob: None,
60//!             }],
61//!         })
62//!     });
63//! ```
64
65use std::collections::HashMap;
66use std::convert::Infallible;
67use std::fmt;
68use std::future::Future;
69use std::pin::Pin;
70use std::sync::Arc;
71use std::task::{Context, Poll};
72
73use tower::util::BoxCloneService;
74use tower_service::Service;
75
76use crate::context::RequestContext;
77use crate::error::{Error, Result};
78use crate::protocol::{
79    ReadResourceResult, ResourceContent, ResourceDefinition, ResourceTemplateDefinition, ToolIcon,
80};
81
82// =============================================================================
83// Service Types for Per-Resource Middleware
84// =============================================================================
85
86/// Request type for resource services.
87///
88/// Contains the request context (for progress reporting, cancellation, etc.)
89/// and the resource URI being read.
90#[derive(Debug, Clone)]
91pub struct ResourceRequest {
92    /// Request context for progress reporting, cancellation, and client requests
93    pub ctx: RequestContext,
94    /// The URI of the resource being read
95    pub uri: String,
96}
97
98impl ResourceRequest {
99    /// Create a new resource request
100    pub fn new(ctx: RequestContext, uri: String) -> Self {
101        Self { ctx, uri }
102    }
103}
104
105/// A boxed, cloneable resource service with `Error = Infallible`.
106///
107/// This is the internal service type that resources use. Middleware errors are
108/// caught and converted to error results, so the service never fails at the Tower level.
109pub type BoxResourceService = BoxCloneService<ResourceRequest, ReadResourceResult, Infallible>;
110
111/// Catches errors from the inner service and converts them to error results.
112///
113/// This wrapper ensures that middleware errors (e.g., timeouts, rate limits)
114/// and handler errors are converted to `Err(Error)` responses wrapped in
115/// `Ok`, rather than propagating as Tower service errors.
116pub struct ResourceCatchError<S> {
117    inner: S,
118}
119
120impl<S> ResourceCatchError<S> {
121    /// Create a new `ResourceCatchError` wrapping the given service.
122    pub fn new(inner: S) -> Self {
123        Self { inner }
124    }
125}
126
127impl<S: Clone> Clone for ResourceCatchError<S> {
128    fn clone(&self) -> Self {
129        Self {
130            inner: self.inner.clone(),
131        }
132    }
133}
134
135impl<S: fmt::Debug> fmt::Debug for ResourceCatchError<S> {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        f.debug_struct("ResourceCatchError")
138            .field("inner", &self.inner)
139            .finish()
140    }
141}
142
143impl<S> Service<ResourceRequest> for ResourceCatchError<S>
144where
145    S: Service<ResourceRequest, Response = ReadResourceResult> + Clone + Send + 'static,
146    S::Error: fmt::Display + Send,
147    S::Future: Send,
148{
149    type Response = ReadResourceResult;
150    type Error = Infallible;
151    type Future =
152        Pin<Box<dyn Future<Output = std::result::Result<ReadResourceResult, Infallible>> + Send>>;
153
154    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<std::result::Result<(), Self::Error>> {
155        // Map any readiness error to Infallible (we catch it on call)
156        match self.inner.poll_ready(cx) {
157            Poll::Ready(Ok(())) => Poll::Ready(Ok(())),
158            Poll::Ready(Err(_)) => Poll::Ready(Ok(())),
159            Poll::Pending => Poll::Pending,
160        }
161    }
162
163    fn call(&mut self, req: ResourceRequest) -> Self::Future {
164        let uri = req.uri.clone();
165        let fut = self.inner.call(req);
166
167        Box::pin(async move {
168            match fut.await {
169                Ok(result) => Ok(result),
170                Err(err) => {
171                    // Return an error result with the error message
172                    Ok(ReadResourceResult {
173                        contents: vec![ResourceContent {
174                            uri,
175                            mime_type: Some("text/plain".to_string()),
176                            text: Some(format!("Error reading resource: {}", err)),
177                            blob: None,
178                        }],
179                    })
180                }
181            }
182        })
183    }
184}
185
186/// A boxed future for resource handlers
187pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
188
189/// Resource handler trait - the core abstraction for resource reading
190pub trait ResourceHandler: Send + Sync {
191    /// Read the resource contents
192    fn read(&self) -> BoxFuture<'_, Result<ReadResourceResult>>;
193
194    /// Read the resource with request context for progress/cancellation support
195    ///
196    /// The default implementation ignores the context and calls `read`.
197    /// Override this to receive progress/cancellation context.
198    fn read_with_context(&self, _ctx: RequestContext) -> BoxFuture<'_, Result<ReadResourceResult>> {
199        self.read()
200    }
201
202    /// Returns true if this handler uses context (for optimization)
203    fn uses_context(&self) -> bool {
204        false
205    }
206}
207
208/// Adapts a `ResourceHandler` to a Tower `Service<ResourceRequest>`.
209///
210/// This is an internal adapter that bridges the handler abstraction to the
211/// service abstraction, enabling middleware composition.
212struct ResourceHandlerService<H> {
213    handler: Arc<H>,
214}
215
216impl<H> ResourceHandlerService<H> {
217    fn new(handler: H) -> Self {
218        Self {
219            handler: Arc::new(handler),
220        }
221    }
222}
223
224impl<H> Clone for ResourceHandlerService<H> {
225    fn clone(&self) -> Self {
226        Self {
227            handler: self.handler.clone(),
228        }
229    }
230}
231
232impl<H> fmt::Debug for ResourceHandlerService<H> {
233    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234        f.debug_struct("ResourceHandlerService")
235            .finish_non_exhaustive()
236    }
237}
238
239impl<H> Service<ResourceRequest> for ResourceHandlerService<H>
240where
241    H: ResourceHandler + 'static,
242{
243    type Response = ReadResourceResult;
244    type Error = Error;
245    type Future =
246        Pin<Box<dyn Future<Output = std::result::Result<ReadResourceResult, Error>> + Send>>;
247
248    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<std::result::Result<(), Self::Error>> {
249        Poll::Ready(Ok(()))
250    }
251
252    fn call(&mut self, req: ResourceRequest) -> Self::Future {
253        let handler = self.handler.clone();
254        Box::pin(async move { handler.read_with_context(req.ctx).await })
255    }
256}
257
258/// A complete resource definition with service-based execution.
259///
260/// Resources are implemented as Tower services internally, enabling middleware
261/// composition via the builder's `.layer()` method. The service is wrapped
262/// in [`ResourceCatchError`] to convert any errors (from handlers or middleware)
263/// into error result responses.
264pub struct Resource {
265    /// Resource URI
266    pub uri: String,
267    /// Human-readable name
268    pub name: String,
269    /// Human-readable title for display purposes
270    pub title: Option<String>,
271    /// Optional description
272    pub description: Option<String>,
273    /// Optional MIME type
274    pub mime_type: Option<String>,
275    /// Optional icons for display in user interfaces
276    pub icons: Option<Vec<ToolIcon>>,
277    /// Optional size in bytes
278    pub size: Option<u64>,
279    /// The boxed service that reads the resource
280    service: BoxResourceService,
281}
282
283impl Clone for Resource {
284    fn clone(&self) -> Self {
285        Self {
286            uri: self.uri.clone(),
287            name: self.name.clone(),
288            title: self.title.clone(),
289            description: self.description.clone(),
290            mime_type: self.mime_type.clone(),
291            icons: self.icons.clone(),
292            size: self.size,
293            service: self.service.clone(),
294        }
295    }
296}
297
298impl std::fmt::Debug for Resource {
299    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300        f.debug_struct("Resource")
301            .field("uri", &self.uri)
302            .field("name", &self.name)
303            .field("title", &self.title)
304            .field("description", &self.description)
305            .field("mime_type", &self.mime_type)
306            .field("icons", &self.icons)
307            .field("size", &self.size)
308            .finish_non_exhaustive()
309    }
310}
311
312// SAFETY: BoxCloneService is Send + Sync (tower provides unsafe impl Sync),
313// and all other fields in Resource are Send + Sync.
314unsafe impl Send for Resource {}
315unsafe impl Sync for Resource {}
316
317impl Resource {
318    /// Create a new resource builder
319    pub fn builder(uri: impl Into<String>) -> ResourceBuilder {
320        ResourceBuilder::new(uri)
321    }
322
323    /// Get the resource definition for resources/list
324    pub fn definition(&self) -> ResourceDefinition {
325        ResourceDefinition {
326            uri: self.uri.clone(),
327            name: self.name.clone(),
328            title: self.title.clone(),
329            description: self.description.clone(),
330            mime_type: self.mime_type.clone(),
331            icons: self.icons.clone(),
332            size: self.size,
333        }
334    }
335
336    /// Read the resource without context
337    ///
338    /// Creates a dummy request context. For full context support, use
339    /// [`read_with_context`](Self::read_with_context).
340    pub fn read(&self) -> BoxFuture<'static, ReadResourceResult> {
341        let ctx = RequestContext::new(crate::protocol::RequestId::Number(0));
342        self.read_with_context(ctx)
343    }
344
345    /// Read the resource with request context
346    ///
347    /// The context provides progress reporting, cancellation support, and
348    /// access to client requests (for sampling, etc.).
349    ///
350    /// # Note
351    ///
352    /// This method returns `ReadResourceResult` directly (not `Result<ReadResourceResult>`).
353    /// Any errors from the handler or middleware are converted to error responses
354    /// in the result contents.
355    pub fn read_with_context(&self, ctx: RequestContext) -> BoxFuture<'static, ReadResourceResult> {
356        use tower::ServiceExt;
357        let service = self.service.clone();
358        let uri = self.uri.clone();
359        Box::pin(async move {
360            // ServiceExt::oneshot properly handles poll_ready before call
361            // Service is Infallible, so unwrap is safe
362            service
363                .oneshot(ResourceRequest::new(ctx, uri))
364                .await
365                .unwrap()
366        })
367    }
368
369    /// Create a resource from a handler (internal helper)
370    #[allow(clippy::too_many_arguments)]
371    fn from_handler<H: ResourceHandler + 'static>(
372        uri: String,
373        name: String,
374        title: Option<String>,
375        description: Option<String>,
376        mime_type: Option<String>,
377        icons: Option<Vec<ToolIcon>>,
378        size: Option<u64>,
379        handler: H,
380    ) -> Self {
381        let handler_service = ResourceHandlerService::new(handler);
382        let catch_error = ResourceCatchError::new(handler_service);
383        let service = BoxCloneService::new(catch_error);
384
385        Self {
386            uri,
387            name,
388            title,
389            description,
390            mime_type,
391            icons,
392            size,
393            service,
394        }
395    }
396}
397
398// =============================================================================
399// Builder API
400// =============================================================================
401
402/// Builder for creating resources with a fluent API
403///
404/// # Example
405///
406/// ```rust
407/// use tower_mcp::resource::ResourceBuilder;
408/// use tower_mcp::protocol::{ReadResourceResult, ResourceContent};
409///
410/// let resource = ResourceBuilder::new("file:///config.json")
411///     .name("Configuration")
412///     .description("Application configuration file")
413///     .mime_type("application/json")
414///     .handler(|| async {
415///         Ok(ReadResourceResult {
416///             contents: vec![ResourceContent {
417///                 uri: "file:///config.json".to_string(),
418///                 mime_type: Some("application/json".to_string()),
419///                 text: Some(r#"{"setting": "value"}"#.to_string()),
420///                 blob: None,
421///             }],
422///         })
423///     })
424///     .build();
425///
426/// assert_eq!(resource.uri, "file:///config.json");
427/// ```
428pub struct ResourceBuilder {
429    uri: String,
430    name: Option<String>,
431    title: Option<String>,
432    description: Option<String>,
433    mime_type: Option<String>,
434    icons: Option<Vec<ToolIcon>>,
435    size: Option<u64>,
436}
437
438impl ResourceBuilder {
439    pub fn new(uri: impl Into<String>) -> Self {
440        Self {
441            uri: uri.into(),
442            name: None,
443            title: None,
444            description: None,
445            mime_type: None,
446            icons: None,
447            size: None,
448        }
449    }
450
451    /// Set the resource name (human-readable)
452    pub fn name(mut self, name: impl Into<String>) -> Self {
453        self.name = Some(name.into());
454        self
455    }
456
457    /// Set a human-readable title for the resource
458    pub fn title(mut self, title: impl Into<String>) -> Self {
459        self.title = Some(title.into());
460        self
461    }
462
463    /// Set the resource description
464    pub fn description(mut self, description: impl Into<String>) -> Self {
465        self.description = Some(description.into());
466        self
467    }
468
469    /// Set the MIME type of the resource
470    pub fn mime_type(mut self, mime_type: impl Into<String>) -> Self {
471        self.mime_type = Some(mime_type.into());
472        self
473    }
474
475    /// Add an icon for the resource
476    pub fn icon(mut self, src: impl Into<String>) -> Self {
477        self.icons.get_or_insert_with(Vec::new).push(ToolIcon {
478            src: src.into(),
479            mime_type: None,
480            sizes: None,
481        });
482        self
483    }
484
485    /// Add an icon with metadata
486    pub fn icon_with_meta(
487        mut self,
488        src: impl Into<String>,
489        mime_type: Option<String>,
490        sizes: Option<Vec<String>>,
491    ) -> Self {
492        self.icons.get_or_insert_with(Vec::new).push(ToolIcon {
493            src: src.into(),
494            mime_type,
495            sizes,
496        });
497        self
498    }
499
500    /// Set the size of the resource in bytes
501    pub fn size(mut self, size: u64) -> Self {
502        self.size = Some(size);
503        self
504    }
505
506    /// Set the handler function for reading the resource.
507    ///
508    /// Returns a [`ResourceBuilderWithHandler`] that can be used to apply
509    /// middleware layers via `.layer()` or build the resource directly via `.build()`.
510    pub fn handler<F, Fut>(self, handler: F) -> ResourceBuilderWithHandler<F>
511    where
512        F: Fn() -> Fut + Send + Sync + 'static,
513        Fut: Future<Output = Result<ReadResourceResult>> + Send + 'static,
514    {
515        ResourceBuilderWithHandler {
516            uri: self.uri,
517            name: self.name,
518            title: self.title,
519            description: self.description,
520            mime_type: self.mime_type,
521            icons: self.icons,
522            size: self.size,
523            handler,
524        }
525    }
526
527    /// Set a context-aware handler for reading the resource.
528    ///
529    /// The handler receives a `RequestContext` for progress reporting and
530    /// cancellation checking.
531    ///
532    /// Returns a [`ResourceBuilderWithContextHandler`] that can be used to apply
533    /// middleware layers via `.layer()` or build the resource directly via `.build()`.
534    pub fn handler_with_context<F, Fut>(self, handler: F) -> ResourceBuilderWithContextHandler<F>
535    where
536        F: Fn(RequestContext) -> Fut + Send + Sync + 'static,
537        Fut: Future<Output = Result<ReadResourceResult>> + Send + 'static,
538    {
539        ResourceBuilderWithContextHandler {
540            uri: self.uri,
541            name: self.name,
542            title: self.title,
543            description: self.description,
544            mime_type: self.mime_type,
545            icons: self.icons,
546            size: self.size,
547            handler,
548        }
549    }
550
551    /// Create a static text resource (convenience method)
552    pub fn text(self, content: impl Into<String>) -> Resource {
553        let uri = self.uri.clone();
554        let content = content.into();
555        let mime_type = self.mime_type.clone();
556
557        self.handler(move || {
558            let uri = uri.clone();
559            let content = content.clone();
560            let mime_type = mime_type.clone();
561            async move {
562                Ok(ReadResourceResult {
563                    contents: vec![ResourceContent {
564                        uri,
565                        mime_type,
566                        text: Some(content),
567                        blob: None,
568                    }],
569                })
570            }
571        })
572        .build()
573    }
574
575    /// Create a static JSON resource (convenience method)
576    pub fn json(mut self, value: serde_json::Value) -> Resource {
577        let uri = self.uri.clone();
578        self.mime_type = Some("application/json".to_string());
579        let text = serde_json::to_string_pretty(&value).unwrap_or_else(|_| "{}".to_string());
580
581        self.handler(move || {
582            let uri = uri.clone();
583            let text = text.clone();
584            async move {
585                Ok(ReadResourceResult {
586                    contents: vec![ResourceContent {
587                        uri,
588                        mime_type: Some("application/json".to_string()),
589                        text: Some(text),
590                        blob: None,
591                    }],
592                })
593            }
594        })
595        .build()
596    }
597}
598
599/// Builder state after handler is specified.
600///
601/// This builder allows applying middleware layers via `.layer()` or building
602/// the resource directly via `.build()`.
603pub struct ResourceBuilderWithHandler<F> {
604    uri: String,
605    name: Option<String>,
606    title: Option<String>,
607    description: Option<String>,
608    mime_type: Option<String>,
609    icons: Option<Vec<ToolIcon>>,
610    size: Option<u64>,
611    handler: F,
612}
613
614impl<F, Fut> ResourceBuilderWithHandler<F>
615where
616    F: Fn() -> Fut + Send + Sync + 'static,
617    Fut: Future<Output = Result<ReadResourceResult>> + Send + 'static,
618{
619    /// Build the resource without any middleware layers.
620    pub fn build(self) -> Resource {
621        let name = self.name.unwrap_or_else(|| self.uri.clone());
622
623        Resource::from_handler(
624            self.uri,
625            name,
626            self.title,
627            self.description,
628            self.mime_type,
629            self.icons,
630            self.size,
631            FnHandler {
632                handler: self.handler,
633            },
634        )
635    }
636
637    /// Apply a Tower layer (middleware) to this resource.
638    ///
639    /// The layer wraps the resource's handler service, enabling functionality like
640    /// timeouts, rate limiting, and metrics collection at the per-resource level.
641    ///
642    /// # Example
643    ///
644    /// ```rust
645    /// use std::time::Duration;
646    /// use tower::timeout::TimeoutLayer;
647    /// use tower_mcp::resource::ResourceBuilder;
648    /// use tower_mcp::protocol::{ReadResourceResult, ResourceContent};
649    ///
650    /// let resource = ResourceBuilder::new("file:///slow.txt")
651    ///     .name("Slow Resource")
652    ///     .handler(|| async {
653    ///         Ok(ReadResourceResult {
654    ///             contents: vec![ResourceContent {
655    ///                 uri: "file:///slow.txt".to_string(),
656    ///                 mime_type: Some("text/plain".to_string()),
657    ///                 text: Some("content".to_string()),
658    ///                 blob: None,
659    ///             }],
660    ///         })
661    ///     })
662    ///     .layer(TimeoutLayer::new(Duration::from_secs(30)))
663    ///     .build();
664    /// ```
665    pub fn layer<L>(self, layer: L) -> ResourceBuilderWithLayer<F, L> {
666        ResourceBuilderWithLayer {
667            uri: self.uri,
668            name: self.name,
669            title: self.title,
670            description: self.description,
671            mime_type: self.mime_type,
672            icons: self.icons,
673            size: self.size,
674            handler: self.handler,
675            layer,
676        }
677    }
678}
679
680/// Builder state after a layer has been applied to the handler.
681///
682/// This builder allows chaining additional layers and building the final resource.
683pub struct ResourceBuilderWithLayer<F, L> {
684    uri: String,
685    name: Option<String>,
686    title: Option<String>,
687    description: Option<String>,
688    mime_type: Option<String>,
689    icons: Option<Vec<ToolIcon>>,
690    size: Option<u64>,
691    handler: F,
692    layer: L,
693}
694
695// Allow private_bounds because these internal types (ResourceHandlerService, FnHandler, etc.)
696// are implementation details that users don't interact with directly.
697#[allow(private_bounds)]
698impl<F, Fut, L> ResourceBuilderWithLayer<F, L>
699where
700    F: Fn() -> Fut + Send + Sync + 'static,
701    Fut: Future<Output = Result<ReadResourceResult>> + Send + 'static,
702    L: tower::Layer<ResourceHandlerService<FnHandler<F>>> + Clone + Send + Sync + 'static,
703    L::Service: Service<ResourceRequest, Response = ReadResourceResult> + Clone + Send + 'static,
704    <L::Service as Service<ResourceRequest>>::Error: fmt::Display + Send,
705    <L::Service as Service<ResourceRequest>>::Future: Send,
706{
707    /// Build the resource with the applied layer(s).
708    pub fn build(self) -> Resource {
709        let name = self.name.unwrap_or_else(|| self.uri.clone());
710
711        let handler_service = ResourceHandlerService::new(FnHandler {
712            handler: self.handler,
713        });
714        let layered = self.layer.layer(handler_service);
715        let catch_error = ResourceCatchError::new(layered);
716        let service = BoxCloneService::new(catch_error);
717
718        Resource {
719            uri: self.uri,
720            name,
721            title: self.title,
722            description: self.description,
723            mime_type: self.mime_type,
724            icons: self.icons,
725            size: self.size,
726            service,
727        }
728    }
729
730    /// Apply an additional Tower layer (middleware).
731    ///
732    /// Layers are applied in order, with earlier layers wrapping later ones.
733    /// This means the first layer added is the outermost middleware.
734    pub fn layer<L2>(
735        self,
736        layer: L2,
737    ) -> ResourceBuilderWithLayer<F, tower::layer::util::Stack<L2, L>> {
738        ResourceBuilderWithLayer {
739            uri: self.uri,
740            name: self.name,
741            title: self.title,
742            description: self.description,
743            mime_type: self.mime_type,
744            icons: self.icons,
745            size: self.size,
746            handler: self.handler,
747            layer: tower::layer::util::Stack::new(layer, self.layer),
748        }
749    }
750}
751
752/// Builder state after context-aware handler is specified.
753pub struct ResourceBuilderWithContextHandler<F> {
754    uri: String,
755    name: Option<String>,
756    title: Option<String>,
757    description: Option<String>,
758    mime_type: Option<String>,
759    icons: Option<Vec<ToolIcon>>,
760    size: Option<u64>,
761    handler: F,
762}
763
764impl<F, Fut> ResourceBuilderWithContextHandler<F>
765where
766    F: Fn(RequestContext) -> Fut + Send + Sync + 'static,
767    Fut: Future<Output = Result<ReadResourceResult>> + Send + 'static,
768{
769    /// Build the resource without any middleware layers.
770    pub fn build(self) -> Resource {
771        let name = self.name.unwrap_or_else(|| self.uri.clone());
772
773        Resource::from_handler(
774            self.uri,
775            name,
776            self.title,
777            self.description,
778            self.mime_type,
779            self.icons,
780            self.size,
781            ContextAwareHandler {
782                handler: self.handler,
783            },
784        )
785    }
786
787    /// Apply a Tower layer (middleware) to this resource.
788    ///
789    /// Works the same as [`ResourceBuilderWithHandler::layer`].
790    pub fn layer<L>(self, layer: L) -> ResourceBuilderWithContextLayer<F, L> {
791        ResourceBuilderWithContextLayer {
792            uri: self.uri,
793            name: self.name,
794            title: self.title,
795            description: self.description,
796            mime_type: self.mime_type,
797            icons: self.icons,
798            size: self.size,
799            handler: self.handler,
800            layer,
801        }
802    }
803}
804
805/// Builder state after a layer has been applied to a context-aware handler.
806pub struct ResourceBuilderWithContextLayer<F, L> {
807    uri: String,
808    name: Option<String>,
809    title: Option<String>,
810    description: Option<String>,
811    mime_type: Option<String>,
812    icons: Option<Vec<ToolIcon>>,
813    size: Option<u64>,
814    handler: F,
815    layer: L,
816}
817
818// Allow private_bounds because these internal types are implementation details.
819#[allow(private_bounds)]
820impl<F, Fut, L> ResourceBuilderWithContextLayer<F, L>
821where
822    F: Fn(RequestContext) -> Fut + Send + Sync + 'static,
823    Fut: Future<Output = Result<ReadResourceResult>> + Send + 'static,
824    L: tower::Layer<ResourceHandlerService<ContextAwareHandler<F>>> + Clone + Send + Sync + 'static,
825    L::Service: Service<ResourceRequest, Response = ReadResourceResult> + Clone + Send + 'static,
826    <L::Service as Service<ResourceRequest>>::Error: fmt::Display + Send,
827    <L::Service as Service<ResourceRequest>>::Future: Send,
828{
829    /// Build the resource with the applied layer(s).
830    pub fn build(self) -> Resource {
831        let name = self.name.unwrap_or_else(|| self.uri.clone());
832
833        let handler_service = ResourceHandlerService::new(ContextAwareHandler {
834            handler: self.handler,
835        });
836        let layered = self.layer.layer(handler_service);
837        let catch_error = ResourceCatchError::new(layered);
838        let service = BoxCloneService::new(catch_error);
839
840        Resource {
841            uri: self.uri,
842            name,
843            title: self.title,
844            description: self.description,
845            mime_type: self.mime_type,
846            icons: self.icons,
847            size: self.size,
848            service,
849        }
850    }
851
852    /// Apply an additional Tower layer (middleware).
853    pub fn layer<L2>(
854        self,
855        layer: L2,
856    ) -> ResourceBuilderWithContextLayer<F, tower::layer::util::Stack<L2, L>> {
857        ResourceBuilderWithContextLayer {
858            uri: self.uri,
859            name: self.name,
860            title: self.title,
861            description: self.description,
862            mime_type: self.mime_type,
863            icons: self.icons,
864            size: self.size,
865            handler: self.handler,
866            layer: tower::layer::util::Stack::new(layer, self.layer),
867        }
868    }
869}
870
871// =============================================================================
872// Handler implementations
873// =============================================================================
874
875/// Handler wrapping a function
876struct FnHandler<F> {
877    handler: F,
878}
879
880impl<F, Fut> ResourceHandler for FnHandler<F>
881where
882    F: Fn() -> Fut + Send + Sync + 'static,
883    Fut: Future<Output = Result<ReadResourceResult>> + Send + 'static,
884{
885    fn read(&self) -> BoxFuture<'_, Result<ReadResourceResult>> {
886        Box::pin((self.handler)())
887    }
888}
889
890/// Handler that receives request context
891struct ContextAwareHandler<F> {
892    handler: F,
893}
894
895impl<F, Fut> ResourceHandler for ContextAwareHandler<F>
896where
897    F: Fn(RequestContext) -> Fut + Send + Sync + 'static,
898    Fut: Future<Output = Result<ReadResourceResult>> + Send + 'static,
899{
900    fn read(&self) -> BoxFuture<'_, Result<ReadResourceResult>> {
901        let ctx = RequestContext::new(crate::protocol::RequestId::Number(0));
902        self.read_with_context(ctx)
903    }
904
905    fn read_with_context(&self, ctx: RequestContext) -> BoxFuture<'_, Result<ReadResourceResult>> {
906        Box::pin((self.handler)(ctx))
907    }
908
909    fn uses_context(&self) -> bool {
910        true
911    }
912}
913
914// =============================================================================
915// Trait-based resource definition
916// =============================================================================
917
918/// Trait for defining resources with full control
919///
920/// Implement this trait when you need more control than the builder provides,
921/// or when you want to define resources as standalone types.
922///
923/// # Example
924///
925/// ```rust
926/// use tower_mcp::resource::McpResource;
927/// use tower_mcp::protocol::{ReadResourceResult, ResourceContent};
928/// use tower_mcp::error::Result;
929///
930/// struct ConfigResource {
931///     config: String,
932/// }
933///
934/// impl McpResource for ConfigResource {
935///     const URI: &'static str = "file:///config.json";
936///     const NAME: &'static str = "Configuration";
937///     const DESCRIPTION: Option<&'static str> = Some("Application configuration");
938///     const MIME_TYPE: Option<&'static str> = Some("application/json");
939///
940///     async fn read(&self) -> Result<ReadResourceResult> {
941///         Ok(ReadResourceResult {
942///             contents: vec![ResourceContent {
943///                 uri: Self::URI.to_string(),
944///                 mime_type: Self::MIME_TYPE.map(|s| s.to_string()),
945///                 text: Some(self.config.clone()),
946///                 blob: None,
947///             }],
948///         })
949///     }
950/// }
951///
952/// let resource = ConfigResource { config: "{}".to_string() }.into_resource();
953/// assert_eq!(resource.uri, "file:///config.json");
954/// ```
955pub trait McpResource: Send + Sync + 'static {
956    const URI: &'static str;
957    const NAME: &'static str;
958    const DESCRIPTION: Option<&'static str> = None;
959    const MIME_TYPE: Option<&'static str> = None;
960
961    fn read(&self) -> impl Future<Output = Result<ReadResourceResult>> + Send;
962
963    /// Convert to a Resource instance
964    fn into_resource(self) -> Resource
965    where
966        Self: Sized,
967    {
968        let resource = Arc::new(self);
969        Resource::from_handler(
970            Self::URI.to_string(),
971            Self::NAME.to_string(),
972            None,
973            Self::DESCRIPTION.map(|s| s.to_string()),
974            Self::MIME_TYPE.map(|s| s.to_string()),
975            None,
976            None,
977            McpResourceHandler { resource },
978        )
979    }
980}
981
982/// Wrapper to make McpResource implement ResourceHandler
983struct McpResourceHandler<T: McpResource> {
984    resource: Arc<T>,
985}
986
987impl<T: McpResource> ResourceHandler for McpResourceHandler<T> {
988    fn read(&self) -> BoxFuture<'_, Result<ReadResourceResult>> {
989        let resource = self.resource.clone();
990        Box::pin(async move { resource.read().await })
991    }
992}
993
994// =============================================================================
995// Resource Templates
996// =============================================================================
997
998/// Handler trait for resource templates
999///
1000/// Unlike [`ResourceHandler`], template handlers receive the extracted
1001/// URI variables as a parameter.
1002pub trait ResourceTemplateHandler: Send + Sync {
1003    /// Read a resource with the given URI variables extracted from the template
1004    fn read(
1005        &self,
1006        uri: &str,
1007        variables: HashMap<String, String>,
1008    ) -> BoxFuture<'_, Result<ReadResourceResult>>;
1009}
1010
1011/// A parameterized resource template
1012///
1013/// Resource templates use URI template syntax (RFC 6570) to match multiple URIs
1014/// and extract variable values. This allows servers to expose dynamic resources
1015/// like file systems or database records.
1016///
1017/// # Example
1018///
1019/// ```rust
1020/// use tower_mcp::resource::ResourceTemplateBuilder;
1021/// use tower_mcp::protocol::{ReadResourceResult, ResourceContent};
1022/// use std::collections::HashMap;
1023///
1024/// let template = ResourceTemplateBuilder::new("file:///{path}")
1025///     .name("Project Files")
1026///     .handler(|uri: String, vars: HashMap<String, String>| async move {
1027///         let path = vars.get("path").unwrap_or(&String::new()).clone();
1028///         Ok(ReadResourceResult {
1029///             contents: vec![ResourceContent {
1030///                 uri,
1031///                 mime_type: Some("text/plain".to_string()),
1032///                 text: Some(format!("Contents of {}", path)),
1033///                 blob: None,
1034///             }],
1035///         })
1036///     });
1037/// ```
1038pub struct ResourceTemplate {
1039    /// The URI template pattern (e.g., `file:///{path}`)
1040    pub uri_template: String,
1041    /// Human-readable name
1042    pub name: String,
1043    /// Human-readable title for display purposes
1044    pub title: Option<String>,
1045    /// Optional description
1046    pub description: Option<String>,
1047    /// Optional MIME type hint
1048    pub mime_type: Option<String>,
1049    /// Optional icons for display in user interfaces
1050    pub icons: Option<Vec<ToolIcon>>,
1051    /// Compiled regex for matching URIs
1052    pattern: regex::Regex,
1053    /// Variable names in order of appearance
1054    variables: Vec<String>,
1055    /// Handler for reading matched resources
1056    handler: Arc<dyn ResourceTemplateHandler>,
1057}
1058
1059impl Clone for ResourceTemplate {
1060    fn clone(&self) -> Self {
1061        Self {
1062            uri_template: self.uri_template.clone(),
1063            name: self.name.clone(),
1064            title: self.title.clone(),
1065            description: self.description.clone(),
1066            mime_type: self.mime_type.clone(),
1067            icons: self.icons.clone(),
1068            pattern: self.pattern.clone(),
1069            variables: self.variables.clone(),
1070            handler: self.handler.clone(),
1071        }
1072    }
1073}
1074
1075impl std::fmt::Debug for ResourceTemplate {
1076    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1077        f.debug_struct("ResourceTemplate")
1078            .field("uri_template", &self.uri_template)
1079            .field("name", &self.name)
1080            .field("title", &self.title)
1081            .field("description", &self.description)
1082            .field("mime_type", &self.mime_type)
1083            .field("icons", &self.icons)
1084            .field("variables", &self.variables)
1085            .finish_non_exhaustive()
1086    }
1087}
1088
1089impl ResourceTemplate {
1090    /// Create a new resource template builder
1091    pub fn builder(uri_template: impl Into<String>) -> ResourceTemplateBuilder {
1092        ResourceTemplateBuilder::new(uri_template)
1093    }
1094
1095    /// Get the template definition for resources/templates/list
1096    pub fn definition(&self) -> ResourceTemplateDefinition {
1097        ResourceTemplateDefinition {
1098            uri_template: self.uri_template.clone(),
1099            name: self.name.clone(),
1100            title: self.title.clone(),
1101            description: self.description.clone(),
1102            mime_type: self.mime_type.clone(),
1103            icons: self.icons.clone(),
1104        }
1105    }
1106
1107    /// Check if a URI matches this template and extract variables
1108    ///
1109    /// Returns `Some(HashMap)` with extracted variables if the URI matches,
1110    /// `None` if it doesn't match.
1111    pub fn match_uri(&self, uri: &str) -> Option<HashMap<String, String>> {
1112        self.pattern.captures(uri).map(|caps| {
1113            self.variables
1114                .iter()
1115                .enumerate()
1116                .filter_map(|(i, name)| {
1117                    caps.get(i + 1)
1118                        .map(|m| (name.clone(), m.as_str().to_string()))
1119                })
1120                .collect()
1121        })
1122    }
1123
1124    /// Read a resource at the given URI using this template's handler
1125    ///
1126    /// # Arguments
1127    ///
1128    /// * `uri` - The actual URI being read
1129    /// * `variables` - Variables extracted from matching the URI against the template
1130    pub fn read(
1131        &self,
1132        uri: &str,
1133        variables: HashMap<String, String>,
1134    ) -> BoxFuture<'_, Result<ReadResourceResult>> {
1135        self.handler.read(uri, variables)
1136    }
1137}
1138
1139/// Builder for creating resource templates
1140///
1141/// # Example
1142///
1143/// ```rust
1144/// use tower_mcp::resource::ResourceTemplateBuilder;
1145/// use tower_mcp::protocol::{ReadResourceResult, ResourceContent};
1146/// use std::collections::HashMap;
1147///
1148/// let template = ResourceTemplateBuilder::new("db://users/{id}")
1149///     .name("User Records")
1150///     .description("Access user records by ID")
1151///     .handler(|uri: String, vars: HashMap<String, String>| async move {
1152///         let id = vars.get("id").unwrap();
1153///         Ok(ReadResourceResult {
1154///             contents: vec![ResourceContent {
1155///                 uri,
1156///                 mime_type: Some("application/json".to_string()),
1157///                 text: Some(format!(r#"{{"id": "{}"}}"#, id)),
1158///                 blob: None,
1159///             }],
1160///         })
1161///     });
1162/// ```
1163pub struct ResourceTemplateBuilder {
1164    uri_template: String,
1165    name: Option<String>,
1166    title: Option<String>,
1167    description: Option<String>,
1168    mime_type: Option<String>,
1169    icons: Option<Vec<ToolIcon>>,
1170}
1171
1172impl ResourceTemplateBuilder {
1173    /// Create a new builder with the given URI template
1174    ///
1175    /// # URI Template Syntax
1176    ///
1177    /// Templates use RFC 6570 Level 1 syntax with simple variable expansion:
1178    /// - `{varname}` - Matches any non-slash characters
1179    ///
1180    /// # Examples
1181    ///
1182    /// - `file:///{path}` - Matches `file:///README.md`
1183    /// - `db://users/{id}` - Matches `db://users/123`
1184    /// - `api://v1/{resource}/{id}` - Matches `api://v1/posts/456`
1185    pub fn new(uri_template: impl Into<String>) -> Self {
1186        Self {
1187            uri_template: uri_template.into(),
1188            name: None,
1189            title: None,
1190            description: None,
1191            mime_type: None,
1192            icons: None,
1193        }
1194    }
1195
1196    /// Set the human-readable name for this template
1197    pub fn name(mut self, name: impl Into<String>) -> Self {
1198        self.name = Some(name.into());
1199        self
1200    }
1201
1202    /// Set a human-readable title for the template
1203    pub fn title(mut self, title: impl Into<String>) -> Self {
1204        self.title = Some(title.into());
1205        self
1206    }
1207
1208    /// Set the description for this template
1209    pub fn description(mut self, description: impl Into<String>) -> Self {
1210        self.description = Some(description.into());
1211        self
1212    }
1213
1214    /// Set the MIME type hint for resources from this template
1215    pub fn mime_type(mut self, mime_type: impl Into<String>) -> Self {
1216        self.mime_type = Some(mime_type.into());
1217        self
1218    }
1219
1220    /// Add an icon for the template
1221    pub fn icon(mut self, src: impl Into<String>) -> Self {
1222        self.icons.get_or_insert_with(Vec::new).push(ToolIcon {
1223            src: src.into(),
1224            mime_type: None,
1225            sizes: None,
1226        });
1227        self
1228    }
1229
1230    /// Add an icon with metadata
1231    pub fn icon_with_meta(
1232        mut self,
1233        src: impl Into<String>,
1234        mime_type: Option<String>,
1235        sizes: Option<Vec<String>>,
1236    ) -> Self {
1237        self.icons.get_or_insert_with(Vec::new).push(ToolIcon {
1238            src: src.into(),
1239            mime_type,
1240            sizes,
1241        });
1242        self
1243    }
1244
1245    /// Set the handler function for reading template resources
1246    ///
1247    /// The handler receives:
1248    /// - `uri`: The full URI being read
1249    /// - `variables`: A map of variable names to their values extracted from the URI
1250    pub fn handler<F, Fut>(self, handler: F) -> ResourceTemplate
1251    where
1252        F: Fn(String, HashMap<String, String>) -> Fut + Send + Sync + 'static,
1253        Fut: Future<Output = Result<ReadResourceResult>> + Send + 'static,
1254    {
1255        let (pattern, variables) = compile_uri_template(&self.uri_template);
1256        let name = self.name.unwrap_or_else(|| self.uri_template.clone());
1257
1258        ResourceTemplate {
1259            uri_template: self.uri_template,
1260            name,
1261            title: self.title,
1262            description: self.description,
1263            mime_type: self.mime_type,
1264            icons: self.icons,
1265            pattern,
1266            variables,
1267            handler: Arc::new(FnTemplateHandler { handler }),
1268        }
1269    }
1270}
1271
1272/// Handler wrapping a function for templates
1273struct FnTemplateHandler<F> {
1274    handler: F,
1275}
1276
1277impl<F, Fut> ResourceTemplateHandler for FnTemplateHandler<F>
1278where
1279    F: Fn(String, HashMap<String, String>) -> Fut + Send + Sync + 'static,
1280    Fut: Future<Output = Result<ReadResourceResult>> + Send + 'static,
1281{
1282    fn read(
1283        &self,
1284        uri: &str,
1285        variables: HashMap<String, String>,
1286    ) -> BoxFuture<'_, Result<ReadResourceResult>> {
1287        let uri = uri.to_string();
1288        Box::pin((self.handler)(uri, variables))
1289    }
1290}
1291
1292/// Compile a URI template into a regex pattern and extract variable names
1293///
1294/// Supports RFC 6570 Level 1 (simple expansion):
1295/// - `{var}` matches any characters except `/`
1296/// - `{+var}` matches any characters including `/` (reserved expansion)
1297///
1298/// Returns the compiled regex and a list of variable names in order.
1299fn compile_uri_template(template: &str) -> (regex::Regex, Vec<String>) {
1300    let mut pattern = String::from("^");
1301    let mut variables = Vec::new();
1302
1303    let mut chars = template.chars().peekable();
1304    while let Some(c) = chars.next() {
1305        if c == '{' {
1306            // Check for + prefix (reserved expansion)
1307            let is_reserved = chars.peek() == Some(&'+');
1308            if is_reserved {
1309                chars.next();
1310            }
1311
1312            // Collect variable name
1313            let var_name: String = chars.by_ref().take_while(|&c| c != '}').collect();
1314            variables.push(var_name);
1315
1316            // Choose pattern based on expansion type
1317            if is_reserved {
1318                // Reserved expansion - match anything
1319                pattern.push_str("(.+)");
1320            } else {
1321                // Simple expansion - match non-slash characters
1322                pattern.push_str("([^/]+)");
1323            }
1324        } else {
1325            // Escape regex special characters
1326            match c {
1327                '.' | '+' | '*' | '?' | '^' | '$' | '(' | ')' | '[' | ']' | '{' | '}' | '|'
1328                | '\\' => {
1329                    pattern.push('\\');
1330                    pattern.push(c);
1331                }
1332                _ => pattern.push(c),
1333            }
1334        }
1335    }
1336
1337    pattern.push('$');
1338
1339    // Compile the regex - panic if template is malformed
1340    let regex = regex::Regex::new(&pattern)
1341        .unwrap_or_else(|e| panic!("Invalid URI template '{}': {}", template, e));
1342
1343    (regex, variables)
1344}
1345
1346#[cfg(test)]
1347mod tests {
1348    use super::*;
1349    use std::time::Duration;
1350    use tower::timeout::TimeoutLayer;
1351
1352    #[tokio::test]
1353    async fn test_builder_resource() {
1354        let resource = ResourceBuilder::new("file:///test.txt")
1355            .name("Test File")
1356            .description("A test file")
1357            .text("Hello, World!");
1358
1359        assert_eq!(resource.uri, "file:///test.txt");
1360        assert_eq!(resource.name, "Test File");
1361        assert_eq!(resource.description.as_deref(), Some("A test file"));
1362
1363        let result = resource.read().await;
1364        assert_eq!(result.contents.len(), 1);
1365        assert_eq!(result.contents[0].text.as_deref(), Some("Hello, World!"));
1366    }
1367
1368    #[tokio::test]
1369    async fn test_json_resource() {
1370        let resource = ResourceBuilder::new("file:///config.json")
1371            .name("Config")
1372            .json(serde_json::json!({"key": "value"}));
1373
1374        assert_eq!(resource.mime_type.as_deref(), Some("application/json"));
1375
1376        let result = resource.read().await;
1377        assert!(result.contents[0].text.as_ref().unwrap().contains("key"));
1378    }
1379
1380    #[tokio::test]
1381    async fn test_handler_resource() {
1382        let resource = ResourceBuilder::new("memory://counter")
1383            .name("Counter")
1384            .handler(|| async {
1385                Ok(ReadResourceResult {
1386                    contents: vec![ResourceContent {
1387                        uri: "memory://counter".to_string(),
1388                        mime_type: Some("text/plain".to_string()),
1389                        text: Some("42".to_string()),
1390                        blob: None,
1391                    }],
1392                })
1393            })
1394            .build();
1395
1396        let result = resource.read().await;
1397        assert_eq!(result.contents[0].text.as_deref(), Some("42"));
1398    }
1399
1400    #[tokio::test]
1401    async fn test_handler_resource_with_layer() {
1402        let resource = ResourceBuilder::new("file:///with-timeout.txt")
1403            .name("Resource with Timeout")
1404            .handler(|| async {
1405                Ok(ReadResourceResult {
1406                    contents: vec![ResourceContent {
1407                        uri: "file:///with-timeout.txt".to_string(),
1408                        mime_type: Some("text/plain".to_string()),
1409                        text: Some("content".to_string()),
1410                        blob: None,
1411                    }],
1412                })
1413            })
1414            .layer(TimeoutLayer::new(Duration::from_secs(30)))
1415            .build();
1416
1417        let result = resource.read().await;
1418        assert_eq!(result.contents[0].text.as_deref(), Some("content"));
1419    }
1420
1421    #[tokio::test]
1422    async fn test_handler_resource_with_timeout_error() {
1423        let resource = ResourceBuilder::new("file:///slow.txt")
1424            .name("Slow Resource")
1425            .handler(|| async {
1426                tokio::time::sleep(Duration::from_millis(100)).await;
1427                Ok(ReadResourceResult {
1428                    contents: vec![ResourceContent {
1429                        uri: "file:///slow.txt".to_string(),
1430                        mime_type: Some("text/plain".to_string()),
1431                        text: Some("content".to_string()),
1432                        blob: None,
1433                    }],
1434                })
1435            })
1436            .layer(TimeoutLayer::new(Duration::from_millis(10)))
1437            .build();
1438
1439        let result = resource.read().await;
1440        // Timeout error should be caught and converted to error content
1441        assert!(
1442            result.contents[0]
1443                .text
1444                .as_ref()
1445                .unwrap()
1446                .contains("Error reading resource")
1447        );
1448    }
1449
1450    #[tokio::test]
1451    async fn test_context_aware_handler() {
1452        let resource = ResourceBuilder::new("file:///ctx.txt")
1453            .name("Context Resource")
1454            .handler_with_context(|_ctx: RequestContext| async {
1455                Ok(ReadResourceResult {
1456                    contents: vec![ResourceContent {
1457                        uri: "file:///ctx.txt".to_string(),
1458                        mime_type: Some("text/plain".to_string()),
1459                        text: Some("context aware".to_string()),
1460                        blob: None,
1461                    }],
1462                })
1463            })
1464            .build();
1465
1466        let result = resource.read().await;
1467        assert_eq!(result.contents[0].text.as_deref(), Some("context aware"));
1468    }
1469
1470    #[tokio::test]
1471    async fn test_context_aware_handler_with_layer() {
1472        let resource = ResourceBuilder::new("file:///ctx-layer.txt")
1473            .name("Context Resource with Layer")
1474            .handler_with_context(|_ctx: RequestContext| async {
1475                Ok(ReadResourceResult {
1476                    contents: vec![ResourceContent {
1477                        uri: "file:///ctx-layer.txt".to_string(),
1478                        mime_type: Some("text/plain".to_string()),
1479                        text: Some("context with layer".to_string()),
1480                        blob: None,
1481                    }],
1482                })
1483            })
1484            .layer(TimeoutLayer::new(Duration::from_secs(30)))
1485            .build();
1486
1487        let result = resource.read().await;
1488        assert_eq!(
1489            result.contents[0].text.as_deref(),
1490            Some("context with layer")
1491        );
1492    }
1493
1494    #[tokio::test]
1495    async fn test_trait_resource() {
1496        struct TestResource;
1497
1498        impl McpResource for TestResource {
1499            const URI: &'static str = "test://resource";
1500            const NAME: &'static str = "Test";
1501            const DESCRIPTION: Option<&'static str> = Some("A test resource");
1502            const MIME_TYPE: Option<&'static str> = Some("text/plain");
1503
1504            async fn read(&self) -> Result<ReadResourceResult> {
1505                Ok(ReadResourceResult {
1506                    contents: vec![ResourceContent {
1507                        uri: Self::URI.to_string(),
1508                        mime_type: Self::MIME_TYPE.map(|s| s.to_string()),
1509                        text: Some("test content".to_string()),
1510                        blob: None,
1511                    }],
1512                })
1513            }
1514        }
1515
1516        let resource = TestResource.into_resource();
1517        assert_eq!(resource.uri, "test://resource");
1518        assert_eq!(resource.name, "Test");
1519
1520        let result = resource.read().await;
1521        assert_eq!(result.contents[0].text.as_deref(), Some("test content"));
1522    }
1523
1524    #[test]
1525    fn test_resource_definition() {
1526        let resource = ResourceBuilder::new("file:///test.txt")
1527            .name("Test")
1528            .description("Description")
1529            .mime_type("text/plain")
1530            .text("content");
1531
1532        let def = resource.definition();
1533        assert_eq!(def.uri, "file:///test.txt");
1534        assert_eq!(def.name, "Test");
1535        assert_eq!(def.description.as_deref(), Some("Description"));
1536        assert_eq!(def.mime_type.as_deref(), Some("text/plain"));
1537    }
1538
1539    #[test]
1540    fn test_resource_request_new() {
1541        let ctx = RequestContext::new(crate::protocol::RequestId::Number(1));
1542        let req = ResourceRequest::new(ctx, "file:///test.txt".to_string());
1543        assert_eq!(req.uri, "file:///test.txt");
1544    }
1545
1546    #[test]
1547    fn test_resource_catch_error_clone() {
1548        let handler = FnHandler {
1549            handler: || async { Ok::<_, Error>(ReadResourceResult { contents: vec![] }) },
1550        };
1551        let service = ResourceHandlerService::new(handler);
1552        let catch_error = ResourceCatchError::new(service);
1553        let _clone = catch_error.clone();
1554    }
1555
1556    #[test]
1557    fn test_resource_catch_error_debug() {
1558        let handler = FnHandler {
1559            handler: || async { Ok::<_, Error>(ReadResourceResult { contents: vec![] }) },
1560        };
1561        let service = ResourceHandlerService::new(handler);
1562        let catch_error = ResourceCatchError::new(service);
1563        let debug = format!("{:?}", catch_error);
1564        assert!(debug.contains("ResourceCatchError"));
1565    }
1566
1567    // =========================================================================
1568    // Resource Template Tests
1569    // =========================================================================
1570
1571    #[test]
1572    fn test_compile_uri_template_simple() {
1573        let (regex, vars) = compile_uri_template("file:///{path}");
1574        assert_eq!(vars, vec!["path"]);
1575        assert!(regex.is_match("file:///README.md"));
1576        assert!(!regex.is_match("file:///foo/bar")); // no slashes in simple expansion
1577    }
1578
1579    #[test]
1580    fn test_compile_uri_template_multiple_vars() {
1581        let (regex, vars) = compile_uri_template("api://v1/{resource}/{id}");
1582        assert_eq!(vars, vec!["resource", "id"]);
1583        assert!(regex.is_match("api://v1/users/123"));
1584        assert!(regex.is_match("api://v1/posts/abc"));
1585        assert!(!regex.is_match("api://v1/users")); // missing id
1586    }
1587
1588    #[test]
1589    fn test_compile_uri_template_reserved_expansion() {
1590        let (regex, vars) = compile_uri_template("file:///{+path}");
1591        assert_eq!(vars, vec!["path"]);
1592        assert!(regex.is_match("file:///README.md"));
1593        assert!(regex.is_match("file:///foo/bar/baz.txt")); // slashes allowed
1594    }
1595
1596    #[test]
1597    fn test_compile_uri_template_special_chars() {
1598        let (regex, vars) = compile_uri_template("http://example.com/api?query={q}");
1599        assert_eq!(vars, vec!["q"]);
1600        assert!(regex.is_match("http://example.com/api?query=hello"));
1601    }
1602
1603    #[test]
1604    fn test_resource_template_match_uri() {
1605        let template = ResourceTemplateBuilder::new("db://users/{id}")
1606            .name("User Records")
1607            .handler(|uri: String, vars: HashMap<String, String>| async move {
1608                Ok(ReadResourceResult {
1609                    contents: vec![ResourceContent {
1610                        uri,
1611                        mime_type: None,
1612                        text: Some(format!("User {}", vars.get("id").unwrap())),
1613                        blob: None,
1614                    }],
1615                })
1616            });
1617
1618        // Test matching
1619        let vars = template.match_uri("db://users/123").unwrap();
1620        assert_eq!(vars.get("id"), Some(&"123".to_string()));
1621
1622        // Test non-matching
1623        assert!(template.match_uri("db://posts/123").is_none());
1624        assert!(template.match_uri("db://users").is_none());
1625    }
1626
1627    #[test]
1628    fn test_resource_template_match_multiple_vars() {
1629        let template = ResourceTemplateBuilder::new("api://{version}/{resource}/{id}")
1630            .name("API Resources")
1631            .handler(|uri: String, _vars: HashMap<String, String>| async move {
1632                Ok(ReadResourceResult {
1633                    contents: vec![ResourceContent {
1634                        uri,
1635                        mime_type: None,
1636                        text: None,
1637                        blob: None,
1638                    }],
1639                })
1640            });
1641
1642        let vars = template.match_uri("api://v2/users/abc-123").unwrap();
1643        assert_eq!(vars.get("version"), Some(&"v2".to_string()));
1644        assert_eq!(vars.get("resource"), Some(&"users".to_string()));
1645        assert_eq!(vars.get("id"), Some(&"abc-123".to_string()));
1646    }
1647
1648    #[tokio::test]
1649    async fn test_resource_template_read() {
1650        let template = ResourceTemplateBuilder::new("file:///{path}")
1651            .name("Files")
1652            .mime_type("text/plain")
1653            .handler(|uri: String, vars: HashMap<String, String>| async move {
1654                let path = vars.get("path").unwrap().clone();
1655                Ok(ReadResourceResult {
1656                    contents: vec![ResourceContent {
1657                        uri,
1658                        mime_type: Some("text/plain".to_string()),
1659                        text: Some(format!("Contents of {}", path)),
1660                        blob: None,
1661                    }],
1662                })
1663            });
1664
1665        let vars = template.match_uri("file:///README.md").unwrap();
1666        let result = template.read("file:///README.md", vars).await.unwrap();
1667
1668        assert_eq!(result.contents.len(), 1);
1669        assert_eq!(result.contents[0].uri, "file:///README.md");
1670        assert_eq!(
1671            result.contents[0].text.as_deref(),
1672            Some("Contents of README.md")
1673        );
1674    }
1675
1676    #[test]
1677    fn test_resource_template_definition() {
1678        let template = ResourceTemplateBuilder::new("db://records/{id}")
1679            .name("Database Records")
1680            .description("Access database records by ID")
1681            .mime_type("application/json")
1682            .handler(|uri: String, _vars: HashMap<String, String>| async move {
1683                Ok(ReadResourceResult {
1684                    contents: vec![ResourceContent {
1685                        uri,
1686                        mime_type: None,
1687                        text: None,
1688                        blob: None,
1689                    }],
1690                })
1691            });
1692
1693        let def = template.definition();
1694        assert_eq!(def.uri_template, "db://records/{id}");
1695        assert_eq!(def.name, "Database Records");
1696        assert_eq!(
1697            def.description.as_deref(),
1698            Some("Access database records by ID")
1699        );
1700        assert_eq!(def.mime_type.as_deref(), Some("application/json"));
1701    }
1702
1703    #[test]
1704    fn test_resource_template_reserved_path() {
1705        let template = ResourceTemplateBuilder::new("file:///{+path}")
1706            .name("Files with subpaths")
1707            .handler(|uri: String, _vars: HashMap<String, String>| async move {
1708                Ok(ReadResourceResult {
1709                    contents: vec![ResourceContent {
1710                        uri,
1711                        mime_type: None,
1712                        text: None,
1713                        blob: None,
1714                    }],
1715                })
1716            });
1717
1718        // Reserved expansion should match slashes
1719        let vars = template.match_uri("file:///src/lib/utils.rs").unwrap();
1720        assert_eq!(vars.get("path"), Some(&"src/lib/utils.rs".to_string()));
1721    }
1722}