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: {}",
59 uri
60 )))
61 }
62
63 async fn unsubscribe(&self, uri: &str) -> McpResult<()> {
71 Err(McpError::protocol(format!(
73 "Subscription not supported for resource: {}",
74 uri
75 )))
76 }
77}
78
79#[async_trait]
82pub trait LegacyResourceHandler: Send + Sync {
83 async fn read(&self, uri: &str) -> McpResult<String>;
91
92 async fn list(&self) -> McpResult<Vec<ResourceInfo>> {
97 Ok(vec![])
99 }
100}
101
102pub struct LegacyResourceAdapter<T> {
104 inner: T,
105}
106
107impl<T> LegacyResourceAdapter<T>
108where
109 T: LegacyResourceHandler,
110{
111 pub fn new(handler: T) -> Self {
112 Self { inner: handler }
113 }
114}
115
116#[async_trait]
117impl<T> ResourceHandler for LegacyResourceAdapter<T>
118where
119 T: LegacyResourceHandler + Send + Sync,
120{
121 async fn read(
122 &self,
123 uri: &str,
124 _params: &HashMap<String, String>,
125 ) -> McpResult<Vec<ResourceContents>> {
126 let content = self.inner.read(uri).await?;
127 Ok(vec![ResourceContents::Text {
128 uri: uri.to_string(),
129 mime_type: Some("text/plain".to_string()),
130 text: content,
131 }])
132 }
133
134 async fn list(&self) -> McpResult<Vec<ResourceInfo>> {
135 self.inner.list().await
136 }
137}
138
139pub struct Resource {
141 pub info: ResourceInfo,
143 pub handler: Box<dyn ResourceHandler>,
145 pub template: Option<ResourceTemplate>,
147 pub enabled: bool,
149}
150
151impl Resource {
152 pub fn new<H>(info: ResourceInfo, handler: H) -> Self
158 where
159 H: ResourceHandler + 'static,
160 {
161 Self {
162 info,
163 handler: Box::new(handler),
164 template: None,
165 enabled: true,
166 }
167 }
168
169 pub fn with_template<H>(template: ResourceTemplate, handler: H) -> Self
175 where
176 H: ResourceHandler + 'static,
177 {
178 let info = ResourceInfo {
179 uri: template.uri_template.clone(),
180 name: Some(template.name.clone()),
181 description: template.description.clone(),
182 mime_type: template.mime_type.clone(),
183 annotations: None,
184 size: None,
185 };
186
187 Self {
188 info,
189 handler: Box::new(handler),
190 template: Some(template),
191 enabled: true,
192 }
193 }
194
195 pub fn enable(&mut self) {
197 self.enabled = true;
198 }
199
200 pub fn disable(&mut self) {
202 self.enabled = false;
203 }
204
205 pub fn is_enabled(&self) -> bool {
207 self.enabled
208 }
209
210 pub async fn read(
219 &self,
220 uri: &str,
221 params: &HashMap<String, String>,
222 ) -> McpResult<Vec<ResourceContents>> {
223 if !self.enabled {
224 return Err(McpError::validation(format!(
225 "Resource '{}' is disabled",
226 self.info.name.as_deref().unwrap_or("unknown")
227 )));
228 }
229
230 self.handler.read(uri, params).await
231 }
232
233 pub async fn list(&self) -> McpResult<Vec<ResourceInfo>> {
235 if !self.enabled {
236 return Ok(vec![]);
237 }
238
239 self.handler.list().await
240 }
241
242 pub async fn subscribe(&self, uri: &str) -> McpResult<()> {
244 if !self.enabled {
245 return Err(McpError::validation(format!(
246 "Resource '{}' is disabled",
247 self.info.name.as_deref().unwrap_or("unknown")
248 )));
249 }
250
251 self.handler.subscribe(uri).await
252 }
253
254 pub async fn unsubscribe(&self, uri: &str) -> McpResult<()> {
256 if !self.enabled {
257 return Err(McpError::validation(format!(
258 "Resource '{}' is disabled",
259 self.info.name.as_deref().unwrap_or("unknown")
260 )));
261 }
262
263 self.handler.unsubscribe(uri).await
264 }
265
266 pub fn matches_uri(&self, uri: &str) -> bool {
268 if let Some(template) = &self.template {
269 uri.starts_with(&template.uri_template.replace("{id}", "").replace("{*}", ""))
272 } else {
273 self.info.uri == uri
274 }
275 }
276}
277
278impl std::fmt::Debug for Resource {
279 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
280 f.debug_struct("Resource")
281 .field("info", &self.info)
282 .field("template", &self.template)
283 .field("enabled", &self.enabled)
284 .finish()
285 }
286}
287
288pub struct TextResource {
292 content: String,
293 mime_type: String,
294}
295
296impl TextResource {
297 pub fn new(content: String, mime_type: Option<String>) -> Self {
299 Self {
300 content,
301 mime_type: mime_type.unwrap_or_else(|| "text/plain".to_string()),
302 }
303 }
304}
305
306#[async_trait]
307impl ResourceHandler for TextResource {
308 async fn read(
309 &self,
310 uri: &str,
311 _params: &HashMap<String, String>,
312 ) -> McpResult<Vec<ResourceContents>> {
313 Ok(vec![ResourceContents::Text {
314 uri: uri.to_string(),
315 mime_type: Some(self.mime_type.clone()),
316 text: self.content.clone(),
317 }])
318 }
319
320 async fn list(&self) -> McpResult<Vec<ResourceInfo>> {
321 Ok(vec![])
323 }
324}
325
326pub struct FileSystemResource {
328 base_path: std::path::PathBuf,
329 allowed_extensions: Option<Vec<String>>,
330}
331
332impl FileSystemResource {
333 pub fn new<P: AsRef<std::path::Path>>(base_path: P) -> Self {
335 Self {
336 base_path: base_path.as_ref().to_path_buf(),
337 allowed_extensions: None,
338 }
339 }
340
341 pub fn with_extensions(mut self, extensions: Vec<String>) -> Self {
343 self.allowed_extensions = Some(extensions);
344 self
345 }
346
347 fn is_allowed_file(&self, path: &std::path::Path) -> bool {
348 if let Some(ref allowed) = self.allowed_extensions {
349 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
350 return allowed.contains(&ext.to_lowercase());
351 }
352 return false;
353 }
354 true
355 }
356
357 fn get_mime_type(&self, path: &std::path::Path) -> String {
358 match path.extension().and_then(|e| e.to_str()) {
359 Some("txt") => "text/plain".to_string(),
360 Some("json") => "application/json".to_string(),
361 Some("html") => "text/html".to_string(),
362 Some("css") => "text/css".to_string(),
363 Some("js") => "application/javascript".to_string(),
364 Some("md") => "text/markdown".to_string(),
365 Some("xml") => "application/xml".to_string(),
366 Some("yaml") | Some("yml") => "application/yaml".to_string(),
367 _ => "application/octet-stream".to_string(),
368 }
369 }
370}
371
372#[async_trait]
373impl ResourceHandler for FileSystemResource {
374 async fn read(
375 &self,
376 uri: &str,
377 _params: &HashMap<String, String>,
378 ) -> McpResult<Vec<ResourceContents>> {
379 let file_path = if uri.starts_with("file://") {
381 uri.strip_prefix("file://").unwrap_or(uri)
382 } else {
383 uri
384 };
385
386 let full_path = self.base_path.join(file_path);
387
388 let canonical_base = self.base_path.canonicalize().map_err(McpError::io)?;
390 let canonical_target = full_path
391 .canonicalize()
392 .map_err(|_| McpError::ResourceNotFound(uri.to_string()))?;
393
394 if !canonical_target.starts_with(&canonical_base) {
395 return Err(McpError::validation("Path outside of allowed directory"));
396 }
397
398 if !self.is_allowed_file(&canonical_target) {
399 return Err(McpError::validation("File type not allowed"));
400 }
401
402 let content = tokio::fs::read_to_string(&canonical_target)
403 .await
404 .map_err(|_| McpError::ResourceNotFound(uri.to_string()))?;
405
406 let mime_type = self.get_mime_type(&canonical_target);
407
408 Ok(vec![ResourceContents::Text {
409 uri: uri.to_string(),
410 mime_type: Some(mime_type),
411 text: content,
412 }])
413 }
414
415 async fn list(&self) -> McpResult<Vec<ResourceInfo>> {
416 let mut resources = Vec::new();
417 let mut stack = vec![self.base_path.clone()];
418
419 while let Some(dir_path) = stack.pop() {
420 let mut dir = tokio::fs::read_dir(&dir_path).await.map_err(McpError::io)?;
421
422 while let Some(entry) = dir.next_entry().await.map_err(McpError::io)? {
423 let path = entry.path();
424
425 if path.is_dir() {
426 stack.push(path);
427 } else if self.is_allowed_file(&path) {
428 let relative_path = path
429 .strip_prefix(&self.base_path)
430 .map_err(|_| McpError::internal("Path computation error"))?;
431
432 let uri = format!("file://{}", relative_path.display());
433 let name = path
434 .file_name()
435 .and_then(|n| n.to_str())
436 .unwrap_or("unnamed")
437 .to_string();
438
439 resources.push(ResourceInfo {
440 uri,
441 name: Some(name),
442 description: None,
443 mime_type: Some(self.get_mime_type(&path)),
444 annotations: None,
445 size: None,
446 });
447 }
448 }
449 }
450
451 Ok(resources)
452 }
453}
454
455pub struct ResourceBuilder {
457 uri: String,
458 name: String,
459 description: Option<String>,
460 mime_type: Option<String>,
461}
462
463impl ResourceBuilder {
464 pub fn new<S: Into<String>>(uri: S, name: S) -> Self {
466 Self {
467 uri: uri.into(),
468 name: name.into(),
469 description: None,
470 mime_type: None,
471 }
472 }
473
474 pub fn description<S: Into<String>>(mut self, description: S) -> Self {
476 self.description = Some(description.into());
477 self
478 }
479
480 pub fn mime_type<S: Into<String>>(mut self, mime_type: S) -> Self {
482 self.mime_type = Some(mime_type.into());
483 self
484 }
485
486 pub fn build<H>(self, handler: H) -> Resource
488 where
489 H: ResourceHandler + 'static,
490 {
491 let info = ResourceInfo {
492 uri: self.uri,
493 name: Some(self.name),
494 description: self.description,
495 mime_type: self.mime_type,
496 annotations: None,
497 size: None,
498 };
499
500 Resource::new(info, handler)
501 }
502}
503
504#[cfg(test)]
505mod tests {
506 use super::*;
507
508 #[tokio::test]
509 async fn test_text_resource() {
510 let resource =
511 TextResource::new("Hello, World!".to_string(), Some("text/plain".to_string()));
512 let params = HashMap::new();
513
514 let content = resource.read("test://resource", ¶ms).await.unwrap();
515 assert_eq!(content.len(), 1);
516 match &content[0] {
517 ResourceContents::Text {
518 text, mime_type, ..
519 } => {
520 assert_eq!(*text, "Hello, World!".to_string());
521 assert_eq!(*mime_type, Some("text/plain".to_string()));
522 }
523 _ => panic!("Expected text content"),
524 }
525 }
526
527 #[test]
528 fn test_resource_creation() {
529 let info = ResourceInfo {
530 uri: "test://resource".to_string(),
531 name: Some("Test Resource".to_string()),
532 description: Some("A test resource".to_string()),
533 mime_type: Some("text/plain".to_string()),
534 annotations: None,
535 size: None,
536 };
537
538 let resource = Resource::new(info.clone(), TextResource::new("test".to_string(), None));
539 assert_eq!(resource.info, info);
540 assert!(resource.is_enabled());
541 }
542
543 #[test]
544 fn test_resource_template() {
545 let template = ResourceTemplate {
546 uri_template: "test://resource/{id}".to_string(),
547 name: "Test Template".to_string(),
548 description: Some("A test template".to_string()),
549 mime_type: Some("text/plain".to_string()),
550 };
551
552 let resource = Resource::with_template(
553 template.clone(),
554 TextResource::new("test".to_string(), None),
555 );
556 assert_eq!(resource.template, Some(template));
557 }
558
559 #[test]
560 fn test_resource_uri_matching() {
561 let template = ResourceTemplate {
562 uri_template: "test://resource/{id}".to_string(),
563 name: "Test Template".to_string(),
564 description: None,
565 mime_type: None,
566 };
567
568 let resource =
569 Resource::with_template(template, TextResource::new("test".to_string(), None));
570
571 assert!(resource.matches_uri("test://resource/123"));
573 assert!(!resource.matches_uri("other://resource/123"));
574 }
575
576 #[test]
577 fn test_resource_builder() {
578 let resource = ResourceBuilder::new("test://resource", "Test Resource")
579 .description("A test resource")
580 .mime_type("text/plain")
581 .build(TextResource::new("test".to_string(), None));
582
583 assert_eq!(resource.info.uri, "test://resource");
584 assert_eq!(resource.info.name, Some("Test Resource".to_string()));
585 assert_eq!(
586 resource.info.description,
587 Some("A test resource".to_string())
588 );
589 assert_eq!(resource.info.mime_type, Some("text/plain".to_string()));
590 }
591}