1use async_trait::async_trait;
7use std::collections::HashMap;
8
9use crate::core::error::{McpError, McpResult};
10use crate::protocol::types::{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 title: None,
185 meta: None,
186 };
187
188 Self {
189 info,
190 handler: Box::new(handler),
191 template: Some(template),
192 enabled: true,
193 }
194 }
195
196 pub fn enable(&mut self) {
198 self.enabled = true;
199 }
200
201 pub fn disable(&mut self) {
203 self.enabled = false;
204 }
205
206 pub fn is_enabled(&self) -> bool {
208 self.enabled
209 }
210
211 pub async fn read(
220 &self,
221 uri: &str,
222 params: &HashMap<String, String>,
223 ) -> McpResult<Vec<ResourceContents>> {
224 if !self.enabled {
225 let name = self.info.name.as_str();
226 return Err(McpError::validation(format!(
227 "Resource '{name}' is disabled"
228 )));
229 }
230
231 self.handler.read(uri, params).await
232 }
233
234 pub async fn list(&self) -> McpResult<Vec<ResourceInfo>> {
236 if !self.enabled {
237 return Ok(vec![]);
238 }
239
240 self.handler.list().await
241 }
242
243 pub async fn subscribe(&self, uri: &str) -> McpResult<()> {
245 if !self.enabled {
246 let name = self.info.name.as_str();
247 return Err(McpError::validation(format!(
248 "Resource '{name}' is disabled"
249 )));
250 }
251
252 self.handler.subscribe(uri).await
253 }
254
255 pub async fn unsubscribe(&self, uri: &str) -> McpResult<()> {
257 if !self.enabled {
258 let name = self.info.name.as_str();
259 return Err(McpError::validation(format!(
260 "Resource '{name}' is disabled"
261 )));
262 }
263
264 self.handler.unsubscribe(uri).await
265 }
266
267 pub fn matches_uri(&self, uri: &str) -> bool {
269 if let Some(template) = &self.template {
270 uri.starts_with(&template.uri_template.replace("{id}", "").replace("{*}", ""))
273 } else {
274 self.info.uri == uri
275 }
276 }
277}
278
279impl std::fmt::Debug for Resource {
280 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281 f.debug_struct("Resource")
282 .field("info", &self.info)
283 .field("template", &self.template)
284 .field("enabled", &self.enabled)
285 .finish()
286 }
287}
288
289pub struct TextResource {
293 content: String,
294 mime_type: String,
295}
296
297impl TextResource {
298 pub fn new(content: String, mime_type: Option<String>) -> Self {
300 Self {
301 content,
302 mime_type: mime_type.unwrap_or_else(|| "text/plain".to_string()),
303 }
304 }
305}
306
307#[async_trait]
308impl ResourceHandler for TextResource {
309 async fn read(
310 &self,
311 uri: &str,
312 _params: &HashMap<String, String>,
313 ) -> McpResult<Vec<ResourceContents>> {
314 Ok(vec![ResourceContents::Text {
315 uri: uri.to_string(),
316 mime_type: Some(self.mime_type.clone()),
317 text: self.content.clone(),
318 meta: None,
319 }])
320 }
321
322 async fn list(&self) -> McpResult<Vec<ResourceInfo>> {
323 Ok(vec![])
325 }
326}
327
328pub struct FileSystemResource {
330 base_path: std::path::PathBuf,
331 allowed_extensions: Option<Vec<String>>,
332}
333
334impl FileSystemResource {
335 pub fn new<P: AsRef<std::path::Path>>(base_path: P) -> Self {
337 Self {
338 base_path: base_path.as_ref().to_path_buf(),
339 allowed_extensions: None,
340 }
341 }
342
343 pub fn with_extensions(mut self, extensions: Vec<String>) -> Self {
345 self.allowed_extensions = Some(extensions);
346 self
347 }
348
349 fn is_allowed_file(&self, path: &std::path::Path) -> bool {
350 if let Some(ref allowed) = self.allowed_extensions {
351 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
352 return allowed.contains(&ext.to_lowercase());
353 }
354 return false;
355 }
356 true
357 }
358
359 fn get_mime_type(&self, path: &std::path::Path) -> String {
360 match path.extension().and_then(|e| e.to_str()) {
361 Some("txt") => "text/plain".to_string(),
362 Some("json") => "application/json".to_string(),
363 Some("html") => "text/html".to_string(),
364 Some("css") => "text/css".to_string(),
365 Some("js") => "application/javascript".to_string(),
366 Some("md") => "text/markdown".to_string(),
367 Some("xml") => "application/xml".to_string(),
368 Some("yaml") | Some("yml") => "application/yaml".to_string(),
369 _ => "application/octet-stream".to_string(),
370 }
371 }
372}
373
374#[async_trait]
375impl ResourceHandler for FileSystemResource {
376 async fn read(
377 &self,
378 uri: &str,
379 _params: &HashMap<String, String>,
380 ) -> McpResult<Vec<ResourceContents>> {
381 let file_path = if uri.starts_with("file://") {
383 uri.strip_prefix("file://").unwrap_or(uri)
384 } else {
385 uri
386 };
387
388 let full_path = self.base_path.join(file_path);
389
390 let canonical_base = self.base_path.canonicalize().map_err(McpError::io)?;
392 let canonical_target = full_path
393 .canonicalize()
394 .map_err(|_| McpError::ResourceNotFound(uri.to_string()))?;
395
396 if !canonical_target.starts_with(&canonical_base) {
397 return Err(McpError::validation("Path outside of allowed directory"));
398 }
399
400 if !self.is_allowed_file(&canonical_target) {
401 return Err(McpError::validation("File type not allowed"));
402 }
403
404 let content = tokio::fs::read_to_string(&canonical_target)
405 .await
406 .map_err(|_| McpError::ResourceNotFound(uri.to_string()))?;
407
408 let mime_type = self.get_mime_type(&canonical_target);
409
410 Ok(vec![ResourceContents::Text {
411 uri: uri.to_string(),
412 mime_type: Some(mime_type),
413 text: content,
414 meta: None,
415 }])
416 }
417
418 async fn list(&self) -> McpResult<Vec<ResourceInfo>> {
419 let mut resources = Vec::new();
420 let mut stack = vec![self.base_path.clone()];
421
422 while let Some(dir_path) = stack.pop() {
423 let mut dir = tokio::fs::read_dir(&dir_path).await.map_err(McpError::io)?;
424
425 while let Some(entry) = dir.next_entry().await.map_err(McpError::io)? {
426 let path = entry.path();
427
428 if path.is_dir() {
429 stack.push(path);
430 } else if self.is_allowed_file(&path) {
431 let relative_path = path
432 .strip_prefix(&self.base_path)
433 .map_err(|_| McpError::internal("Path computation error"))?;
434
435 let path_display = relative_path.display();
436 let uri = format!("file://{path_display}");
437 let name = path
438 .file_name()
439 .and_then(|n| n.to_str())
440 .unwrap_or("unnamed")
441 .to_string();
442
443 resources.push(ResourceInfo {
444 uri,
445 name,
446 description: None,
447 mime_type: Some(self.get_mime_type(&path)),
448 annotations: None,
449 size: None,
450 title: None,
451 meta: None,
452 });
453 }
454 }
455 }
456
457 Ok(resources)
458 }
459}
460
461pub struct ResourceBuilder {
463 uri: String,
464 name: String,
465 description: Option<String>,
466 mime_type: Option<String>,
467}
468
469impl ResourceBuilder {
470 pub fn new<S: Into<String>>(uri: S, name: S) -> Self {
472 Self {
473 uri: uri.into(),
474 name: name.into(),
475 description: None,
476 mime_type: None,
477 }
478 }
479
480 pub fn description<S: Into<String>>(mut self, description: S) -> Self {
482 self.description = Some(description.into());
483 self
484 }
485
486 pub fn mime_type<S: Into<String>>(mut self, mime_type: S) -> Self {
488 self.mime_type = Some(mime_type.into());
489 self
490 }
491
492 pub fn build<H>(self, handler: H) -> Resource
494 where
495 H: ResourceHandler + 'static,
496 {
497 let info = ResourceInfo {
498 uri: self.uri,
499 name: self.name,
500 description: self.description,
501 mime_type: self.mime_type,
502 annotations: None,
503 size: None,
504 title: None,
505 meta: None,
506 };
507
508 Resource::new(info, handler)
509 }
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515
516 #[tokio::test]
517 async fn test_text_resource() {
518 let resource =
519 TextResource::new("Hello, World!".to_string(), Some("text/plain".to_string()));
520 let params = HashMap::new();
521
522 let content = resource.read("test://resource", ¶ms).await.unwrap();
523 assert_eq!(content.len(), 1);
524 match &content[0] {
525 ResourceContents::Text {
526 text, mime_type, ..
527 } => {
528 assert_eq!(*text, "Hello, World!".to_string());
529 assert_eq!(*mime_type, Some("text/plain".to_string()));
530 }
531 _ => panic!("Expected text content"),
532 }
533 }
534
535 #[test]
536 fn test_resource_creation() {
537 let info = ResourceInfo {
538 uri: "test://resource".to_string(),
539 name: "Test Resource".to_string(),
540 description: Some("A test resource".to_string()),
541 mime_type: Some("text/plain".to_string()),
542 annotations: None,
543 size: None,
544 title: None,
545 meta: None,
546 };
547
548 let resource = Resource::new(info.clone(), TextResource::new("test".to_string(), None));
549 assert_eq!(resource.info, info);
550 assert!(resource.is_enabled());
551 }
552
553 #[test]
554 fn test_resource_template() {
555 let template = ResourceTemplate {
556 uri_template: "test://resource/{id}".to_string(),
557 name: "Test Template".to_string(),
558 description: Some("A test template".to_string()),
559 mime_type: Some("text/plain".to_string()),
560 };
561
562 let resource = Resource::with_template(
563 template.clone(),
564 TextResource::new("test".to_string(), None),
565 );
566 assert_eq!(resource.template, Some(template));
567 }
568
569 #[test]
570 fn test_resource_uri_matching() {
571 let template = ResourceTemplate {
572 uri_template: "test://resource/{id}".to_string(),
573 name: "Test Template".to_string(),
574 description: None,
575 mime_type: None,
576 };
577
578 let resource =
579 Resource::with_template(template, TextResource::new("test".to_string(), None));
580
581 assert!(resource.matches_uri("test://resource/123"));
583 assert!(!resource.matches_uri("other://resource/123"));
584 }
585
586 #[test]
587 fn test_resource_builder() {
588 let resource = ResourceBuilder::new("test://resource", "Test Resource")
589 .description("A test resource")
590 .mime_type("text/plain")
591 .build(TextResource::new("test".to_string(), None));
592
593 assert_eq!(resource.info.uri, "test://resource");
594 assert_eq!(resource.info.name, "Test Resource");
595 assert_eq!(
596 resource.info.description,
597 Some("A test resource".to_string())
598 );
599 assert_eq!(resource.info.mime_type, Some("text/plain".to_string()));
600 }
601}