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(uri: impl Into<String>, data: impl Into<String>, mime_type: impl Into<String>) -> Self {
143        Self {
144            uri: uri.into(),
145            mime_type: Some(mime_type.into()),
146            text: None,
147            blob: Some(data.into()),
148        }
149    }
150}
151
152/// Resource template with URI template pattern.
153#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
154pub struct ResourceTemplate {
155    /// RFC6570 URI template
156    pub uri_template: String,
157    /// Template name
158    pub name: String,
159    /// Human-readable title
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub title: Option<String>,
162    /// Description
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub description: Option<String>,
165    /// Default MIME type for resources from this template
166    #[serde(skip_serializing_if = "Option::is_none")]
167    pub mime_type: Option<String>,
168}
169
170impl ResourceTemplate {
171    /// Create a new resource template.
172    pub fn new(uri_template: impl Into<String>, name: impl Into<String>) -> Self {
173        Self {
174            uri_template: uri_template.into(),
175            name: name.into(),
176            title: None,
177            description: None,
178            mime_type: None,
179        }
180    }
181}
182
183/// MCP Prompt definition.
184#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
185pub struct Prompt {
186    /// Unique prompt name
187    pub name: String,
188    /// Human-readable title
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub title: Option<String>,
191    /// Description of the prompt
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub description: Option<String>,
194    /// Prompt arguments
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub arguments: Option<Vec<PromptArgument>>,
197}
198
199impl Prompt {
200    /// Create a new prompt.
201    pub fn new(name: impl Into<String>) -> Self {
202        Self {
203            name: name.into(),
204            title: None,
205            description: None,
206            arguments: None,
207        }
208    }
209
210    /// Create a builder for a prompt.
211    pub fn builder(name: impl Into<String>) -> PromptBuilder {
212        PromptBuilder::new(name)
213    }
214}
215
216/// Builder for Prompt.
217#[derive(Debug)]
218pub struct PromptBuilder {
219    prompt: Prompt,
220}
221
222impl PromptBuilder {
223    /// Create a new prompt builder.
224    pub fn new(name: impl Into<String>) -> Self {
225        Self {
226            prompt: Prompt::new(name),
227        }
228    }
229
230    /// Set the title.
231    pub fn title(mut self, title: impl Into<String>) -> Self {
232        self.prompt.title = Some(title.into());
233        self
234    }
235
236    /// Set the description.
237    pub fn description(mut self, description: impl Into<String>) -> Self {
238        self.prompt.description = Some(description.into());
239        self
240    }
241
242    /// Add an argument.
243    pub fn argument(mut self, arg: PromptArgument) -> Self {
244        self.prompt
245            .arguments
246            .get_or_insert_with(Vec::new)
247            .push(arg);
248        self
249    }
250
251    /// Build the prompt.
252    pub fn build(self) -> Prompt {
253        self.prompt
254    }
255}
256
257/// Prompt argument definition.
258#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
259pub struct PromptArgument {
260    /// Argument name
261    pub name: String,
262    /// Description
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub description: Option<String>,
265    /// Whether the argument is required
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub required: Option<bool>,
268}
269
270impl PromptArgument {
271    /// Create a new prompt argument.
272    pub fn new(name: impl Into<String>) -> Self {
273        Self {
274            name: name.into(),
275            description: None,
276            required: None,
277        }
278    }
279
280    /// Create a required argument.
281    pub fn required(name: impl Into<String>, description: impl Into<String>) -> Self {
282        Self {
283            name: name.into(),
284            description: Some(description.into()),
285            required: Some(true),
286        }
287    }
288
289    /// Create an optional argument.
290    pub fn optional(name: impl Into<String>, description: impl Into<String>) -> Self {
291        Self {
292            name: name.into(),
293            description: Some(description.into()),
294            required: Some(false),
295        }
296    }
297}
298
299/// Message returned when getting a prompt.
300#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
301pub struct PromptMessage {
302    /// Role: "user" or "assistant"
303    pub role: String,
304    /// Message content
305    pub content: PromptContent,
306}
307
308impl PromptMessage {
309    /// Create a user message with text content.
310    pub fn user_text(text: impl Into<String>) -> Self {
311        Self {
312            role: "user".to_string(),
313            content: PromptContent::Text {
314                r#type: "text".to_string(),
315                text: text.into(),
316            },
317        }
318    }
319
320    /// Create an assistant message with text content.
321    pub fn assistant_text(text: impl Into<String>) -> Self {
322        Self {
323            role: "assistant".to_string(),
324            content: PromptContent::Text {
325                r#type: "text".to_string(),
326                text: text.into(),
327            },
328        }
329    }
330}
331
332/// Prompt content variants.
333#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
334#[serde(untagged)]
335pub enum PromptContent {
336    /// Text content
337    Text {
338        r#type: String,
339        text: String,
340    },
341    /// Image content
342    Image {
343        r#type: String,
344        data: String,
345        mime_type: String,
346    },
347    /// Audio content
348    Audio {
349        r#type: String,
350        data: String,
351        mime_type: String,
352    },
353    /// Embedded resource
354    Resource {
355        r#type: String,
356        resource: EmbeddedResource,
357    },
358}
359
360/// Embedded resource in prompt content.
361#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
362pub struct EmbeddedResource {
363    /// Resource URI
364    pub uri: String,
365    /// MIME type
366    #[serde(skip_serializing_if = "Option::is_none")]
367    pub mime_type: Option<String>,
368    /// Text content
369    #[serde(skip_serializing_if = "Option::is_none")]
370    pub text: Option<String>,
371    /// Base64-encoded blob
372    #[serde(skip_serializing_if = "Option::is_none")]
373    pub blob: Option<String>,
374}
375
376/// Result of getting a prompt.
377#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
378pub struct GetPromptResult {
379    /// Description of the rendered prompt
380    #[serde(skip_serializing_if = "Option::is_none")]
381    pub description: Option<String>,
382    /// Messages in the prompt
383    pub messages: Vec<PromptMessage>,
384}
385
386impl GetPromptResult {
387    /// Create a new prompt result.
388    pub fn new(messages: Vec<PromptMessage>) -> Self {
389        Self {
390            description: None,
391            messages,
392        }
393    }
394
395    /// Create a prompt result with description.
396    pub fn with_description(description: impl Into<String>, messages: Vec<PromptMessage>) -> Self {
397        Self {
398            description: Some(description.into()),
399            messages,
400        }
401    }
402}
403
404/// Result of listing resources.
405#[derive(Debug, Clone, Serialize, Deserialize, Default)]
406pub struct ResourceListResult {
407    /// List of resources
408    pub resources: Vec<Resource>,
409    /// Pagination cursor for next page
410    #[serde(skip_serializing_if = "Option::is_none")]
411    pub next_cursor: Option<String>,
412}
413
414/// Result of listing prompts.
415#[derive(Debug, Clone, Serialize, Deserialize, Default)]
416pub struct PromptListResult {
417    /// List of prompts
418    pub prompts: Vec<Prompt>,
419    /// Pagination cursor for next page
420    #[serde(skip_serializing_if = "Option::is_none")]
421    pub next_cursor: Option<String>,
422}
423
424/// Result of listing resource templates.
425#[derive(Debug, Clone, Serialize, Deserialize, Default)]
426pub struct ResourceTemplateListResult {
427    /// List of templates
428    pub resource_templates: Vec<ResourceTemplate>,
429    /// Pagination cursor for next page
430    #[serde(skip_serializing_if = "Option::is_none")]
431    pub next_cursor: Option<String>,
432}
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437
438    #[test]
439    fn test_resource_creation() {
440        let resource = Resource::new("file:///path/to/file.txt", "file.txt");
441        assert_eq!(resource.uri, "file:///path/to/file.txt");
442        assert_eq!(resource.name, "file.txt");
443    }
444
445    #[test]
446    fn test_resource_builder() {
447        let resource = Resource::builder("file:///test.md", "test.md")
448            .title("Test File")
449            .description("A test markdown file")
450            .mime_type("text/markdown")
451            .size(1024)
452            .build();
453
454        assert_eq!(resource.title, Some("Test File".to_string()));
455        assert_eq!(resource.size, Some(1024));
456    }
457
458    #[test]
459    fn test_resource_contents_text() {
460        let contents = ResourceContents::text("file:///test.txt", "Hello, World!");
461        assert!(contents.text.is_some());
462        assert!(contents.blob.is_none());
463    }
464
465    #[test]
466    fn test_resource_contents_blob() {
467        let contents = ResourceContents::blob("file:///image.png", "base64data", "image/png");
468        assert!(contents.blob.is_some());
469        assert!(contents.text.is_none());
470    }
471
472    #[test]
473    fn test_prompt_creation() {
474        let prompt = Prompt::new("code_review");
475        assert_eq!(prompt.name, "code_review");
476    }
477
478    #[test]
479    fn test_prompt_builder() {
480        let prompt = Prompt::builder("code_review")
481            .title("Code Review")
482            .description("Review code for best practices")
483            .argument(PromptArgument::required("code", "Code to review"))
484            .argument(PromptArgument::optional("language", "Programming language"))
485            .build();
486
487        assert_eq!(prompt.title, Some("Code Review".to_string()));
488        assert_eq!(prompt.arguments.as_ref().unwrap().len(), 2);
489    }
490
491    #[test]
492    fn test_prompt_message() {
493        let user_msg = PromptMessage::user_text("Please review this code");
494        assert_eq!(user_msg.role, "user");
495
496        let asst_msg = PromptMessage::assistant_text("I'll review the code");
497        assert_eq!(asst_msg.role, "assistant");
498    }
499
500    #[test]
501    fn test_get_prompt_result() {
502        let result = GetPromptResult::with_description(
503            "Code review prompt",
504            vec![PromptMessage::user_text("Review this")],
505        );
506        assert_eq!(result.description, Some("Code review prompt".to_string()));
507        assert_eq!(result.messages.len(), 1);
508    }
509
510    #[test]
511    fn test_resource_serialization() {
512        let resource = Resource::builder("file:///test.txt", "test.txt")
513            .mime_type("text/plain")
514            .build();
515        
516        let json = serde_json::to_string(&resource).unwrap();
517        let parsed: Resource = serde_json::from_str(&json).unwrap();
518        assert_eq!(resource, parsed);
519    }
520
521    #[test]
522    fn test_prompt_serialization() {
523        let prompt = Prompt::builder("test")
524            .description("Test prompt")
525            .argument(PromptArgument::required("input", "Input text"))
526            .build();
527        
528        let json = serde_json::to_string(&prompt).unwrap();
529        let parsed: Prompt = serde_json::from_str(&json).unwrap();
530        assert_eq!(prompt, parsed);
531    }
532}