1use async_trait::async_trait;
7use std::collections::HashMap;
8
9use crate::core::error::{McpError, McpResult};
10use crate::protocol::types::{Icon, Resource as ResourceInfo, ResourceContents};
11
12#[derive(Debug, Clone, PartialEq)]
14pub struct ResourceTemplate {
15 pub uri_template: String,
17 pub name: String,
19 pub description: Option<String>,
21 pub mime_type: Option<String>,
23}
24
25#[async_trait]
27pub trait ResourceHandler: Send + Sync {
28 async fn read(
37 &self,
38 uri: &str,
39 params: &HashMap<String, String>,
40 ) -> McpResult<Vec<ResourceContents>>;
41
42 async fn list(&self) -> McpResult<Vec<ResourceInfo>>;
47
48 async fn subscribe(&self, uri: &str) -> McpResult<()> {
56 Err(McpError::protocol(format!(
58 "Subscription not supported for resource: {uri}"
59 )))
60 }
61
62 async fn unsubscribe(&self, uri: &str) -> McpResult<()> {
70 Err(McpError::protocol(format!(
72 "Subscription not supported for resource: {uri}"
73 )))
74 }
75}
76
77#[async_trait]
80pub trait LegacyResourceHandler: Send + Sync {
81 async fn read(&self, uri: &str) -> McpResult<String>;
89
90 async fn list(&self) -> McpResult<Vec<ResourceInfo>> {
95 Ok(vec![])
97 }
98}
99
100pub struct LegacyResourceAdapter<T> {
102 inner: T,
103}
104
105impl<T> LegacyResourceAdapter<T>
106where
107 T: LegacyResourceHandler,
108{
109 pub fn new(handler: T) -> Self {
110 Self { inner: handler }
111 }
112}
113
114#[async_trait]
115impl<T> ResourceHandler for LegacyResourceAdapter<T>
116where
117 T: LegacyResourceHandler + Send + Sync,
118{
119 async fn read(
120 &self,
121 uri: &str,
122 _params: &HashMap<String, String>,
123 ) -> McpResult<Vec<ResourceContents>> {
124 let content = self.inner.read(uri).await?;
125 Ok(vec![ResourceContents::Text {
126 uri: uri.to_string(),
127 mime_type: Some("text/plain".to_string()),
128 text: content,
129 meta: None,
130 }])
131 }
132
133 async fn list(&self) -> McpResult<Vec<ResourceInfo>> {
134 self.inner.list().await
135 }
136}
137
138pub struct Resource {
140 pub info: ResourceInfo,
142 pub handler: Box<dyn ResourceHandler>,
144 pub template: Option<ResourceTemplate>,
146 pub enabled: bool,
148}
149
150impl Resource {
151 pub fn new<H>(info: ResourceInfo, handler: H) -> Self
157 where
158 H: ResourceHandler + 'static,
159 {
160 Self {
161 info,
162 handler: Box::new(handler),
163 template: None,
164 enabled: true,
165 }
166 }
167
168 pub fn with_template<H>(template: ResourceTemplate, handler: H) -> Self
174 where
175 H: ResourceHandler + 'static,
176 {
177 let info = ResourceInfo {
178 uri: template.uri_template.clone(),
179 name: template.name.clone(),
180 description: template.description.clone(),
181 mime_type: template.mime_type.clone(),
182 annotations: None,
183 size: None,
184 icons: None,
185 title: None,
186 meta: None,
187 };
188
189 Self {
190 info,
191 handler: Box::new(handler),
192 template: Some(template),
193 enabled: true,
194 }
195 }
196
197 pub fn enable(&mut self) {
199 self.enabled = true;
200 }
201
202 pub fn disable(&mut self) {
204 self.enabled = false;
205 }
206
207 pub fn is_enabled(&self) -> bool {
209 self.enabled
210 }
211
212 pub async fn read(
221 &self,
222 uri: &str,
223 params: &HashMap<String, String>,
224 ) -> McpResult<Vec<ResourceContents>> {
225 if !self.enabled {
226 let name = self.info.name.as_str();
227 return Err(McpError::validation(format!(
228 "Resource '{name}' is disabled"
229 )));
230 }
231
232 self.handler.read(uri, params).await
233 }
234
235 pub async fn list(&self) -> McpResult<Vec<ResourceInfo>> {
237 if !self.enabled {
238 return Ok(vec![]);
239 }
240
241 self.handler.list().await
242 }
243
244 pub async fn subscribe(&self, uri: &str) -> McpResult<()> {
246 if !self.enabled {
247 let name = self.info.name.as_str();
248 return Err(McpError::validation(format!(
249 "Resource '{name}' is disabled"
250 )));
251 }
252
253 self.handler.subscribe(uri).await
254 }
255
256 pub async fn unsubscribe(&self, uri: &str) -> McpResult<()> {
258 if !self.enabled {
259 let name = self.info.name.as_str();
260 return Err(McpError::validation(format!(
261 "Resource '{name}' is disabled"
262 )));
263 }
264
265 self.handler.unsubscribe(uri).await
266 }
267
268 pub fn matches_uri(&self, uri: &str) -> bool {
270 if let Some(template) = &self.template {
271 uri.starts_with(&template.uri_template.replace("{id}", "").replace("{*}", ""))
274 } else {
275 self.info.uri == uri
276 }
277 }
278}
279
280impl std::fmt::Debug for Resource {
281 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
282 f.debug_struct("Resource")
283 .field("info", &self.info)
284 .field("template", &self.template)
285 .field("enabled", &self.enabled)
286 .finish()
287 }
288}
289
290pub struct TextResource {
294 content: String,
295 mime_type: String,
296}
297
298impl TextResource {
299 pub fn new(content: String, mime_type: Option<String>) -> Self {
301 Self {
302 content,
303 mime_type: mime_type.unwrap_or_else(|| "text/plain".to_string()),
304 }
305 }
306}
307
308#[async_trait]
309impl ResourceHandler for TextResource {
310 async fn read(
311 &self,
312 uri: &str,
313 _params: &HashMap<String, String>,
314 ) -> McpResult<Vec<ResourceContents>> {
315 Ok(vec![ResourceContents::Text {
316 uri: uri.to_string(),
317 mime_type: Some(self.mime_type.clone()),
318 text: self.content.clone(),
319 meta: None,
320 }])
321 }
322
323 async fn list(&self) -> McpResult<Vec<ResourceInfo>> {
324 Ok(vec![])
326 }
327}
328
329pub struct FileSystemResource {
331 base_path: std::path::PathBuf,
332 allowed_extensions: Option<Vec<String>>,
333}
334
335impl FileSystemResource {
336 pub fn new<P: AsRef<std::path::Path>>(base_path: P) -> Self {
338 Self {
339 base_path: base_path.as_ref().to_path_buf(),
340 allowed_extensions: None,
341 }
342 }
343
344 pub fn with_extensions(mut self, extensions: Vec<String>) -> Self {
346 self.allowed_extensions = Some(extensions);
347 self
348 }
349
350 fn is_allowed_file(&self, path: &std::path::Path) -> bool {
351 if let Some(ref allowed) = self.allowed_extensions {
352 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
353 return allowed.contains(&ext.to_lowercase());
354 }
355 return false;
356 }
357 true
358 }
359
360 fn get_mime_type(&self, path: &std::path::Path) -> String {
361 match path.extension().and_then(|e| e.to_str()) {
362 Some("txt") => "text/plain".to_string(),
363 Some("json") => "application/json".to_string(),
364 Some("html") => "text/html".to_string(),
365 Some("css") => "text/css".to_string(),
366 Some("js") => "application/javascript".to_string(),
367 Some("md") => "text/markdown".to_string(),
368 Some("xml") => "application/xml".to_string(),
369 Some("yaml") | Some("yml") => "application/yaml".to_string(),
370 _ => "application/octet-stream".to_string(),
371 }
372 }
373}
374
375#[async_trait]
376impl ResourceHandler for FileSystemResource {
377 async fn read(
378 &self,
379 uri: &str,
380 _params: &HashMap<String, String>,
381 ) -> McpResult<Vec<ResourceContents>> {
382 let file_path = if uri.starts_with("file://") {
384 uri.strip_prefix("file://").unwrap_or(uri)
385 } else {
386 uri
387 };
388
389 let full_path = self.base_path.join(file_path);
390
391 let canonical_base = self.base_path.canonicalize().map_err(McpError::io)?;
393 let canonical_target = full_path
394 .canonicalize()
395 .map_err(|_| McpError::ResourceNotFound(uri.to_string()))?;
396
397 if !canonical_target.starts_with(&canonical_base) {
398 return Err(McpError::validation("Path outside of allowed directory"));
399 }
400
401 if !self.is_allowed_file(&canonical_target) {
402 return Err(McpError::validation("File type not allowed"));
403 }
404
405 let content = tokio::fs::read_to_string(&canonical_target)
406 .await
407 .map_err(|_| McpError::ResourceNotFound(uri.to_string()))?;
408
409 let mime_type = self.get_mime_type(&canonical_target);
410
411 Ok(vec![ResourceContents::Text {
412 uri: uri.to_string(),
413 mime_type: Some(mime_type),
414 text: content,
415 meta: None,
416 }])
417 }
418
419 async fn list(&self) -> McpResult<Vec<ResourceInfo>> {
420 let mut resources = Vec::new();
421 let mut stack = vec![self.base_path.clone()];
422
423 while let Some(dir_path) = stack.pop() {
424 let mut dir = tokio::fs::read_dir(&dir_path).await.map_err(McpError::io)?;
425
426 while let Some(entry) = dir.next_entry().await.map_err(McpError::io)? {
427 let path = entry.path();
428
429 if path.is_dir() {
430 stack.push(path);
431 } else if self.is_allowed_file(&path) {
432 let relative_path = path
433 .strip_prefix(&self.base_path)
434 .map_err(|_| McpError::internal("Path computation error"))?;
435
436 let path_display = relative_path.display();
437 let uri = format!("file://{path_display}");
438 let name = path
439 .file_name()
440 .and_then(|n| n.to_str())
441 .unwrap_or("unnamed")
442 .to_string();
443
444 resources.push(ResourceInfo {
445 uri,
446 name,
447 description: None,
448 mime_type: Some(self.get_mime_type(&path)),
449 annotations: None,
450 size: None,
451 icons: None,
452 title: None,
453 meta: None,
454 });
455 }
456 }
457 }
458
459 Ok(resources)
460 }
461}
462
463pub struct ResourceBuilder {
465 uri: String,
466 name: String,
467 description: Option<String>,
468 mime_type: Option<String>,
469 title: Option<String>,
470 icons: Option<Vec<Icon>>,
471}
472
473impl ResourceBuilder {
474 pub fn new<S: Into<String>>(uri: S, name: S) -> Self {
476 Self {
477 uri: uri.into(),
478 name: name.into(),
479 description: None,
480 mime_type: None,
481 title: None,
482 icons: None,
483 }
484 }
485
486 pub fn description<S: Into<String>>(mut self, description: S) -> Self {
488 self.description = Some(description.into());
489 self
490 }
491
492 pub fn mime_type<S: Into<String>>(mut self, mime_type: S) -> Self {
494 self.mime_type = Some(mime_type.into());
495 self
496 }
497
498 pub fn title<S: Into<String>>(mut self, title: S) -> Self {
500 self.title = Some(title.into());
501 self
502 }
503
504 pub fn icons(mut self, icons: Vec<Icon>) -> Self {
506 self.icons = Some(icons);
507 self
508 }
509
510 pub fn icon(mut self, icon: Icon) -> Self {
512 self.icons.get_or_insert_with(Vec::new).push(icon);
513 self
514 }
515
516 pub fn build<H>(self, handler: H) -> Resource
518 where
519 H: ResourceHandler + 'static,
520 {
521 let info = ResourceInfo {
522 uri: self.uri,
523 name: self.name,
524 description: self.description,
525 mime_type: self.mime_type,
526 annotations: None,
527 size: None,
528 icons: self.icons,
529 title: self.title,
530 meta: None,
531 };
532
533 Resource::new(info, handler)
534 }
535}
536
537#[cfg(test)]
538mod tests {
539 use super::*;
540
541 #[tokio::test]
542 async fn test_text_resource() {
543 let resource =
544 TextResource::new("Hello, World!".to_string(), Some("text/plain".to_string()));
545 let params = HashMap::new();
546
547 let content = resource.read("test://resource", ¶ms).await.unwrap();
548 assert_eq!(content.len(), 1);
549 match &content[0] {
550 ResourceContents::Text {
551 text, mime_type, ..
552 } => {
553 assert_eq!(*text, "Hello, World!".to_string());
554 assert_eq!(*mime_type, Some("text/plain".to_string()));
555 }
556 _ => panic!("Expected text content"),
557 }
558 }
559
560 #[test]
561 fn test_resource_creation() {
562 let info = ResourceInfo {
563 uri: "test://resource".to_string(),
564 name: "Test Resource".to_string(),
565 description: Some("A test resource".to_string()),
566 mime_type: Some("text/plain".to_string()),
567 annotations: None,
568 size: None,
569 icons: None,
570 title: None,
571 meta: None,
572 };
573
574 let resource = Resource::new(info.clone(), TextResource::new("test".to_string(), None));
575 assert_eq!(resource.info, info);
576 assert!(resource.is_enabled());
577 }
578
579 #[test]
580 fn test_resource_template() {
581 let template = ResourceTemplate {
582 uri_template: "test://resource/{id}".to_string(),
583 name: "Test Template".to_string(),
584 description: Some("A test template".to_string()),
585 mime_type: Some("text/plain".to_string()),
586 };
587
588 let resource = Resource::with_template(
589 template.clone(),
590 TextResource::new("test".to_string(), None),
591 );
592 assert_eq!(resource.template, Some(template));
593 }
594
595 #[test]
596 fn test_resource_uri_matching() {
597 let template = ResourceTemplate {
598 uri_template: "test://resource/{id}".to_string(),
599 name: "Test Template".to_string(),
600 description: None,
601 mime_type: None,
602 };
603
604 let resource =
605 Resource::with_template(template, TextResource::new("test".to_string(), None));
606
607 assert!(resource.matches_uri("test://resource/123"));
609 assert!(!resource.matches_uri("other://resource/123"));
610 }
611
612 #[test]
613 fn test_resource_builder() {
614 let resource = ResourceBuilder::new("test://resource", "Test Resource")
615 .description("A test resource")
616 .mime_type("text/plain")
617 .title("Test Resource Title")
618 .icon(Icon {
619 src: "https://example.com/resource-icon.svg".to_string(),
620 mime_type: Some("image/svg+xml".to_string()),
621 sizes: None,
622 theme: None,
623 })
624 .build(TextResource::new("test".to_string(), None));
625
626 assert_eq!(resource.info.uri, "test://resource");
627 assert_eq!(resource.info.name, "Test Resource");
628 assert_eq!(
629 resource.info.description,
630 Some("A test resource".to_string())
631 );
632 assert_eq!(resource.info.mime_type, Some("text/plain".to_string()));
633 assert_eq!(resource.info.title, Some("Test Resource Title".to_string()));
634 assert_eq!(resource.info.icons.as_ref().map(Vec::len), Some(1));
635 }
636}