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}