Skip to main content

turbomcp_client/client/operations/
resources.rs

1//! Resource operations for MCP client
2//!
3//! This module provides resource-related functionality including listing resources,
4//! reading resource content, and managing resource templates.
5
6use std::sync::atomic::Ordering;
7
8use turbomcp_protocol::types::{
9    Cursor, ListResourceTemplatesRequest, ListResourceTemplatesResult, ListResourcesRequest,
10    ListResourcesResult, ReadResourceRequest, ReadResourceResult, Resource, ResourceTemplate,
11};
12use turbomcp_protocol::{Error, Result};
13
14/// Maximum number of pagination pages to prevent infinite loops from misbehaving servers.
15const MAX_PAGINATION_PAGES: usize = 1000;
16
17impl<T: turbomcp_transport::Transport + 'static> super::super::core::Client<T> {
18    /// List available resources from the MCP server
19    ///
20    /// Returns a list of resources with their full metadata including URIs, names,
21    /// descriptions, MIME types, and other attributes provided by the server.
22    /// Resources represent data or content that can be accessed by the client.
23    ///
24    /// # Returns
25    ///
26    /// Returns a vector of `Resource` objects containing full metadata that can be
27    /// read using `read_resource()`.
28    ///
29    /// # Errors
30    ///
31    /// Returns an error if:
32    /// - The client is not initialized
33    /// - The server doesn't support resources
34    /// - The request fails
35    ///
36    /// # Examples
37    ///
38    /// ```rust,no_run
39    /// # use turbomcp_client::Client;
40    /// # use turbomcp_transport::stdio::StdioTransport;
41    /// # async fn example() -> turbomcp_protocol::Result<()> {
42    /// let mut client = Client::new(StdioTransport::new());
43    /// client.initialize().await?;
44    ///
45    /// let resources = client.list_resources().await?;
46    /// for resource in resources {
47    ///     println!("Resource: {} ({})", resource.name, resource.uri);
48    ///     if let Some(desc) = &resource.description {
49    ///         println!("  Description: {}", desc);
50    ///     }
51    /// }
52    /// # Ok(())
53    /// # }
54    /// ```
55    pub async fn list_resources(&self) -> Result<Vec<Resource>> {
56        if !self.inner.initialized.load(Ordering::Relaxed) {
57            return Err(Error::invalid_request("Client not initialized"));
58        }
59
60        let mut all_resources = Vec::new();
61        let mut cursor = None;
62        for _ in 0..MAX_PAGINATION_PAGES {
63            let result = self.list_resources_paginated(cursor).await?;
64            let page_empty = result.resources.is_empty();
65            all_resources.extend(result.resources);
66            match result.next_cursor {
67                Some(c) if !page_empty => cursor = Some(c),
68                _ => break,
69            }
70        }
71        Ok(all_resources)
72    }
73
74    /// List resources with pagination support
75    ///
76    /// Returns the full `ListResourcesResult` including `next_cursor` for manual
77    /// pagination control. Use `list_resources()` for automatic pagination.
78    ///
79    /// # Arguments
80    ///
81    /// * `cursor` - Optional cursor from a previous `ListResourcesResult::next_cursor`
82    pub async fn list_resources_paginated(
83        &self,
84        cursor: Option<Cursor>,
85    ) -> Result<ListResourcesResult> {
86        if !self.inner.initialized.load(Ordering::Relaxed) {
87            return Err(Error::invalid_request("Client not initialized"));
88        }
89
90        let request = ListResourcesRequest {
91            cursor,
92            _meta: None,
93        };
94        let params = if request.cursor.is_some() {
95            Some(serde_json::to_value(&request)?)
96        } else {
97            None
98        };
99        self.inner.protocol.request("resources/list", params).await
100    }
101
102    /// Read the content of a specific resource by URI
103    ///
104    /// Retrieves the content of a resource identified by its URI.
105    /// Resources can contain text, binary data, or structured content.
106    ///
107    /// # Arguments
108    ///
109    /// * `uri` - The URI of the resource to read
110    ///
111    /// # Returns
112    ///
113    /// Returns `ReadResourceResult` containing the resource content and metadata.
114    ///
115    /// # Errors
116    ///
117    /// Returns an error if:
118    /// - The client is not initialized
119    /// - The URI is empty or invalid
120    /// - The resource doesn't exist
121    /// - Access to the resource is denied
122    ///
123    /// # Examples
124    ///
125    /// ```rust,no_run
126    /// # use turbomcp_client::Client;
127    /// # use turbomcp_transport::stdio::StdioTransport;
128    /// # async fn example() -> turbomcp_protocol::Result<()> {
129    /// let mut client = Client::new(StdioTransport::new());
130    /// client.initialize().await?;
131    ///
132    /// let result = client.read_resource("file:///path/to/document.txt").await?;
133    /// for content in result.contents {
134    ///     println!("Resource content: {:?}", content);
135    /// }
136    /// # Ok(())
137    /// # }
138    /// ```
139    pub async fn read_resource(&self, uri: &str) -> Result<ReadResourceResult> {
140        if !self.inner.initialized.load(Ordering::Relaxed) {
141            return Err(Error::invalid_request("Client not initialized"));
142        }
143
144        if uri.is_empty() {
145            return Err(Error::invalid_request("Resource URI cannot be empty"));
146        }
147
148        // Send read_resource request
149        let request = ReadResourceRequest {
150            uri: uri.into(),
151            _meta: None,
152        };
153
154        let response: ReadResourceResult = self
155            .inner
156            .protocol
157            .request("resources/read", Some(serde_json::to_value(request)?))
158            .await?;
159        Ok(response)
160    }
161
162    /// List available resource templates from the MCP server
163    ///
164    /// Returns a list of resource template URIs that define patterns for
165    /// generating resource URIs. Templates allow servers to describe
166    /// families of related resources without listing each individual resource.
167    ///
168    /// # Returns
169    ///
170    /// Returns a vector of resource templates with their full metadata.
171    ///
172    /// # Errors
173    ///
174    /// Returns an error if:
175    /// - The client is not initialized
176    /// - The server doesn't support resource templates
177    /// - The request fails
178    ///
179    /// # Examples
180    ///
181    /// ```rust,no_run
182    /// # use turbomcp_client::Client;
183    /// # use turbomcp_transport::stdio::StdioTransport;
184    /// # async fn example() -> turbomcp_protocol::Result<()> {
185    /// let mut client = Client::new(StdioTransport::new());
186    /// client.initialize().await?;
187    ///
188    /// let templates = client.list_resource_templates().await?;
189    /// for template in templates {
190    ///     println!("Resource template: {}", template.uri_template);
191    /// }
192    /// # Ok(())
193    /// # }
194    /// ```
195    pub async fn list_resource_templates(&self) -> Result<Vec<ResourceTemplate>> {
196        if !self.inner.initialized.load(Ordering::Relaxed) {
197            return Err(Error::invalid_request("Client not initialized"));
198        }
199
200        let mut all_templates = Vec::new();
201        let mut cursor = None;
202        for _ in 0..MAX_PAGINATION_PAGES {
203            let result = self.list_resource_templates_paginated(cursor).await?;
204            let page_empty = result.resource_templates.is_empty();
205            all_templates.extend(result.resource_templates);
206            match result.next_cursor {
207                Some(c) if !page_empty => cursor = Some(c),
208                _ => break,
209            }
210        }
211        Ok(all_templates)
212    }
213
214    /// List resource templates with pagination support
215    ///
216    /// Returns the full `ListResourceTemplatesResult` including `next_cursor`
217    /// for manual pagination control. Use `list_resource_templates()` for
218    /// automatic pagination.
219    ///
220    /// # Arguments
221    ///
222    /// * `cursor` - Optional cursor from a previous result's `next_cursor`
223    pub async fn list_resource_templates_paginated(
224        &self,
225        cursor: Option<Cursor>,
226    ) -> Result<ListResourceTemplatesResult> {
227        if !self.inner.initialized.load(Ordering::Relaxed) {
228            return Err(Error::invalid_request("Client not initialized"));
229        }
230
231        let request = ListResourceTemplatesRequest {
232            cursor,
233            _meta: None,
234        };
235        let params = if request.cursor.is_some() {
236            Some(serde_json::to_value(&request)?)
237        } else {
238            None
239        };
240        self.inner
241            .protocol
242            .request("resources/templates/list", params)
243            .await
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::super::super::core::Client;
250    use super::*;
251    use std::collections::VecDeque;
252    use std::future::Future;
253    use std::pin::Pin;
254    use std::sync::Mutex;
255    use turbomcp_protocol::MessageId;
256    use turbomcp_transport::{
257        Transport, TransportCapabilities, TransportError, TransportMessage, TransportMetrics,
258        TransportResult, TransportState, TransportType,
259    };
260
261    #[derive(Debug)]
262    struct TemplateTransport {
263        capabilities: TransportCapabilities,
264        responses: Mutex<VecDeque<TransportMessage>>,
265    }
266
267    impl TemplateTransport {
268        fn new() -> Self {
269            Self {
270                capabilities: TransportCapabilities::default(),
271                responses: Mutex::new(VecDeque::new()),
272            }
273        }
274    }
275
276    impl Transport for TemplateTransport {
277        fn transport_type(&self) -> TransportType {
278            TransportType::Stdio
279        }
280
281        fn capabilities(&self) -> &TransportCapabilities {
282            &self.capabilities
283        }
284
285        fn state(&self) -> Pin<Box<dyn Future<Output = TransportState> + Send + '_>> {
286            Box::pin(async { TransportState::Connected })
287        }
288
289        fn connect(&self) -> Pin<Box<dyn Future<Output = TransportResult<()>> + Send + '_>> {
290            Box::pin(async { Ok(()) })
291        }
292
293        fn disconnect(&self) -> Pin<Box<dyn Future<Output = TransportResult<()>> + Send + '_>> {
294            Box::pin(async { Ok(()) })
295        }
296
297        fn send(
298            &self,
299            message: TransportMessage,
300        ) -> Pin<Box<dyn Future<Output = TransportResult<()>> + Send + '_>> {
301            let request: serde_json::Value = match serde_json::from_slice(&message.payload) {
302                Ok(request) => request,
303                Err(e) => {
304                    return Box::pin(async move {
305                        Err(TransportError::SerializationFailed(e.to_string()))
306                    });
307                }
308            };
309
310            assert_eq!(request["method"], "resources/templates/list");
311            let response = serde_json::json!({
312                "jsonrpc": "2.0",
313                "id": request["id"].clone(),
314                "result": {
315                    "resourceTemplates": [
316                        {
317                            "uriTemplate": "repo://{owner}/{name}",
318                            "name": "repo",
319                            "title": "Repository",
320                            "description": "Repository metadata",
321                            "mimeType": "application/json",
322                            "icons": [
323                                {
324                                    "src": "https://example.com/repo.png",
325                                    "mimeType": "image/png",
326                                    "sizes": ["64x64"]
327                                }
328                            ],
329                            "annotations": {
330                                "audience": ["user"],
331                                "priority": 0.7,
332                                "lastModified": "2026-05-08T12:00:00Z"
333                            },
334                            "_meta": {
335                                "x-test": true
336                            }
337                        }
338                    ]
339                }
340            });
341            let payload = match serde_json::to_vec(&response) {
342                Ok(payload) => payload,
343                Err(e) => {
344                    return Box::pin(async move {
345                        Err(TransportError::SerializationFailed(e.to_string()))
346                    });
347                }
348            };
349            self.responses
350                .lock()
351                .expect("response queue poisoned")
352                .push_back(TransportMessage::new(
353                    MessageId::from("response-1"),
354                    payload.into(),
355                ));
356            Box::pin(async { Ok(()) })
357        }
358
359        fn receive(
360            &self,
361        ) -> Pin<Box<dyn Future<Output = TransportResult<Option<TransportMessage>>> + Send + '_>>
362        {
363            let response = self
364                .responses
365                .lock()
366                .expect("response queue poisoned")
367                .pop_front();
368            Box::pin(async move { Ok(response) })
369        }
370
371        fn metrics(&self) -> Pin<Box<dyn Future<Output = TransportMetrics> + Send + '_>> {
372            Box::pin(async { TransportMetrics::default() })
373        }
374    }
375
376    #[tokio::test]
377    async fn list_resource_templates_preserves_full_template_metadata() {
378        let client = Client::new(TemplateTransport::new());
379        client.inner.initialized.store(true, Ordering::Relaxed);
380
381        let templates = client
382            .list_resource_templates()
383            .await
384            .expect("resource templates");
385
386        assert_eq!(templates.len(), 1);
387        let template = &templates[0];
388        assert_eq!(template.uri_template, "repo://{owner}/{name}");
389        assert_eq!(template.name, "repo");
390        assert_eq!(template.title.as_deref(), Some("Repository"));
391        assert_eq!(template.description.as_deref(), Some("Repository metadata"));
392        assert_eq!(template.mime_type.as_deref(), Some("application/json"));
393        assert_eq!(
394            template.icons.as_ref().expect("icons")[0].src,
395            "https://example.com/repo.png"
396        );
397        assert_eq!(
398            template.annotations.as_ref().expect("annotations").priority,
399            Some(0.7)
400        );
401        assert_eq!(
402            template.meta.as_ref().expect("meta")["x-test"],
403            serde_json::json!(true)
404        );
405    }
406}