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::{initialize, Client};
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(transport, init_result, client_info, self.capabilities))
123    }
124
125    /// Build and connect the client with a custom handler.
126    ///
127    /// The handler receives server-initiated requests for sampling, elicitation, and roots.
128    ///
129    /// # Errors
130    ///
131    /// Returns an error if the handshake fails or the transport encounters an error.
132    pub async fn build_with_handler<T: Transport + 'static, H: crate::handler::ClientHandler + 'static>(
133        self,
134        transport: T,
135        handler: H,
136    ) -> Result<Client<T, H>, McpError> {
137        let client_info = ClientInfo::new(&self.name, &self.version);
138        let init_result = initialize(&transport, &client_info, &self.capabilities).await?;
139        Ok(Client::with_handler(transport, init_result, client_info, self.capabilities, handler))
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_builder_defaults() {
149        let builder = ClientBuilder::new();
150        assert_eq!(builder.name, "mcp-client");
151        assert!(!builder.capabilities.has_sampling());
152        assert!(!builder.capabilities.has_roots());
153    }
154
155    #[test]
156    fn test_builder_fluent() {
157        let builder = ClientBuilder::new()
158            .name("test-client")
159            .version("1.0.0")
160            .with_sampling()
161            .with_roots();
162
163        assert_eq!(builder.name, "test-client");
164        assert_eq!(builder.version, "1.0.0");
165        assert!(builder.capabilities.has_sampling());
166        assert!(builder.capabilities.has_roots());
167    }
168}