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}