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