Skip to main content

thulp_mcp/
resources.rs

1//! MCP Resources support.
2//!
3//! This module provides support for MCP resources protocol methods:
4//! - `resources/list` - List available resources
5//! - `resources/read` - Read resource contents
6//! - `resources/templates/list` - List resource templates
7//! - `resources/subscribe` / `resources/unsubscribe` - Resource subscriptions
8
9use crate::Result;
10use std::collections::HashMap;
11use std::sync::RwLock;
12use thulp_core::{
13    Resource, ResourceContents, ResourceListResult, ResourceTemplate, ResourceTemplateListResult,
14};
15
16/// MCP Resources client for managing and accessing resources.
17pub struct ResourcesClient {
18    /// Cached resources
19    cache: RwLock<HashMap<String, Resource>>,
20    /// Cached templates
21    templates_cache: RwLock<Vec<ResourceTemplate>>,
22    /// Subscribed resource URIs
23    subscriptions: RwLock<Vec<String>>,
24}
25
26impl ResourcesClient {
27    /// Create a new resources client.
28    pub fn new() -> Self {
29        Self {
30            cache: RwLock::new(HashMap::new()),
31            templates_cache: RwLock::new(Vec::new()),
32            subscriptions: RwLock::new(Vec::new()),
33        }
34    }
35
36    /// List all available resources.
37    ///
38    /// In a full implementation, this would call `resources/list` on the MCP server.
39    pub async fn list(&self) -> Result<ResourceListResult> {
40        let cache = self.cache.read().unwrap();
41        Ok(ResourceListResult {
42            resources: cache.values().cloned().collect(),
43            next_cursor: None,
44        })
45    }
46
47    /// Read a resource by URI.
48    ///
49    /// In a full implementation, this would call `resources/read` on the MCP server.
50    pub async fn read(&self, uri: &str) -> Result<ResourceContents> {
51        // Placeholder - would call MCP server
52        Ok(ResourceContents::text(uri, format!("Content of {}", uri)))
53    }
54
55    /// List available resource templates.
56    ///
57    /// In a full implementation, this would call `resources/templates/list`.
58    pub async fn list_templates(&self) -> Result<ResourceTemplateListResult> {
59        let cache = self.templates_cache.read().unwrap();
60        Ok(ResourceTemplateListResult {
61            resource_templates: cache.clone(),
62            next_cursor: None,
63        })
64    }
65
66    /// Subscribe to resource changes.
67    pub async fn subscribe(&self, uri: &str) -> Result<()> {
68        let mut subs = self.subscriptions.write().unwrap();
69        if !subs.contains(&uri.to_string()) {
70            subs.push(uri.to_string());
71        }
72        Ok(())
73    }
74
75    /// Unsubscribe from resource changes.
76    pub async fn unsubscribe(&self, uri: &str) -> Result<()> {
77        let mut subs = self.subscriptions.write().unwrap();
78        subs.retain(|s| s != uri);
79        Ok(())
80    }
81
82    /// Get list of subscribed resources.
83    pub fn subscriptions(&self) -> Vec<String> {
84        self.subscriptions.read().unwrap().clone()
85    }
86
87    /// Register a resource (for testing/local use).
88    pub fn register(&self, resource: Resource) {
89        let mut cache = self.cache.write().unwrap();
90        cache.insert(resource.uri.clone(), resource);
91    }
92
93    /// Register a template (for testing/local use).
94    pub fn register_template(&self, template: ResourceTemplate) {
95        let mut cache = self.templates_cache.write().unwrap();
96        cache.push(template);
97    }
98
99    /// Clear all caches.
100    pub fn clear(&self) {
101        self.cache.write().unwrap().clear();
102        self.templates_cache.write().unwrap().clear();
103        self.subscriptions.write().unwrap().clear();
104    }
105
106    /// Get a resource by URI from cache.
107    pub fn get(&self, uri: &str) -> Option<Resource> {
108        self.cache.read().unwrap().get(uri).cloned()
109    }
110}
111
112impl Default for ResourcesClient {
113    fn default() -> Self {
114        Self::new()
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[tokio::test]
123    async fn test_resources_client_creation() {
124        let client = ResourcesClient::new();
125        let result = client.list().await.unwrap();
126        assert!(result.resources.is_empty());
127    }
128
129    #[tokio::test]
130    async fn test_register_resource() {
131        let client = ResourcesClient::new();
132        let resource = Resource::new("file:///test.txt", "test.txt");
133        client.register(resource);
134
135        let result = client.list().await.unwrap();
136        assert_eq!(result.resources.len(), 1);
137    }
138
139    #[tokio::test]
140    async fn test_get_resource() {
141        let client = ResourcesClient::new();
142        client.register(Resource::new("file:///test.txt", "test.txt"));
143
144        let resource = client.get("file:///test.txt");
145        assert!(resource.is_some());
146        assert_eq!(resource.unwrap().name, "test.txt");
147    }
148
149    #[tokio::test]
150    async fn test_read_resource() {
151        let client = ResourcesClient::new();
152        let contents = client.read("file:///test.txt").await.unwrap();
153        assert!(contents.text.is_some());
154    }
155
156    #[tokio::test]
157    async fn test_subscribe_unsubscribe() {
158        let client = ResourcesClient::new();
159
160        client.subscribe("file:///test.txt").await.unwrap();
161        assert_eq!(client.subscriptions().len(), 1);
162
163        client.unsubscribe("file:///test.txt").await.unwrap();
164        assert!(client.subscriptions().is_empty());
165    }
166
167    #[tokio::test]
168    async fn test_list_templates() {
169        let client = ResourcesClient::new();
170        client.register_template(ResourceTemplate::new("file:///{path}", "file"));
171
172        let result = client.list_templates().await.unwrap();
173        assert_eq!(result.resource_templates.len(), 1);
174    }
175
176    #[tokio::test]
177    async fn test_clear() {
178        let client = ResourcesClient::new();
179        client.register(Resource::new("file:///test.txt", "test.txt"));
180        client.subscribe("file:///test.txt").await.unwrap();
181
182        client.clear();
183        assert!(client.list().await.unwrap().resources.is_empty());
184        assert!(client.subscriptions().is_empty());
185    }
186}