Skip to main content

astrid_core/
elicitation.rs

1//! Elicitation types for MCP server-initiated user input requests.
2//!
3//! These types implement the elicitation protocol where an MCP server
4//! can request structured input from the user (text, secrets, selections,
5//! confirmations) or redirect them to an external URL flow (OAuth, payments).
6
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use uuid::Uuid;
10
11/// MCP elicitation request - server asking for user input.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ElicitationRequest {
14    /// Unique request ID
15    pub request_id: Uuid,
16    /// Server that is requesting the elicitation
17    pub server_name: String,
18    /// Schema describing what input is needed
19    pub schema: ElicitationSchema,
20    /// Human-readable message
21    pub message: String,
22    /// Whether this is required or optional
23    pub required: bool,
24}
25
26impl ElicitationRequest {
27    /// Create a new elicitation request.
28    #[must_use]
29    pub fn new(server_name: impl Into<String>, message: impl Into<String>) -> Self {
30        Self {
31            request_id: Uuid::new_v4(),
32            server_name: server_name.into(),
33            schema: ElicitationSchema::Text {
34                placeholder: None,
35                max_length: None,
36            },
37            message: message.into(),
38            required: true,
39        }
40    }
41
42    /// Set the schema.
43    #[must_use]
44    pub fn with_schema(mut self, schema: ElicitationSchema) -> Self {
45        self.schema = schema;
46        self
47    }
48
49    /// Set as optional.
50    #[must_use]
51    pub fn optional(mut self) -> Self {
52        self.required = false;
53        self
54    }
55}
56
57/// Schema for elicitation input.
58#[derive(Debug, Clone, Serialize, Deserialize)]
59#[serde(rename_all = "snake_case")]
60pub enum ElicitationSchema {
61    /// Free-form text input
62    Text {
63        /// Placeholder text
64        placeholder: Option<String>,
65        /// Maximum length
66        max_length: Option<usize>,
67    },
68    /// Password/secret input (masked)
69    Secret {
70        /// Placeholder text
71        placeholder: Option<String>,
72    },
73    /// Selection from options
74    Select {
75        /// Available options
76        options: Vec<SelectOption>,
77        /// Allow multiple selection
78        multiple: bool,
79    },
80    /// Boolean choice
81    Confirm {
82        /// Default value
83        default: bool,
84    },
85}
86
87/// Option for select schema.
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct SelectOption {
90    /// Value to submit
91    pub value: String,
92    /// Display label
93    pub label: String,
94    /// Description
95    pub description: Option<String>,
96}
97
98impl SelectOption {
99    /// Create a new select option.
100    #[must_use]
101    pub fn new(value: impl Into<String>, label: impl Into<String>) -> Self {
102        Self {
103            value: value.into(),
104            label: label.into(),
105            description: None,
106        }
107    }
108
109    /// Add a description.
110    #[must_use]
111    pub fn with_description(mut self, description: impl Into<String>) -> Self {
112        self.description = Some(description.into());
113        self
114    }
115}
116
117/// Response to an elicitation request.
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct ElicitationResponse {
120    /// Request ID this responds to
121    pub request_id: Uuid,
122    /// The action taken
123    pub action: ElicitationAction,
124}
125
126impl ElicitationResponse {
127    /// Create a submit response.
128    #[must_use]
129    pub fn submit(request_id: Uuid, value: serde_json::Value) -> Self {
130        Self {
131            request_id,
132            action: ElicitationAction::Submit { value },
133        }
134    }
135
136    /// Create a cancel response.
137    #[must_use]
138    pub fn cancel(request_id: Uuid) -> Self {
139        Self {
140            request_id,
141            action: ElicitationAction::Cancel,
142        }
143    }
144
145    /// Create a dismiss response.
146    #[must_use]
147    pub fn dismiss(request_id: Uuid) -> Self {
148        Self {
149            request_id,
150            action: ElicitationAction::Dismiss,
151        }
152    }
153}
154
155/// Action taken in response to elicitation.
156#[derive(Debug, Clone, Serialize, Deserialize)]
157#[serde(rename_all = "snake_case")]
158pub enum ElicitationAction {
159    /// User submitted a value
160    Submit {
161        /// The submitted value
162        value: serde_json::Value,
163    },
164    /// User cancelled
165    Cancel,
166    /// User dismissed (optional elicitation)
167    Dismiss,
168}
169
170/// URL-mode elicitation for OAuth, payments, etc.
171#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct UrlElicitationRequest {
173    /// Unique request ID
174    pub request_id: Uuid,
175    /// Server that is requesting
176    pub server_name: String,
177    /// URL to present to the user
178    pub url: String,
179    /// Human-readable message
180    pub message: String,
181    /// Type of URL elicitation
182    pub elicitation_type: UrlElicitationType,
183}
184
185impl UrlElicitationRequest {
186    /// Create a new URL elicitation request.
187    #[must_use]
188    pub fn new(
189        server_name: impl Into<String>,
190        url: impl Into<String>,
191        message: impl Into<String>,
192    ) -> Self {
193        Self {
194            request_id: Uuid::new_v4(),
195            server_name: server_name.into(),
196            url: url.into(),
197            message: message.into(),
198            elicitation_type: UrlElicitationType::OAuth,
199        }
200    }
201
202    /// Set the elicitation type.
203    #[must_use]
204    pub fn with_type(mut self, elicitation_type: UrlElicitationType) -> Self {
205        self.elicitation_type = elicitation_type;
206        self
207    }
208}
209
210/// Type of URL elicitation.
211#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
212#[serde(rename_all = "snake_case")]
213pub enum UrlElicitationType {
214    /// OAuth authentication flow
215    OAuth,
216    /// Payment flow
217    Payment,
218    /// Credential collection
219    Credentials,
220    /// Generic external action
221    External,
222}
223
224/// Response to a URL elicitation flow.
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct UrlElicitationResponse {
227    /// Request ID this responds to.
228    pub request_id: Uuid,
229    /// Whether the user completed the flow.
230    pub completed: bool,
231    /// Callback data from the flow (e.g., OAuth authorization code).
232    pub callback_data: Option<HashMap<String, String>>,
233    /// Error if the flow failed.
234    pub error: Option<String>,
235}
236
237impl UrlElicitationResponse {
238    /// Create a successful response (user completed the flow).
239    #[must_use]
240    pub fn completed(request_id: Uuid) -> Self {
241        Self {
242            request_id,
243            completed: true,
244            callback_data: None,
245            error: None,
246        }
247    }
248
249    /// Create a response indicating the user did not complete the flow.
250    #[must_use]
251    pub fn not_completed(request_id: Uuid) -> Self {
252        Self {
253            request_id,
254            completed: false,
255            callback_data: None,
256            error: None,
257        }
258    }
259
260    /// Attach callback data (e.g., OAuth code).
261    #[must_use]
262    pub fn with_callback_data(mut self, data: HashMap<String, String>) -> Self {
263        self.callback_data = Some(data);
264        self
265    }
266}