mcpkit_server/capability/
elicitation.rs

1//! Elicitation capability implementation.
2//!
3//! This module provides support for user input elicitation
4//! in MCP servers.
5
6use crate::context::Context;
7use mcpkit_core::error::McpError;
8use mcpkit_core::types::elicitation::{
9    ElicitAction, ElicitRequest, ElicitResult, ElicitationSchema,
10};
11use serde_json::Value;
12use std::future::Future;
13use std::pin::Pin;
14
15/// A boxed async function for handling elicitation requests.
16pub type BoxedElicitationFn = Box<
17    dyn for<'a> Fn(
18            ElicitRequest,
19            &'a Context<'a>,
20        )
21            -> Pin<Box<dyn Future<Output = Result<ElicitResult, McpError>> + Send + 'a>>
22        + Send
23        + Sync,
24>;
25
26/// Service for handling elicitation requests.
27///
28/// Elicitation allows servers to request input from users
29/// through the client interface.
30pub struct ElicitationService {
31    handler: Option<BoxedElicitationFn>,
32}
33
34impl Default for ElicitationService {
35    fn default() -> Self {
36        Self::new()
37    }
38}
39
40impl ElicitationService {
41    /// Create a new elicitation service without a handler.
42    #[must_use]
43    pub fn new() -> Self {
44        Self { handler: None }
45    }
46
47    /// Set the elicitation handler.
48    pub fn with_handler<F, Fut>(mut self, handler: F) -> Self
49    where
50        F: Fn(ElicitRequest, &Context<'_>) -> Fut + Send + Sync + 'static,
51        Fut: Future<Output = Result<ElicitResult, McpError>> + Send + 'static,
52    {
53        self.handler = Some(Box::new(move |req, ctx| Box::pin(handler(req, ctx))));
54        self
55    }
56
57    /// Check if elicitation is supported.
58    #[must_use]
59    pub fn is_supported(&self) -> bool {
60        self.handler.is_some()
61    }
62
63    /// Create an elicitation request.
64    pub async fn elicit(
65        &self,
66        request: ElicitRequest,
67        ctx: &Context<'_>,
68    ) -> Result<ElicitResult, McpError> {
69        let handler = self
70            .handler
71            .as_ref()
72            .ok_or_else(|| McpError::invalid_request("Elicitation not supported"))?;
73
74        (handler)(request, ctx).await
75    }
76}
77
78/// Builder for creating elicitation requests.
79pub struct ElicitationRequestBuilder {
80    message: String,
81    schema: Option<ElicitationSchema>,
82}
83
84impl ElicitationRequestBuilder {
85    /// Create a new request builder.
86    pub fn new(message: impl Into<String>) -> Self {
87        Self {
88            message: message.into(),
89            schema: None,
90        }
91    }
92
93    /// Set the response schema.
94    #[must_use]
95    pub fn schema(mut self, schema: ElicitationSchema) -> Self {
96        self.schema = Some(schema);
97        self
98    }
99
100    /// Request a text response.
101    pub fn text_response(self, field_name: impl Into<String>) -> Self {
102        self.schema(ElicitationSchema::object().property(
103            field_name,
104            mcpkit_core::types::elicitation::PropertySchema::string(),
105        ))
106    }
107
108    /// Request a boolean response.
109    pub fn boolean_response(self, field_name: impl Into<String>) -> Self {
110        self.schema(ElicitationSchema::object().property(
111            field_name,
112            mcpkit_core::types::elicitation::PropertySchema::boolean(),
113        ))
114    }
115
116    /// Build the request.
117    pub fn build(self) -> ElicitRequest {
118        ElicitRequest {
119            message: self.message,
120            requested_schema: self.schema.unwrap_or_else(ElicitationSchema::object),
121        }
122    }
123}
124
125/// Builder for creating elicitation results.
126pub struct ElicitationResultBuilder {
127    action: ElicitAction,
128    content: Option<serde_json::Map<String, Value>>,
129}
130
131impl ElicitationResultBuilder {
132    /// Create a new result builder.
133    #[must_use]
134    pub const fn new() -> Self {
135        Self {
136            action: ElicitAction::Accept,
137            content: None,
138        }
139    }
140
141    /// Set the result as accepted with content.
142    #[must_use]
143    pub fn accepted(mut self, content: Value) -> Self {
144        self.action = ElicitAction::Accept;
145        // Convert Value to Map if it's an object, otherwise wrap it
146        self.content = match content {
147            Value::Object(map) => Some(map),
148            other => {
149                let mut map = serde_json::Map::new();
150                map.insert("value".to_string(), other);
151                Some(map)
152            }
153        };
154        self
155    }
156
157    /// Set the result as accepted with a map.
158    #[must_use]
159    pub fn accepted_map(mut self, content: serde_json::Map<String, Value>) -> Self {
160        self.action = ElicitAction::Accept;
161        self.content = Some(content);
162        self
163    }
164
165    /// Set the result as declined.
166    #[must_use]
167    pub fn declined(mut self) -> Self {
168        self.action = ElicitAction::Decline;
169        self.content = None;
170        self
171    }
172
173    /// Set the result as cancelled.
174    #[must_use]
175    pub fn cancelled(mut self) -> Self {
176        self.action = ElicitAction::Cancel;
177        self.content = None;
178        self
179    }
180
181    /// Build the result.
182    #[must_use]
183    pub fn build(self) -> ElicitResult {
184        ElicitResult {
185            action: self.action,
186            content: self.content,
187        }
188    }
189}
190
191impl Default for ElicitationResultBuilder {
192    fn default() -> Self {
193        Self::new()
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn test_elicitation_request_builder() {
203        let request = ElicitationRequestBuilder::new("Please enter your name")
204            .text_response("name")
205            .build();
206
207        assert_eq!(request.message, "Please enter your name");
208    }
209
210    #[test]
211    fn test_elicitation_result_builder() {
212        let result = ElicitationResultBuilder::new()
213            .accepted(serde_json::json!({"name": "John Doe"}))
214            .build();
215
216        assert_eq!(result.action, ElicitAction::Accept);
217        assert!(result.content.is_some());
218    }
219
220    #[test]
221    fn test_elicitation_service_default() {
222        let service = ElicitationService::new();
223        assert!(!service.is_supported());
224    }
225
226    #[tokio::test]
227    async fn test_elicitation_service_with_handler() {
228        let service = ElicitationService::new().with_handler(|_req, _ctx| async {
229            Ok(ElicitationResultBuilder::new()
230                .accepted(serde_json::json!({"response": "test"}))
231                .build())
232        });
233
234        assert!(service.is_supported());
235    }
236}