turbomcp-client 3.0.8

MCP client with full protocol support, bidirectional communication, and plugin middleware
Documentation
//! Tool operations for MCP client
//!
//! This module provides tool-related functionality including listing tools,
//! calling tools, and processing tool results.

use std::collections::HashMap;
use std::sync::atomic::Ordering;

use turbomcp_protocol::types::{
    CallToolRequest, CallToolResult, Cursor, ListToolsRequest, ListToolsResult, Tool,
};
use turbomcp_protocol::{Error, Result};

/// Maximum number of pagination pages to prevent infinite loops from misbehaving servers.
const MAX_PAGINATION_PAGES: usize = 1000;

impl<T: turbomcp_transport::Transport + 'static> super::super::core::Client<T> {
    /// List all available tools from the MCP server
    ///
    /// Returns complete tool definitions with schemas that can be used
    /// for form generation, validation, and documentation. Tools represent
    /// executable functions provided by the server.
    ///
    /// # Returns
    ///
    /// Returns a vector of Tool objects with complete metadata including names,
    /// descriptions, and input schemas. These schemas can be used to generate
    /// user interfaces for tool invocation.
    ///
    /// # Examples
    ///
    /// ```rust,no_run
    /// # use turbomcp_client::Client;
    /// # use turbomcp_transport::stdio::StdioTransport;
    /// # async fn example() -> turbomcp_protocol::Result<()> {
    /// let mut client = Client::new(StdioTransport::new());
    /// client.initialize().await?;
    ///
    /// let tools = client.list_tools().await?;
    /// for tool in tools {
    ///     println!("Tool: {} - {}", tool.name, tool.description.as_deref().unwrap_or("No description"));
    /// }
    /// # Ok(())
    /// # }
    /// ```
    pub async fn list_tools(&self) -> Result<Vec<Tool>> {
        if !self.inner.initialized.load(Ordering::Relaxed) {
            return Err(Error::invalid_request("Client not initialized"));
        }

        let mut all_tools = Vec::new();
        let mut cursor = None;
        for _ in 0..MAX_PAGINATION_PAGES {
            let result = self.list_tools_paginated(cursor).await?;
            let page_empty = result.tools.is_empty();
            all_tools.extend(result.tools);
            match result.next_cursor {
                Some(c) if !page_empty => cursor = Some(c),
                _ => break,
            }
        }
        Ok(all_tools)
    }

    /// List tools with pagination support
    ///
    /// Returns the full `ListToolsResult` including `next_cursor` for manual
    /// pagination control. Use `list_tools()` for automatic pagination.
    ///
    /// # Arguments
    ///
    /// * `cursor` - Optional cursor from a previous `ListToolsResult::next_cursor`
    pub async fn list_tools_paginated(&self, cursor: Option<Cursor>) -> Result<ListToolsResult> {
        if !self.inner.initialized.load(Ordering::Relaxed) {
            return Err(Error::invalid_request("Client not initialized"));
        }

        let request = ListToolsRequest {
            cursor,
            _meta: None,
        };
        let params = if request.cursor.is_some() {
            Some(serde_json::to_value(&request)?)
        } else {
            None
        };
        self.inner.protocol.request("tools/list", params).await
    }

    /// List available tool names from the MCP server
    ///
    /// Returns only the tool names for cases where full schemas are not needed.
    /// For most use cases, prefer `list_tools()` which provides complete tool definitions.
    ///
    /// # Returns
    ///
    /// Returns a vector of tool names available on the server.
    ///
    /// # Examples
    ///
    /// ```rust,no_run
    /// # use turbomcp_client::Client;
    /// # use turbomcp_transport::stdio::StdioTransport;
    /// # async fn example() -> turbomcp_protocol::Result<()> {
    /// let mut client = Client::new(StdioTransport::new());
    /// client.initialize().await?;
    ///
    /// let tool_names = client.list_tool_names().await?;
    /// for name in tool_names {
    ///     println!("Available tool: {}", name);
    /// }
    /// # Ok(())
    /// # }
    /// ```
    pub async fn list_tool_names(&self) -> Result<Vec<String>> {
        let tools = self.list_tools().await?;
        Ok(tools.into_iter().map(|tool| tool.name).collect())
    }

    /// Call a tool on the server
    ///
    /// Executes a tool on the server with the provided arguments and returns
    /// the complete MCP `CallToolResult`.
    ///
    /// # Arguments
    ///
    /// * `name` - The name of the tool to call
    /// * `arguments` - Optional arguments to pass to the tool
    /// * `task` - Optional task metadata for task-augmented requests (MCP 2025-11-25 draft)
    ///
    /// # Returns
    ///
    /// Returns the complete `CallToolResult` with:
    /// - `content: Vec<ContentBlock>` - All content blocks (text, image, resource, audio, etc.)
    /// - `is_error: Option<bool>` - Whether the tool execution resulted in an error
    /// - `structured_content: Option<serde_json::Value>` - Schema-validated structured output
    /// - `_meta: Option<serde_json::Value>` - Metadata for client applications (not exposed to LLMs)
    /// - `task_id: Option<String>` - Task identifier if task-augmented
    ///
    /// # Examples
    ///
    /// ## Basic Usage
    ///
    /// ```rust,no_run
    /// # use turbomcp_client::Client;
    /// # use turbomcp_transport::stdio::StdioTransport;
    /// # use turbomcp_protocol::types::ContentBlock;
    /// # use std::collections::HashMap;
    /// # async fn example() -> turbomcp_protocol::Result<()> {
    /// let mut client = Client::new(StdioTransport::new());
    /// client.initialize().await?;
    ///
    /// let mut args = HashMap::new();
    /// args.insert("input".to_string(), serde_json::json!("test"));
    ///
    /// let result = client.call_tool("my_tool", Some(args), None).await?;
    /// # Ok(())
    /// # }
    /// ```
    pub async fn call_tool(
        &self,
        name: &str,
        arguments: Option<HashMap<String, serde_json::Value>>,
        task: Option<turbomcp_protocol::types::tasks::TaskMetadata>,
    ) -> Result<CallToolResult> {
        if !self.inner.initialized.load(Ordering::Relaxed) {
            return Err(Error::invalid_request("Client not initialized"));
        }

        let request_data = CallToolRequest {
            name: name.to_string(),
            arguments: Some(arguments.unwrap_or_default()),
            task,
            _meta: None,
        };

        // Core protocol call
        let result: CallToolResult = self
            .inner
            .protocol
            .request("tools/call", Some(serde_json::to_value(&request_data)?))
            .await?;

        Ok(result) // Return full CallToolResult - MCP spec compliant!
    }
}