Skip to main content

mcp_kit/types/
elicitation.rs

1//! Elicitation types for server-initiated user input requests.
2//!
3//! Elicitation allows MCP servers to request additional information from users
4//! through the client. This is useful for:
5//! - Confirming dangerous operations
6//! - Requesting missing parameters
7//! - Getting user preferences
8//! - Multi-step workflows requiring user decisions
9//!
10//! # Example
11//!
12//! ```rust,ignore
13//! use mcp_kit::types::elicitation::*;
14//!
15//! // Request user confirmation
16//! let request = ElicitRequest {
17//!     message: "Delete all files in /tmp?".to_string(),
18//!     requested_schema: ElicitSchema::boolean(),
19//! };
20//!
21//! // Or request structured input
22//! let request = ElicitRequest {
23//!     message: "Configure backup settings".to_string(),
24//!     requested_schema: ElicitSchema::object(serde_json::json!({
25//!         "type": "object",
26//!         "properties": {
27//!             "path": { "type": "string", "description": "Backup path" },
28//!             "compress": { "type": "boolean", "default": true }
29//!         },
30//!         "required": ["path"]
31//!     })),
32//! };
33//! ```
34
35use serde::{Deserialize, Serialize};
36use serde_json::Value;
37
38/// Request to elicit information from the user.
39#[derive(Debug, Clone, Serialize, Deserialize)]
40#[serde(rename_all = "camelCase")]
41pub struct ElicitRequest {
42    /// Human-readable message explaining what information is needed.
43    pub message: String,
44
45    /// Schema describing the expected response format.
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub requested_schema: Option<ElicitSchema>,
48}
49
50impl ElicitRequest {
51    /// Create a new elicitation request with just a message.
52    pub fn new(message: impl Into<String>) -> Self {
53        Self {
54            message: message.into(),
55            requested_schema: None,
56        }
57    }
58
59    /// Create a request with a specific schema.
60    pub fn with_schema(message: impl Into<String>, schema: ElicitSchema) -> Self {
61        Self {
62            message: message.into(),
63            requested_schema: Some(schema),
64        }
65    }
66
67    /// Create a yes/no confirmation request.
68    pub fn confirm(message: impl Into<String>) -> Self {
69        Self::with_schema(message, ElicitSchema::boolean())
70    }
71
72    /// Create a text input request.
73    pub fn text(message: impl Into<String>) -> Self {
74        Self::with_schema(message, ElicitSchema::string())
75    }
76
77    /// Create a choice selection request.
78    pub fn choice(message: impl Into<String>, options: Vec<String>) -> Self {
79        Self::with_schema(message, ElicitSchema::enum_values(options))
80    }
81}
82
83/// Schema for elicitation response validation.
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct ElicitSchema {
86    /// JSON Schema definition.
87    #[serde(flatten)]
88    pub schema: Value,
89}
90
91impl ElicitSchema {
92    /// Create a schema from a JSON value.
93    pub fn new(schema: Value) -> Self {
94        Self { schema }
95    }
96
97    /// Create a boolean (yes/no) schema.
98    pub fn boolean() -> Self {
99        Self::new(serde_json::json!({
100            "type": "boolean",
101            "description": "Confirmation response (true/false)"
102        }))
103    }
104
105    /// Create a string input schema.
106    pub fn string() -> Self {
107        Self::new(serde_json::json!({
108            "type": "string"
109        }))
110    }
111
112    /// Create a string with description.
113    pub fn string_with_desc(description: impl Into<String>) -> Self {
114        Self::new(serde_json::json!({
115            "type": "string",
116            "description": description.into()
117        }))
118    }
119
120    /// Create an enum schema for selecting from options.
121    pub fn enum_values(values: Vec<String>) -> Self {
122        Self::new(serde_json::json!({
123            "type": "string",
124            "enum": values
125        }))
126    }
127
128    /// Create a number input schema.
129    pub fn number() -> Self {
130        Self::new(serde_json::json!({
131            "type": "number"
132        }))
133    }
134
135    /// Create a number with min/max constraints.
136    pub fn number_range(min: f64, max: f64) -> Self {
137        Self::new(serde_json::json!({
138            "type": "number",
139            "minimum": min,
140            "maximum": max
141        }))
142    }
143
144    /// Create an integer input schema.
145    pub fn integer() -> Self {
146        Self::new(serde_json::json!({
147            "type": "integer"
148        }))
149    }
150
151    /// Create an object schema from a JSON Schema definition.
152    pub fn object(schema: Value) -> Self {
153        Self::new(schema)
154    }
155}
156
157/// Result of an elicitation request.
158#[derive(Debug, Clone, Serialize, Deserialize)]
159#[serde(rename_all = "camelCase")]
160pub struct ElicitResult {
161    /// The action taken by the user.
162    pub action: ElicitAction,
163
164    /// The content provided by the user (if accepted).
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub content: Option<Value>,
167}
168
169impl ElicitResult {
170    /// Create an accepted result with content.
171    pub fn accepted(content: Value) -> Self {
172        Self {
173            action: ElicitAction::Accepted,
174            content: Some(content),
175        }
176    }
177
178    /// Create a declined result.
179    pub fn declined() -> Self {
180        Self {
181            action: ElicitAction::Declined,
182            content: None,
183        }
184    }
185
186    /// Create a cancelled result.
187    pub fn cancelled() -> Self {
188        Self {
189            action: ElicitAction::Cancelled,
190            content: None,
191        }
192    }
193
194    /// Check if the user accepted.
195    pub fn is_accepted(&self) -> bool {
196        matches!(self.action, ElicitAction::Accepted)
197    }
198
199    /// Check if the user declined.
200    pub fn is_declined(&self) -> bool {
201        matches!(self.action, ElicitAction::Declined)
202    }
203
204    /// Check if the request was cancelled.
205    pub fn is_cancelled(&self) -> bool {
206        matches!(self.action, ElicitAction::Cancelled)
207    }
208
209    /// Get the content as a specific type.
210    pub fn content_as<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
211        self.content
212            .as_ref()
213            .and_then(|v| serde_json::from_value(v.clone()).ok())
214    }
215
216    /// Get content as bool (for confirmation requests).
217    pub fn as_bool(&self) -> Option<bool> {
218        self.content_as()
219    }
220
221    /// Get content as string.
222    pub fn as_string(&self) -> Option<String> {
223        self.content_as()
224    }
225}
226
227/// The action taken by the user in response to an elicitation.
228#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
229#[serde(rename_all = "lowercase")]
230pub enum ElicitAction {
231    /// User provided the requested information.
232    Accepted,
233    /// User explicitly declined to provide information.
234    Declined,
235    /// Request was cancelled (e.g., timeout, user closed dialog).
236    Cancelled,
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    #[test]
244    fn test_confirm_request() {
245        let req = ElicitRequest::confirm("Delete all files?");
246        assert_eq!(req.message, "Delete all files?");
247        assert!(req.requested_schema.is_some());
248    }
249
250    #[test]
251    fn test_choice_request() {
252        let req = ElicitRequest::choice(
253            "Select format",
254            vec!["json".into(), "yaml".into(), "toml".into()],
255        );
256        assert_eq!(req.message, "Select format");
257    }
258
259    #[test]
260    fn test_result_accepted() {
261        let result = ElicitResult::accepted(serde_json::json!(true));
262        assert!(result.is_accepted());
263        assert_eq!(result.as_bool(), Some(true));
264    }
265
266    #[test]
267    fn test_result_declined() {
268        let result = ElicitResult::declined();
269        assert!(result.is_declined());
270        assert!(result.content.is_none());
271    }
272}