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