1use base64::engine::{Engine, general_purpose::STANDARD as BASE64_STANDARD};
2use mcp_error_rs::{Error, Result};
3use serde::{Deserialize, Serialize};
4
5use crate::{
6 Annotation, ResourceContents,
7 content::{EmbeddedResource, ImageContent},
8};
9
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct Prompt {
14 pub name: String,
16 #[serde(skip_serializing_if = "Option::is_none")]
18 pub description: Option<String>,
19 #[serde(skip_serializing_if = "Option::is_none")]
21 pub arguments: Option<Vec<PromptArgument>>,
22}
23
24impl Prompt {
25 pub fn new<N, D>(
27 name: N,
28 description: Option<D>,
29 arguments: Option<Vec<PromptArgument>>,
30 ) -> Self
31 where
32 N: Into<String>,
33 D: Into<String>,
34 {
35 Prompt {
36 name: name.into(),
37 description: description.map(Into::into),
38 arguments,
39 }
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
45pub struct PromptArgument {
46 pub name: String,
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub description: Option<String>,
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub required: Option<bool>,
54}
55
56#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
58#[serde(rename_all = "lowercase")]
59pub enum PromptMessageRole {
60 User,
61 Assistant,
62}
63
64#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
66#[serde(tag = "type", rename_all = "lowercase")]
67pub enum PromptMessageContent {
68 Text { text: String },
70 Image { image: ImageContent },
72 Resource { resource: EmbeddedResource },
74}
75
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78pub struct PromptMessage {
79 pub role: PromptMessageRole,
81 pub content: PromptMessageContent,
83}
84
85impl PromptMessage {
86 pub fn new_text<S: Into<String>>(role: PromptMessageRole, text: S) -> Self {
88 Self {
89 role,
90 content: PromptMessageContent::Text { text: text.into() },
91 }
92 }
93
94 pub fn new_image<S: Into<String>>(
95 role: PromptMessageRole,
96 data: S,
97 mime_type: S,
98 annotation: Option<Annotation>,
99 ) -> Result<Self> {
100 let data = data.into();
101 let mime_type = mime_type.into();
102
103 BASE64_STANDARD
105 .decode(&data)
106 .map_err(|_| Error::InvalidParameters("Image data must be valid base64".to_string()))?;
107
108 if !mime_type.starts_with("image/") {
110 return Err(Error::InvalidParameters(
111 "MIME type must be a valid image type (e.g. image/jpeg)".to_string(),
112 ));
113 }
114
115 Ok(Self {
116 role,
117 content: PromptMessageContent::Image {
118 image: ImageContent {
119 data,
120 mime_type,
121 annotation,
122 },
123 },
124 })
125 }
126
127 pub fn new_resource(
129 role: PromptMessageRole,
130 uri: String,
131 mime_type: String,
132 text: Option<String>,
133 annotation: Option<Annotation>,
134 ) -> Self {
135 let resource_contents = ResourceContents::TextResourceContents {
136 uri,
137 mime_type: Some(mime_type),
138 text: text.unwrap_or_default(),
139 };
140
141 Self {
142 role,
143 content: PromptMessageContent::Resource {
144 resource: EmbeddedResource {
145 resource: resource_contents,
146 annotation,
147 },
148 },
149 }
150 }
151}
152
153#[derive(Debug, Serialize, Deserialize)]
155pub struct PromptTemplate {
156 pub id: String,
157 pub template: String,
158 pub arguments: Vec<PromptArgumentTemplate>,
159}
160
161#[derive(Debug, Serialize, Deserialize)]
163pub struct PromptArgumentTemplate {
164 pub name: String,
165 pub description: Option<String>,
166 pub required: Option<bool>,
167}