thulp_mcp/
client.rs

1//! MCP client implementation.
2
3use crate::{McpTransport, Result};
4use std::collections::HashMap;
5use thulp_core::{ToolCall, ToolDefinition, ToolResult, Transport};
6
7/// MCP client wrapper.
8pub struct McpClient {
9    transport: McpTransport,
10    tool_cache: HashMap<String, ToolDefinition>,
11    session_id: String,
12}
13
14impl McpClient {
15    /// Create a new MCP client.
16    pub fn new(transport: McpTransport) -> Self {
17        Self {
18            transport,
19            tool_cache: HashMap::new(),
20            session_id: uuid::Uuid::new_v4().to_string(),
21        }
22    }
23
24    /// Create a client builder.
25    pub fn builder() -> McpClientBuilder {
26        McpClientBuilder::new()
27    }
28
29    /// Connect to the MCP server.
30    pub async fn connect(&mut self) -> Result<()> {
31        self.transport.connect().await?;
32        Ok(())
33    }
34
35    /// Disconnect from the MCP server.
36    pub async fn disconnect(&mut self) -> Result<()> {
37        self.transport.disconnect().await?;
38        self.tool_cache.clear();
39        Ok(())
40    }
41
42    /// Check if connected.
43    pub fn is_connected(&self) -> bool {
44        self.transport.is_connected()
45    }
46
47    /// List available tools.
48    pub async fn list_tools(&mut self) -> Result<Vec<ToolDefinition>> {
49        if self.tool_cache.is_empty() {
50            let tools = self.transport.list_tools().await?;
51            for tool in &tools {
52                self.tool_cache.insert(tool.name.clone(), tool.clone());
53            }
54        }
55
56        Ok(self.tool_cache.values().cloned().collect())
57    }
58
59    /// Get a specific tool definition.
60    pub async fn get_tool(&mut self, name: &str) -> Result<Option<ToolDefinition>> {
61        if !self.tool_cache.contains_key(name) {
62            // Refresh cache if tool not found
63            self.list_tools().await?;
64        }
65        Ok(self.tool_cache.get(name).cloned())
66    }
67
68    /// Execute a tool call.
69    pub async fn call_tool(&self, name: &str, arguments: serde_json::Value) -> Result<ToolResult> {
70        let call = ToolCall {
71            tool: name.to_string(),
72            arguments,
73        };
74        self.transport.call(&call).await
75    }
76
77    /// Get the session ID.
78    pub fn session_id(&self) -> &str {
79        &self.session_id
80    }
81
82    /// Clear the tool cache.
83    pub fn clear_cache(&mut self) {
84        self.tool_cache.clear();
85    }
86}
87
88/// Builder for [`McpClient`].
89pub struct McpClientBuilder {
90    transport: Option<McpTransport>,
91}
92
93impl McpClientBuilder {
94    /// Create a new builder.
95    pub fn new() -> Self {
96        Self { transport: None }
97    }
98
99    /// Set the transport.
100    pub fn transport(mut self, transport: McpTransport) -> Self {
101        self.transport = Some(transport);
102        self
103    }
104
105    /// Build the client.
106    pub fn build(self) -> Result<McpClient> {
107        use thulp_core::Error;
108        let transport = self
109            .transport
110            .ok_or_else(|| Error::InvalidConfig("transport not set".to_string()))?;
111
112        Ok(McpClient::new(transport))
113    }
114}
115
116impl Default for McpClientBuilder {
117    fn default() -> Self {
118        Self::new()
119    }
120}
121
122/// Convenience functions for common connection patterns.
123impl McpClient {
124    /// Connect to an MCP server via HTTP.
125    pub async fn connect_http(name: String, url: String) -> Result<McpClient> {
126        let transport = McpTransport::new_http(name, url);
127        let mut client = McpClient::new(transport);
128
129        client.connect().await?;
130        Ok(client)
131    }
132
133    /// Connect to an MCP server via STDIO.
134    pub async fn connect_stdio(
135        name: String,
136        command: String,
137        args: Option<Vec<String>>,
138    ) -> Result<McpClient> {
139        let transport = McpTransport::new_stdio(name, command, args);
140        let mut client = McpClient::new(transport);
141
142        client.connect().await?;
143        Ok(client)
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[tokio::test]
152    async fn client_creation() {
153        let transport =
154            McpTransport::new_http("test".to_string(), "http://localhost:8080".to_string());
155        let client = McpClient::new(transport);
156        assert!(!client.is_connected());
157    }
158
159    #[tokio::test]
160    async fn client_builder() {
161        let client = McpClient::builder()
162            .transport(McpTransport::new_http(
163                "test".to_string(),
164                "http://localhost:8080".to_string(),
165            ))
166            .build()
167            .unwrap();
168        assert!(!client.is_connected());
169    }
170
171    #[tokio::test]
172    async fn client_convenience() {
173        // This is a placeholder test since we can't actually connect to MCP servers in tests
174        // In real usage, this would connect to a real MCP server
175        assert!(true);
176    }
177}