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, ToolIcon,
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 title: Option<String>,
60 pub description: Option<String>,
61 pub mime_type: Option<String>,
62 pub icons: Option<Vec<ToolIcon>>,
63 pub size: Option<u64>,
64 handler: Arc<dyn ResourceHandler>,
65}
66
67impl Clone for Resource {
68 fn clone(&self) -> Self {
69 Self {
70 uri: self.uri.clone(),
71 name: self.name.clone(),
72 title: self.title.clone(),
73 description: self.description.clone(),
74 mime_type: self.mime_type.clone(),
75 icons: self.icons.clone(),
76 size: self.size,
77 handler: self.handler.clone(),
78 }
79 }
80}
81
82impl std::fmt::Debug for Resource {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 f.debug_struct("Resource")
85 .field("uri", &self.uri)
86 .field("name", &self.name)
87 .field("title", &self.title)
88 .field("description", &self.description)
89 .field("mime_type", &self.mime_type)
90 .field("icons", &self.icons)
91 .field("size", &self.size)
92 .finish_non_exhaustive()
93 }
94}
95
96impl Resource {
97 pub fn builder(uri: impl Into<String>) -> ResourceBuilder {
99 ResourceBuilder::new(uri)
100 }
101
102 pub fn definition(&self) -> ResourceDefinition {
104 ResourceDefinition {
105 uri: self.uri.clone(),
106 name: self.name.clone(),
107 title: self.title.clone(),
108 description: self.description.clone(),
109 mime_type: self.mime_type.clone(),
110 icons: self.icons.clone(),
111 size: self.size,
112 }
113 }
114
115 pub fn read(&self) -> BoxFuture<'_, Result<ReadResourceResult>> {
117 self.handler.read()
118 }
119}
120
121pub struct ResourceBuilder {
151 uri: String,
152 name: Option<String>,
153 title: Option<String>,
154 description: Option<String>,
155 mime_type: Option<String>,
156 icons: Option<Vec<ToolIcon>>,
157 size: Option<u64>,
158}
159
160impl ResourceBuilder {
161 pub fn new(uri: impl Into<String>) -> Self {
162 Self {
163 uri: uri.into(),
164 name: None,
165 title: None,
166 description: None,
167 mime_type: None,
168 icons: None,
169 size: None,
170 }
171 }
172
173 pub fn name(mut self, name: impl Into<String>) -> Self {
175 self.name = Some(name.into());
176 self
177 }
178
179 pub fn title(mut self, title: impl Into<String>) -> Self {
181 self.title = Some(title.into());
182 self
183 }
184
185 pub fn description(mut self, description: impl Into<String>) -> Self {
187 self.description = Some(description.into());
188 self
189 }
190
191 pub fn mime_type(mut self, mime_type: impl Into<String>) -> Self {
193 self.mime_type = Some(mime_type.into());
194 self
195 }
196
197 pub fn icon(mut self, src: impl Into<String>) -> Self {
199 self.icons.get_or_insert_with(Vec::new).push(ToolIcon {
200 src: src.into(),
201 mime_type: None,
202 sizes: None,
203 });
204 self
205 }
206
207 pub fn icon_with_meta(
209 mut self,
210 src: impl Into<String>,
211 mime_type: Option<String>,
212 sizes: Option<Vec<String>>,
213 ) -> Self {
214 self.icons.get_or_insert_with(Vec::new).push(ToolIcon {
215 src: src.into(),
216 mime_type,
217 sizes,
218 });
219 self
220 }
221
222 pub fn size(mut self, size: u64) -> Self {
224 self.size = Some(size);
225 self
226 }
227
228 pub fn handler<F, Fut>(self, handler: F) -> Resource
230 where
231 F: Fn() -> Fut + Send + Sync + 'static,
232 Fut: Future<Output = Result<ReadResourceResult>> + Send + 'static,
233 {
234 let name = self.name.unwrap_or_else(|| self.uri.clone());
236
237 Resource {
238 uri: self.uri.clone(),
239 name,
240 title: self.title,
241 description: self.description,
242 mime_type: self.mime_type,
243 icons: self.icons,
244 size: self.size,
245 handler: Arc::new(FnHandler { handler }),
246 }
247 }
248
249 pub fn text(self, content: impl Into<String>) -> Resource {
251 let uri = self.uri.clone();
252 let content = content.into();
253 let mime_type = self.mime_type.clone();
254
255 self.handler(move || {
256 let uri = uri.clone();
257 let content = content.clone();
258 let mime_type = mime_type.clone();
259 async move {
260 Ok(ReadResourceResult {
261 contents: vec![ResourceContent {
262 uri,
263 mime_type,
264 text: Some(content),
265 blob: None,
266 }],
267 })
268 }
269 })
270 }
271
272 pub fn json(mut self, value: serde_json::Value) -> Resource {
274 let uri = self.uri.clone();
275 self.mime_type = Some("application/json".to_string());
276 let text = serde_json::to_string_pretty(&value).unwrap_or_else(|_| "{}".to_string());
277
278 self.handler(move || {
279 let uri = uri.clone();
280 let text = text.clone();
281 async move {
282 Ok(ReadResourceResult {
283 contents: vec![ResourceContent {
284 uri,
285 mime_type: Some("application/json".to_string()),
286 text: Some(text),
287 blob: None,
288 }],
289 })
290 }
291 })
292 }
293}
294
295struct FnHandler<F> {
301 handler: F,
302}
303
304impl<F, Fut> ResourceHandler for FnHandler<F>
305where
306 F: Fn() -> Fut + Send + Sync + 'static,
307 Fut: Future<Output = Result<ReadResourceResult>> + Send + 'static,
308{
309 fn read(&self) -> BoxFuture<'_, Result<ReadResourceResult>> {
310 Box::pin((self.handler)())
311 }
312}
313
314pub trait McpResource: Send + Sync + 'static {
356 const URI: &'static str;
357 const NAME: &'static str;
358 const DESCRIPTION: Option<&'static str> = None;
359 const MIME_TYPE: Option<&'static str> = None;
360
361 fn read(&self) -> impl Future<Output = Result<ReadResourceResult>> + Send;
362
363 fn into_resource(self) -> Resource
365 where
366 Self: Sized,
367 {
368 let resource = Arc::new(self);
369 Resource {
370 uri: Self::URI.to_string(),
371 name: Self::NAME.to_string(),
372 title: None,
373 description: Self::DESCRIPTION.map(|s| s.to_string()),
374 mime_type: Self::MIME_TYPE.map(|s| s.to_string()),
375 icons: None,
376 size: None,
377 handler: Arc::new(McpResourceHandler { resource }),
378 }
379 }
380}
381
382struct McpResourceHandler<T: McpResource> {
384 resource: Arc<T>,
385}
386
387impl<T: McpResource> ResourceHandler for McpResourceHandler<T> {
388 fn read(&self) -> BoxFuture<'_, Result<ReadResourceResult>> {
389 let resource = self.resource.clone();
390 Box::pin(async move { resource.read().await })
391 }
392}
393
394pub trait ResourceTemplateHandler: Send + Sync {
403 fn read(
405 &self,
406 uri: &str,
407 variables: HashMap<String, String>,
408 ) -> BoxFuture<'_, Result<ReadResourceResult>>;
409}
410
411pub struct ResourceTemplate {
439 pub uri_template: String,
441 pub name: String,
443 pub title: Option<String>,
445 pub description: Option<String>,
447 pub mime_type: Option<String>,
449 pub icons: Option<Vec<ToolIcon>>,
451 pattern: regex::Regex,
453 variables: Vec<String>,
455 handler: Arc<dyn ResourceTemplateHandler>,
457}
458
459impl Clone for ResourceTemplate {
460 fn clone(&self) -> Self {
461 Self {
462 uri_template: self.uri_template.clone(),
463 name: self.name.clone(),
464 title: self.title.clone(),
465 description: self.description.clone(),
466 mime_type: self.mime_type.clone(),
467 icons: self.icons.clone(),
468 pattern: self.pattern.clone(),
469 variables: self.variables.clone(),
470 handler: self.handler.clone(),
471 }
472 }
473}
474
475impl std::fmt::Debug for ResourceTemplate {
476 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
477 f.debug_struct("ResourceTemplate")
478 .field("uri_template", &self.uri_template)
479 .field("name", &self.name)
480 .field("title", &self.title)
481 .field("description", &self.description)
482 .field("mime_type", &self.mime_type)
483 .field("icons", &self.icons)
484 .field("variables", &self.variables)
485 .finish_non_exhaustive()
486 }
487}
488
489impl ResourceTemplate {
490 pub fn builder(uri_template: impl Into<String>) -> ResourceTemplateBuilder {
492 ResourceTemplateBuilder::new(uri_template)
493 }
494
495 pub fn definition(&self) -> ResourceTemplateDefinition {
497 ResourceTemplateDefinition {
498 uri_template: self.uri_template.clone(),
499 name: self.name.clone(),
500 title: self.title.clone(),
501 description: self.description.clone(),
502 mime_type: self.mime_type.clone(),
503 icons: self.icons.clone(),
504 }
505 }
506
507 pub fn match_uri(&self, uri: &str) -> Option<HashMap<String, String>> {
512 self.pattern.captures(uri).map(|caps| {
513 self.variables
514 .iter()
515 .enumerate()
516 .filter_map(|(i, name)| {
517 caps.get(i + 1)
518 .map(|m| (name.clone(), m.as_str().to_string()))
519 })
520 .collect()
521 })
522 }
523
524 pub fn read(
531 &self,
532 uri: &str,
533 variables: HashMap<String, String>,
534 ) -> BoxFuture<'_, Result<ReadResourceResult>> {
535 self.handler.read(uri, variables)
536 }
537}
538
539pub struct ResourceTemplateBuilder {
564 uri_template: String,
565 name: Option<String>,
566 title: Option<String>,
567 description: Option<String>,
568 mime_type: Option<String>,
569 icons: Option<Vec<ToolIcon>>,
570}
571
572impl ResourceTemplateBuilder {
573 pub fn new(uri_template: impl Into<String>) -> Self {
586 Self {
587 uri_template: uri_template.into(),
588 name: None,
589 title: None,
590 description: None,
591 mime_type: None,
592 icons: None,
593 }
594 }
595
596 pub fn name(mut self, name: impl Into<String>) -> Self {
598 self.name = Some(name.into());
599 self
600 }
601
602 pub fn title(mut self, title: impl Into<String>) -> Self {
604 self.title = Some(title.into());
605 self
606 }
607
608 pub fn description(mut self, description: impl Into<String>) -> Self {
610 self.description = Some(description.into());
611 self
612 }
613
614 pub fn mime_type(mut self, mime_type: impl Into<String>) -> Self {
616 self.mime_type = Some(mime_type.into());
617 self
618 }
619
620 pub fn icon(mut self, src: impl Into<String>) -> Self {
622 self.icons.get_or_insert_with(Vec::new).push(ToolIcon {
623 src: src.into(),
624 mime_type: None,
625 sizes: None,
626 });
627 self
628 }
629
630 pub fn icon_with_meta(
632 mut self,
633 src: impl Into<String>,
634 mime_type: Option<String>,
635 sizes: Option<Vec<String>>,
636 ) -> Self {
637 self.icons.get_or_insert_with(Vec::new).push(ToolIcon {
638 src: src.into(),
639 mime_type,
640 sizes,
641 });
642 self
643 }
644
645 pub fn handler<F, Fut>(self, handler: F) -> ResourceTemplate
651 where
652 F: Fn(String, HashMap<String, String>) -> Fut + Send + Sync + 'static,
653 Fut: Future<Output = Result<ReadResourceResult>> + Send + 'static,
654 {
655 let (pattern, variables) = compile_uri_template(&self.uri_template);
656 let name = self.name.unwrap_or_else(|| self.uri_template.clone());
657
658 ResourceTemplate {
659 uri_template: self.uri_template,
660 name,
661 title: self.title,
662 description: self.description,
663 mime_type: self.mime_type,
664 icons: self.icons,
665 pattern,
666 variables,
667 handler: Arc::new(FnTemplateHandler { handler }),
668 }
669 }
670}
671
672struct FnTemplateHandler<F> {
674 handler: F,
675}
676
677impl<F, Fut> ResourceTemplateHandler for FnTemplateHandler<F>
678where
679 F: Fn(String, HashMap<String, String>) -> Fut + Send + Sync + 'static,
680 Fut: Future<Output = Result<ReadResourceResult>> + Send + 'static,
681{
682 fn read(
683 &self,
684 uri: &str,
685 variables: HashMap<String, String>,
686 ) -> BoxFuture<'_, Result<ReadResourceResult>> {
687 let uri = uri.to_string();
688 Box::pin((self.handler)(uri, variables))
689 }
690}
691
692fn compile_uri_template(template: &str) -> (regex::Regex, Vec<String>) {
700 let mut pattern = String::from("^");
701 let mut variables = Vec::new();
702
703 let mut chars = template.chars().peekable();
704 while let Some(c) = chars.next() {
705 if c == '{' {
706 let is_reserved = chars.peek() == Some(&'+');
708 if is_reserved {
709 chars.next();
710 }
711
712 let var_name: String = chars.by_ref().take_while(|&c| c != '}').collect();
714 variables.push(var_name);
715
716 if is_reserved {
718 pattern.push_str("(.+)");
720 } else {
721 pattern.push_str("([^/]+)");
723 }
724 } else {
725 match c {
727 '.' | '+' | '*' | '?' | '^' | '$' | '(' | ')' | '[' | ']' | '{' | '}' | '|'
728 | '\\' => {
729 pattern.push('\\');
730 pattern.push(c);
731 }
732 _ => pattern.push(c),
733 }
734 }
735 }
736
737 pattern.push('$');
738
739 let regex = regex::Regex::new(&pattern)
741 .unwrap_or_else(|e| panic!("Invalid URI template '{}': {}", template, e));
742
743 (regex, variables)
744}
745
746#[cfg(test)]
747mod tests {
748 use super::*;
749
750 #[tokio::test]
751 async fn test_builder_resource() {
752 let resource = ResourceBuilder::new("file:///test.txt")
753 .name("Test File")
754 .description("A test file")
755 .text("Hello, World!");
756
757 assert_eq!(resource.uri, "file:///test.txt");
758 assert_eq!(resource.name, "Test File");
759 assert_eq!(resource.description.as_deref(), Some("A test file"));
760
761 let result = resource.read().await.unwrap();
762 assert_eq!(result.contents.len(), 1);
763 assert_eq!(result.contents[0].text.as_deref(), Some("Hello, World!"));
764 }
765
766 #[tokio::test]
767 async fn test_json_resource() {
768 let resource = ResourceBuilder::new("file:///config.json")
769 .name("Config")
770 .json(serde_json::json!({"key": "value"}));
771
772 assert_eq!(resource.mime_type.as_deref(), Some("application/json"));
773
774 let result = resource.read().await.unwrap();
775 assert!(result.contents[0].text.as_ref().unwrap().contains("key"));
776 }
777
778 #[tokio::test]
779 async fn test_handler_resource() {
780 let resource = ResourceBuilder::new("memory://counter")
781 .name("Counter")
782 .handler(|| async {
783 Ok(ReadResourceResult {
784 contents: vec![ResourceContent {
785 uri: "memory://counter".to_string(),
786 mime_type: Some("text/plain".to_string()),
787 text: Some("42".to_string()),
788 blob: None,
789 }],
790 })
791 });
792
793 let result = resource.read().await.unwrap();
794 assert_eq!(result.contents[0].text.as_deref(), Some("42"));
795 }
796
797 #[tokio::test]
798 async fn test_trait_resource() {
799 struct TestResource;
800
801 impl McpResource for TestResource {
802 const URI: &'static str = "test://resource";
803 const NAME: &'static str = "Test";
804 const DESCRIPTION: Option<&'static str> = Some("A test resource");
805 const MIME_TYPE: Option<&'static str> = Some("text/plain");
806
807 async fn read(&self) -> Result<ReadResourceResult> {
808 Ok(ReadResourceResult {
809 contents: vec![ResourceContent {
810 uri: Self::URI.to_string(),
811 mime_type: Self::MIME_TYPE.map(|s| s.to_string()),
812 text: Some("test content".to_string()),
813 blob: None,
814 }],
815 })
816 }
817 }
818
819 let resource = TestResource.into_resource();
820 assert_eq!(resource.uri, "test://resource");
821 assert_eq!(resource.name, "Test");
822
823 let result = resource.read().await.unwrap();
824 assert_eq!(result.contents[0].text.as_deref(), Some("test content"));
825 }
826
827 #[test]
828 fn test_resource_definition() {
829 let resource = ResourceBuilder::new("file:///test.txt")
830 .name("Test")
831 .description("Description")
832 .mime_type("text/plain")
833 .text("content");
834
835 let def = resource.definition();
836 assert_eq!(def.uri, "file:///test.txt");
837 assert_eq!(def.name, "Test");
838 assert_eq!(def.description.as_deref(), Some("Description"));
839 assert_eq!(def.mime_type.as_deref(), Some("text/plain"));
840 }
841
842 #[test]
847 fn test_compile_uri_template_simple() {
848 let (regex, vars) = compile_uri_template("file:///{path}");
849 assert_eq!(vars, vec!["path"]);
850 assert!(regex.is_match("file:///README.md"));
851 assert!(!regex.is_match("file:///foo/bar")); }
853
854 #[test]
855 fn test_compile_uri_template_multiple_vars() {
856 let (regex, vars) = compile_uri_template("api://v1/{resource}/{id}");
857 assert_eq!(vars, vec!["resource", "id"]);
858 assert!(regex.is_match("api://v1/users/123"));
859 assert!(regex.is_match("api://v1/posts/abc"));
860 assert!(!regex.is_match("api://v1/users")); }
862
863 #[test]
864 fn test_compile_uri_template_reserved_expansion() {
865 let (regex, vars) = compile_uri_template("file:///{+path}");
866 assert_eq!(vars, vec!["path"]);
867 assert!(regex.is_match("file:///README.md"));
868 assert!(regex.is_match("file:///foo/bar/baz.txt")); }
870
871 #[test]
872 fn test_compile_uri_template_special_chars() {
873 let (regex, vars) = compile_uri_template("http://example.com/api?query={q}");
874 assert_eq!(vars, vec!["q"]);
875 assert!(regex.is_match("http://example.com/api?query=hello"));
876 }
877
878 #[test]
879 fn test_resource_template_match_uri() {
880 let template = ResourceTemplateBuilder::new("db://users/{id}")
881 .name("User Records")
882 .handler(|uri: String, vars: HashMap<String, String>| async move {
883 Ok(ReadResourceResult {
884 contents: vec![ResourceContent {
885 uri,
886 mime_type: None,
887 text: Some(format!("User {}", vars.get("id").unwrap())),
888 blob: None,
889 }],
890 })
891 });
892
893 let vars = template.match_uri("db://users/123").unwrap();
895 assert_eq!(vars.get("id"), Some(&"123".to_string()));
896
897 assert!(template.match_uri("db://posts/123").is_none());
899 assert!(template.match_uri("db://users").is_none());
900 }
901
902 #[test]
903 fn test_resource_template_match_multiple_vars() {
904 let template = ResourceTemplateBuilder::new("api://{version}/{resource}/{id}")
905 .name("API Resources")
906 .handler(|uri: String, _vars: HashMap<String, String>| async move {
907 Ok(ReadResourceResult {
908 contents: vec![ResourceContent {
909 uri,
910 mime_type: None,
911 text: None,
912 blob: None,
913 }],
914 })
915 });
916
917 let vars = template.match_uri("api://v2/users/abc-123").unwrap();
918 assert_eq!(vars.get("version"), Some(&"v2".to_string()));
919 assert_eq!(vars.get("resource"), Some(&"users".to_string()));
920 assert_eq!(vars.get("id"), Some(&"abc-123".to_string()));
921 }
922
923 #[tokio::test]
924 async fn test_resource_template_read() {
925 let template = ResourceTemplateBuilder::new("file:///{path}")
926 .name("Files")
927 .mime_type("text/plain")
928 .handler(|uri: String, vars: HashMap<String, String>| async move {
929 let path = vars.get("path").unwrap().clone();
930 Ok(ReadResourceResult {
931 contents: vec![ResourceContent {
932 uri,
933 mime_type: Some("text/plain".to_string()),
934 text: Some(format!("Contents of {}", path)),
935 blob: None,
936 }],
937 })
938 });
939
940 let vars = template.match_uri("file:///README.md").unwrap();
941 let result = template.read("file:///README.md", vars).await.unwrap();
942
943 assert_eq!(result.contents.len(), 1);
944 assert_eq!(result.contents[0].uri, "file:///README.md");
945 assert_eq!(
946 result.contents[0].text.as_deref(),
947 Some("Contents of README.md")
948 );
949 }
950
951 #[test]
952 fn test_resource_template_definition() {
953 let template = ResourceTemplateBuilder::new("db://records/{id}")
954 .name("Database Records")
955 .description("Access database records by ID")
956 .mime_type("application/json")
957 .handler(|uri: String, _vars: HashMap<String, String>| async move {
958 Ok(ReadResourceResult {
959 contents: vec![ResourceContent {
960 uri,
961 mime_type: None,
962 text: None,
963 blob: None,
964 }],
965 })
966 });
967
968 let def = template.definition();
969 assert_eq!(def.uri_template, "db://records/{id}");
970 assert_eq!(def.name, "Database Records");
971 assert_eq!(
972 def.description.as_deref(),
973 Some("Access database records by ID")
974 );
975 assert_eq!(def.mime_type.as_deref(), Some("application/json"));
976 }
977
978 #[test]
979 fn test_resource_template_reserved_path() {
980 let template = ResourceTemplateBuilder::new("file:///{+path}")
981 .name("Files with subpaths")
982 .handler(|uri: String, _vars: HashMap<String, String>| async move {
983 Ok(ReadResourceResult {
984 contents: vec![ResourceContent {
985 uri,
986 mime_type: None,
987 text: None,
988 blob: None,
989 }],
990 })
991 });
992
993 let vars = template.match_uri("file:///src/lib/utils.rs").unwrap();
995 assert_eq!(vars.get("path"), Some(&"src/lib/utils.rs".to_string()));
996 }
997}