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}