turbomcp_client/client/operations/
resources.rs1use 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
14const MAX_PAGINATION_PAGES: usize = 1000;
16
17impl<T: turbomcp_transport::Transport + 'static> super::super::core::Client<T> {
18 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 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 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 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 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 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}