Skip to main content

thulp_core/
mcp.rs

1//! MCP Resources and Prompts types.
2//!
3//! Types for MCP protocol resources and prompts capabilities.
4
5use serde::{Deserialize, Serialize};
6/// MCP Resource definition.
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
8pub struct Resource {
9    /// Unique URI identifier for the resource (RFC3986)
10    pub uri: String,
11    /// Resource name
12    pub name: String,
13    /// Human-readable display title
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub title: Option<String>,
16    /// Description of the resource
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub description: Option<String>,
19    /// MIME type of the resource content
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub mime_type: Option<String>,
22    /// Size in bytes
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub size: Option<u64>,
25    /// Resource annotations
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub annotations: Option<ResourceAnnotations>,
28}
29
30impl Resource {
31    /// Create a new resource with required fields.
32    pub fn new(uri: impl Into<String>, name: impl Into<String>) -> Self {
33        Self {
34            uri: uri.into(),
35            name: name.into(),
36            title: None,
37            description: None,
38            mime_type: None,
39            size: None,
40            annotations: None,
41        }
42    }
43
44    /// Create a builder for a resource.
45    pub fn builder(uri: impl Into<String>, name: impl Into<String>) -> ResourceBuilder {
46        ResourceBuilder::new(uri, name)
47    }
48}
49
50/// Builder for Resource.
51#[derive(Debug)]
52pub struct ResourceBuilder {
53    resource: Resource,
54}
55
56impl ResourceBuilder {
57    /// Create a new resource builder.
58    pub fn new(uri: impl Into<String>, name: impl Into<String>) -> Self {
59        Self {
60            resource: Resource::new(uri, name),
61        }
62    }
63
64    /// Set the title.
65    pub fn title(mut self, title: impl Into<String>) -> Self {
66        self.resource.title = Some(title.into());
67        self
68    }
69
70    /// Set the description.
71    pub fn description(mut self, description: impl Into<String>) -> Self {
72        self.resource.description = Some(description.into());
73        self
74    }
75
76    /// Set the MIME type.
77    pub fn mime_type(mut self, mime_type: impl Into<String>) -> Self {
78        self.resource.mime_type = Some(mime_type.into());
79        self
80    }
81
82    /// Set the size.
83    pub fn size(mut self, size: u64) -> Self {
84        self.resource.size = Some(size);
85        self
86    }
87
88    /// Set the annotations.
89    pub fn annotations(mut self, annotations: ResourceAnnotations) -> Self {
90        self.resource.annotations = Some(annotations);
91        self
92    }
93
94    /// Build the resource.
95    pub fn build(self) -> Resource {
96        self.resource
97    }
98}
99
100/// Resource annotations for additional metadata.
101#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
102pub struct ResourceAnnotations {
103    /// Target audience ("user" or "assistant")
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub audience: Option<Vec<String>>,
106    /// Priority from 0.0 to 1.0
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub priority: Option<f64>,
109    /// Last modified timestamp (ISO 8601)
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub last_modified: Option<String>,
112}
113
114/// Resource contents after reading.
115#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
116pub struct ResourceContents {
117    /// The resource URI
118    pub uri: String,
119    /// MIME type of the content
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub mime_type: Option<String>,
122    /// Text content (for text resources)
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub text: Option<String>,
125    /// Base64-encoded blob (for binary resources)
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub blob: Option<String>,
128}
129
130impl ResourceContents {
131    /// Create text resource contents.
132    pub fn text(uri: impl Into<String>, content: impl Into<String>) -> Self {
133        Self {
134            uri: uri.into(),
135            mime_type: Some("text/plain".to_string()),
136            text: Some(content.into()),
137            blob: None,
138        }
139    }
140
141    /// Create blob resource contents.
142    pub fn blob(
143        uri: impl Into<String>,
144        data: impl Into<String>,
145        mime_type: impl Into<String>,
146    ) -> Self {
147        Self {
148            uri: uri.into(),
149            mime_type: Some(mime_type.into()),
150            text: None,
151            blob: Some(data.into()),
152        }
153    }
154}
155
156/// Resource template with URI template pattern.
157#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
158pub struct ResourceTemplate {
159    /// RFC6570 URI template
160    pub uri_template: String,
161    /// Template name
162    pub name: String,
163    /// Human-readable title
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub title: Option<String>,
166    /// Description
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub description: Option<String>,
169    /// Default MIME type for resources from this template
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub mime_type: Option<String>,
172}
173
174impl ResourceTemplate {
175    /// Create a new resource template.
176    pub fn new(uri_template: impl Into<String>, name: impl Into<String>) -> Self {
177        Self {
178            uri_template: uri_template.into(),
179            name: name.into(),
180            title: None,
181            description: None,
182            mime_type: None,
183        }
184    }
185}
186
187/// MCP Prompt definition.
188#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
189pub struct Prompt {
190    /// Unique prompt name
191    pub name: String,
192    /// Human-readable title
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub title: Option<String>,
195    /// Description of the prompt
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub description: Option<String>,
198    /// Prompt arguments
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub arguments: Option<Vec<PromptArgument>>,
201}
202
203impl Prompt {
204    /// Create a new prompt.
205    pub fn new(name: impl Into<String>) -> Self {
206        Self {
207            name: name.into(),
208            title: None,
209            description: None,
210            arguments: None,
211        }
212    }
213
214    /// Create a builder for a prompt.
215    pub fn builder(name: impl Into<String>) -> PromptBuilder {
216        PromptBuilder::new(name)
217    }
218}
219
220/// Builder for Prompt.
221#[derive(Debug)]
222pub struct PromptBuilder {
223    prompt: Prompt,
224}
225
226impl PromptBuilder {
227    /// Create a new prompt builder.
228    pub fn new(name: impl Into<String>) -> Self {
229        Self {
230            prompt: Prompt::new(name),
231        }
232    }
233
234    /// Set the title.
235    pub fn title(mut self, title: impl Into<String>) -> Self {
236        self.prompt.title = Some(title.into());
237        self
238    }
239
240    /// Set the description.
241    pub fn description(mut self, description: impl Into<String>) -> Self {
242        self.prompt.description = Some(description.into());
243        self
244    }
245
246    /// Add an argument.
247    pub fn argument(mut self, arg: PromptArgument) -> Self {
248        self.prompt.arguments.get_or_insert_with(Vec::new).push(arg);
249        self
250    }
251
252    /// Build the prompt.
253    pub fn build(self) -> Prompt {
254        self.prompt
255    }
256}
257
258/// Prompt argument definition.
259#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
260pub struct PromptArgument {
261    /// Argument name
262    pub name: String,
263    /// Description
264    #[serde(skip_serializing_if = "Option::is_none")]
265    pub description: Option<String>,
266    /// Whether the argument is required
267    #[serde(skip_serializing_if = "Option::is_none")]
268    pub required: Option<bool>,
269}
270
271impl PromptArgument {
272    /// Create a new prompt argument.
273    pub fn new(name: impl Into<String>) -> Self {
274        Self {
275            name: name.into(),
276            description: None,
277            required: None,
278        }
279    }
280
281    /// Create a required argument.
282    pub fn required(name: impl Into<String>, description: impl Into<String>) -> Self {
283        Self {
284            name: name.into(),
285            description: Some(description.into()),
286            required: Some(true),
287        }
288    }
289
290    /// Create an optional argument.
291    pub fn optional(name: impl Into<String>, description: impl Into<String>) -> Self {
292        Self {
293            name: name.into(),
294            description: Some(description.into()),
295            required: Some(false),
296        }
297    }
298}
299
300/// Message returned when getting a prompt.
301#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
302pub struct PromptMessage {
303    /// Role: "user" or "assistant"
304    pub role: String,
305    /// Message content
306    pub content: PromptContent,
307}
308
309impl PromptMessage {
310    /// Create a user message with text content.
311    pub fn user_text(text: impl Into<String>) -> Self {
312        Self {
313            role: "user".to_string(),
314            content: PromptContent::Text {
315                r#type: "text".to_string(),
316                text: text.into(),
317            },
318        }
319    }
320
321    /// Create an assistant message with text content.
322    pub fn assistant_text(text: impl Into<String>) -> Self {
323        Self {
324            role: "assistant".to_string(),
325            content: PromptContent::Text {
326                r#type: "text".to_string(),
327                text: text.into(),
328            },
329        }
330    }
331}
332
333/// Prompt content variants.
334#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
335#[serde(untagged)]
336pub enum PromptContent {
337    /// Text content
338    Text { r#type: String, text: String },
339    /// Image content
340    Image {
341        r#type: String,
342        data: String,
343        mime_type: String,
344    },
345    /// Audio content
346    Audio {
347        r#type: String,
348        data: String,
349        mime_type: String,
350    },
351    /// Embedded resource
352    Resource {
353        r#type: String,
354        resource: EmbeddedResource,
355    },
356}
357
358/// Embedded resource in prompt content.
359#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
360pub struct EmbeddedResource {
361    /// Resource URI
362    pub uri: String,
363    /// MIME type
364    #[serde(skip_serializing_if = "Option::is_none")]
365    pub mime_type: Option<String>,
366    /// Text content
367    #[serde(skip_serializing_if = "Option::is_none")]
368    pub text: Option<String>,
369    /// Base64-encoded blob
370    #[serde(skip_serializing_if = "Option::is_none")]
371    pub blob: Option<String>,
372}
373
374/// Result of getting a prompt.
375#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
376pub struct GetPromptResult {
377    /// Description of the rendered prompt
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub description: Option<String>,
380    /// Messages in the prompt
381    pub messages: Vec<PromptMessage>,
382}
383
384impl GetPromptResult {
385    /// Create a new prompt result.
386    pub fn new(messages: Vec<PromptMessage>) -> Self {
387        Self {
388            description: None,
389            messages,
390        }
391    }
392
393    /// Create a prompt result with description.
394    pub fn with_description(description: impl Into<String>, messages: Vec<PromptMessage>) -> Self {
395        Self {
396            description: Some(description.into()),
397            messages,
398        }
399    }
400}
401
402/// Result of listing resources.
403#[derive(Debug, Clone, Serialize, Deserialize, Default)]
404pub struct ResourceListResult {
405    /// List of resources
406    pub resources: Vec<Resource>,
407    /// Pagination cursor for next page
408    #[serde(skip_serializing_if = "Option::is_none")]
409    pub next_cursor: Option<String>,
410}
411
412/// Result of listing prompts.
413#[derive(Debug, Clone, Serialize, Deserialize, Default)]
414pub struct PromptListResult {
415    /// List of prompts
416    pub prompts: Vec<Prompt>,
417    /// Pagination cursor for next page
418    #[serde(skip_serializing_if = "Option::is_none")]
419    pub next_cursor: Option<String>,
420}
421
422/// Result of listing resource templates.
423#[derive(Debug, Clone, Serialize, Deserialize, Default)]
424pub struct ResourceTemplateListResult {
425    /// List of templates
426    pub resource_templates: Vec<ResourceTemplate>,
427    /// Pagination cursor for next page
428    #[serde(skip_serializing_if = "Option::is_none")]
429    pub next_cursor: Option<String>,
430}
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435
436    #[test]
437    fn test_resource_creation() {
438        let resource = Resource::new("file:///path/to/file.txt", "file.txt");
439        assert_eq!(resource.uri, "file:///path/to/file.txt");
440        assert_eq!(resource.name, "file.txt");
441    }
442
443    #[test]
444    fn test_resource_builder() {
445        let resource = Resource::builder("file:///test.md", "test.md")
446            .title("Test File")
447            .description("A test markdown file")
448            .mime_type("text/markdown")
449            .size(1024)
450            .build();
451
452        assert_eq!(resource.title, Some("Test File".to_string()));
453        assert_eq!(resource.size, Some(1024));
454    }
455
456    #[test]
457    fn test_resource_contents_text() {
458        let contents = ResourceContents::text("file:///test.txt", "Hello, World!");
459        assert!(contents.text.is_some());
460        assert!(contents.blob.is_none());
461    }
462
463    #[test]
464    fn test_resource_contents_blob() {
465        let contents = ResourceContents::blob("file:///image.png", "base64data", "image/png");
466        assert!(contents.blob.is_some());
467        assert!(contents.text.is_none());
468    }
469
470    #[test]
471    fn test_prompt_creation() {
472        let prompt = Prompt::new("code_review");
473        assert_eq!(prompt.name, "code_review");
474    }
475
476    #[test]
477    fn test_prompt_builder() {
478        let prompt = Prompt::builder("code_review")
479            .title("Code Review")
480            .description("Review code for best practices")
481            .argument(PromptArgument::required("code", "Code to review"))
482            .argument(PromptArgument::optional("language", "Programming language"))
483            .build();
484
485        assert_eq!(prompt.title, Some("Code Review".to_string()));
486        assert_eq!(prompt.arguments.as_ref().unwrap().len(), 2);
487    }
488
489    #[test]
490    fn test_prompt_message() {
491        let user_msg = PromptMessage::user_text("Please review this code");
492        assert_eq!(user_msg.role, "user");
493
494        let asst_msg = PromptMessage::assistant_text("I'll review the code");
495        assert_eq!(asst_msg.role, "assistant");
496    }
497
498    #[test]
499    fn test_get_prompt_result() {
500        let result = GetPromptResult::with_description(
501            "Code review prompt",
502            vec![PromptMessage::user_text("Review this")],
503        );
504        assert_eq!(result.description, Some("Code review prompt".to_string()));
505        assert_eq!(result.messages.len(), 1);
506    }
507
508    #[test]
509    fn test_resource_serialization() {
510        let resource = Resource::builder("file:///test.txt", "test.txt")
511            .mime_type("text/plain")
512            .build();
513
514        let json = serde_json::to_string(&resource).unwrap();
515        let parsed: Resource = serde_json::from_str(&json).unwrap();
516        assert_eq!(resource, parsed);
517    }
518
519    #[test]
520    fn test_prompt_serialization() {
521        let prompt = Prompt::builder("test")
522            .description("Test prompt")
523            .argument(PromptArgument::required("input", "Input text"))
524            .build();
525
526        let json = serde_json::to_string(&prompt).unwrap();
527        let parsed: Prompt = serde_json::from_str(&json).unwrap();
528        assert_eq!(prompt, parsed);
529    }
530}