Skip to main content

mcp_plugin_api/
utils.rs

1//! Memory management utilities for plugins
2//!
3//! This module provides safe wrappers around the unsafe FFI memory management
4//! operations required by the plugin API.
5
6use serde_json::Value;
7use std::mem::ManuallyDrop;
8
9/// Return a success result to the framework
10///
11/// This handles all the unsafe memory management details:
12/// - Converts the value to JSON
13/// - Allocates a buffer
14/// - Shrinks to minimize memory usage
15/// - Returns the pointer and capacity to the framework
16///
17/// # Safety
18///
19/// The caller must ensure that:
20/// - `result_buf` points to valid, properly aligned memory for writing a pointer
21/// - `result_len` points to valid, properly aligned memory for writing a usize
22/// - These pointers remain valid for the duration of the call
23/// - The pointers are not aliased (no other mutable references exist)
24///
25/// # Example
26///
27/// ```ignore
28/// unsafe {
29///     let result = json!({"status": "ok"});
30///     return return_success(result, result_buf, result_len);
31/// }
32/// ```
33pub unsafe fn return_success(data: Value, result_buf: *mut *mut u8, result_len: *mut usize) -> i32 {
34    prepare_result(data, result_buf, result_len);
35
36    0 // Success code
37}
38
39/// Return an error result to the framework
40///
41/// This wraps the error message in a JSON object and returns it
42/// with an error code.
43///
44/// # Safety
45///
46/// The caller must ensure that:
47/// - `result_buf` points to valid, properly aligned memory for writing a pointer
48/// - `result_len` points to valid, properly aligned memory for writing a usize
49/// - These pointers remain valid for the duration of the call
50/// - The pointers are not aliased (no other mutable references exist)
51///
52/// # Example
53///
54/// ```ignore
55/// unsafe {
56///     return return_error("Product not found", result_buf, result_len);
57/// }
58/// ```
59pub unsafe fn return_error(error: &str, result_buf: *mut *mut u8, result_len: *mut usize) -> i32 {
60    let error_json = serde_json::json!({
61        "error": error
62    });
63
64    prepare_result(error_json, result_buf, result_len);
65
66    1 // Error code
67}
68
69/// Prepare a result for return to the framework
70///
71/// Internal helper function that handles the common memory management
72/// for both success and error results.
73///
74/// # Safety
75///
76/// The caller must ensure that:
77/// - `result_buf` points to valid, properly aligned memory for writing a pointer
78/// - `result_len` points to valid, properly aligned memory for writing a usize
79/// - These pointers remain valid for the duration of the call
80/// - The pointers are not aliased (no other mutable references exist)
81pub unsafe fn prepare_result(data: Value, result_buf: *mut *mut u8, result_len: *mut usize) {
82    let json_string = data.to_string();
83    let mut vec = json_string.into_bytes();
84    vec.shrink_to_fit();
85
86    *result_len = vec.capacity();
87    *result_buf = vec.as_mut_ptr();
88    let _ = ManuallyDrop::new(vec);
89}
90
91/// Standard free_string implementation
92///
93/// This can be used directly in the `declare_plugin!` macro.
94/// It safely deallocates memory that was allocated by the plugin
95/// and passed to the framework.
96///
97/// # Safety
98///
99/// The pointer and capacity must match the values returned by
100/// `return_success` or `return_error`.
101///
102/// # Example
103///
104/// ```ignore
105/// declare_plugin! {
106///     list_tools: generated_list_tools,
107///     execute_tool: generated_execute_tool,
108///     free_string: mcp_plugin_api::utils::standard_free_string
109/// }
110/// ```
111pub unsafe extern "C" fn standard_free_string(ptr: *mut u8, capacity: usize) {
112    if !ptr.is_null() && capacity > 0 {
113        // Reconstruct the Vec with the same capacity that was returned
114        let _ = Vec::from_raw_parts(ptr, capacity, capacity);
115        // Vec is dropped here, freeing the memory
116    }
117}
118
119// ============================================================================
120// Content Helpers - MCP-compliant content construction
121// ============================================================================
122
123/// Helper to create a text content response
124///
125/// Creates a standard MCP text content response:
126/// ```json
127/// {
128///   "content": [{
129///     "type": "text",
130///     "text": "your text here"
131///   }]
132/// }
133/// ```
134///
135/// # Example
136///
137/// ```ignore
138/// fn handle_get_price(args: &Value) -> Result<Value, String> {
139///     let price = 29.99;
140///     Ok(text_content(format!("Price: ${:.2}", price)))
141/// }
142/// ```
143pub fn text_content(text: impl Into<String>) -> Value {
144    serde_json::json!({
145        "content": [{
146            "type": "text",
147            "text": text.into()
148        }]
149    })
150}
151
152/// Helper to create a JSON content response
153///
154/// Creates a standard MCP JSON content response with structured data:
155/// ```json
156/// {
157///   "content": [{
158///     "type": "json",
159///     "json": { ... }
160///   }]
161/// }
162/// ```
163///
164/// **Use case**: Structured data for programmatic clients
165///
166/// # Example
167///
168/// ```ignore
169/// fn handle_get_product(args: &Value) -> Result<Value, String> {
170///     let product = get_product_from_db()?;
171///     Ok(json_content(serde_json::to_value(product)?))
172/// }
173/// ```
174pub fn json_content(json: Value) -> Value {
175    serde_json::json!({
176        "content": [{
177            "type": "json",
178            "json": json
179        }]
180    })
181}
182
183/// Helper to create an HTML content response
184///
185/// Creates a standard MCP HTML content response:
186/// ```json
187/// {
188///   "content": [{
189///     "type": "html",
190///     "html": "<div>...</div>"
191///   }]
192/// }
193/// ```
194///
195/// **Use case**: Rich HTML content for UIs
196///
197/// # Example
198///
199/// ```ignore
200/// fn handle_get_formatted(args: &Value) -> Result<Value, String> {
201///     let html = format!("<div><h1>{}</h1><p>{}</p></div>", title, body);
202///     Ok(html_content(html))
203/// }
204/// ```
205pub fn html_content(html: impl Into<String>) -> Value {
206    serde_json::json!({
207        "content": [{
208            "type": "html",
209            "html": html.into()
210        }]
211    })
212}
213
214/// Helper to create a Markdown content response
215///
216/// Creates a standard MCP Markdown content response:
217/// ```json
218/// {
219///   "content": [{
220///     "type": "markdown",
221///     "markdown": "# Title\n\nContent..."
222///   }]
223/// }
224/// ```
225///
226/// **Use case**: Formatted text for chat clients
227///
228/// # Example
229///
230/// ```ignore
231/// fn handle_get_readme(args: &Value) -> Result<Value, String> {
232///     let markdown = format!("# {}\n\n{}", title, content);
233///     Ok(markdown_content(markdown))
234/// }
235/// ```
236pub fn markdown_content(markdown: impl Into<String>) -> Value {
237    serde_json::json!({
238        "content": [{
239            "type": "markdown",
240            "markdown": markdown.into()
241        }]
242    })
243}
244
245/// Helper to create an image content response with URL
246///
247/// Creates a standard MCP image content response with image URL:
248/// ```json
249/// {
250///   "content": [{
251///     "type": "image",
252///     "imageUrl": "https://example.com/image.png",
253///     "mimeType": "image/png"
254///   }]
255/// }
256/// ```
257///
258/// **Use case**: Return image by URL reference
259///
260/// # Example
261///
262/// ```ignore
263/// fn handle_get_product_image(args: &Value) -> Result<Value, String> {
264///     let url = format!("https://cdn.example.com/products/{}.jpg", product_id);
265///     Ok(image_url_content(url, Some("image/jpeg".to_string())))
266/// }
267/// ```
268pub fn image_url_content(url: impl Into<String>, mime_type: Option<String>) -> Value {
269    let mut img = serde_json::json!({
270        "type": "image",
271        "imageUrl": url.into()
272    });
273    
274    if let Some(mt) = mime_type {
275        img["mimeType"] = serde_json::json!(mt);
276    }
277    
278    serde_json::json!({
279        "content": [img]
280    })
281}
282
283/// Helper to create an image content response with base64 data
284///
285/// Creates a standard MCP image content response with embedded data:
286/// ```json
287/// {
288///   "content": [{
289///     "type": "image",
290///     "imageData": "base64-encoded-data",
291///     "mimeType": "image/png"
292///   }]
293/// }
294/// ```
295///
296/// **Use case**: Return embedded image data
297///
298/// # Example
299///
300/// ```ignore
301/// fn handle_get_chart(args: &Value) -> Result<Value, String> {
302///     let chart_bytes = generate_chart()?;
303///     let base64_data = base64::encode(chart_bytes);
304///     Ok(image_data_content(base64_data, Some("image/png".to_string())))
305/// }
306/// ```
307pub fn image_data_content(data: impl Into<String>, mime_type: Option<String>) -> Value {
308    let mut img = serde_json::json!({
309        "type": "image",
310        "imageData": data.into()
311    });
312    
313    if let Some(mt) = mime_type {
314        img["mimeType"] = serde_json::json!(mt);
315    }
316    
317    serde_json::json!({
318        "content": [img]
319    })
320}
321
322/// Helper to create an image content response (legacy)
323///
324/// **DEPRECATED**: Use `image_url_content` or `image_data_content` instead.
325///
326/// This function is kept for backward compatibility but uses the old field name.
327#[deprecated(since = "0.2.0", note = "Use image_url_content or image_data_content instead")]
328pub fn image_content(data: impl Into<String>, mime_type: impl Into<String>) -> Value {
329    image_data_content(data, Some(mime_type.into()))
330}
331
332/// Helper to create a resource content response
333///
334/// Creates a standard MCP resource content response:
335/// ```json
336/// {
337///   "content": [{
338///     "type": "resource",
339///     "uri": "https://example.com/resource",
340///     "mimeType": "text/html",  // optional
341///     "text": "content"         // optional
342///   }]
343/// }
344/// ```
345///
346/// # Example
347///
348/// ```ignore
349/// fn handle_get_resource(args: &Value) -> Result<Value, String> {
350///     Ok(resource_content(
351///         "https://example.com/docs",
352///         Some("text/html".to_string()),
353///         None
354///     ))
355/// }
356/// ```
357pub fn resource_content(
358    uri: impl Into<String>,
359    mime_type: Option<String>,
360    text: Option<String>,
361) -> Value {
362    let mut res = serde_json::json!({
363        "type": "resource",
364        "uri": uri.into()
365    });
366
367    if let Some(mt) = mime_type {
368        res["mimeType"] = serde_json::json!(mt);
369    }
370    if let Some(t) = text {
371        res["text"] = serde_json::json!(t);
372    }
373
374    serde_json::json!({
375        "content": [res]
376    })
377}
378
379/// Helper to create a multi-content response
380///
381/// Creates a response with multiple content items (text, images, resources):
382/// ```json
383/// {
384///   "content": [
385///     {"type": "text", "text": "..."},
386///     {"type": "image", "data": "...", "mimeType": "..."},
387///     {"type": "resource", "uri": "..."}
388///   ]
389/// }
390/// ```
391///
392/// # Example
393///
394/// ```ignore
395/// fn handle_get_product(args: &Value) -> Result<Value, String> {
396///     Ok(multi_content(vec![
397///         serde_json::json!({"type": "text", "text": "Product info"}),
398///         serde_json::json!({
399///             "type": "image",
400///             "data": base64_image,
401///             "mimeType": "image/jpeg"
402///         })
403///     ]))
404/// }
405/// ```
406pub fn multi_content(items: Vec<Value>) -> Value {
407    serde_json::json!({
408        "content": items
409    })
410}