1use std::collections::HashMap;
37use std::future::Future;
38use std::pin::Pin;
39use std::sync::Arc;
40
41use crate::error::Result;
42use crate::protocol::{
43 ReadResourceResult, ResourceContent, ResourceDefinition, ResourceTemplateDefinition,
44};
45
46pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
48
49pub trait ResourceHandler: Send + Sync {
51 fn read(&self) -> BoxFuture<'_, Result<ReadResourceResult>>;
53}
54
55pub struct Resource {
57 pub uri: String,
58 pub name: String,
59 pub description: Option<String>,
60 pub mime_type: Option<String>,
61 handler: Arc<dyn ResourceHandler>,
62}
63
64impl Clone for Resource {
65 fn clone(&self) -> Self {
66 Self {
67 uri: self.uri.clone(),
68 name: self.name.clone(),
69 description: self.description.clone(),
70 mime_type: self.mime_type.clone(),
71 handler: self.handler.clone(),
72 }
73 }
74}
75
76impl std::fmt::Debug for Resource {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 f.debug_struct("Resource")
79 .field("uri", &self.uri)
80 .field("name", &self.name)
81 .field("description", &self.description)
82 .field("mime_type", &self.mime_type)
83 .finish_non_exhaustive()
84 }
85}
86
87impl Resource {
88 pub fn builder(uri: impl Into<String>) -> ResourceBuilder {
90 ResourceBuilder::new(uri)
91 }
92
93 pub fn definition(&self) -> ResourceDefinition {
95 ResourceDefinition {
96 uri: self.uri.clone(),
97 name: self.name.clone(),
98 description: self.description.clone(),
99 mime_type: self.mime_type.clone(),
100 }
101 }
102
103 pub fn read(&self) -> BoxFuture<'_, Result<ReadResourceResult>> {
105 self.handler.read()
106 }
107}
108
109pub struct ResourceBuilder {
139 uri: String,
140 name: Option<String>,
141 description: Option<String>,
142 mime_type: Option<String>,
143}
144
145impl ResourceBuilder {
146 pub fn new(uri: impl Into<String>) -> Self {
147 Self {
148 uri: uri.into(),
149 name: None,
150 description: None,
151 mime_type: None,
152 }
153 }
154
155 pub fn name(mut self, name: impl Into<String>) -> Self {
157 self.name = Some(name.into());
158 self
159 }
160
161 pub fn description(mut self, description: impl Into<String>) -> Self {
163 self.description = Some(description.into());
164 self
165 }
166
167 pub fn mime_type(mut self, mime_type: impl Into<String>) -> Self {
169 self.mime_type = Some(mime_type.into());
170 self
171 }
172
173 pub fn handler<F, Fut>(self, handler: F) -> Resource
175 where
176 F: Fn() -> Fut + Send + Sync + 'static,
177 Fut: Future<Output = Result<ReadResourceResult>> + Send + 'static,
178 {
179 let name = self.name.unwrap_or_else(|| self.uri.clone());
181
182 Resource {
183 uri: self.uri.clone(),
184 name,
185 description: self.description,
186 mime_type: self.mime_type,
187 handler: Arc::new(FnHandler { handler }),
188 }
189 }
190
191 pub fn text(self, content: impl Into<String>) -> Resource {
193 let uri = self.uri.clone();
194 let content = content.into();
195 let mime_type = self.mime_type.clone();
196
197 self.handler(move || {
198 let uri = uri.clone();
199 let content = content.clone();
200 let mime_type = mime_type.clone();
201 async move {
202 Ok(ReadResourceResult {
203 contents: vec![ResourceContent {
204 uri,
205 mime_type,
206 text: Some(content),
207 blob: None,
208 }],
209 })
210 }
211 })
212 }
213
214 pub fn json(mut self, value: serde_json::Value) -> Resource {
216 let uri = self.uri.clone();
217 self.mime_type = Some("application/json".to_string());
218 let text = serde_json::to_string_pretty(&value).unwrap_or_else(|_| "{}".to_string());
219
220 self.handler(move || {
221 let uri = uri.clone();
222 let text = text.clone();
223 async move {
224 Ok(ReadResourceResult {
225 contents: vec![ResourceContent {
226 uri,
227 mime_type: Some("application/json".to_string()),
228 text: Some(text),
229 blob: None,
230 }],
231 })
232 }
233 })
234 }
235}
236
237struct FnHandler<F> {
243 handler: F,
244}
245
246impl<F, Fut> ResourceHandler for FnHandler<F>
247where
248 F: Fn() -> Fut + Send + Sync + 'static,
249 Fut: Future<Output = Result<ReadResourceResult>> + Send + 'static,
250{
251 fn read(&self) -> BoxFuture<'_, Result<ReadResourceResult>> {
252 Box::pin((self.handler)())
253 }
254}
255
256pub trait McpResource: Send + Sync + 'static {
298 const URI: &'static str;
299 const NAME: &'static str;
300 const DESCRIPTION: Option<&'static str> = None;
301 const MIME_TYPE: Option<&'static str> = None;
302
303 fn read(&self) -> impl Future<Output = Result<ReadResourceResult>> + Send;
304
305 fn into_resource(self) -> Resource
307 where
308 Self: Sized,
309 {
310 let resource = Arc::new(self);
311 Resource {
312 uri: Self::URI.to_string(),
313 name: Self::NAME.to_string(),
314 description: Self::DESCRIPTION.map(|s| s.to_string()),
315 mime_type: Self::MIME_TYPE.map(|s| s.to_string()),
316 handler: Arc::new(McpResourceHandler { resource }),
317 }
318 }
319}
320
321struct McpResourceHandler<T: McpResource> {
323 resource: Arc<T>,
324}
325
326impl<T: McpResource> ResourceHandler for McpResourceHandler<T> {
327 fn read(&self) -> BoxFuture<'_, Result<ReadResourceResult>> {
328 let resource = self.resource.clone();
329 Box::pin(async move { resource.read().await })
330 }
331}
332
333pub trait ResourceTemplateHandler: Send + Sync {
342 fn read(
344 &self,
345 uri: &str,
346 variables: HashMap<String, String>,
347 ) -> BoxFuture<'_, Result<ReadResourceResult>>;
348}
349
350pub struct ResourceTemplate {
378 pub uri_template: String,
380 pub name: String,
382 pub description: Option<String>,
384 pub mime_type: Option<String>,
386 pattern: regex::Regex,
388 variables: Vec<String>,
390 handler: Arc<dyn ResourceTemplateHandler>,
392}
393
394impl Clone for ResourceTemplate {
395 fn clone(&self) -> Self {
396 Self {
397 uri_template: self.uri_template.clone(),
398 name: self.name.clone(),
399 description: self.description.clone(),
400 mime_type: self.mime_type.clone(),
401 pattern: self.pattern.clone(),
402 variables: self.variables.clone(),
403 handler: self.handler.clone(),
404 }
405 }
406}
407
408impl std::fmt::Debug for ResourceTemplate {
409 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
410 f.debug_struct("ResourceTemplate")
411 .field("uri_template", &self.uri_template)
412 .field("name", &self.name)
413 .field("description", &self.description)
414 .field("mime_type", &self.mime_type)
415 .field("variables", &self.variables)
416 .finish_non_exhaustive()
417 }
418}
419
420impl ResourceTemplate {
421 pub fn builder(uri_template: impl Into<String>) -> ResourceTemplateBuilder {
423 ResourceTemplateBuilder::new(uri_template)
424 }
425
426 pub fn definition(&self) -> ResourceTemplateDefinition {
428 ResourceTemplateDefinition {
429 uri_template: self.uri_template.clone(),
430 name: self.name.clone(),
431 description: self.description.clone(),
432 mime_type: self.mime_type.clone(),
433 }
434 }
435
436 pub fn match_uri(&self, uri: &str) -> Option<HashMap<String, String>> {
441 self.pattern.captures(uri).map(|caps| {
442 self.variables
443 .iter()
444 .enumerate()
445 .filter_map(|(i, name)| {
446 caps.get(i + 1)
447 .map(|m| (name.clone(), m.as_str().to_string()))
448 })
449 .collect()
450 })
451 }
452
453 pub fn read(
460 &self,
461 uri: &str,
462 variables: HashMap<String, String>,
463 ) -> BoxFuture<'_, Result<ReadResourceResult>> {
464 self.handler.read(uri, variables)
465 }
466}
467
468pub struct ResourceTemplateBuilder {
493 uri_template: String,
494 name: Option<String>,
495 description: Option<String>,
496 mime_type: Option<String>,
497}
498
499impl ResourceTemplateBuilder {
500 pub fn new(uri_template: impl Into<String>) -> Self {
513 Self {
514 uri_template: uri_template.into(),
515 name: None,
516 description: None,
517 mime_type: None,
518 }
519 }
520
521 pub fn name(mut self, name: impl Into<String>) -> Self {
523 self.name = Some(name.into());
524 self
525 }
526
527 pub fn description(mut self, description: impl Into<String>) -> Self {
529 self.description = Some(description.into());
530 self
531 }
532
533 pub fn mime_type(mut self, mime_type: impl Into<String>) -> Self {
535 self.mime_type = Some(mime_type.into());
536 self
537 }
538
539 pub fn handler<F, Fut>(self, handler: F) -> ResourceTemplate
545 where
546 F: Fn(String, HashMap<String, String>) -> Fut + Send + Sync + 'static,
547 Fut: Future<Output = Result<ReadResourceResult>> + Send + 'static,
548 {
549 let (pattern, variables) = compile_uri_template(&self.uri_template);
550 let name = self.name.unwrap_or_else(|| self.uri_template.clone());
551
552 ResourceTemplate {
553 uri_template: self.uri_template,
554 name,
555 description: self.description,
556 mime_type: self.mime_type,
557 pattern,
558 variables,
559 handler: Arc::new(FnTemplateHandler { handler }),
560 }
561 }
562}
563
564struct FnTemplateHandler<F> {
566 handler: F,
567}
568
569impl<F, Fut> ResourceTemplateHandler for FnTemplateHandler<F>
570where
571 F: Fn(String, HashMap<String, String>) -> Fut + Send + Sync + 'static,
572 Fut: Future<Output = Result<ReadResourceResult>> + Send + 'static,
573{
574 fn read(
575 &self,
576 uri: &str,
577 variables: HashMap<String, String>,
578 ) -> BoxFuture<'_, Result<ReadResourceResult>> {
579 let uri = uri.to_string();
580 Box::pin((self.handler)(uri, variables))
581 }
582}
583
584fn compile_uri_template(template: &str) -> (regex::Regex, Vec<String>) {
592 let mut pattern = String::from("^");
593 let mut variables = Vec::new();
594
595 let mut chars = template.chars().peekable();
596 while let Some(c) = chars.next() {
597 if c == '{' {
598 let is_reserved = chars.peek() == Some(&'+');
600 if is_reserved {
601 chars.next();
602 }
603
604 let var_name: String = chars.by_ref().take_while(|&c| c != '}').collect();
606 variables.push(var_name);
607
608 if is_reserved {
610 pattern.push_str("(.+)");
612 } else {
613 pattern.push_str("([^/]+)");
615 }
616 } else {
617 match c {
619 '.' | '+' | '*' | '?' | '^' | '$' | '(' | ')' | '[' | ']' | '{' | '}' | '|'
620 | '\\' => {
621 pattern.push('\\');
622 pattern.push(c);
623 }
624 _ => pattern.push(c),
625 }
626 }
627 }
628
629 pattern.push('$');
630
631 let regex = regex::Regex::new(&pattern)
633 .unwrap_or_else(|e| panic!("Invalid URI template '{}': {}", template, e));
634
635 (regex, variables)
636}
637
638#[cfg(test)]
639mod tests {
640 use super::*;
641
642 #[tokio::test]
643 async fn test_builder_resource() {
644 let resource = ResourceBuilder::new("file:///test.txt")
645 .name("Test File")
646 .description("A test file")
647 .text("Hello, World!");
648
649 assert_eq!(resource.uri, "file:///test.txt");
650 assert_eq!(resource.name, "Test File");
651 assert_eq!(resource.description.as_deref(), Some("A test file"));
652
653 let result = resource.read().await.unwrap();
654 assert_eq!(result.contents.len(), 1);
655 assert_eq!(result.contents[0].text.as_deref(), Some("Hello, World!"));
656 }
657
658 #[tokio::test]
659 async fn test_json_resource() {
660 let resource = ResourceBuilder::new("file:///config.json")
661 .name("Config")
662 .json(serde_json::json!({"key": "value"}));
663
664 assert_eq!(resource.mime_type.as_deref(), Some("application/json"));
665
666 let result = resource.read().await.unwrap();
667 assert!(result.contents[0].text.as_ref().unwrap().contains("key"));
668 }
669
670 #[tokio::test]
671 async fn test_handler_resource() {
672 let resource = ResourceBuilder::new("memory://counter")
673 .name("Counter")
674 .handler(|| async {
675 Ok(ReadResourceResult {
676 contents: vec![ResourceContent {
677 uri: "memory://counter".to_string(),
678 mime_type: Some("text/plain".to_string()),
679 text: Some("42".to_string()),
680 blob: None,
681 }],
682 })
683 });
684
685 let result = resource.read().await.unwrap();
686 assert_eq!(result.contents[0].text.as_deref(), Some("42"));
687 }
688
689 #[tokio::test]
690 async fn test_trait_resource() {
691 struct TestResource;
692
693 impl McpResource for TestResource {
694 const URI: &'static str = "test://resource";
695 const NAME: &'static str = "Test";
696 const DESCRIPTION: Option<&'static str> = Some("A test resource");
697 const MIME_TYPE: Option<&'static str> = Some("text/plain");
698
699 async fn read(&self) -> Result<ReadResourceResult> {
700 Ok(ReadResourceResult {
701 contents: vec![ResourceContent {
702 uri: Self::URI.to_string(),
703 mime_type: Self::MIME_TYPE.map(|s| s.to_string()),
704 text: Some("test content".to_string()),
705 blob: None,
706 }],
707 })
708 }
709 }
710
711 let resource = TestResource.into_resource();
712 assert_eq!(resource.uri, "test://resource");
713 assert_eq!(resource.name, "Test");
714
715 let result = resource.read().await.unwrap();
716 assert_eq!(result.contents[0].text.as_deref(), Some("test content"));
717 }
718
719 #[test]
720 fn test_resource_definition() {
721 let resource = ResourceBuilder::new("file:///test.txt")
722 .name("Test")
723 .description("Description")
724 .mime_type("text/plain")
725 .text("content");
726
727 let def = resource.definition();
728 assert_eq!(def.uri, "file:///test.txt");
729 assert_eq!(def.name, "Test");
730 assert_eq!(def.description.as_deref(), Some("Description"));
731 assert_eq!(def.mime_type.as_deref(), Some("text/plain"));
732 }
733
734 #[test]
739 fn test_compile_uri_template_simple() {
740 let (regex, vars) = compile_uri_template("file:///{path}");
741 assert_eq!(vars, vec!["path"]);
742 assert!(regex.is_match("file:///README.md"));
743 assert!(!regex.is_match("file:///foo/bar")); }
745
746 #[test]
747 fn test_compile_uri_template_multiple_vars() {
748 let (regex, vars) = compile_uri_template("api://v1/{resource}/{id}");
749 assert_eq!(vars, vec!["resource", "id"]);
750 assert!(regex.is_match("api://v1/users/123"));
751 assert!(regex.is_match("api://v1/posts/abc"));
752 assert!(!regex.is_match("api://v1/users")); }
754
755 #[test]
756 fn test_compile_uri_template_reserved_expansion() {
757 let (regex, vars) = compile_uri_template("file:///{+path}");
758 assert_eq!(vars, vec!["path"]);
759 assert!(regex.is_match("file:///README.md"));
760 assert!(regex.is_match("file:///foo/bar/baz.txt")); }
762
763 #[test]
764 fn test_compile_uri_template_special_chars() {
765 let (regex, vars) = compile_uri_template("http://example.com/api?query={q}");
766 assert_eq!(vars, vec!["q"]);
767 assert!(regex.is_match("http://example.com/api?query=hello"));
768 }
769
770 #[test]
771 fn test_resource_template_match_uri() {
772 let template = ResourceTemplateBuilder::new("db://users/{id}")
773 .name("User Records")
774 .handler(|uri: String, vars: HashMap<String, String>| async move {
775 Ok(ReadResourceResult {
776 contents: vec![ResourceContent {
777 uri,
778 mime_type: None,
779 text: Some(format!("User {}", vars.get("id").unwrap())),
780 blob: None,
781 }],
782 })
783 });
784
785 let vars = template.match_uri("db://users/123").unwrap();
787 assert_eq!(vars.get("id"), Some(&"123".to_string()));
788
789 assert!(template.match_uri("db://posts/123").is_none());
791 assert!(template.match_uri("db://users").is_none());
792 }
793
794 #[test]
795 fn test_resource_template_match_multiple_vars() {
796 let template = ResourceTemplateBuilder::new("api://{version}/{resource}/{id}")
797 .name("API Resources")
798 .handler(|uri: String, _vars: HashMap<String, String>| async move {
799 Ok(ReadResourceResult {
800 contents: vec![ResourceContent {
801 uri,
802 mime_type: None,
803 text: None,
804 blob: None,
805 }],
806 })
807 });
808
809 let vars = template.match_uri("api://v2/users/abc-123").unwrap();
810 assert_eq!(vars.get("version"), Some(&"v2".to_string()));
811 assert_eq!(vars.get("resource"), Some(&"users".to_string()));
812 assert_eq!(vars.get("id"), Some(&"abc-123".to_string()));
813 }
814
815 #[tokio::test]
816 async fn test_resource_template_read() {
817 let template = ResourceTemplateBuilder::new("file:///{path}")
818 .name("Files")
819 .mime_type("text/plain")
820 .handler(|uri: String, vars: HashMap<String, String>| async move {
821 let path = vars.get("path").unwrap().clone();
822 Ok(ReadResourceResult {
823 contents: vec![ResourceContent {
824 uri,
825 mime_type: Some("text/plain".to_string()),
826 text: Some(format!("Contents of {}", path)),
827 blob: None,
828 }],
829 })
830 });
831
832 let vars = template.match_uri("file:///README.md").unwrap();
833 let result = template.read("file:///README.md", vars).await.unwrap();
834
835 assert_eq!(result.contents.len(), 1);
836 assert_eq!(result.contents[0].uri, "file:///README.md");
837 assert_eq!(
838 result.contents[0].text.as_deref(),
839 Some("Contents of README.md")
840 );
841 }
842
843 #[test]
844 fn test_resource_template_definition() {
845 let template = ResourceTemplateBuilder::new("db://records/{id}")
846 .name("Database Records")
847 .description("Access database records by ID")
848 .mime_type("application/json")
849 .handler(|uri: String, _vars: HashMap<String, String>| async move {
850 Ok(ReadResourceResult {
851 contents: vec![ResourceContent {
852 uri,
853 mime_type: None,
854 text: None,
855 blob: None,
856 }],
857 })
858 });
859
860 let def = template.definition();
861 assert_eq!(def.uri_template, "db://records/{id}");
862 assert_eq!(def.name, "Database Records");
863 assert_eq!(
864 def.description.as_deref(),
865 Some("Access database records by ID")
866 );
867 assert_eq!(def.mime_type.as_deref(), Some("application/json"));
868 }
869
870 #[test]
871 fn test_resource_template_reserved_path() {
872 let template = ResourceTemplateBuilder::new("file:///{+path}")
873 .name("Files with subpaths")
874 .handler(|uri: String, _vars: HashMap<String, String>| async move {
875 Ok(ReadResourceResult {
876 contents: vec![ResourceContent {
877 uri,
878 mime_type: None,
879 text: None,
880 blob: None,
881 }],
882 })
883 });
884
885 let vars = template.match_uri("file:///src/lib/utils.rs").unwrap();
887 assert_eq!(vars.get("path"), Some(&"src/lib/utils.rs".to_string()));
888 }
889}