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}