1use std::collections::HashMap;
6use std::sync::{Arc, RwLock};
7
8use async_trait::async_trait;
9use chrono_machines::{BackoffStrategy, ExponentialBackoff};
10use dashmap::DashMap;
11use rand::SeedableRng;
12use rand::rngs::SmallRng;
13use serde::{Deserialize, Serialize};
14use thiserror::Error;
15use tokio::sync::mpsc;
16
17use crate::content::resource::ResourceContent;
18use crate::server::session::Session;
19use crate::server::visibility::{ExecutionContext, VisibilityContext};
20use crate::transport::traits::JsonRpcNotification;
21
22#[derive(Debug, Error)]
24pub enum ResourceError {
25 #[error("Resource not found: {0}")]
27 NotFound(String),
28
29 #[error("Invalid URI: {0}")]
31 InvalidUri(String),
32
33 #[error("Read error: {0}")]
35 Read(String),
36
37 #[error("Internal error: {0}")]
39 Internal(String),
40
41 #[error("Retry exhausted after {attempts} attempts: {message}")]
43 RetryExhausted { attempts: u8, message: String },
44}
45
46pub type ResourceLookupResult = Result<(Arc<dyn Resource>, HashMap<String, String>), ResourceError>;
48
49impl ResourceError {
50 pub fn is_retryable(&self) -> bool {
52 matches!(self, Self::Read(_) | Self::Internal(_))
53 }
54}
55
56#[derive(Debug, Clone)]
58pub struct ResourceRetryConfig {
59 pub max_attempts: u8,
61 pub base_delay_ms: u64,
63 pub multiplier: f64,
65 pub max_delay_ms: u64,
67 pub jitter_factor: f64,
69}
70
71impl Default for ResourceRetryConfig {
72 fn default() -> Self {
73 Self {
74 max_attempts: 3,
75 base_delay_ms: 100,
76 multiplier: 2.0,
77 max_delay_ms: 10_000,
78 jitter_factor: 1.0, }
80 }
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct ResourceInfo {
86 pub uri: String,
88
89 pub name: String,
91
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub description: Option<String>,
95
96 #[serde(skip_serializing_if = "Option::is_none", rename = "mimeType")]
98 pub mime_type: Option<String>,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct ResourceTemplateInfo {
105 pub uri_template: String,
107
108 pub name: String,
110
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub title: Option<String>,
114
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub description: Option<String>,
118
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub mime_type: Option<String>,
122}
123
124#[async_trait]
126pub trait Resource: Send + Sync {
127 fn uri(&self) -> &str;
129
130 fn name(&self) -> &str;
132
133 fn description(&self) -> Option<&str> {
135 None
136 }
137
138 fn mime_type(&self) -> Option<&str> {
140 None
141 }
142
143 fn is_visible(&self, _ctx: &VisibilityContext) -> bool {
148 true
149 }
150
151 async fn read(&self, ctx: ExecutionContext<'_>) -> Result<Vec<ResourceContent>, ResourceError>;
171
172 fn text_content(&self, text: &str) -> ResourceContent {
185 ResourceContent {
186 uri: self.uri().to_string(),
187 mime_type: self.mime_type().map(|s| s.to_string()),
188 text: Some(text.to_string()),
189 blob: None,
190 }
191 }
192
193 fn blob_content(&self, data: &str) -> ResourceContent {
203 ResourceContent {
204 uri: self.uri().to_string(),
205 mime_type: self.mime_type().map(|s| s.to_string()),
206 text: None,
207 blob: Some(data.to_string()),
208 }
209 }
210}
211
212#[async_trait]
217pub trait ResourceTemplate: Send + Sync {
218 fn uri_template(&self) -> &str;
220
221 fn name(&self) -> &str;
223
224 fn title(&self) -> Option<&str> {
226 None
227 }
228
229 fn description(&self) -> Option<&str> {
231 None
232 }
233
234 fn mime_type(&self) -> Option<&str> {
236 None
237 }
238
239 fn is_visible(&self, _ctx: &VisibilityContext) -> bool {
241 true
242 }
243
244 async fn read(&self, ctx: ExecutionContext<'_>) -> Result<Vec<ResourceContent>, ResourceError>;
266}
267
268struct DynamicTemplateResource {
270 template: Arc<dyn ResourceTemplate>,
271 uri: String,
272}
273
274#[async_trait]
275impl Resource for DynamicTemplateResource {
276 fn uri(&self) -> &str {
277 &self.uri
278 }
279
280 fn name(&self) -> &str {
281 self.template.name()
282 }
283
284 fn description(&self) -> Option<&str> {
285 self.template.description()
286 }
287
288 fn mime_type(&self) -> Option<&str> {
289 self.template.mime_type()
290 }
291
292 fn is_visible(&self, ctx: &VisibilityContext) -> bool {
293 self.template.is_visible(ctx)
294 }
295
296 async fn read(&self, ctx: ExecutionContext<'_>) -> Result<Vec<ResourceContent>, ResourceError> {
297 self.template.read(ctx).await
298 }
299}
300
301#[derive(Clone)]
303pub struct ResourceManager {
304 resources: Arc<DashMap<String, Arc<dyn Resource>>>,
305 templates: Arc<DashMap<String, Arc<dyn ResourceTemplate>>>,
306 retry_config: Arc<RwLock<ResourceRetryConfig>>,
307 notification_tx: Option<mpsc::UnboundedSender<JsonRpcNotification>>,
308}
309
310impl ResourceManager {
311 pub fn new() -> Self {
313 Self {
314 resources: Arc::new(DashMap::new()),
315 templates: Arc::new(DashMap::new()),
316 retry_config: Arc::new(RwLock::new(ResourceRetryConfig::default())),
317 notification_tx: None,
318 }
319 }
320
321 pub fn with_notifications(notification_tx: mpsc::UnboundedSender<JsonRpcNotification>) -> Self {
323 Self {
324 resources: Arc::new(DashMap::new()),
325 templates: Arc::new(DashMap::new()),
326 retry_config: Arc::new(RwLock::new(ResourceRetryConfig::default())),
327 notification_tx: Some(notification_tx),
328 }
329 }
330
331 pub fn set_notification_tx(&mut self, tx: mpsc::UnboundedSender<JsonRpcNotification>) {
333 self.notification_tx = Some(tx);
334 }
335
336 pub fn set_retry_config(&self, config: ResourceRetryConfig) {
338 if let Ok(mut cfg) = self.retry_config.write() {
339 *cfg = config;
340 }
341 }
342
343 fn send_notification(&self, method: &str, params: Option<serde_json::Value>) {
345 if let Some(tx) = &self.notification_tx {
346 let notification = JsonRpcNotification::new(method, params);
347 let _ = tx.send(notification);
348 }
349 }
350
351 fn notify_resources_changed(&self) {
353 self.send_notification("notifications/resources/list_changed", None);
354 }
355
356 fn notify_message(&self, level: &str, logger: &str, message: &str) {
358 self.send_notification(
359 "notifications/message",
360 Some(serde_json::json!({
361 "level": level,
362 "logger": logger,
363 "data": message
364 })),
365 );
366 }
367
368 pub fn register<R: Resource + 'static>(&self, resource: R) {
370 let uri = resource.uri().to_string();
371 self.resources.insert(uri, Arc::new(resource));
372 }
373
374 pub fn register_boxed(&self, resource: Arc<dyn Resource>) {
376 let uri = resource.uri().to_string();
377 self.resources.insert(uri, resource);
378 }
379
380 pub fn register_template<T: ResourceTemplate + 'static>(&self, template: T) {
382 let name = template.name().to_string();
383 self.templates.insert(name, Arc::new(template));
384 }
385
386 pub fn register_template_boxed(&self, template: Arc<dyn ResourceTemplate>) {
388 let name = template.name().to_string();
389 self.templates.insert(name, template);
390 }
391
392 pub fn get_template(&self, name: &str) -> Option<Arc<dyn ResourceTemplate>> {
394 self.templates.get(name).map(|t| Arc::clone(&t))
395 }
396
397 pub fn get(&self, uri: &str) -> Option<Arc<dyn Resource>> {
399 self.resources.get(uri).map(|r| Arc::clone(&r))
400 }
401
402 pub fn list(&self) -> Vec<ResourceInfo> {
404 self.resources
405 .iter()
406 .map(|entry| {
407 let resource = entry.value();
408 ResourceInfo {
409 uri: resource.uri().to_string(),
410 name: resource.name().to_string(),
411 description: resource.description().map(|s| s.to_string()),
412 mime_type: resource.mime_type().map(|s| s.to_string()),
413 }
414 })
415 .collect()
416 }
417
418 pub fn list_templates(&self) -> Vec<ResourceTemplateInfo> {
420 self.templates
421 .iter()
422 .map(|entry| {
423 let template = entry.value();
424 ResourceTemplateInfo {
425 uri_template: template.uri_template().to_string(),
426 name: template.name().to_string(),
427 title: template.title().map(|s| s.to_string()),
428 description: template.description().map(|s| s.to_string()),
429 mime_type: template.mime_type().map(|s| s.to_string()),
430 }
431 })
432 .collect()
433 }
434
435 pub fn list_templates_for_session(
437 &self,
438 _session: &Session,
439 ctx: &VisibilityContext<'_>,
440 ) -> Vec<ResourceTemplateInfo> {
441 self.templates
442 .iter()
443 .filter(|entry| entry.value().is_visible(ctx))
444 .map(|entry| {
445 let template = entry.value();
446 ResourceTemplateInfo {
447 uri_template: template.uri_template().to_string(),
448 name: template.name().to_string(),
449 title: template.title().map(|s| s.to_string()),
450 description: template.description().map(|s| s.to_string()),
451 mime_type: template.mime_type().map(|s| s.to_string()),
452 }
453 })
454 .collect()
455 }
456
457 pub async fn read(
459 &self,
460 uri: &str,
461 params: HashMap<String, String>,
462 session: &Session,
463 logger: &crate::logging::McpLogger,
464 ) -> Result<Vec<ResourceContent>, ResourceError> {
465 let (resource, combined_params) = self.find_resource(uri, params)?;
467
468 let retry_config = self
470 .retry_config
471 .read()
472 .map(|c| c.clone())
473 .unwrap_or_default();
474
475 let backoff = ExponentialBackoff::default()
477 .max_attempts(retry_config.max_attempts)
478 .base_delay_ms(retry_config.base_delay_ms)
479 .multiplier(retry_config.multiplier)
480 .max_delay_ms(retry_config.max_delay_ms)
481 .jitter_factor(retry_config.jitter_factor);
482
483 let mut rng = SmallRng::from_os_rng();
484 let mut attempt: u8 = 1;
485
486 loop {
487 let ctx = ExecutionContext::for_resource(combined_params.clone(), session, logger);
489
490 match resource.read(ctx).await {
491 Ok(content) => {
492 if attempt > 1 {
494 self.notify_message(
495 "info",
496 "chrono-machines",
497 &format!("Resource '{}' succeeded after {} attempts", uri, attempt),
498 );
499 }
500 return Ok(content);
501 }
502 Err(e) => {
503 if !e.is_retryable() {
505 return Err(e);
506 }
507
508 if !backoff.should_retry(attempt) {
510 self.notify_resources_changed();
512 self.notify_message(
513 "warning",
514 "chrono-machines",
515 &format!(
516 "Resource '{}' failed after {} attempts: {}",
517 uri, attempt, e
518 ),
519 );
520
521 return Err(ResourceError::RetryExhausted {
522 attempts: attempt,
523 message: e.to_string(),
524 });
525 }
526
527 if let Some(delay_ms) = backoff.delay(attempt, &mut rng) {
529 tokio::time::sleep(tokio::time::Duration::from_millis(delay_ms)).await;
530 }
531
532 attempt = attempt.saturating_add(1);
533 }
534 }
535 }
536 }
537
538 fn find_resource(&self, uri: &str, params: HashMap<String, String>) -> ResourceLookupResult {
540 if let Some(resource) = self.get(uri) {
542 return Ok((resource, params));
543 }
544
545 for entry in self.resources.iter() {
547 let template_uri = entry.key();
548 if let Some(extracted_params) = self.match_template(template_uri, uri) {
549 let resource = Arc::clone(entry.value());
550 let mut combined_params = params.clone();
551 combined_params.extend(extracted_params);
552 return Ok((resource, combined_params));
553 }
554 }
555
556 for entry in self.templates.iter() {
558 let template = entry.value();
559 let template_uri = template.uri_template();
560 if let Some(extracted_params) = self.match_template(template_uri, uri) {
561 let dynamic_resource = DynamicTemplateResource {
563 template: Arc::clone(template),
564 uri: uri.to_string(),
565 };
566 let mut combined_params = params.clone();
567 combined_params.extend(extracted_params);
568 return Ok((Arc::new(dynamic_resource), combined_params));
569 }
570 }
571
572 Err(ResourceError::NotFound(uri.to_string()))
573 }
574
575 fn match_template(&self, template: &str, uri: &str) -> Option<HashMap<String, String>> {
579 let template_parts: Vec<&str> = template.split('/').collect();
580 let uri_parts: Vec<&str> = uri.split('/').collect();
581
582 if template_parts.len() > uri_parts.len() {
584 return None;
585 }
586
587 let mut params = HashMap::new();
588 let last_idx = template_parts.len() - 1;
589
590 for (i, template_part) in template_parts.iter().enumerate() {
591 if template_part.starts_with('{') && template_part.ends_with('}') {
592 let param_name = &template_part[1..template_part.len() - 1];
593
594 if i == last_idx {
595 let remaining = uri_parts[i..].join("/");
597 params.insert(param_name.to_string(), remaining);
598 break;
599 } else {
600 if i >= uri_parts.len() {
602 return None;
603 }
604 params.insert(param_name.to_string(), uri_parts[i].to_string());
605 }
606 } else if i >= uri_parts.len() || template_part != &uri_parts[i] {
607 return None;
609 }
610 }
611
612 Some(params)
613 }
614
615 pub fn list_for_session(
624 &self,
625 session: &Session,
626 ctx: &VisibilityContext<'_>,
627 ) -> Vec<ResourceInfo> {
628 let mut resources = std::collections::HashMap::new();
629
630 for entry in self.resources.iter() {
632 let uri = entry.key().clone();
633 if !session.is_resource_hidden(&uri) {
634 let resource = entry.value();
635 if resource.is_visible(ctx) {
636 resources.insert(
637 uri,
638 ResourceInfo {
639 uri: resource.uri().to_string(),
640 name: resource.name().to_string(),
641 description: resource.description().map(|s| s.to_string()),
642 mime_type: resource.mime_type().map(|s| s.to_string()),
643 },
644 );
645 }
646 }
647 }
648
649 for entry in session.resource_extras().iter() {
651 let uri = entry.key().clone();
652 let resource = entry.value();
653 if resource.is_visible(ctx) {
654 resources.insert(
655 uri,
656 ResourceInfo {
657 uri: resource.uri().to_string(),
658 name: resource.name().to_string(),
659 description: resource.description().map(|s| s.to_string()),
660 mime_type: resource.mime_type().map(|s| s.to_string()),
661 },
662 );
663 }
664 }
665
666 for entry in session.resource_overrides().iter() {
668 let uri = entry.key().clone();
669 let resource = entry.value();
670 if resource.is_visible(ctx) {
671 resources.insert(
672 uri,
673 ResourceInfo {
674 uri: resource.uri().to_string(),
675 name: resource.name().to_string(),
676 description: resource.description().map(|s| s.to_string()),
677 mime_type: resource.mime_type().map(|s| s.to_string()),
678 },
679 );
680 }
681 }
682
683 resources.into_values().collect()
684 }
685
686 pub async fn read_for_session(
695 &self,
696 uri: &str,
697 params: HashMap<String, String>,
698 session: &Session,
699 logger: &crate::logging::McpLogger,
700 visibility_ctx: &VisibilityContext<'_>,
701 ) -> Result<Vec<ResourceContent>, ResourceError> {
702 let exec_ctx = match visibility_ctx.environment {
704 Some(env) => ExecutionContext::for_resource_with_environment(
705 params.clone(),
706 session,
707 logger,
708 env,
709 ),
710 None => ExecutionContext::for_resource(params.clone(), session, logger),
711 };
712
713 if let Some(resource) = session.get_resource_override(uri) {
715 if !resource.is_visible(visibility_ctx) {
716 return Err(ResourceError::NotFound(uri.to_string()));
717 }
718 return resource.read(exec_ctx).await;
719 }
720
721 if let Some(resource) = session.get_resource_extra(uri) {
723 if !resource.is_visible(visibility_ctx) {
724 return Err(ResourceError::NotFound(uri.to_string()));
725 }
726 return resource.read(exec_ctx).await;
727 }
728
729 if session.is_resource_hidden(uri) {
731 return Err(ResourceError::NotFound(uri.to_string()));
732 }
733
734 if let Some(resource) = self.get(uri)
736 && !resource.is_visible(visibility_ctx)
737 {
738 return Err(ResourceError::NotFound(uri.to_string()));
739 }
740
741 self.read(uri, params, session, logger).await
743 }
744
745 pub fn len(&self) -> usize {
747 self.resources.len()
748 }
749
750 pub fn is_empty(&self) -> bool {
752 self.resources.is_empty()
753 }
754}
755
756impl Default for ResourceManager {
757 fn default() -> Self {
758 Self::new()
759 }
760}
761
762#[cfg(test)]
763mod tests {
764 use super::*;
765
766 struct HelloResource;
768
769 #[async_trait]
770 impl Resource for HelloResource {
771 fn uri(&self) -> &str {
772 "test://hello"
773 }
774
775 fn name(&self) -> &str {
776 "hello"
777 }
778
779 fn description(&self) -> Option<&str> {
780 Some("Returns a greeting")
781 }
782
783 fn mime_type(&self) -> Option<&str> {
784 Some("text/plain")
785 }
786
787 async fn read(
788 &self,
789 _ctx: ExecutionContext<'_>,
790 ) -> Result<Vec<ResourceContent>, ResourceError> {
791 Ok(vec![self.text_content("Hello, World!")])
792 }
793 }
794
795 struct UserResource;
797
798 #[async_trait]
799 impl Resource for UserResource {
800 fn uri(&self) -> &str {
801 "test://users/{id}"
802 }
803
804 fn name(&self) -> &str {
805 "user"
806 }
807
808 fn description(&self) -> Option<&str> {
809 Some("Returns user information")
810 }
811
812 async fn read(
813 &self,
814 ctx: ExecutionContext<'_>,
815 ) -> Result<Vec<ResourceContent>, ResourceError> {
816 let id = ctx
817 .get_uri_param("id")
818 .ok_or_else(|| ResourceError::InvalidUri("Missing 'id' parameter".to_string()))?;
819
820 Ok(vec![self.text_content(&format!("User ID: {}", id))])
821 }
822 }
823
824 #[test]
825 fn test_manager_creation() {
826 let manager = ResourceManager::new();
827 assert!(manager.is_empty());
828 }
829
830 #[test]
831 fn test_resource_registration() {
832 let manager = ResourceManager::new();
833 manager.register(HelloResource);
834
835 assert_eq!(manager.len(), 1);
836 assert!(!manager.is_empty());
837 }
838
839 #[test]
840 fn test_get_resource() {
841 let manager = ResourceManager::new();
842 manager.register(HelloResource);
843
844 let resource = manager.get("test://hello");
845 assert!(resource.is_some());
846 assert_eq!(resource.unwrap().name(), "hello");
847
848 let missing = manager.get("test://nonexistent");
849 assert!(missing.is_none());
850 }
851
852 #[test]
853 fn test_list_resources() {
854 let manager = ResourceManager::new();
855 manager.register(HelloResource);
856
857 let resources = manager.list();
858 assert_eq!(resources.len(), 1);
859 assert_eq!(resources[0].uri, "test://hello");
860 assert_eq!(resources[0].name, "hello");
861 assert_eq!(
862 resources[0].description,
863 Some("Returns a greeting".to_string())
864 );
865 assert_eq!(resources[0].mime_type, Some("text/plain".to_string()));
866 }
867
868 #[tokio::test]
869 async fn test_read_static_resource() {
870 let (_tx, _rx) = tokio::sync::mpsc::unbounded_channel();
871 let logger = crate::logging::McpLogger::new(_tx, "test");
872 let manager = ResourceManager::new();
873 manager.register(HelloResource);
874 let session = Session::new();
875
876 let result = manager
877 .read("test://hello", HashMap::new(), &session, &logger)
878 .await
879 .unwrap();
880 assert_eq!(result.len(), 1);
881 }
882
883 #[tokio::test]
884 async fn test_read_missing_resource() {
885 let (_tx, _rx) = tokio::sync::mpsc::unbounded_channel();
886 let logger = crate::logging::McpLogger::new(_tx, "test");
887 let manager = ResourceManager::new();
888 let session = Session::new();
889
890 let result = manager
891 .read("test://nonexistent", HashMap::new(), &session, &logger)
892 .await;
893 assert!(matches!(result, Err(ResourceError::NotFound(_))));
894 }
895
896 #[tokio::test]
897 async fn test_template_matching() {
898 let (_tx, _rx) = tokio::sync::mpsc::unbounded_channel();
899 let logger = crate::logging::McpLogger::new(_tx, "test");
900 let manager = ResourceManager::new();
901 manager.register(UserResource);
902 let session = Session::new();
903
904 let result = manager
906 .read("test://users/123", HashMap::new(), &session, &logger)
907 .await
908 .unwrap();
909 assert_eq!(result.len(), 1);
910 }
911
912 #[test]
913 fn test_template_matching_internal() {
914 let manager = ResourceManager::new();
915
916 let params = manager.match_template("test://users/{id}", "test://users/123");
918 assert!(params.is_some());
919 let params = params.unwrap();
920 assert_eq!(params.get("id"), Some(&"123".to_string()));
921
922 let params = manager.match_template(
924 "test://org/{org}/repo/{repo}",
925 "test://org/myorg/repo/myrepo",
926 );
927 assert!(params.is_some());
928 let params = params.unwrap();
929 assert_eq!(params.get("org"), Some(&"myorg".to_string()));
930 assert_eq!(params.get("repo"), Some(&"myrepo".to_string()));
931
932 let params = manager.match_template("test://users/{id}", "test://posts/123");
934 assert!(params.is_none());
935
936 let params = manager.match_template("test://users/{id}", "test://users/123/extra");
938 assert!(params.is_some());
939 let params = params.unwrap();
940 assert_eq!(params.get("id"), Some(&"123/extra".to_string()));
941 }
942
943 #[tokio::test]
944 async fn test_resource_non_matching_template() {
945 let (_tx, _rx) = tokio::sync::mpsc::unbounded_channel();
946 let logger = crate::logging::McpLogger::new(_tx, "test");
947 let manager = ResourceManager::new();
948 manager.register(UserResource);
949 let session = Session::new();
950
951 let result = manager
953 .read("test://posts/123", HashMap::new(), &session, &logger)
954 .await;
955 assert!(matches!(result, Err(ResourceError::NotFound(_))));
956 }
957
958 #[tokio::test]
959 async fn test_resource_greedy_matching() {
960 let (_tx, _rx) = tokio::sync::mpsc::unbounded_channel();
961 let logger = crate::logging::McpLogger::new(_tx, "test");
962 let manager = ResourceManager::new();
963 manager.register(UserResource);
964 let session = Session::new();
965
966 let result = manager
968 .read("test://users/123/extra", HashMap::new(), &session, &logger)
969 .await;
970 assert!(result.is_ok());
971 let content = result.unwrap();
972 assert_eq!(content.len(), 1);
973 assert!(content[0].text.as_ref().unwrap().contains("123/extra"));
975 }
976
977 struct FileTemplate;
981
982 #[async_trait]
983 impl ResourceTemplate for FileTemplate {
984 fn uri_template(&self) -> &str {
985 "file:///{path}"
986 }
987
988 fn name(&self) -> &str {
989 "project_files"
990 }
991
992 fn title(&self) -> Option<&str> {
993 Some("Project Files")
994 }
995
996 fn description(&self) -> Option<&str> {
997 Some("Access files in the project directory")
998 }
999
1000 fn mime_type(&self) -> Option<&str> {
1001 Some("application/octet-stream")
1002 }
1003
1004 async fn read(
1005 &self,
1006 ctx: ExecutionContext<'_>,
1007 ) -> Result<Vec<ResourceContent>, ResourceError> {
1008 let path = ctx
1009 .get_uri_param("path")
1010 .ok_or_else(|| ResourceError::InvalidUri("Missing 'path' parameter".to_string()))?;
1011
1012 Ok(vec![ResourceContent::text(
1013 format!("file:///{}", path),
1014 format!("Mock content for file: {}", path),
1015 )])
1016 }
1017 }
1018
1019 struct AdminOnlyTemplate;
1021
1022 #[async_trait]
1023 impl ResourceTemplate for AdminOnlyTemplate {
1024 fn uri_template(&self) -> &str {
1025 "admin:///{resource}"
1026 }
1027
1028 fn name(&self) -> &str {
1029 "admin_resources"
1030 }
1031
1032 fn is_visible(&self, ctx: &VisibilityContext) -> bool {
1033 ctx.has_role("admin")
1034 }
1035
1036 async fn read(
1037 &self,
1038 _ctx: ExecutionContext<'_>,
1039 ) -> Result<Vec<ResourceContent>, ResourceError> {
1040 Ok(vec![ResourceContent::text(
1041 "admin:///test",
1042 "Admin content",
1043 )])
1044 }
1045 }
1046
1047 #[test]
1048 fn test_template_registration() {
1049 let manager = ResourceManager::new();
1050 manager.register_template(FileTemplate);
1051
1052 let template = manager.get_template("project_files");
1053 assert!(template.is_some());
1054 assert_eq!(template.unwrap().name(), "project_files");
1055 }
1056
1057 #[test]
1058 fn test_get_template() {
1059 let manager = ResourceManager::new();
1060 manager.register_template(FileTemplate);
1061
1062 let template = manager.get_template("project_files");
1063 assert!(template.is_some());
1064 assert_eq!(template.unwrap().uri_template(), "file:///{path}");
1065
1066 let missing = manager.get_template("nonexistent");
1067 assert!(missing.is_none());
1068 }
1069
1070 #[test]
1071 fn test_list_templates() {
1072 let manager = ResourceManager::new();
1073 manager.register_template(FileTemplate);
1074
1075 let templates = manager.list_templates();
1076 assert_eq!(templates.len(), 1);
1077 assert_eq!(templates[0].uri_template, "file:///{path}");
1078 assert_eq!(templates[0].name, "project_files");
1079 assert_eq!(templates[0].title, Some("Project Files".to_string()));
1080 assert_eq!(
1081 templates[0].description,
1082 Some("Access files in the project directory".to_string())
1083 );
1084 assert_eq!(
1085 templates[0].mime_type,
1086 Some("application/octet-stream".to_string())
1087 );
1088 }
1089
1090 #[test]
1091 fn test_list_templates_for_session_visibility() {
1092 let manager = ResourceManager::new();
1093 manager.register_template(FileTemplate);
1094 manager.register_template(AdminOnlyTemplate);
1095
1096 let session = Session::new();
1098 let ctx = VisibilityContext::new(&session);
1099 let templates = manager.list_templates_for_session(&session, &ctx);
1100
1101 assert_eq!(templates.len(), 1);
1103 assert_eq!(templates[0].name, "project_files");
1104
1105 let admin_session = Session::new();
1107 admin_session.set_state("roles", serde_json::json!(["admin"]));
1108 let admin_ctx = VisibilityContext::new(&admin_session);
1109 let admin_templates = manager.list_templates_for_session(&admin_session, &admin_ctx);
1110
1111 assert_eq!(admin_templates.len(), 2);
1113 let names: Vec<_> = admin_templates.iter().map(|t| t.name.as_str()).collect();
1114 assert!(names.contains(&"project_files"));
1115 assert!(names.contains(&"admin_resources"));
1116 }
1117
1118 #[test]
1119 fn test_template_info_serialization() {
1120 let manager = ResourceManager::new();
1121 manager.register_template(FileTemplate);
1122
1123 let templates = manager.list_templates();
1124 let serialized = serde_json::to_value(&templates[0]).unwrap();
1125
1126 assert!(serialized.get("uriTemplate").is_some());
1128 assert_eq!(serialized["uriTemplate"], "file:///{path}");
1129 assert_eq!(serialized["name"], "project_files");
1130 assert_eq!(serialized["mimeType"], "application/octet-stream");
1131
1132 assert!(serialized.get("uri_template").is_none());
1134 assert!(serialized.get("mime_type").is_none());
1135 }
1136
1137 #[tokio::test]
1138 async fn test_template_read_with_uri_params() {
1139 let (_tx, _rx) = tokio::sync::mpsc::unbounded_channel();
1140 let logger = crate::logging::McpLogger::new(_tx, "test");
1141 let session = Session::new();
1142 let ctx = ExecutionContext::for_resource(
1143 vec![("path".to_string(), "src/main.rs".to_string())]
1144 .into_iter()
1145 .collect(),
1146 &session,
1147 &logger,
1148 );
1149
1150 let template = FileTemplate;
1151 let result = template.read(ctx).await.unwrap();
1152
1153 assert_eq!(result.len(), 1);
1154 assert_eq!(result[0].uri, "file:///src/main.rs");
1155 assert!(result[0].text.as_ref().unwrap().contains("src/main.rs"));
1156 }
1157
1158 #[tokio::test]
1159 async fn test_template_read_missing_param() {
1160 let (_tx, _rx) = tokio::sync::mpsc::unbounded_channel();
1161 let logger = crate::logging::McpLogger::new(_tx, "test");
1162 let session = Session::new();
1163 let ctx = ExecutionContext::for_resource(HashMap::new(), &session, &logger);
1164
1165 let template = FileTemplate;
1166 let result = template.read(ctx).await;
1167
1168 assert!(matches!(result, Err(ResourceError::InvalidUri(_))));
1170 }
1171}