Skip to main content

mcp_plugin_api/
resource.rs

1//! Type-safe MCP resource definitions
2//!
3//! This module provides a high-level API for defining MCP resources with
4//! compile-time type checking and automatic JSON generation for the
5//! resources/list and resources/read protocol messages.
6
7use serde_json::{json, Value};
8
9/// Content of a resource, either text or binary (base64-encoded).
10///
11/// Matches the MCP resources/read response format:
12/// - Text content: `{ "uri", "mimeType?", "text" }`
13/// - Binary content: `{ "uri", "mimeType?", "blob" }` (base64)
14#[derive(Debug, Clone)]
15pub struct ResourceContent {
16    /// Resource URI
17    pub uri: String,
18    /// Optional MIME type
19    pub mime_type: Option<String>,
20    /// Text content (use for UTF-8 text)
21    pub text: Option<String>,
22    /// Base64-encoded binary content (use for binary data)
23    pub blob: Option<String>,
24}
25
26impl ResourceContent {
27    /// Create text content
28    pub fn text(uri: impl Into<String>, content: impl Into<String>, mime_type: Option<String>) -> Self {
29        Self {
30            uri: uri.into(),
31            mime_type,
32            text: Some(content.into()),
33            blob: None,
34        }
35    }
36
37    /// Create binary content (base64-encoded)
38    pub fn blob(uri: impl Into<String>, base64_data: impl Into<String>, mime_type: Option<String>) -> Self {
39        Self {
40            uri: uri.into(),
41            mime_type,
42            text: None,
43            blob: Some(base64_data.into()),
44        }
45    }
46
47    /// Convert to MCP content block JSON
48    pub fn to_json(&self) -> Value {
49        let mut obj = serde_json::Map::new();
50        obj.insert("uri".to_string(), json!(self.uri));
51        if let Some(ref mt) = self.mime_type {
52            obj.insert("mimeType".to_string(), json!(mt));
53        }
54        if let Some(ref t) = self.text {
55            obj.insert("text".to_string(), json!(t));
56        }
57        if let Some(ref b) = self.blob {
58            obj.insert("blob".to_string(), json!(b));
59        }
60        Value::Object(obj)
61    }
62}
63
64/// Collection of resource contents (e.g. multi-part resource)
65pub type ResourceContents = Vec<ResourceContent>;
66
67/// Resource read handler function type
68///
69/// Given a URI, returns the resource contents or an error.
70pub type ResourceHandler = fn(&str) -> Result<ResourceContents, String>;
71
72/// A resource definition for the resources/list response
73///
74/// Represents a single resource with its metadata. The handler is called
75/// when the client requests the resource via resources/read.
76#[derive(Debug, Clone)]
77pub struct Resource {
78    /// Unique resource URI
79    pub uri: String,
80    /// Resource name (e.g. filename)
81    pub name: Option<String>,
82    /// Human-readable title for display
83    pub title: Option<String>,
84    /// Description of the resource
85    pub description: Option<String>,
86    /// Optional MIME type
87    pub mime_type: Option<String>,
88    /// Handler to read resource content when requested
89    pub handler: ResourceHandler,
90}
91
92impl Resource {
93    /// Create a new resource with a builder
94    ///
95    /// # Example
96    ///
97    /// ```ignore
98    /// Resource::builder("file:///docs/readme", read_readme)
99    ///     .name("readme.md")
100    ///     .description("Project documentation")
101    ///     .mime_type("text/markdown")
102    /// ```
103    pub fn builder(uri: impl Into<String>, handler: ResourceHandler) -> ResourceBuilder {
104        ResourceBuilder {
105            uri: uri.into(),
106            name: None,
107            title: None,
108            description: None,
109            mime_type: None,
110            handler,
111        }
112    }
113
114    /// Convert to MCP resources/list item format
115    ///
116    /// Returns a JSON object compatible with MCP protocol:
117    /// ```json
118    /// {
119    ///   "uri": "file:///...",
120    ///   "name": "main.rs",
121    ///   "title": "Optional title",
122    ///   "description": "Optional description",
123    ///   "mimeType": "text/x-rust"
124    /// }
125    /// ```
126    pub fn to_list_item(&self) -> Value {
127        let mut obj = serde_json::Map::new();
128        obj.insert("uri".to_string(), json!(self.uri));
129        if let Some(ref n) = self.name {
130            obj.insert("name".to_string(), json!(n));
131        }
132        if let Some(ref t) = self.title {
133            obj.insert("title".to_string(), json!(t));
134        }
135        if let Some(ref d) = self.description {
136            obj.insert("description".to_string(), json!(d));
137        }
138        if let Some(ref mt) = self.mime_type {
139            obj.insert("mimeType".to_string(), json!(mt));
140        }
141        Value::Object(obj)
142    }
143}
144
145/// Builder for creating resources with a fluent API
146pub struct ResourceBuilder {
147    uri: String,
148    name: Option<String>,
149    title: Option<String>,
150    description: Option<String>,
151    mime_type: Option<String>,
152    handler: ResourceHandler,
153}
154
155impl ResourceBuilder {
156    /// Set the resource name
157    pub fn name(mut self, name: impl Into<String>) -> Self {
158        self.name = Some(name.into());
159        self
160    }
161
162    /// Set the human-readable title
163    pub fn title(mut self, title: impl Into<String>) -> Self {
164        self.title = Some(title.into());
165        self
166    }
167
168    /// Set the description
169    pub fn description(mut self, description: impl Into<String>) -> Self {
170        self.description = Some(description.into());
171        self
172    }
173
174    /// Set the MIME type
175    pub fn mime_type(mut self, mime_type: impl Into<String>) -> Self {
176        self.mime_type = Some(mime_type.into());
177        self
178    }
179
180    /// Finalize and build the Resource
181    pub fn build(self) -> Resource {
182        Resource {
183            uri: self.uri,
184            name: self.name,
185            title: self.title,
186            description: self.description,
187            mime_type: self.mime_type,
188            handler: self.handler,
189        }
190    }
191}