mcpkit_client/
builder.rs

1//! Client builder for fluent construction.
2//!
3//! The [`ClientBuilder`] provides a fluent API for constructing MCP clients
4//! with customizable options.
5
6use mcpkit_core::capability::{ClientCapabilities, ClientInfo};
7use mcpkit_core::error::McpError;
8use mcpkit_transport::Transport;
9
10use crate::client::{Client, initialize};
11
12/// Builder for constructing MCP clients.
13///
14/// Use this builder to configure and create an MCP client connection.
15///
16/// # Example
17///
18/// ```no_run
19/// use mcpkit_client::ClientBuilder;
20/// use mcpkit_transport::SpawnedTransport;
21///
22/// # async fn example() -> Result<(), mcpkit_core::error::McpError> {
23/// let transport = SpawnedTransport::spawn("my-server", &[] as &[&str]).await?;
24/// let client = ClientBuilder::new()
25///     .name("my-client")
26///     .version("1.0.0")
27///     .with_sampling()
28///     .with_roots()
29///     .build(transport)
30///     .await?;
31/// # Ok(())
32/// # }
33/// ```
34pub struct ClientBuilder {
35    name: String,
36    version: String,
37    capabilities: ClientCapabilities,
38}
39
40impl Default for ClientBuilder {
41    fn default() -> Self {
42        Self::new()
43    }
44}
45
46impl ClientBuilder {
47    /// Create a new client builder with default values.
48    #[must_use]
49    pub fn new() -> Self {
50        Self {
51            name: "mcp-client".to_string(),
52            version: env!("CARGO_PKG_VERSION").to_string(),
53            capabilities: ClientCapabilities::default(),
54        }
55    }
56
57    /// Set the client name.
58    #[must_use]
59    pub fn name(mut self, name: impl Into<String>) -> Self {
60        self.name = name.into();
61        self
62    }
63
64    /// Set the client version.
65    #[must_use]
66    pub fn version(mut self, version: impl Into<String>) -> Self {
67        self.version = version.into();
68        self
69    }
70
71    /// Enable sampling capability.
72    ///
73    /// When enabled, the server can request LLM completions from this client.
74    #[must_use]
75    pub fn with_sampling(mut self) -> Self {
76        self.capabilities = self.capabilities.with_sampling();
77        self
78    }
79
80    /// Enable elicitation capability.
81    ///
82    /// When enabled, the server can request user input from this client.
83    #[must_use]
84    pub fn with_elicitation(mut self) -> Self {
85        self.capabilities = self.capabilities.with_elicitation();
86        self
87    }
88
89    /// Enable roots capability.
90    ///
91    /// When enabled, the client exposes file system roots to the server.
92    #[must_use]
93    pub fn with_roots(mut self) -> Self {
94        self.capabilities = self.capabilities.with_roots();
95        self
96    }
97
98    /// Enable roots capability with change notifications.
99    #[must_use]
100    pub fn with_roots_and_changes(mut self) -> Self {
101        self.capabilities = self.capabilities.with_roots_and_changes();
102        self
103    }
104
105    /// Set custom capabilities.
106    #[must_use]
107    pub fn capabilities(mut self, capabilities: ClientCapabilities) -> Self {
108        self.capabilities = capabilities;
109        self
110    }
111
112    /// Build and connect the client using the given transport.
113    ///
114    /// This performs the MCP handshake and returns a connected client.
115    ///
116    /// # Errors
117    ///
118    /// Returns an error if the handshake fails or the transport encounters an error.
119    pub async fn build<T: Transport + 'static>(self, transport: T) -> Result<Client<T>, McpError> {
120        let client_info = ClientInfo::new(&self.name, &self.version);
121        let init_result = initialize(&transport, &client_info, &self.capabilities).await?;
122        Ok(Client::new(
123            transport,
124            init_result,
125            client_info,
126            self.capabilities,
127        ))
128    }
129
130    /// Build and connect the client with a custom handler.
131    ///
132    /// The handler receives server-initiated requests for sampling, elicitation, and roots.
133    ///
134    /// # Errors
135    ///
136    /// Returns an error if the handshake fails or the transport encounters an error.
137    pub async fn build_with_handler<
138        T: Transport + 'static,
139        H: crate::handler::ClientHandler + 'static,
140    >(
141        self,
142        transport: T,
143        handler: H,
144    ) -> Result<Client<T, H>, McpError> {
145        let client_info = ClientInfo::new(&self.name, &self.version);
146        let init_result = initialize(&transport, &client_info, &self.capabilities).await?;
147        Ok(Client::with_handler(
148            transport,
149            init_result,
150            client_info,
151            self.capabilities,
152            handler,
153        ))
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn test_builder_defaults() {
163        let builder = ClientBuilder::new();
164        assert_eq!(builder.name, "mcp-client");
165        assert!(!builder.capabilities.has_sampling());
166        assert!(!builder.capabilities.has_roots());
167    }
168
169    #[test]
170    fn test_builder_fluent() {
171        let builder = ClientBuilder::new()
172            .name("test-client")
173            .version("1.0.0")
174            .with_sampling()
175            .with_roots();
176
177        assert_eq!(builder.name, "test-client");
178        assert_eq!(builder.version, "1.0.0");
179        assert!(builder.capabilities.has_sampling());
180        assert!(builder.capabilities.has_roots());
181    }
182}