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, TaskId, TaskProgress,
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    // Notification Handlers
109    // =========================================================================
110
111    /// Called when a task makes progress.
112    ///
113    /// Override this to track task progress updates from the server.
114    fn on_task_progress(
115        &self,
116        _task_id: TaskId,
117        _progress: TaskProgress,
118    ) -> impl Future<Output = ()> + Send {
119        async {}
120    }
121
122    /// Called when a resource has been updated.
123    ///
124    /// Override this to react to resource changes (requires subscription).
125    fn on_resource_updated(&self, _uri: String) -> impl Future<Output = ()> + Send {
126        async {}
127    }
128
129    /// Called when the list of available resources has changed.
130    ///
131    /// Override this to refresh your cached resource list.
132    fn on_resources_list_changed(&self) -> impl Future<Output = ()> + Send {
133        async {}
134    }
135
136    /// Called when the list of available tools has changed.
137    ///
138    /// Override this to refresh your cached tool list.
139    fn on_tools_list_changed(&self) -> impl Future<Output = ()> + Send {
140        async {}
141    }
142
143    /// Called when the list of available prompts has changed.
144    ///
145    /// Override this to refresh your cached prompt list.
146    fn on_prompts_list_changed(&self) -> impl Future<Output = ()> + Send {
147        async {}
148    }
149}
150
151/// A root directory that the client exposes to servers.
152#[derive(Debug, Clone)]
153pub struct Root {
154    /// URI of the root (e.g., "<file:///home/user/project>").
155    pub uri: String,
156    /// Human-readable name for the root.
157    pub name: Option<String>,
158}
159
160impl Root {
161    /// Create a new root.
162    pub fn new(uri: impl Into<String>) -> Self {
163        Self {
164            uri: uri.into(),
165            name: None,
166        }
167    }
168
169    /// Set the name.
170    pub fn name(mut self, name: impl Into<String>) -> Self {
171        self.name = Some(name.into());
172        self
173    }
174}
175
176/// A no-op handler that rejects all server requests.
177///
178/// Use this as a default handler when you don't need to handle
179/// any server-initiated requests.
180pub struct NoOpHandler;
181
182impl ClientHandler for NoOpHandler {}
183
184/// A handler that supports sampling by delegating to a closure.
185pub struct SamplingHandler<F> {
186    handler: F,
187}
188
189impl<F, Fut> SamplingHandler<F>
190where
191    F: Fn(CreateMessageRequest) -> Fut + Send + Sync,
192    Fut: Future<Output = Result<CreateMessageResult, McpError>> + Send,
193{
194    /// Create a new sampling handler.
195    pub const fn new(handler: F) -> Self {
196        Self { handler }
197    }
198}
199
200impl<F, Fut> ClientHandler for SamplingHandler<F>
201where
202    F: Fn(CreateMessageRequest) -> Fut + Send + Sync,
203    Fut: Future<Output = Result<CreateMessageResult, McpError>> + Send,
204{
205    fn create_message(
206        &self,
207        request: CreateMessageRequest,
208    ) -> impl Future<Output = Result<CreateMessageResult, McpError>> + Send {
209        (self.handler)(request)
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn test_root_builder() {
219        let root = Root::new("file:///home/user/project").name("My Project");
220        assert!(root.uri.contains("project"));
221        assert_eq!(root.name, Some("My Project".to_string()));
222    }
223
224    #[tokio::test]
225    async fn test_noop_handler() {
226        let handler = NoOpHandler;
227        let result = handler
228            .create_message(CreateMessageRequest {
229                messages: vec![],
230                model_preferences: None,
231                system_prompt: None,
232                include_context: None,
233                temperature: None,
234                max_tokens: 100,
235                stop_sequences: None,
236                metadata: None,
237            })
238            .await;
239        assert!(result.is_err());
240    }
241}