claude_agent/mcp/
resources.rs

1//! MCP Resource management
2//!
3//! This module provides utilities for working with MCP resources.
4
5use super::{McpContent, McpError, McpManager, McpResourceDefinition, McpResult};
6
7/// Resource subscription handle
8pub struct ResourceSubscription {
9    /// Server name
10    pub server_name: String,
11    /// Resource URI
12    pub uri: String,
13    /// Whether the subscription is active
14    pub active: bool,
15}
16
17/// Resource manager for handling MCP resource operations
18pub struct ResourceManager {
19    /// Reference to the MCP manager
20    manager: std::sync::Arc<McpManager>,
21    /// Active subscriptions
22    subscriptions: Vec<ResourceSubscription>,
23}
24
25impl ResourceManager {
26    /// Create a new resource manager
27    pub fn new(manager: std::sync::Arc<McpManager>) -> Self {
28        Self {
29            manager,
30            subscriptions: Vec::new(),
31        }
32    }
33
34    /// List all available resources from all connected servers
35    pub async fn list_all(&self) -> Vec<(String, McpResourceDefinition)> {
36        self.manager.list_resources().await
37    }
38
39    /// List resources from a specific server
40    #[cfg(feature = "mcp")]
41    pub async fn list_from_server(
42        &self,
43        server_name: &str,
44    ) -> McpResult<Vec<McpResourceDefinition>> {
45        let all_resources = self.manager.list_resources().await;
46        let resources: Vec<_> = all_resources
47            .into_iter()
48            .filter(|(name, _)| name == server_name)
49            .map(|(_, r)| r)
50            .collect();
51
52        if resources.is_empty() {
53            // Check if server exists
54            let servers = self.manager.list_servers().await;
55            if !servers.contains(&server_name.to_string()) {
56                return Err(McpError::ServerNotFound {
57                    name: server_name.to_string(),
58                });
59            }
60        }
61
62        Ok(resources)
63    }
64
65    /// List resources from a specific server (stub when feature disabled)
66    #[cfg(not(feature = "mcp"))]
67    pub async fn list_from_server(
68        &self,
69        _server_name: &str,
70    ) -> McpResult<Vec<McpResourceDefinition>> {
71        Ok(Vec::new())
72    }
73
74    /// Read a resource
75    pub async fn read(&self, server_name: &str, uri: &str) -> McpResult<Vec<McpContent>> {
76        self.manager.read_resource(server_name, uri).await
77    }
78
79    /// Read a resource as text
80    pub async fn read_text(&self, server_name: &str, uri: &str) -> McpResult<String> {
81        let contents = self.read(server_name, uri).await?;
82
83        let text: Vec<String> = contents
84            .iter()
85            .filter_map(|c| c.as_text().map(|s| s.to_string()))
86            .collect();
87
88        if text.is_empty() {
89            Err(McpError::ResourceNotFound {
90                uri: format!("{} (no text content)", uri),
91            })
92        } else {
93            Ok(text.join("\n"))
94        }
95    }
96
97    /// Subscribe to resource changes (placeholder for future implementation)
98    ///
99    /// Returns the index of the newly created subscription, which can be used
100    /// with `get_subscription()` to retrieve it.
101    pub fn subscribe(&mut self, server_name: &str, uri: &str) -> usize {
102        let subscription = ResourceSubscription {
103            server_name: server_name.to_string(),
104            uri: uri.to_string(),
105            active: true,
106        };
107        self.subscriptions.push(subscription);
108        self.subscriptions.len() - 1
109    }
110
111    /// Get a subscription by index
112    pub fn get_subscription(&self, index: usize) -> Option<&ResourceSubscription> {
113        self.subscriptions.get(index)
114    }
115
116    /// Unsubscribe from resource changes
117    pub fn unsubscribe(&mut self, server_name: &str, uri: &str) {
118        self.subscriptions
119            .retain(|s| !(s.server_name == server_name && s.uri == uri));
120    }
121
122    /// Get active subscriptions
123    pub fn subscriptions(&self) -> &[ResourceSubscription] {
124        &self.subscriptions
125    }
126
127    /// Find resources by URI pattern
128    pub async fn find_by_pattern(&self, pattern: &str) -> Vec<(String, McpResourceDefinition)> {
129        let all_resources = self.manager.list_resources().await;
130
131        // Simple glob-like pattern matching
132        let is_prefix_match = pattern.ends_with('*');
133        let pattern = pattern.trim_end_matches('*');
134
135        all_resources
136            .into_iter()
137            .filter(|(_, r)| {
138                if is_prefix_match {
139                    r.uri.starts_with(pattern)
140                } else {
141                    r.uri == pattern
142                }
143            })
144            .collect()
145    }
146}
147
148/// Builder for resource queries
149pub struct ResourceQuery {
150    /// Server filter
151    server: Option<String>,
152    /// URI pattern
153    pattern: Option<String>,
154    /// MIME type filter
155    mime_type: Option<String>,
156}
157
158impl ResourceQuery {
159    /// Create a new resource query
160    pub fn new() -> Self {
161        Self {
162            server: None,
163            pattern: None,
164            mime_type: None,
165        }
166    }
167
168    /// Filter by server name
169    pub fn server(mut self, name: impl Into<String>) -> Self {
170        self.server = Some(name.into());
171        self
172    }
173
174    /// Filter by URI pattern
175    pub fn pattern(mut self, pattern: impl Into<String>) -> Self {
176        self.pattern = Some(pattern.into());
177        self
178    }
179
180    /// Filter by MIME type
181    pub fn mime_type(mut self, mime_type: impl Into<String>) -> Self {
182        self.mime_type = Some(mime_type.into());
183        self
184    }
185
186    /// Execute the query against a resource manager
187    pub async fn execute(&self, manager: &ResourceManager) -> Vec<(String, McpResourceDefinition)> {
188        let mut results = manager.list_all().await;
189
190        // Filter by server
191        if let Some(ref server) = self.server {
192            results.retain(|(s, _)| s == server);
193        }
194
195        // Filter by pattern
196        if let Some(ref pattern) = self.pattern {
197            let is_prefix = pattern.ends_with('*');
198            let pattern = pattern.trim_end_matches('*');
199            results.retain(|(_, r)| {
200                if is_prefix {
201                    r.uri.starts_with(pattern)
202                } else {
203                    r.uri == pattern
204                }
205            });
206        }
207
208        // Filter by MIME type
209        if let Some(ref mime_type) = self.mime_type {
210            results.retain(|(_, r)| {
211                r.mime_type
212                    .as_ref()
213                    .map(|m| m == mime_type)
214                    .unwrap_or(false)
215            });
216        }
217
218        results
219    }
220}
221
222impl Default for ResourceQuery {
223    fn default() -> Self {
224        Self::new()
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn test_resource_query_builder() {
234        let query = ResourceQuery::new()
235            .server("filesystem")
236            .pattern("file://*")
237            .mime_type("text/plain");
238
239        assert_eq!(query.server, Some("filesystem".to_string()));
240        assert_eq!(query.pattern, Some("file://*".to_string()));
241        assert_eq!(query.mime_type, Some("text/plain".to_string()));
242    }
243}