turul_mcp_protocol_2025_06_18/
prompts.rs

1//! MCP Prompts Protocol Types
2//!
3//! This module defines the types used for the MCP prompts functionality.
4
5use crate::meta::Cursor;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use std::collections::HashMap;
9
10
11/// Prompt annotations structure (matches TypeScript PromptAnnotations)
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct PromptAnnotations {
14    /// Display name (precedence: Prompt.title > Prompt.name)
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub title: Option<String>,
17    // Additional annotation fields can be added here as needed
18}
19
20impl Default for PromptAnnotations {
21    fn default() -> Self {
22        Self::new()
23    }
24}
25
26impl PromptAnnotations {
27    pub fn new() -> Self {
28        Self { title: None }
29    }
30
31    pub fn with_title(mut self, title: impl Into<String>) -> Self {
32        self.title = Some(title.into());
33        self
34    }
35}
36
37/// A prompt descriptor (matches TypeScript Prompt interface exactly)
38#[derive(Debug, Clone, Serialize, Deserialize)]
39#[serde(rename_all = "camelCase")]
40pub struct Prompt {
41    /// Programmatic identifier (from BaseMetadata)
42    pub name: String,
43    /// Human-readable display name (from BaseMetadata)
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub title: Option<String>,
46    /// Optional human-readable description
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub description: Option<String>,
49    /// Arguments that the prompt accepts
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub arguments: Option<Vec<PromptArgument>>,
52    /// Optional MCP meta information
53    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
54    pub meta: Option<HashMap<String, Value>>,
55}
56
57impl Prompt {
58    pub fn new(name: impl Into<String>) -> Self {
59        Self {
60            name: name.into(),
61            title: None,
62            description: None,
63            arguments: None,
64            meta: None,
65        }
66    }
67
68    pub fn with_title(mut self, title: impl Into<String>) -> Self {
69        self.title = Some(title.into());
70        self
71    }
72
73    pub fn with_description(mut self, description: impl Into<String>) -> Self {
74        self.description = Some(description.into());
75        self
76    }
77
78    pub fn with_arguments(mut self, arguments: Vec<PromptArgument>) -> Self {
79        self.arguments = Some(arguments);
80        self
81    }
82
83    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
84        self.meta = Some(meta);
85        self
86    }
87}
88
89/// The sender or recipient of messages and data in a conversation (matches MCP spec)
90#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
91#[serde(rename_all = "lowercase")]
92pub enum Role {
93    #[serde(rename = "user")]
94    User,
95    #[serde(rename = "assistant")]
96    Assistant,
97}
98
99/// Argument definition for prompts (extends BaseMetadata per MCP spec)
100#[derive(Debug, Clone, Serialize, Deserialize)]
101#[serde(rename_all = "camelCase")]
102pub struct PromptArgument {
103    /// Name of the argument (from BaseMetadata)
104    pub name: String,
105    /// Human-readable display name (from BaseMetadata)
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub title: Option<String>,
108    /// Human-readable description of the argument
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub description: Option<String>,
111    /// Whether the argument is required
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub required: Option<bool>,
114}
115
116impl PromptArgument {
117    pub fn new(name: impl Into<String>) -> Self {
118        Self {
119            name: name.into(),
120            title: None,
121            description: None,
122            required: None,
123        }
124    }
125
126    pub fn with_title(mut self, title: impl Into<String>) -> Self {
127        self.title = Some(title.into());
128        self
129    }
130
131    pub fn with_description(mut self, description: impl Into<String>) -> Self {
132        self.description = Some(description.into());
133        self
134    }
135
136    pub fn required(mut self) -> Self {
137        self.required = Some(true);
138        self
139    }
140
141    pub fn optional(mut self) -> Self {
142        self.required = Some(false);
143        self
144    }
145}
146
147/// Parameters for prompts/list request
148#[derive(Debug, Clone, Serialize, Deserialize)]
149#[serde(rename_all = "camelCase")]
150pub struct ListPromptsParams {
151    /// Optional cursor for pagination
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub cursor: Option<Cursor>,
154    /// Meta information (optional _meta field inside params)
155    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
156    pub meta: Option<HashMap<String, Value>>,
157}
158
159impl Default for ListPromptsParams {
160    fn default() -> Self {
161        Self::new()
162    }
163}
164
165impl ListPromptsParams {
166    pub fn new() -> Self {
167        Self {
168            cursor: None,
169            meta: None,
170        }
171    }
172
173    pub fn with_cursor(mut self, cursor: Cursor) -> Self {
174        self.cursor = Some(cursor);
175        self
176    }
177
178    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
179        self.meta = Some(meta);
180        self
181    }
182}
183
184/// Complete prompts/list request (matches TypeScript ListPromptsRequest interface)
185#[derive(Debug, Clone, Serialize, Deserialize)]
186#[serde(rename_all = "camelCase")]
187pub struct ListPromptsRequest {
188    /// Method name (always "prompts/list")
189    pub method: String,
190    /// Request parameters
191    pub params: ListPromptsParams,
192}
193
194impl Default for ListPromptsRequest {
195    fn default() -> Self {
196        Self::new()
197    }
198}
199
200impl ListPromptsRequest {
201    pub fn new() -> Self {
202        Self {
203            method: "prompts/list".to_string(),
204            params: ListPromptsParams::new(),
205        }
206    }
207
208    pub fn with_cursor(mut self, cursor: Cursor) -> Self {
209        self.params = self.params.with_cursor(cursor);
210        self
211    }
212
213    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
214        self.params = self.params.with_meta(meta);
215        self
216    }
217}
218
219/// Result for prompts/list (per MCP spec)
220#[derive(Debug, Clone, Serialize, Deserialize)]
221#[serde(rename_all = "camelCase")]
222pub struct ListPromptsResult {
223    /// Available prompts
224    pub prompts: Vec<Prompt>,
225    /// Optional cursor for next page
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub next_cursor: Option<Cursor>,
228    /// Meta information (follows MCP Result interface)
229    #[serde(
230        default,
231        skip_serializing_if = "Option::is_none",
232        alias = "_meta",
233        rename = "_meta"
234    )]
235    pub meta: Option<HashMap<String, Value>>,
236}
237
238impl ListPromptsResult {
239    pub fn new(prompts: Vec<Prompt>) -> Self {
240        Self {
241            prompts,
242            next_cursor: None,
243            meta: None,
244        }
245    }
246
247    pub fn with_next_cursor(mut self, cursor: Cursor) -> Self {
248        self.next_cursor = Some(cursor);
249        self
250    }
251
252    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
253        self.meta = Some(meta);
254        self
255    }
256}
257
258/// Parameters for prompts/get request (matches MCP GetPromptRequest.params exactly)
259#[derive(Debug, Clone, Serialize, Deserialize)]
260#[serde(rename_all = "camelCase")]
261pub struct GetPromptParams {
262    /// Name of the prompt to get
263    pub name: String,
264    /// Arguments to pass to the prompt (MCP spec: { [key: string]: string })
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub arguments: Option<HashMap<String, String>>,
267    /// Meta information (optional _meta field inside params)
268    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
269    pub meta: Option<HashMap<String, Value>>,
270}
271
272impl GetPromptParams {
273    pub fn new(name: impl Into<String>) -> Self {
274        Self {
275            name: name.into(),
276            arguments: None,
277            meta: None,
278        }
279    }
280
281    pub fn with_arguments(mut self, arguments: HashMap<String, String>) -> Self {
282        self.arguments = Some(arguments);
283        self
284    }
285
286    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
287        self.meta = Some(meta);
288        self
289    }
290}
291
292/// Complete prompts/get request (matches TypeScript GetPromptRequest interface)
293#[derive(Debug, Clone, Serialize, Deserialize)]
294#[serde(rename_all = "camelCase")]
295pub struct GetPromptRequest {
296    /// Method name (always "prompts/get")
297    pub method: String,
298    /// Request parameters
299    pub params: GetPromptParams,
300}
301
302impl GetPromptRequest {
303    pub fn new(name: impl Into<String>) -> Self {
304        Self {
305            method: "prompts/get".to_string(),
306            params: GetPromptParams::new(name),
307        }
308    }
309
310    pub fn with_arguments(mut self, arguments: HashMap<String, String>) -> Self {
311        self.params = self.params.with_arguments(arguments);
312        self
313    }
314
315    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
316        self.params = self.params.with_meta(meta);
317        self
318    }
319}
320
321/// Message content for prompts (matches MCP PromptMessage interface exactly)
322#[derive(Debug, Clone, Serialize, Deserialize)]
323#[serde(rename_all = "camelCase")]
324pub struct PromptMessage {
325    /// The role of the message sender
326    pub role: Role,
327    /// The content of the message (ContentBlock from MCP spec)
328    pub content: ContentBlock,
329}
330
331/// Content block within a prompt message - now imports from content module
332pub use crate::content::{ContentBlock, ResourceContents, ResourceReference};
333
334impl PromptMessage {
335    pub fn user_text(content: impl Into<String>) -> Self {
336        Self {
337            role: Role::User,
338            content: ContentBlock::text(content),
339        }
340    }
341
342    pub fn assistant_text(content: impl Into<String>) -> Self {
343        Self {
344            role: Role::Assistant,
345            content: ContentBlock::text(content),
346        }
347    }
348
349    pub fn user_image(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
350        Self {
351            role: Role::User,
352            content: ContentBlock::image(data, mime_type),
353        }
354    }
355
356    pub fn text(content: impl Into<String>) -> Self {
357        // Backward compatibility - defaults to user
358        Self::user_text(content)
359    }
360}
361
362/// Result for prompts/get (per MCP spec)
363#[derive(Debug, Clone, Serialize, Deserialize)]
364#[serde(rename_all = "camelCase")]
365pub struct GetPromptResult {
366    /// Optional description of the prompt
367    #[serde(skip_serializing_if = "Option::is_none")]
368    pub description: Option<String>,
369    /// Messages that make up the prompt
370    pub messages: Vec<PromptMessage>,
371    /// Meta information (follows MCP Result interface)
372    #[serde(
373        default,
374        skip_serializing_if = "Option::is_none",
375        alias = "_meta",
376        rename = "_meta"
377    )]
378    pub meta: Option<HashMap<String, Value>>,
379}
380
381impl GetPromptResult {
382    pub fn new(messages: Vec<PromptMessage>) -> Self {
383        Self {
384            description: None,
385            messages,
386            meta: None,
387        }
388    }
389
390    pub fn with_description(mut self, description: impl Into<String>) -> Self {
391        self.description = Some(description.into());
392        self
393    }
394
395    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
396        self.meta = Some(meta);
397        self
398    }
399}
400
401// Trait implementations for prompts
402
403use crate::traits::*;
404
405// Trait implementations for ListPromptsParams
406impl Params for ListPromptsParams {}
407
408impl HasListPromptsParams for ListPromptsParams {
409    fn cursor(&self) -> Option<&Cursor> {
410        self.cursor.as_ref()
411    }
412}
413
414impl HasMetaParam for ListPromptsParams {
415    fn meta(&self) -> Option<&HashMap<String, Value>> {
416        self.meta.as_ref()
417    }
418}
419
420// Trait implementations for ListPromptsRequest
421impl HasMethod for ListPromptsRequest {
422    fn method(&self) -> &str {
423        &self.method
424    }
425}
426
427impl HasParams for ListPromptsRequest {
428    fn params(&self) -> Option<&dyn Params> {
429        Some(&self.params)
430    }
431}
432
433// Trait implementations for ListPromptsResult
434impl HasData for ListPromptsResult {
435    fn data(&self) -> HashMap<String, Value> {
436        let mut data = HashMap::new();
437        data.insert(
438            "prompts".to_string(),
439            serde_json::to_value(&self.prompts).unwrap_or(Value::Null),
440        );
441        if let Some(ref next_cursor) = self.next_cursor {
442            data.insert(
443                "nextCursor".to_string(),
444                Value::String(next_cursor.as_str().to_string()),
445            );
446        }
447        data
448    }
449}
450
451impl HasMeta for ListPromptsResult {
452    fn meta(&self) -> Option<HashMap<String, Value>> {
453        self.meta.clone()
454    }
455}
456
457impl RpcResult for ListPromptsResult {}
458
459impl crate::traits::ListPromptsResult for ListPromptsResult {
460    fn prompts(&self) -> &Vec<Prompt> {
461        &self.prompts
462    }
463
464    fn next_cursor(&self) -> Option<&Cursor> {
465        self.next_cursor.as_ref()
466    }
467}
468
469// Trait implementations for GetPromptParams
470impl Params for GetPromptParams {}
471
472impl HasGetPromptParams for GetPromptParams {
473    fn name(&self) -> &String {
474        &self.name
475    }
476
477    fn arguments(&self) -> Option<&HashMap<String, String>> {
478        self.arguments.as_ref()
479    }
480}
481
482impl HasMetaParam for GetPromptParams {
483    fn meta(&self) -> Option<&HashMap<String, Value>> {
484        self.meta.as_ref()
485    }
486}
487
488// Trait implementations for GetPromptRequest
489impl HasMethod for GetPromptRequest {
490    fn method(&self) -> &str {
491        &self.method
492    }
493}
494
495impl HasParams for GetPromptRequest {
496    fn params(&self) -> Option<&dyn Params> {
497        Some(&self.params)
498    }
499}
500
501// Trait implementations for GetPromptResult
502impl HasData for GetPromptResult {
503    fn data(&self) -> HashMap<String, Value> {
504        let mut data = HashMap::new();
505        data.insert(
506            "messages".to_string(),
507            serde_json::to_value(&self.messages).unwrap_or(Value::Null),
508        );
509        if let Some(ref description) = self.description {
510            data.insert(
511                "description".to_string(),
512                Value::String(description.clone()),
513            );
514        }
515        data
516    }
517}
518
519impl HasMeta for GetPromptResult {
520    fn meta(&self) -> Option<HashMap<String, Value>> {
521        self.meta.clone()
522    }
523}
524
525impl RpcResult for GetPromptResult {}
526
527impl crate::traits::GetPromptResult for GetPromptResult {
528    fn description(&self) -> Option<&String> {
529        self.description.as_ref()
530    }
531
532    fn messages(&self) -> &Vec<PromptMessage> {
533        &self.messages
534    }
535}
536
537#[cfg(test)]
538mod tests {
539    use super::*;
540
541    #[test]
542    fn test_prompt_creation() {
543        let arg = PromptArgument::new("topic")
544            .with_description("The topic to write about")
545            .required();
546
547        let prompt = Prompt::new("write_essay")
548            .with_description("Write an essay about a topic")
549            .with_arguments(vec![arg]);
550
551        assert_eq!(prompt.name, "write_essay");
552        assert!(prompt.description.is_some());
553        assert!(prompt.arguments.is_some());
554    }
555
556    #[test]
557    fn test_prompt_message() {
558        let text_msg = PromptMessage::text("Hello, world!");
559        let user_image_msg = PromptMessage::user_image("base64data", "image/png");
560        let assistant_text_msg = PromptMessage::assistant_text("Response text");
561
562        // Verify structure matches MCP spec: role + content
563        assert_eq!(text_msg.role, Role::User);
564        assert!(matches!(text_msg.content, ContentBlock::Text { .. }));
565
566        assert_eq!(user_image_msg.role, Role::User);
567        assert!(matches!(user_image_msg.content, ContentBlock::Image { .. }));
568
569        assert_eq!(assistant_text_msg.role, Role::Assistant);
570        assert!(matches!(
571            assistant_text_msg.content,
572            ContentBlock::Text { .. }
573        ));
574    }
575
576    #[test]
577    fn test_get_prompt_request() {
578        let mut args = HashMap::new();
579        args.insert("topic".to_string(), "AI Safety".to_string()); // Now uses String instead of Value
580
581        let request = GetPromptRequest::new("write_essay").with_arguments(args);
582
583        assert_eq!(request.params.name, "write_essay");
584        assert!(request.params.arguments.is_some());
585
586        // Verify arguments are string-to-string mapping per MCP spec
587        if let Some(ref arguments) = request.params.arguments {
588            assert_eq!(arguments.get("topic"), Some(&"AI Safety".to_string()));
589        }
590    }
591
592    #[test]
593    fn test_get_prompt_response() {
594        let messages = vec![
595            PromptMessage::user_text("Write an essay about: "),
596            PromptMessage::assistant_text("AI Safety"),
597        ];
598
599        let response = GetPromptResult::new(messages).with_description("Generated essay prompt");
600
601        assert_eq!(response.messages.len(), 2);
602        assert!(response.description.is_some());
603
604        // Verify messages have proper role structure per MCP spec
605        assert_eq!(response.messages[0].role, Role::User);
606        assert_eq!(response.messages[1].role, Role::Assistant);
607    }
608
609    #[test]
610    fn test_serialization() {
611        let prompt = Prompt::new("test_prompt").with_description("A test prompt");
612
613        let json = serde_json::to_string(&prompt).unwrap();
614        assert!(json.contains("test_prompt"));
615        assert!(json.contains("A test prompt"));
616
617        let parsed: Prompt = serde_json::from_str(&json).unwrap();
618        assert_eq!(parsed.name, "test_prompt");
619    }
620}