mcpkit_client/
handler.rs

1//! Client handler traits for server-initiated requests.
2//!
3//! MCP servers can initiate certain requests to clients, such as:
4//!
5//! - **Sampling**: Request the client's LLM to generate a response
6//! - **Elicitation**: Request user input through the client
7//! - **Roots**: Get file system roots that the client exposes
8//!
9//! This module defines traits that clients can implement to handle these requests.
10
11use mcpkit_core::error::McpError;
12use mcpkit_core::types::{
13    CreateMessageRequest, CreateMessageResult, ElicitRequest, ElicitResult,
14};
15use std::future::Future;
16
17/// Handler trait for server-initiated requests.
18///
19/// Implement this trait to handle requests that servers send to clients.
20/// All methods have default implementations that return "not supported" errors.
21///
22/// # Example
23///
24/// ```rust
25/// use mcpkit_client::ClientHandler;
26/// use mcpkit_core::types::{CreateMessageRequest, CreateMessageResult};
27/// use mcpkit_core::error::McpError;
28///
29/// struct MyHandler;
30///
31/// impl ClientHandler for MyHandler {
32///     // Override methods as needed to handle server requests
33/// }
34/// ```
35pub trait ClientHandler: Send + Sync {
36    /// Handle a sampling request from the server.
37    ///
38    /// The server is asking the client's LLM to generate a response.
39    /// This is used for agentic workflows where the server needs LLM capabilities.
40    ///
41    /// # Errors
42    ///
43    /// Returns an error if sampling is not supported or the request fails.
44    fn create_message(
45        &self,
46        _request: CreateMessageRequest,
47    ) -> impl Future<Output = Result<CreateMessageResult, McpError>> + Send {
48        async {
49            Err(McpError::CapabilityNotSupported {
50                capability: "sampling".to_string(),
51                available: Box::new([]),
52            })
53        }
54    }
55
56    /// Handle an elicitation request from the server.
57    ///
58    /// The server is asking for user input. The client should present
59    /// the request to the user and return their response.
60    ///
61    /// # Errors
62    ///
63    /// Returns an error if elicitation is not supported or the request fails.
64    fn elicit(
65        &self,
66        _request: ElicitRequest,
67    ) -> impl Future<Output = Result<ElicitResult, McpError>> + Send {
68        async {
69            Err(McpError::CapabilityNotSupported {
70                capability: "elicitation".to_string(),
71                available: Box::new([]),
72            })
73        }
74    }
75
76    /// List roots that the client exposes.
77    ///
78    /// Roots are file system paths that the server can access.
79    /// This is typically used for file-based operations.
80    ///
81    /// # Errors
82    ///
83    /// Returns an error if roots are not supported.
84    fn list_roots(&self) -> impl Future<Output = Result<Vec<Root>, McpError>> + Send {
85        async {
86            Err(McpError::CapabilityNotSupported {
87                capability: "roots".to_string(),
88                available: Box::new([]),
89            })
90        }
91    }
92
93    /// Called when the connection is established.
94    ///
95    /// Override this to perform setup after initialization.
96    fn on_connected(&self) -> impl Future<Output = ()> + Send {
97        async {}
98    }
99
100    /// Called when the connection is closed.
101    ///
102    /// Override this to perform cleanup.
103    fn on_disconnected(&self) -> impl Future<Output = ()> + Send {
104        async {}
105    }
106}
107
108/// A root directory that the client exposes to servers.
109#[derive(Debug, Clone)]
110pub struct Root {
111    /// URI of the root (e.g., "file:///home/user/project").
112    pub uri: String,
113    /// Human-readable name for the root.
114    pub name: Option<String>,
115}
116
117impl Root {
118    /// Create a new root.
119    pub fn new(uri: impl Into<String>) -> Self {
120        Self {
121            uri: uri.into(),
122            name: None,
123        }
124    }
125
126    /// Set the name.
127    pub fn name(mut self, name: impl Into<String>) -> Self {
128        self.name = Some(name.into());
129        self
130    }
131}
132
133/// A no-op handler that rejects all server requests.
134///
135/// Use this as a default handler when you don't need to handle
136/// any server-initiated requests.
137pub struct NoOpHandler;
138
139impl ClientHandler for NoOpHandler {}
140
141/// A handler that supports sampling by delegating to a closure.
142pub struct SamplingHandler<F> {
143    handler: F,
144}
145
146impl<F, Fut> SamplingHandler<F>
147where
148    F: Fn(CreateMessageRequest) -> Fut + Send + Sync,
149    Fut: Future<Output = Result<CreateMessageResult, McpError>> + Send,
150{
151    /// Create a new sampling handler.
152    pub fn new(handler: F) -> Self {
153        Self { handler }
154    }
155}
156
157impl<F, Fut> ClientHandler for SamplingHandler<F>
158where
159    F: Fn(CreateMessageRequest) -> Fut + Send + Sync,
160    Fut: Future<Output = Result<CreateMessageResult, McpError>> + Send,
161{
162    fn create_message(
163        &self,
164        request: CreateMessageRequest,
165    ) -> impl Future<Output = Result<CreateMessageResult, McpError>> + Send {
166        (self.handler)(request)
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn test_root_builder() {
176        let root = Root::new("file:///home/user/project").name("My Project");
177        assert!(root.uri.contains("project"));
178        assert_eq!(root.name, Some("My Project".to_string()));
179    }
180
181    #[tokio::test]
182    async fn test_noop_handler() {
183        let handler = NoOpHandler;
184        let result = handler
185            .create_message(CreateMessageRequest {
186                messages: vec![],
187                model_preferences: None,
188                system_prompt: None,
189                include_context: None,
190                temperature: None,
191                max_tokens: 100,
192                stop_sequences: None,
193                metadata: None,
194            })
195            .await;
196        assert!(result.is_err());
197    }
198}