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 std::collections::HashMap;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use crate::meta::Cursor;
9
10// ===========================================
11// === Prompt Definition Trait Hierarchy ===
12// ===========================================
13
14/// Base metadata trait - matches TypeScript BaseMetadata interface
15pub trait HasPromptMetadata {
16    /// Programmatic identifier (fallback display name)
17    fn name(&self) -> &str;
18    
19    /// Human-readable display name (UI contexts)
20    fn title(&self) -> Option<&str> { None }
21}
22
23/// Prompt description trait
24pub trait HasPromptDescription {
25    fn description(&self) -> Option<&str> { None }
26}
27
28/// Prompt arguments trait
29pub trait HasPromptArguments {
30    fn arguments(&self) -> Option<&Vec<PromptArgument>> { None }
31}
32
33/// Prompt annotations trait
34pub trait HasPromptAnnotations {
35    fn annotations(&self) -> Option<&PromptAnnotations> { None }
36}
37
38/// Prompt-specific meta trait (separate from RPC _meta)
39pub trait HasPromptMeta {
40    fn prompt_meta(&self) -> Option<&HashMap<String, Value>> { None }
41}
42
43/// Complete prompt definition - composed from fine-grained traits
44pub trait PromptDefinition: 
45    HasPromptMetadata +       // name, title (from BaseMetadata)
46    HasPromptDescription +    // description
47    HasPromptArguments +      // arguments
48    HasPromptAnnotations +    // annotations
49    HasPromptMeta +           // _meta (prompt-specific)
50    Send + 
51    Sync 
52{
53    /// Display name precedence: title > name (matches TypeScript spec)
54    fn display_name(&self) -> &str {
55        self.title().unwrap_or_else(|| self.name())
56    }
57    
58    /// Convert to concrete Prompt struct for protocol serialization
59    fn to_prompt(&self) -> Prompt {
60        Prompt {
61            name: self.name().to_string(),
62            title: self.title().map(String::from),
63            description: self.description().map(String::from),
64            arguments: self.arguments().cloned(),
65            meta: self.prompt_meta().cloned(),
66        }
67    }
68}
69
70/// Prompt annotations structure (matches TypeScript PromptAnnotations)
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct PromptAnnotations {
73    /// Display name (precedence: Prompt.title > Prompt.name)
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub title: Option<String>,
76    // Additional annotation fields can be added here as needed
77}
78
79impl PromptAnnotations {
80    pub fn new() -> Self {
81        Self {
82            title: None,
83        }
84    }
85    
86    pub fn with_title(mut self, title: impl Into<String>) -> Self {
87        self.title = Some(title.into());
88        self
89    }
90}
91
92/// A prompt descriptor (matches TypeScript Prompt interface exactly)
93#[derive(Debug, Clone, Serialize, Deserialize)]
94#[serde(rename_all = "camelCase")]
95pub struct Prompt {
96    /// Programmatic identifier (from BaseMetadata)
97    pub name: String,
98    /// Human-readable display name (from BaseMetadata)
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub title: Option<String>,
101    /// Optional human-readable description
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub description: Option<String>,
104    /// Arguments that the prompt accepts
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub arguments: Option<Vec<PromptArgument>>,
107    /// Optional MCP meta information
108    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
109    pub meta: Option<HashMap<String, Value>>,
110}
111
112impl Prompt {
113    pub fn new(name: impl Into<String>) -> Self {
114        Self {
115            name: name.into(),
116            title: None,
117            description: None,
118            arguments: None,
119            meta: None,
120        }
121    }
122
123    pub fn with_title(mut self, title: impl Into<String>) -> Self {
124        self.title = Some(title.into());
125        self
126    }
127
128    pub fn with_description(mut self, description: impl Into<String>) -> Self {
129        self.description = Some(description.into());
130        self
131    }
132
133    pub fn with_arguments(mut self, arguments: Vec<PromptArgument>) -> Self {
134        self.arguments = Some(arguments);
135        self
136    }
137
138    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
139        self.meta = Some(meta);
140        self
141    }
142}
143
144// ================== TRAIT IMPLEMENTATIONS ==================
145// Implement fine-grained traits for the concrete Prompt struct
146
147impl HasPromptMetadata for Prompt {
148    fn name(&self) -> &str { &self.name }
149    fn title(&self) -> Option<&str> { self.title.as_deref() }
150}
151
152impl HasPromptDescription for Prompt {
153    fn description(&self) -> Option<&str> { self.description.as_deref() }
154}
155
156impl HasPromptArguments for Prompt {
157    fn arguments(&self) -> Option<&Vec<PromptArgument>> { self.arguments.as_ref() }
158}
159
160impl HasPromptAnnotations for Prompt {
161    fn annotations(&self) -> Option<&PromptAnnotations> { None } // Prompt doesn't have annotations per MCP spec
162}
163
164impl HasPromptMeta for Prompt {
165    fn prompt_meta(&self) -> Option<&HashMap<String, Value>> { self.meta.as_ref() }
166}
167
168// Blanket implementation: any type implementing all fine-grained traits automatically implements PromptDefinition
169impl<T> PromptDefinition for T 
170where 
171    T: HasPromptMetadata + HasPromptDescription + HasPromptArguments + HasPromptAnnotations + HasPromptMeta + Send + Sync 
172{}
173
174/// The sender or recipient of messages and data in a conversation (matches MCP spec)
175#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
176#[serde(rename_all = "lowercase")]
177pub enum Role {
178    #[serde(rename = "user")]
179    User,
180    #[serde(rename = "assistant")]
181    Assistant,
182}
183
184/// Argument definition for prompts (extends BaseMetadata per MCP spec)
185#[derive(Debug, Clone, Serialize, Deserialize)]
186#[serde(rename_all = "camelCase")]
187pub struct PromptArgument {
188    /// Name of the argument (from BaseMetadata)
189    pub name: String,
190    /// Human-readable display name (from BaseMetadata)
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub title: Option<String>,
193    /// Human-readable description of the argument
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub description: Option<String>,
196    /// Whether the argument is required
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub required: Option<bool>,
199}
200
201impl PromptArgument {
202    pub fn new(name: impl Into<String>) -> Self {
203        Self {
204            name: name.into(),
205            title: None,
206            description: None,
207            required: None,
208        }
209    }
210
211    pub fn with_title(mut self, title: impl Into<String>) -> Self {
212        self.title = Some(title.into());
213        self
214    }
215
216    pub fn with_description(mut self, description: impl Into<String>) -> Self {
217        self.description = Some(description.into());
218        self
219    }
220
221    pub fn required(mut self) -> Self {
222        self.required = Some(true);
223        self
224    }
225
226    pub fn optional(mut self) -> Self {
227        self.required = Some(false);
228        self
229    }
230}
231
232/// Parameters for prompts/list request
233#[derive(Debug, Clone, Serialize, Deserialize)]
234#[serde(rename_all = "camelCase")]
235pub struct ListPromptsParams {
236    /// Optional cursor for pagination
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub cursor: Option<Cursor>,
239    /// Meta information (optional _meta field inside params)
240    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
241    pub meta: Option<HashMap<String, Value>>,
242}
243
244impl ListPromptsParams {
245    pub fn new() -> Self {
246        Self { 
247            cursor: None,
248            meta: None,
249        }
250    }
251
252    pub fn with_cursor(mut self, cursor: Cursor) -> Self {
253        self.cursor = Some(cursor);
254        self
255    }
256
257    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
258        self.meta = Some(meta);
259        self
260    }
261}
262
263impl Default for ListPromptsParams {
264    fn default() -> Self {
265        Self::new()
266    }
267}
268
269/// Complete prompts/list request (matches TypeScript ListPromptsRequest interface)
270#[derive(Debug, Clone, Serialize, Deserialize)]
271#[serde(rename_all = "camelCase")]
272pub struct ListPromptsRequest {
273    /// Method name (always "prompts/list")
274    pub method: String,
275    /// Request parameters
276    pub params: ListPromptsParams,
277}
278
279impl ListPromptsRequest {
280    pub fn new() -> Self {
281        Self {
282            method: "prompts/list".to_string(),
283            params: ListPromptsParams::new(),
284        }
285    }
286
287    pub fn with_cursor(mut self, cursor: Cursor) -> Self {
288        self.params = self.params.with_cursor(cursor);
289        self
290    }
291
292    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
293        self.params = self.params.with_meta(meta);
294        self
295    }
296}
297
298/// Result for prompts/list (per MCP spec)
299#[derive(Debug, Clone, Serialize, Deserialize)]
300#[serde(rename_all = "camelCase")]
301pub struct ListPromptsResult {
302    /// Available prompts
303    pub prompts: Vec<Prompt>,
304    /// Optional cursor for next page
305    #[serde(skip_serializing_if = "Option::is_none")]
306    pub next_cursor: Option<Cursor>,
307    /// Meta information (follows MCP Result interface)
308    #[serde(
309        default,
310        skip_serializing_if = "Option::is_none",
311        alias = "_meta",
312        rename = "_meta"
313    )]
314    pub meta: Option<HashMap<String, Value>>,
315}
316
317impl ListPromptsResult {
318    pub fn new(prompts: Vec<Prompt>) -> Self {
319        Self {
320            prompts,
321            next_cursor: None,
322            meta: None,
323        }
324    }
325
326    pub fn with_next_cursor(mut self, cursor: Cursor) -> Self {
327        self.next_cursor = Some(cursor);
328        self
329    }
330
331    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
332        self.meta = Some(meta);
333        self
334    }
335}
336
337/// Parameters for prompts/get request (matches MCP GetPromptRequest.params exactly)
338#[derive(Debug, Clone, Serialize, Deserialize)]
339#[serde(rename_all = "camelCase")]
340pub struct GetPromptParams {
341    /// Name of the prompt to get
342    pub name: String,
343    /// Arguments to pass to the prompt (MCP spec: { [key: string]: string })
344    #[serde(skip_serializing_if = "Option::is_none")]
345    pub arguments: Option<HashMap<String, String>>,
346    /// Meta information (optional _meta field inside params)
347    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
348    pub meta: Option<HashMap<String, Value>>,
349}
350
351impl GetPromptParams {
352    pub fn new(name: impl Into<String>) -> Self {
353        Self {
354            name: name.into(),
355            arguments: None,
356            meta: None,
357        }
358    }
359
360    pub fn with_arguments(mut self, arguments: HashMap<String, String>) -> Self {
361        self.arguments = Some(arguments);
362        self
363    }
364
365    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
366        self.meta = Some(meta);
367        self
368    }
369}
370
371/// Complete prompts/get request (matches TypeScript GetPromptRequest interface)
372#[derive(Debug, Clone, Serialize, Deserialize)]
373#[serde(rename_all = "camelCase")]
374pub struct GetPromptRequest {
375    /// Method name (always "prompts/get")
376    pub method: String,
377    /// Request parameters
378    pub params: GetPromptParams,
379}
380
381impl GetPromptRequest {
382    pub fn new(name: impl Into<String>) -> Self {
383        Self {
384            method: "prompts/get".to_string(),
385            params: GetPromptParams::new(name),
386        }
387    }
388
389    pub fn with_arguments(mut self, arguments: HashMap<String, String>) -> Self {
390        self.params = self.params.with_arguments(arguments);
391        self
392    }
393
394    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
395        self.params = self.params.with_meta(meta);
396        self
397    }
398}
399
400/// Message content for prompts (matches MCP PromptMessage interface exactly)
401#[derive(Debug, Clone, Serialize, Deserialize)]
402#[serde(rename_all = "camelCase")]
403pub struct PromptMessage {
404    /// The role of the message sender
405    pub role: Role,
406    /// The content of the message (ContentBlock from MCP spec)
407    pub content: ContentBlock,
408}
409
410/// Content block within a prompt message (from MCP ContentBlock type)
411#[derive(Debug, Clone, Serialize, Deserialize)]
412#[serde(tag = "type", rename_all = "snake_case")]
413pub enum ContentBlock {
414    /// Text content
415    Text {
416        text: String,
417    },
418    /// Image content
419    Image {
420        data: String,
421        #[serde(rename = "mimeType")]
422        mime_type: String,
423    },
424    /// Resource link (ResourceLink from MCP spec)
425    ResourceLink {
426        #[serde(flatten)]
427        resource: ResourceReference,
428    },
429    /// Embedded resource (EmbeddedResource from MCP spec)
430    Resource {
431        resource: ResourceContents,
432        /// Optional annotations for the client
433        #[serde(skip_serializing_if = "Option::is_none")]
434        annotations: Option<Value>, // Using Value temporarily - proper Annotations type needed
435        /// Meta information
436        #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
437        meta: Option<HashMap<String, Value>>,
438    },
439}
440
441/// Resource reference for resource links
442#[derive(Debug, Clone, Serialize, Deserialize)]
443#[serde(rename_all = "camelCase")]
444pub struct ResourceReference {
445    pub uri: String,
446    pub name: String,
447    #[serde(skip_serializing_if = "Option::is_none")]
448    pub title: Option<String>,
449    #[serde(skip_serializing_if = "Option::is_none")]
450    pub description: Option<String>,
451    #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
452    pub mime_type: Option<String>,
453}
454
455/// Resource contents for embedded resources
456#[derive(Debug, Clone, Serialize, Deserialize)]
457#[serde(tag = "type", rename_all = "snake_case")]
458pub enum ResourceContents {
459    /// Text resource contents
460    Text {
461        uri: String,
462        #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
463        mime_type: Option<String>,
464        text: String,
465    },
466    /// Binary resource contents
467    Blob {
468        uri: String,
469        #[serde(rename = "mimeType")]
470        mime_type: String,
471        blob: String, // base64 encoded
472    },
473}
474
475impl PromptMessage {
476    pub fn user_text(content: impl Into<String>) -> Self {
477        Self {
478            role: Role::User,
479            content: ContentBlock::Text {
480                text: content.into(),
481            },
482        }
483    }
484
485    pub fn assistant_text(content: impl Into<String>) -> Self {
486        Self {
487            role: Role::Assistant,
488            content: ContentBlock::Text {
489                text: content.into(),
490            },
491        }
492    }
493
494    pub fn user_image(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
495        Self {
496            role: Role::User,
497            content: ContentBlock::Image {
498                data: data.into(),
499                mime_type: mime_type.into(),
500            },
501        }
502    }
503
504    pub fn text(content: impl Into<String>) -> Self {
505        // Backward compatibility - defaults to user
506        Self::user_text(content)
507    }
508}
509
510/// Result for prompts/get (per MCP spec)
511#[derive(Debug, Clone, Serialize, Deserialize)]
512#[serde(rename_all = "camelCase")]
513pub struct GetPromptResult {
514    /// Optional description of the prompt
515    #[serde(skip_serializing_if = "Option::is_none")]
516    pub description: Option<String>,
517    /// Messages that make up the prompt
518    pub messages: Vec<PromptMessage>,
519    /// Meta information (follows MCP Result interface)
520    #[serde(
521        default,
522        skip_serializing_if = "Option::is_none",
523        alias = "_meta",
524        rename = "_meta"
525    )]
526    pub meta: Option<HashMap<String, Value>>,
527}
528
529impl GetPromptResult {
530    pub fn new(messages: Vec<PromptMessage>) -> Self {
531        Self {
532            description: None,
533            messages,
534            meta: None,
535        }
536    }
537
538    pub fn with_description(mut self, description: impl Into<String>) -> Self {
539        self.description = Some(description.into());
540        self
541    }
542
543    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
544        self.meta = Some(meta);
545        self
546    }
547}
548
549
550// Trait implementations for prompts
551
552use crate::traits::*;
553
554// Trait implementations for ListPromptsParams
555impl Params for ListPromptsParams {}
556
557impl HasListPromptsParams for ListPromptsParams {
558    fn cursor(&self) -> Option<&Cursor> {
559        self.cursor.as_ref()
560    }
561}
562
563impl HasMetaParam for ListPromptsParams {
564    fn meta(&self) -> Option<&HashMap<String, Value>> {
565        self.meta.as_ref()
566    }
567}
568
569// Trait implementations for ListPromptsRequest
570impl HasMethod for ListPromptsRequest {
571    fn method(&self) -> &str {
572        &self.method
573    }
574}
575
576impl HasParams for ListPromptsRequest {
577    fn params(&self) -> Option<&dyn Params> {
578        Some(&self.params)
579    }
580}
581
582// Trait implementations for ListPromptsResult
583impl HasData for ListPromptsResult {
584    fn data(&self) -> HashMap<String, Value> {
585        let mut data = HashMap::new();
586        data.insert("prompts".to_string(), serde_json::to_value(&self.prompts).unwrap_or(Value::Null));
587        if let Some(ref next_cursor) = self.next_cursor {
588            data.insert("nextCursor".to_string(), Value::String(next_cursor.as_str().to_string()));
589        }
590        data
591    }
592}
593
594impl HasMeta for ListPromptsResult {
595    fn meta(&self) -> Option<HashMap<String, Value>> {
596        self.meta.clone()
597    }
598}
599
600impl RpcResult for ListPromptsResult {}
601
602impl crate::traits::ListPromptsResult for ListPromptsResult {
603    fn prompts(&self) -> &Vec<Prompt> {
604        &self.prompts
605    }
606    
607    fn next_cursor(&self) -> Option<&Cursor> {
608        self.next_cursor.as_ref()
609    }
610}
611
612// Trait implementations for GetPromptParams
613impl Params for GetPromptParams {}
614
615impl HasGetPromptParams for GetPromptParams {
616    fn name(&self) -> &String {
617        &self.name
618    }
619    
620    fn arguments(&self) -> Option<&HashMap<String, String>> {
621        self.arguments.as_ref()
622    }
623}
624
625impl HasMetaParam for GetPromptParams {
626    fn meta(&self) -> Option<&HashMap<String, Value>> {
627        self.meta.as_ref()
628    }
629}
630
631// Trait implementations for GetPromptRequest
632impl HasMethod for GetPromptRequest {
633    fn method(&self) -> &str {
634        &self.method
635    }
636}
637
638impl HasParams for GetPromptRequest {
639    fn params(&self) -> Option<&dyn Params> {
640        Some(&self.params)
641    }
642}
643
644// Trait implementations for GetPromptResult
645impl HasData for GetPromptResult {
646    fn data(&self) -> HashMap<String, Value> {
647        let mut data = HashMap::new();
648        data.insert("messages".to_string(), serde_json::to_value(&self.messages).unwrap_or(Value::Null));
649        if let Some(ref description) = self.description {
650            data.insert("description".to_string(), Value::String(description.clone()));
651        }
652        data
653    }
654}
655
656impl HasMeta for GetPromptResult {
657    fn meta(&self) -> Option<HashMap<String, Value>> {
658        self.meta.clone()
659    }
660}
661
662impl RpcResult for GetPromptResult {}
663
664impl crate::traits::GetPromptResult for GetPromptResult {
665    fn description(&self) -> Option<&String> {
666        self.description.as_ref()
667    }
668    
669    fn messages(&self) -> &Vec<PromptMessage> {
670        &self.messages
671    }
672}
673
674#[cfg(test)]
675mod tests {
676    use super::*;
677
678    #[test]
679    fn test_prompt_creation() {
680        let arg = PromptArgument::new("topic")
681            .with_description("The topic to write about")
682            .required();
683
684        let prompt = Prompt::new("write_essay")
685            .with_description("Write an essay about a topic")
686            .with_arguments(vec![arg]);
687
688        assert_eq!(prompt.name, "write_essay");
689        assert!(prompt.description.is_some());
690        assert!(prompt.arguments.is_some());
691    }
692
693    #[test]
694    fn test_prompt_message() {
695        let text_msg = PromptMessage::text("Hello, world!");
696        let user_image_msg = PromptMessage::user_image("base64data", "image/png");
697        let assistant_text_msg = PromptMessage::assistant_text("Response text");
698
699        // Verify structure matches MCP spec: role + content
700        assert_eq!(text_msg.role, Role::User);
701        assert!(matches!(text_msg.content, ContentBlock::Text { .. }));
702        
703        assert_eq!(user_image_msg.role, Role::User);
704        assert!(matches!(user_image_msg.content, ContentBlock::Image { .. }));
705        
706        assert_eq!(assistant_text_msg.role, Role::Assistant);
707        assert!(matches!(assistant_text_msg.content, ContentBlock::Text { .. }));
708    }
709
710    #[test]
711    fn test_get_prompt_request() {
712        let mut args = HashMap::new();
713        args.insert("topic".to_string(), "AI Safety".to_string()); // Now uses String instead of Value
714
715        let request = GetPromptRequest::new("write_essay")
716            .with_arguments(args);
717
718        assert_eq!(request.params.name, "write_essay");
719        assert!(request.params.arguments.is_some());
720        
721        // Verify arguments are string-to-string mapping per MCP spec
722        if let Some(ref arguments) = request.params.arguments {
723            assert_eq!(arguments.get("topic"), Some(&"AI Safety".to_string()));
724        }
725    }
726
727    #[test]
728    fn test_get_prompt_response() {
729        let messages = vec![
730            PromptMessage::user_text("Write an essay about: "),
731            PromptMessage::assistant_text("AI Safety"),
732        ];
733
734        let response = GetPromptResult::new(messages)
735            .with_description("Generated essay prompt");
736
737        assert_eq!(response.messages.len(), 2);
738        assert!(response.description.is_some());
739        
740        // Verify messages have proper role structure per MCP spec
741        assert_eq!(response.messages[0].role, Role::User);
742        assert_eq!(response.messages[1].role, Role::Assistant);
743    }
744
745    #[test]
746    fn test_serialization() {
747        let prompt = Prompt::new("test_prompt")
748            .with_description("A test prompt");
749
750        let json = serde_json::to_string(&prompt).unwrap();
751        assert!(json.contains("test_prompt"));
752        assert!(json.contains("A test prompt"));
753
754        let parsed: Prompt = serde_json::from_str(&json).unwrap();
755        assert_eq!(parsed.name, "test_prompt");
756    }
757}