1use crate::{McpTransport, Result};
4use std::collections::HashMap;
5use thulp_core::{ToolCall, ToolDefinition, ToolResult, Transport};
6
7pub struct McpClient {
9 transport: McpTransport,
10 tool_cache: HashMap<String, ToolDefinition>,
11 session_id: String,
12}
13
14impl McpClient {
15 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 pub fn builder() -> McpClientBuilder {
26 McpClientBuilder::new()
27 }
28
29 pub async fn connect(&mut self) -> Result<()> {
31 self.transport.connect().await?;
32 Ok(())
33 }
34
35 pub async fn disconnect(&mut self) -> Result<()> {
37 self.transport.disconnect().await?;
38 self.tool_cache.clear();
39 Ok(())
40 }
41
42 pub fn is_connected(&self) -> bool {
44 self.transport.is_connected()
45 }
46
47 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 pub async fn get_tool(&mut self, name: &str) -> Result<Option<ToolDefinition>> {
61 if !self.tool_cache.contains_key(name) {
62 self.list_tools().await?;
64 }
65 Ok(self.tool_cache.get(name).cloned())
66 }
67
68 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 pub fn session_id(&self) -> &str {
79 &self.session_id
80 }
81
82 pub fn clear_cache(&mut self) {
84 self.tool_cache.clear();
85 }
86}
87
88pub struct McpClientBuilder {
90 transport: Option<McpTransport>,
91}
92
93impl McpClientBuilder {
94 pub fn new() -> Self {
96 Self { transport: None }
97 }
98
99 pub fn transport(mut self, transport: McpTransport) -> Self {
101 self.transport = Some(transport);
102 self
103 }
104
105 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
122impl McpClient {
124 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 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 assert!(true);
176 }
177}