Skip to main content

turbomcp_client/client/operations/
tools.rs

1//! Tool operations for MCP client
2//!
3//! This module provides tool-related functionality including listing tools,
4//! calling tools, and processing tool results.
5
6use std::collections::HashMap;
7use std::sync::atomic::Ordering;
8
9use turbomcp_protocol::types::{
10    CallToolRequest, CallToolResult, CreateTaskResult, Cursor, ListToolsRequest, ListToolsResult,
11    TaskMetadata, Tool,
12};
13use turbomcp_protocol::{Error, Result};
14
15/// Maximum number of pagination pages to prevent infinite loops from misbehaving servers.
16const MAX_PAGINATION_PAGES: usize = 1000;
17
18/// Response shape for `tools/call`.
19#[derive(Debug, Clone)]
20pub enum CallToolResponse {
21    /// Immediate tool result.
22    Result(CallToolResult),
23    /// Task handle for task-augmented execution.
24    Task(CreateTaskResult),
25}
26
27impl<T: turbomcp_transport::Transport + 'static> super::super::core::Client<T> {
28    /// List all available tools from the MCP server
29    ///
30    /// Returns complete tool definitions with schemas that can be used
31    /// for form generation, validation, and documentation. Tools represent
32    /// executable functions provided by the server.
33    ///
34    /// # Returns
35    ///
36    /// Returns a vector of Tool objects with complete metadata including names,
37    /// descriptions, and input schemas. These schemas can be used to generate
38    /// user interfaces for tool invocation.
39    ///
40    /// # Examples
41    ///
42    /// ```rust,no_run
43    /// # use turbomcp_client::Client;
44    /// # use turbomcp_transport::stdio::StdioTransport;
45    /// # async fn example() -> turbomcp_protocol::Result<()> {
46    /// let mut client = Client::new(StdioTransport::new());
47    /// client.initialize().await?;
48    ///
49    /// let tools = client.list_tools().await?;
50    /// for tool in tools {
51    ///     println!("Tool: {} - {}", tool.name, tool.description.as_deref().unwrap_or("No description"));
52    /// }
53    /// # Ok(())
54    /// # }
55    /// ```
56    pub async fn list_tools(&self) -> Result<Vec<Tool>> {
57        if !self.inner.initialized.load(Ordering::Relaxed) {
58            return Err(Error::invalid_request("Client not initialized"));
59        }
60
61        let mut all_tools = Vec::new();
62        let mut cursor = None;
63        for _ in 0..MAX_PAGINATION_PAGES {
64            let result = self.list_tools_paginated(cursor).await?;
65            let page_empty = result.tools.is_empty();
66            all_tools.extend(result.tools);
67            match result.next_cursor {
68                Some(c) if !page_empty => cursor = Some(c),
69                _ => break,
70            }
71        }
72        Ok(all_tools)
73    }
74
75    /// List tools with pagination support
76    ///
77    /// Returns the full `ListToolsResult` including `next_cursor` for manual
78    /// pagination control. Use `list_tools()` for automatic pagination.
79    ///
80    /// # Arguments
81    ///
82    /// * `cursor` - Optional cursor from a previous `ListToolsResult::next_cursor`
83    pub async fn list_tools_paginated(&self, cursor: Option<Cursor>) -> Result<ListToolsResult> {
84        if !self.inner.initialized.load(Ordering::Relaxed) {
85            return Err(Error::invalid_request("Client not initialized"));
86        }
87
88        let request = ListToolsRequest {
89            cursor,
90            _meta: None,
91        };
92        let params = if request.cursor.is_some() {
93            Some(serde_json::to_value(&request)?)
94        } else {
95            None
96        };
97        self.inner.protocol.request("tools/list", params).await
98    }
99
100    /// List available tool names from the MCP server
101    ///
102    /// Returns only the tool names for cases where full schemas are not needed.
103    /// For most use cases, prefer `list_tools()` which provides complete tool definitions.
104    ///
105    /// # Returns
106    ///
107    /// Returns a vector of tool names available on the server.
108    ///
109    /// # Examples
110    ///
111    /// ```rust,no_run
112    /// # use turbomcp_client::Client;
113    /// # use turbomcp_transport::stdio::StdioTransport;
114    /// # async fn example() -> turbomcp_protocol::Result<()> {
115    /// let mut client = Client::new(StdioTransport::new());
116    /// client.initialize().await?;
117    ///
118    /// let tool_names = client.list_tool_names().await?;
119    /// for name in tool_names {
120    ///     println!("Available tool: {}", name);
121    /// }
122    /// # Ok(())
123    /// # }
124    /// ```
125    pub async fn list_tool_names(&self) -> Result<Vec<String>> {
126        let tools = self.list_tools().await?;
127        Ok(tools.into_iter().map(|tool| tool.name).collect())
128    }
129
130    /// Call a tool on the server
131    ///
132    /// Executes a tool on the server with the provided arguments and returns
133    /// the complete MCP `CallToolResult`.
134    ///
135    /// # Arguments
136    ///
137    /// * `name` - The name of the tool to call
138    /// * `arguments` - Optional arguments to pass to the tool
139    /// * `task` - Must be `None`; task-augmented calls return `CreateTaskResult`.
140    ///   Use [`Self::call_tool_task`] or [`Self::call_tool_response`] for task
141    ///   execution.
142    ///
143    /// # Returns
144    ///
145    /// Returns the complete `CallToolResult` with:
146    /// - `content: Vec<ContentBlock>` - All content blocks (text, image, resource, audio, etc.)
147    /// - `is_error: Option<bool>` - Whether the tool execution resulted in an error
148    /// - `structured_content: Option<serde_json::Value>` - Schema-validated structured output
149    /// - `_meta: Option<serde_json::Value>` - Metadata for client applications (not exposed to LLMs)
150    ///
151    /// # Examples
152    ///
153    /// ## Basic Usage
154    ///
155    /// ```rust,no_run
156    /// # use turbomcp_client::Client;
157    /// # use turbomcp_transport::stdio::StdioTransport;
158    /// # use turbomcp_protocol::types::ContentBlock;
159    /// # use std::collections::HashMap;
160    /// # async fn example() -> turbomcp_protocol::Result<()> {
161    /// let mut client = Client::new(StdioTransport::new());
162    /// client.initialize().await?;
163    ///
164    /// let mut args = HashMap::new();
165    /// args.insert("input".to_string(), serde_json::json!("test"));
166    ///
167    /// let result = client.call_tool("my_tool", Some(args), None).await?;
168    /// # Ok(())
169    /// # }
170    /// ```
171    pub async fn call_tool(
172        &self,
173        name: &str,
174        arguments: Option<HashMap<String, serde_json::Value>>,
175        task: Option<TaskMetadata>,
176    ) -> Result<CallToolResult> {
177        if task.is_some() {
178            return Err(Error::invalid_request(
179                "task-augmented tools/call returns CreateTaskResult; use call_tool_task or call_tool_response",
180            ));
181        }
182
183        match self.call_tool_response(name, arguments, None).await? {
184            CallToolResponse::Result(result) => Ok(result),
185            CallToolResponse::Task(_) => Err(Error::invalid_request(
186                "task-augmented tools/call returned CreateTaskResult; use call_tool_task or call_tool_response",
187            )),
188        }
189    }
190
191    /// Call a tool and preserve the spec-level response variant.
192    ///
193    /// MCP 2025-11-25 task-augmented `tools/call` returns `CreateTaskResult`
194    /// immediately. Non-task calls return `CallToolResult`.
195    pub async fn call_tool_response(
196        &self,
197        name: &str,
198        arguments: Option<HashMap<String, serde_json::Value>>,
199        task: Option<TaskMetadata>,
200    ) -> Result<CallToolResponse> {
201        if !self.inner.initialized.load(Ordering::Relaxed) {
202            return Err(Error::invalid_request("Client not initialized"));
203        }
204
205        let is_task_augmented = task.is_some();
206        let request_data = CallToolRequest {
207            name: name.to_string(),
208            arguments: Some(arguments.unwrap_or_default()),
209            task,
210            _meta: None,
211        };
212
213        let raw_result: serde_json::Value = self
214            .inner
215            .protocol
216            .request("tools/call", Some(serde_json::to_value(&request_data)?))
217            .await?;
218
219        if is_task_augmented {
220            serde_json::from_value(raw_result)
221                .map(CallToolResponse::Task)
222                .map_err(|e| {
223                    Error::internal(format!("Failed to deserialize CreateTaskResult: {e}"))
224                })
225        } else {
226            serde_json::from_value(raw_result)
227                .map(CallToolResponse::Result)
228                .map_err(|e| Error::internal(format!("Failed to deserialize CallToolResult: {e}")))
229        }
230    }
231
232    /// Call a tool using MCP task-augmented execution.
233    ///
234    /// Returns the created task handle. Retrieve the final result with the
235    /// Tasks API once the server reports completion.
236    pub async fn call_tool_task(
237        &self,
238        name: &str,
239        arguments: Option<HashMap<String, serde_json::Value>>,
240        task: TaskMetadata,
241    ) -> Result<CreateTaskResult> {
242        match self.call_tool_response(name, arguments, Some(task)).await? {
243            CallToolResponse::Task(result) => Ok(result),
244            CallToolResponse::Result(_) => Err(Error::invalid_request(
245                "task-augmented tools/call returned CallToolResult instead of CreateTaskResult",
246            )),
247        }
248    }
249}