mcp_probe_core/messages/
prompts.rs

1//! Prompt-related message types for MCP prompt templates and completion.
2//!
3//! This module provides types for:
4//! - Prompt discovery (listing available prompts)
5//! - Prompt templates with parameter substitution
6//! - Prompt generation with arguments
7//! - Prompt content handling
8
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use std::collections::HashMap;
12
13/// Request to list available prompts from the server.
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub struct ListPromptsRequest {
16    /// Optional cursor for pagination
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub cursor: Option<String>,
19}
20
21/// Response containing the list of available prompts.
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct ListPromptsResponse {
24    /// List of available prompts
25    pub prompts: Vec<Prompt>,
26
27    /// Optional cursor for next page of results
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub next_cursor: Option<String>,
30}
31
32/// Prompt definition including schema and metadata.
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct Prompt {
35    /// Unique name of the prompt
36    pub name: String,
37
38    /// Human-readable description of the prompt
39    pub description: String,
40
41    /// JSON Schema for the prompt's arguments
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub arguments: Option<Value>,
44}
45
46impl Prompt {
47    /// Create a new prompt definition.
48    pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
49        Self {
50            name: name.into(),
51            description: description.into(),
52            arguments: None,
53        }
54    }
55
56    /// Set the arguments schema for this prompt.
57    pub fn with_arguments(mut self, arguments: Value) -> Self {
58        self.arguments = Some(arguments);
59        self
60    }
61}
62
63/// Request to get a prompt with specific arguments.
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65pub struct GetPromptRequest {
66    /// Name of the prompt to get
67    pub name: String,
68
69    /// Arguments to substitute in the prompt
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub arguments: Option<Value>,
72}
73
74/// Response containing the generated prompt content.
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
76pub struct GetPromptResponse {
77    /// Description of the prompt
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub description: Option<String>,
80
81    /// Generated messages for the prompt
82    #[serde(default)]
83    pub messages: Vec<PromptMessage>,
84}
85
86/// A message in a prompt template.
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88pub struct PromptMessage {
89    /// Role of the message (system, user, assistant)
90    pub role: MessageRole,
91
92    /// Content of the message
93    pub content: PromptContent,
94}
95
96impl PromptMessage {
97    /// Create a new prompt message.
98    pub fn new(role: MessageRole, content: PromptContent) -> Self {
99        Self { role, content }
100    }
101
102    /// Create a system message.
103    pub fn system(content: impl Into<String>) -> Self {
104        Self::new(MessageRole::System, PromptContent::text(content))
105    }
106
107    /// Create a user message.
108    pub fn user(content: impl Into<String>) -> Self {
109        Self::new(MessageRole::User, PromptContent::text(content))
110    }
111
112    /// Create an assistant message.
113    pub fn assistant(content: impl Into<String>) -> Self {
114        Self::new(MessageRole::Assistant, PromptContent::text(content))
115    }
116}
117
118/// Role of a message in a conversation.
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
120#[serde(rename_all = "lowercase")]
121pub enum MessageRole {
122    /// System message (instructions)
123    System,
124    /// User message (human input)
125    User,
126    /// Assistant message (AI response)
127    Assistant,
128}
129
130/// Content of a prompt message.
131#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
132#[serde(tag = "type")]
133pub enum PromptContent {
134    /// Text content
135    #[serde(rename = "text")]
136    Text {
137        /// The text content
138        text: String,
139    },
140
141    /// Image content
142    #[serde(rename = "image")]
143    Image {
144        /// Image data (base64 encoded or URL)
145        data: String,
146
147        /// MIME type of the image
148        #[serde(rename = "mimeType")]
149        mime_type: String,
150    },
151
152    /// Resource reference
153    #[serde(rename = "resource")]
154    Resource {
155        /// Resource reference
156        resource: ResourceReference,
157    },
158}
159
160impl PromptContent {
161    /// Create text content.
162    pub fn text(text: impl Into<String>) -> Self {
163        Self::Text { text: text.into() }
164    }
165
166    /// Create image content.
167    pub fn image(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
168        Self::Image {
169            data: data.into(),
170            mime_type: mime_type.into(),
171        }
172    }
173
174    /// Create resource content.
175    pub fn resource(uri: impl Into<String>) -> Self {
176        Self::Resource {
177            resource: ResourceReference {
178                uri: uri.into(),
179                text: None,
180            },
181        }
182    }
183
184    /// Create resource content with description.
185    pub fn resource_with_text(uri: impl Into<String>, text: impl Into<String>) -> Self {
186        Self::Resource {
187            resource: ResourceReference {
188                uri: uri.into(),
189                text: Some(text.into()),
190            },
191        }
192    }
193}
194
195/// Reference to a resource within prompt content.
196#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
197pub struct ResourceReference {
198    /// URI of the resource
199    pub uri: String,
200
201    /// Optional description of the resource
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub text: Option<String>,
204}
205
206/// Notification that the list of prompts has changed.
207#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
208pub struct PromptListChangedNotification {
209    /// Additional metadata about the change
210    #[serde(flatten)]
211    pub metadata: HashMap<String, Value>,
212}
213
214impl PromptListChangedNotification {
215    /// Create a new prompt list changed notification.
216    pub fn new() -> Self {
217        Self::default()
218    }
219
220    /// Add metadata to the notification.
221    pub fn with_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
222        self.metadata.insert(key.into(), value);
223        self
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230    use serde_json::json;
231
232    #[test]
233    fn test_prompt_creation() {
234        let prompt =
235            Prompt::new("code_review", "Review code for best practices").with_arguments(json!({
236                "type": "object",
237                "properties": {
238                    "language": {"type": "string"},
239                    "code": {"type": "string"}
240                },
241                "required": ["code"]
242            }));
243
244        assert_eq!(prompt.name, "code_review");
245        assert_eq!(prompt.description, "Review code for best practices");
246        assert!(prompt.arguments.is_some());
247    }
248
249    #[test]
250    fn test_list_prompts_request() {
251        let request = ListPromptsRequest { cursor: None };
252        let json = serde_json::to_string(&request).unwrap();
253        let deserialized: ListPromptsRequest = serde_json::from_str(&json).unwrap();
254        assert_eq!(request, deserialized);
255    }
256
257    #[test]
258    fn test_get_prompt_request() {
259        let request = GetPromptRequest {
260            name: "code_review".to_string(),
261            arguments: Some(json!({"language": "rust", "code": "fn main() {}"})),
262        };
263
264        let json = serde_json::to_string(&request).unwrap();
265        let deserialized: GetPromptRequest = serde_json::from_str(&json).unwrap();
266        assert_eq!(request, deserialized);
267    }
268
269    #[test]
270    fn test_prompt_message_creation() {
271        let system_msg = PromptMessage::system("You are a helpful assistant");
272        let user_msg = PromptMessage::user("Hello, how are you?");
273        let assistant_msg = PromptMessage::assistant("I'm doing well, thank you!");
274
275        assert_eq!(system_msg.role, MessageRole::System);
276        assert_eq!(user_msg.role, MessageRole::User);
277        assert_eq!(assistant_msg.role, MessageRole::Assistant);
278    }
279
280    #[test]
281    fn test_prompt_content_text() {
282        let content = PromptContent::text("Hello world");
283        let json = serde_json::to_value(&content).unwrap();
284        assert_eq!(json["type"], "text");
285        assert_eq!(json["text"], "Hello world");
286    }
287
288    #[test]
289    fn test_prompt_content_image() {
290        let content = PromptContent::image("base64data", "image/png");
291        let json = serde_json::to_value(&content).unwrap();
292        assert_eq!(json["type"], "image");
293        assert_eq!(json["data"], "base64data");
294        assert_eq!(json["mimeType"], "image/png");
295    }
296
297    #[test]
298    fn test_prompt_content_resource() {
299        let content = PromptContent::resource_with_text("file:///test.txt", "A test file");
300        let json = serde_json::to_value(&content).unwrap();
301        assert_eq!(json["type"], "resource");
302        assert_eq!(json["resource"]["uri"], "file:///test.txt");
303        assert_eq!(json["resource"]["text"], "A test file");
304    }
305
306    #[test]
307    fn test_message_role_serialization() {
308        let system_role = MessageRole::System;
309        let json = serde_json::to_string(&system_role).unwrap();
310        assert_eq!(json, "\"system\"");
311
312        let user_role = MessageRole::User;
313        let json = serde_json::to_string(&user_role).unwrap();
314        assert_eq!(json, "\"user\"");
315
316        let assistant_role = MessageRole::Assistant;
317        let json = serde_json::to_string(&assistant_role).unwrap();
318        assert_eq!(json, "\"assistant\"");
319    }
320}