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 let last_was_greedy_param = template_parts
614 .last()
615 .is_some_and(|p| p.starts_with('{') && p.ends_with('}'));
616
617 if !last_was_greedy_param && template_parts.len() != uri_parts.len() {
618 return None;
619 }
620
621 Some(params)
622 }
623
624 pub fn list_for_session(
633 &self,
634 session: &Session,
635 ctx: &VisibilityContext<'_>,
636 ) -> Vec<ResourceInfo> {
637 let mut resources = std::collections::HashMap::new();
638
639 for entry in self.resources.iter() {
641 let uri = entry.key().clone();
642 if !session.is_resource_hidden(&uri) {
643 let resource = entry.value();
644 if resource.is_visible(ctx) {
645 resources.insert(
646 uri,
647 ResourceInfo {
648 uri: resource.uri().to_string(),
649 name: resource.name().to_string(),
650 description: resource.description().map(|s| s.to_string()),
651 mime_type: resource.mime_type().map(|s| s.to_string()),
652 },
653 );
654 }
655 }
656 }
657
658 for entry in session.resource_extras().iter() {
660 let uri = entry.key().clone();
661 let resource = entry.value();
662 if resource.is_visible(ctx) {
663 resources.insert(
664 uri,
665 ResourceInfo {
666 uri: resource.uri().to_string(),
667 name: resource.name().to_string(),
668 description: resource.description().map(|s| s.to_string()),
669 mime_type: resource.mime_type().map(|s| s.to_string()),
670 },
671 );
672 }
673 }
674
675 for entry in session.resource_overrides().iter() {
677 let uri = entry.key().clone();
678 let resource = entry.value();
679 if resource.is_visible(ctx) {
680 resources.insert(
681 uri,
682 ResourceInfo {
683 uri: resource.uri().to_string(),
684 name: resource.name().to_string(),
685 description: resource.description().map(|s| s.to_string()),
686 mime_type: resource.mime_type().map(|s| s.to_string()),
687 },
688 );
689 }
690 }
691
692 resources.into_values().collect()
693 }
694
695 pub async fn read_for_session(
704 &self,
705 uri: &str,
706 params: HashMap<String, String>,
707 session: &Session,
708 logger: &crate::logging::McpLogger,
709 visibility_ctx: &VisibilityContext<'_>,
710 ) -> Result<Vec<ResourceContent>, ResourceError> {
711 let exec_ctx = match visibility_ctx.environment {
713 Some(env) => ExecutionContext::for_resource_with_environment(
714 params.clone(),
715 session,
716 logger,
717 env,
718 ),
719 None => ExecutionContext::for_resource(params.clone(), session, logger),
720 };
721
722 if let Some(resource) = session.get_resource_override(uri) {
724 if !resource.is_visible(visibility_ctx) {
725 return Err(ResourceError::NotFound(uri.to_string()));
726 }
727 return resource.read(exec_ctx).await;
728 }
729
730 if let Some(resource) = session.get_resource_extra(uri) {
732 if !resource.is_visible(visibility_ctx) {
733 return Err(ResourceError::NotFound(uri.to_string()));
734 }
735 return resource.read(exec_ctx).await;
736 }
737
738 if session.is_resource_hidden(uri) {
740 return Err(ResourceError::NotFound(uri.to_string()));
741 }
742
743 if let Some(resource) = self.get(uri)
745 && !resource.is_visible(visibility_ctx)
746 {
747 return Err(ResourceError::NotFound(uri.to_string()));
748 }
749
750 self.read(uri, params, session, logger).await
752 }
753
754 pub fn len(&self) -> usize {
756 self.resources.len()
757 }
758
759 pub fn is_empty(&self) -> bool {
761 self.resources.is_empty()
762 }
763}
764
765impl Default for ResourceManager {
766 fn default() -> Self {
767 Self::new()
768 }
769}
770
771#[cfg(test)]
772mod tests {
773 use super::*;
774
775 struct HelloResource;
777
778 #[async_trait]
779 impl Resource for HelloResource {
780 fn uri(&self) -> &str {
781 "test://hello"
782 }
783
784 fn name(&self) -> &str {
785 "hello"
786 }
787
788 fn description(&self) -> Option<&str> {
789 Some("Returns a greeting")
790 }
791
792 fn mime_type(&self) -> Option<&str> {
793 Some("text/plain")
794 }
795
796 async fn read(
797 &self,
798 _ctx: ExecutionContext<'_>,
799 ) -> Result<Vec<ResourceContent>, ResourceError> {
800 Ok(vec![self.text_content("Hello, World!")])
801 }
802 }
803
804 struct UserResource;
806
807 #[async_trait]
808 impl Resource for UserResource {
809 fn uri(&self) -> &str {
810 "test://users/{id}"
811 }
812
813 fn name(&self) -> &str {
814 "user"
815 }
816
817 fn description(&self) -> Option<&str> {
818 Some("Returns user information")
819 }
820
821 async fn read(
822 &self,
823 ctx: ExecutionContext<'_>,
824 ) -> Result<Vec<ResourceContent>, ResourceError> {
825 let id = ctx
826 .get_uri_param("id")
827 .ok_or_else(|| ResourceError::InvalidUri("Missing 'id' parameter".to_string()))?;
828
829 Ok(vec![self.text_content(&format!("User ID: {}", id))])
830 }
831 }
832
833 #[test]
834 fn test_manager_creation() {
835 let manager = ResourceManager::new();
836 assert!(manager.is_empty());
837 }
838
839 #[test]
840 fn test_resource_registration() {
841 let manager = ResourceManager::new();
842 manager.register(HelloResource);
843
844 assert_eq!(manager.len(), 1);
845 assert!(!manager.is_empty());
846 }
847
848 #[test]
849 fn test_get_resource() {
850 let manager = ResourceManager::new();
851 manager.register(HelloResource);
852
853 let resource = manager.get("test://hello");
854 assert!(resource.is_some());
855 assert_eq!(resource.unwrap().name(), "hello");
856
857 let missing = manager.get("test://nonexistent");
858 assert!(missing.is_none());
859 }
860
861 #[test]
862 fn test_list_resources() {
863 let manager = ResourceManager::new();
864 manager.register(HelloResource);
865
866 let resources = manager.list();
867 assert_eq!(resources.len(), 1);
868 assert_eq!(resources[0].uri, "test://hello");
869 assert_eq!(resources[0].name, "hello");
870 assert_eq!(
871 resources[0].description,
872 Some("Returns a greeting".to_string())
873 );
874 assert_eq!(resources[0].mime_type, Some("text/plain".to_string()));
875 }
876
877 #[tokio::test]
878 async fn test_read_static_resource() {
879 let (_tx, _rx) = tokio::sync::mpsc::unbounded_channel();
880 let logger = crate::logging::McpLogger::new(_tx, "test");
881 let manager = ResourceManager::new();
882 manager.register(HelloResource);
883 let session = Session::new();
884
885 let result = manager
886 .read("test://hello", HashMap::new(), &session, &logger)
887 .await
888 .unwrap();
889 assert_eq!(result.len(), 1);
890 }
891
892 #[tokio::test]
893 async fn test_read_missing_resource() {
894 let (_tx, _rx) = tokio::sync::mpsc::unbounded_channel();
895 let logger = crate::logging::McpLogger::new(_tx, "test");
896 let manager = ResourceManager::new();
897 let session = Session::new();
898
899 let result = manager
900 .read("test://nonexistent", HashMap::new(), &session, &logger)
901 .await;
902 assert!(matches!(result, Err(ResourceError::NotFound(_))));
903 }
904
905 #[tokio::test]
906 async fn test_template_matching() {
907 let (_tx, _rx) = tokio::sync::mpsc::unbounded_channel();
908 let logger = crate::logging::McpLogger::new(_tx, "test");
909 let manager = ResourceManager::new();
910 manager.register(UserResource);
911 let session = Session::new();
912
913 let result = manager
915 .read("test://users/123", HashMap::new(), &session, &logger)
916 .await
917 .unwrap();
918 assert_eq!(result.len(), 1);
919 }
920
921 #[test]
922 fn test_template_matching_internal() {
923 let manager = ResourceManager::new();
924
925 let params = manager.match_template("test://users/{id}", "test://users/123");
927 assert!(params.is_some());
928 let params = params.unwrap();
929 assert_eq!(params.get("id"), Some(&"123".to_string()));
930
931 let params = manager.match_template(
933 "test://org/{org}/repo/{repo}",
934 "test://org/myorg/repo/myrepo",
935 );
936 assert!(params.is_some());
937 let params = params.unwrap();
938 assert_eq!(params.get("org"), Some(&"myorg".to_string()));
939 assert_eq!(params.get("repo"), Some(&"myrepo".to_string()));
940
941 let params = manager.match_template("test://users/{id}", "test://posts/123");
943 assert!(params.is_none());
944
945 let params = manager.match_template("test://users/{id}", "test://users/123/extra");
947 assert!(params.is_some());
948 let params = params.unwrap();
949 assert_eq!(params.get("id"), Some(&"123/extra".to_string()));
950 }
951
952 #[tokio::test]
953 async fn test_resource_non_matching_template() {
954 let (_tx, _rx) = tokio::sync::mpsc::unbounded_channel();
955 let logger = crate::logging::McpLogger::new(_tx, "test");
956 let manager = ResourceManager::new();
957 manager.register(UserResource);
958 let session = Session::new();
959
960 let result = manager
962 .read("test://posts/123", HashMap::new(), &session, &logger)
963 .await;
964 assert!(matches!(result, Err(ResourceError::NotFound(_))));
965 }
966
967 #[tokio::test]
968 async fn test_resource_greedy_matching() {
969 let (_tx, _rx) = tokio::sync::mpsc::unbounded_channel();
970 let logger = crate::logging::McpLogger::new(_tx, "test");
971 let manager = ResourceManager::new();
972 manager.register(UserResource);
973 let session = Session::new();
974
975 let result = manager
977 .read("test://users/123/extra", HashMap::new(), &session, &logger)
978 .await;
979 assert!(result.is_ok());
980 let content = result.unwrap();
981 assert_eq!(content.len(), 1);
982 assert!(content[0].text.as_ref().unwrap().contains("123/extra"));
984 }
985
986 struct FileTemplate;
990
991 #[async_trait]
992 impl ResourceTemplate for FileTemplate {
993 fn uri_template(&self) -> &str {
994 "file:///{path}"
995 }
996
997 fn name(&self) -> &str {
998 "project_files"
999 }
1000
1001 fn title(&self) -> Option<&str> {
1002 Some("Project Files")
1003 }
1004
1005 fn description(&self) -> Option<&str> {
1006 Some("Access files in the project directory")
1007 }
1008
1009 fn mime_type(&self) -> Option<&str> {
1010 Some("application/octet-stream")
1011 }
1012
1013 async fn read(
1014 &self,
1015 ctx: ExecutionContext<'_>,
1016 ) -> Result<Vec<ResourceContent>, ResourceError> {
1017 let path = ctx
1018 .get_uri_param("path")
1019 .ok_or_else(|| ResourceError::InvalidUri("Missing 'path' parameter".to_string()))?;
1020
1021 Ok(vec![ResourceContent::text(
1022 format!("file:///{}", path),
1023 format!("Mock content for file: {}", path),
1024 )])
1025 }
1026 }
1027
1028 struct AdminOnlyTemplate;
1030
1031 #[async_trait]
1032 impl ResourceTemplate for AdminOnlyTemplate {
1033 fn uri_template(&self) -> &str {
1034 "admin:///{resource}"
1035 }
1036
1037 fn name(&self) -> &str {
1038 "admin_resources"
1039 }
1040
1041 fn is_visible(&self, ctx: &VisibilityContext) -> bool {
1042 ctx.has_role("admin")
1043 }
1044
1045 async fn read(
1046 &self,
1047 _ctx: ExecutionContext<'_>,
1048 ) -> Result<Vec<ResourceContent>, ResourceError> {
1049 Ok(vec![ResourceContent::text(
1050 "admin:///test",
1051 "Admin content",
1052 )])
1053 }
1054 }
1055
1056 #[test]
1057 fn test_template_registration() {
1058 let manager = ResourceManager::new();
1059 manager.register_template(FileTemplate);
1060
1061 let template = manager.get_template("project_files");
1062 assert!(template.is_some());
1063 assert_eq!(template.unwrap().name(), "project_files");
1064 }
1065
1066 #[test]
1067 fn test_get_template() {
1068 let manager = ResourceManager::new();
1069 manager.register_template(FileTemplate);
1070
1071 let template = manager.get_template("project_files");
1072 assert!(template.is_some());
1073 assert_eq!(template.unwrap().uri_template(), "file:///{path}");
1074
1075 let missing = manager.get_template("nonexistent");
1076 assert!(missing.is_none());
1077 }
1078
1079 #[test]
1080 fn test_list_templates() {
1081 let manager = ResourceManager::new();
1082 manager.register_template(FileTemplate);
1083
1084 let templates = manager.list_templates();
1085 assert_eq!(templates.len(), 1);
1086 assert_eq!(templates[0].uri_template, "file:///{path}");
1087 assert_eq!(templates[0].name, "project_files");
1088 assert_eq!(templates[0].title, Some("Project Files".to_string()));
1089 assert_eq!(
1090 templates[0].description,
1091 Some("Access files in the project directory".to_string())
1092 );
1093 assert_eq!(
1094 templates[0].mime_type,
1095 Some("application/octet-stream".to_string())
1096 );
1097 }
1098
1099 #[test]
1100 fn test_list_templates_for_session_visibility() {
1101 let manager = ResourceManager::new();
1102 manager.register_template(FileTemplate);
1103 manager.register_template(AdminOnlyTemplate);
1104
1105 let session = Session::new();
1107 let ctx = VisibilityContext::new(&session);
1108 let templates = manager.list_templates_for_session(&session, &ctx);
1109
1110 assert_eq!(templates.len(), 1);
1112 assert_eq!(templates[0].name, "project_files");
1113
1114 let admin_session = Session::new();
1116 admin_session.set_state("roles", serde_json::json!(["admin"]));
1117 let admin_ctx = VisibilityContext::new(&admin_session);
1118 let admin_templates = manager.list_templates_for_session(&admin_session, &admin_ctx);
1119
1120 assert_eq!(admin_templates.len(), 2);
1122 let names: Vec<_> = admin_templates.iter().map(|t| t.name.as_str()).collect();
1123 assert!(names.contains(&"project_files"));
1124 assert!(names.contains(&"admin_resources"));
1125 }
1126
1127 #[test]
1128 fn test_template_info_serialization() {
1129 let manager = ResourceManager::new();
1130 manager.register_template(FileTemplate);
1131
1132 let templates = manager.list_templates();
1133 let serialized = serde_json::to_value(&templates[0]).unwrap();
1134
1135 assert!(serialized.get("uriTemplate").is_some());
1137 assert_eq!(serialized["uriTemplate"], "file:///{path}");
1138 assert_eq!(serialized["name"], "project_files");
1139 assert_eq!(serialized["mimeType"], "application/octet-stream");
1140
1141 assert!(serialized.get("uri_template").is_none());
1143 assert!(serialized.get("mime_type").is_none());
1144 }
1145
1146 #[tokio::test]
1147 async fn test_template_read_with_uri_params() {
1148 let (_tx, _rx) = tokio::sync::mpsc::unbounded_channel();
1149 let logger = crate::logging::McpLogger::new(_tx, "test");
1150 let session = Session::new();
1151 let ctx = ExecutionContext::for_resource(
1152 vec![("path".to_string(), "src/main.rs".to_string())]
1153 .into_iter()
1154 .collect(),
1155 &session,
1156 &logger,
1157 );
1158
1159 let template = FileTemplate;
1160 let result = template.read(ctx).await.unwrap();
1161
1162 assert_eq!(result.len(), 1);
1163 assert_eq!(result[0].uri, "file:///src/main.rs");
1164 assert!(result[0].text.as_ref().unwrap().contains("src/main.rs"));
1165 }
1166
1167 #[tokio::test]
1168 async fn test_template_read_missing_param() {
1169 let (_tx, _rx) = tokio::sync::mpsc::unbounded_channel();
1170 let logger = crate::logging::McpLogger::new(_tx, "test");
1171 let session = Session::new();
1172 let ctx = ExecutionContext::for_resource(HashMap::new(), &session, &logger);
1173
1174 let template = FileTemplate;
1175 let result = template.read(ctx).await;
1176
1177 assert!(matches!(result, Err(ResourceError::InvalidUri(_))));
1179 }
1180}