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/// Build MCP resources/list response JSON
92///
93/// Creates `{ "resources": [...], "nextCursor"?: "..." }` per MCP spec.
94pub fn resource_list_response(
95    resources: Vec<Value>,
96    next_cursor: Option<&str>,
97) -> Value {
98    let mut obj = serde_json::Map::new();
99    obj.insert("resources".to_string(), Value::Array(resources));
100    if let Some(c) = next_cursor {
101        obj.insert("nextCursor".to_string(), serde_json::json!(c));
102    }
103    Value::Object(obj)
104}
105
106/// Build MCP resources/read response JSON
107///
108/// Creates `{ "contents": [...] }` per MCP spec.
109pub fn resource_read_response(contents: &[crate::resource::ResourceContent]) -> Value {
110    let items: Vec<Value> = contents.iter().map(|c| c.to_json()).collect();
111    serde_json::json!({ "contents": items })
112}
113
114/// Standard free_string implementation
115///
116/// This can be used directly in the `declare_plugin!` macro.
117/// It safely deallocates memory that was allocated by the plugin
118/// and passed to the framework.
119///
120/// # Safety
121///
122/// The pointer and capacity must match the values returned by
123/// `return_success` or `return_error`.
124///
125/// # Example
126///
127/// ```ignore
128/// declare_plugin! {
129///     list_tools: generated_list_tools,
130///     execute_tool: generated_execute_tool,
131///     free_string: mcp_plugin_api::utils::standard_free_string
132/// }
133/// ```
134pub unsafe extern "C" fn standard_free_string(ptr: *mut u8, capacity: usize) {
135    if !ptr.is_null() && capacity > 0 {
136        // Reconstruct the Vec with the same capacity that was returned
137        let _ = Vec::from_raw_parts(ptr, capacity, capacity);
138        // Vec is dropped here, freeing the memory
139    }
140}
141
142// ============================================================================
143// Content Helpers - MCP-compliant content construction
144// ============================================================================
145
146/// Helper to create a text content response
147///
148/// Creates a standard MCP text content response:
149/// ```json
150/// {
151///   "content": [{
152///     "type": "text",
153///     "text": "your text here"
154///   }]
155/// }
156/// ```
157///
158/// # Example
159///
160/// ```ignore
161/// fn handle_get_price(args: &Value) -> Result<Value, String> {
162///     let price = 29.99;
163///     Ok(text_content(format!("Price: ${:.2}", price)))
164/// }
165/// ```
166pub fn text_content(text: impl Into<String>) -> Value {
167    serde_json::json!({
168        "content": [{
169            "type": "text",
170            "text": text.into()
171        }]
172    })
173}
174
175/// Helper to create a JSON content response
176///
177/// Creates a standard MCP JSON content response with structured data:
178/// ```json
179/// {
180///   "content": [{
181///     "type": "json",
182///     "json": { ... }
183///   }]
184/// }
185/// ```
186///
187/// **Use case**: Structured data for programmatic clients
188///
189/// # Example
190///
191/// ```ignore
192/// fn handle_get_product(args: &Value) -> Result<Value, String> {
193///     let product = get_product_from_db()?;
194///     Ok(json_content(serde_json::to_value(product)?))
195/// }
196/// ```
197pub fn json_content(json: Value) -> Value {
198    serde_json::json!({
199        "content": [{
200            "type": "json",
201            "json": json
202        }]
203    })
204}
205
206/// Helper to create an HTML content response
207///
208/// Creates a standard MCP HTML content response:
209/// ```json
210/// {
211///   "content": [{
212///     "type": "html",
213///     "html": "<div>...</div>"
214///   }]
215/// }
216/// ```
217///
218/// **Use case**: Rich HTML content for UIs
219///
220/// # Example
221///
222/// ```ignore
223/// fn handle_get_formatted(args: &Value) -> Result<Value, String> {
224///     let html = format!("<div><h1>{}</h1><p>{}</p></div>", title, body);
225///     Ok(html_content(html))
226/// }
227/// ```
228pub fn html_content(html: impl Into<String>) -> Value {
229    serde_json::json!({
230        "content": [{
231            "type": "html",
232            "html": html.into()
233        }]
234    })
235}
236
237/// Helper to create a Markdown content response
238///
239/// Creates a standard MCP Markdown content response:
240/// ```json
241/// {
242///   "content": [{
243///     "type": "markdown",
244///     "markdown": "# Title\n\nContent..."
245///   }]
246/// }
247/// ```
248///
249/// **Use case**: Formatted text for chat clients
250///
251/// # Example
252///
253/// ```ignore
254/// fn handle_get_readme(args: &Value) -> Result<Value, String> {
255///     let markdown = format!("# {}\n\n{}", title, content);
256///     Ok(markdown_content(markdown))
257/// }
258/// ```
259pub fn markdown_content(markdown: impl Into<String>) -> Value {
260    serde_json::json!({
261        "content": [{
262            "type": "markdown",
263            "markdown": markdown.into()
264        }]
265    })
266}
267
268/// Helper to create an image content response with URL
269///
270/// Creates a standard MCP image content response with image URL:
271/// ```json
272/// {
273///   "content": [{
274///     "type": "image",
275///     "imageUrl": "https://example.com/image.png",
276///     "mimeType": "image/png"
277///   }]
278/// }
279/// ```
280///
281/// **Use case**: Return image by URL reference
282///
283/// # Example
284///
285/// ```ignore
286/// fn handle_get_product_image(args: &Value) -> Result<Value, String> {
287///     let url = format!("https://cdn.example.com/products/{}.jpg", product_id);
288///     Ok(image_url_content(url, Some("image/jpeg".to_string())))
289/// }
290/// ```
291pub fn image_url_content(url: impl Into<String>, mime_type: Option<String>) -> Value {
292    let mut img = serde_json::json!({
293        "type": "image",
294        "imageUrl": url.into()
295    });
296    
297    if let Some(mt) = mime_type {
298        img["mimeType"] = serde_json::json!(mt);
299    }
300    
301    serde_json::json!({
302        "content": [img]
303    })
304}
305
306/// Helper to create an image content response with base64 data
307///
308/// Creates a standard MCP image content response with embedded data:
309/// ```json
310/// {
311///   "content": [{
312///     "type": "image",
313///     "imageData": "base64-encoded-data",
314///     "mimeType": "image/png"
315///   }]
316/// }
317/// ```
318///
319/// **Use case**: Return embedded image data
320///
321/// # Example
322///
323/// ```ignore
324/// fn handle_get_chart(args: &Value) -> Result<Value, String> {
325///     let chart_bytes = generate_chart()?;
326///     let base64_data = base64::encode(chart_bytes);
327///     Ok(image_data_content(base64_data, Some("image/png".to_string())))
328/// }
329/// ```
330pub fn image_data_content(data: impl Into<String>, mime_type: Option<String>) -> Value {
331    let mut img = serde_json::json!({
332        "type": "image",
333        "imageData": data.into()
334    });
335    
336    if let Some(mt) = mime_type {
337        img["mimeType"] = serde_json::json!(mt);
338    }
339    
340    serde_json::json!({
341        "content": [img]
342    })
343}
344
345/// Helper to create an image content response (legacy)
346///
347/// **DEPRECATED**: Use `image_url_content` or `image_data_content` instead.
348///
349/// This function is kept for backward compatibility but uses the old field name.
350#[deprecated(since = "0.2.0", note = "Use image_url_content or image_data_content instead")]
351pub fn image_content(data: impl Into<String>, mime_type: impl Into<String>) -> Value {
352    image_data_content(data, Some(mime_type.into()))
353}
354
355/// Helper to create a resource content response
356///
357/// Creates a standard MCP resource content response:
358/// ```json
359/// {
360///   "content": [{
361///     "type": "resource",
362///     "uri": "https://example.com/resource",
363///     "mimeType": "text/html",  // optional
364///     "text": "content"         // optional
365///   }]
366/// }
367/// ```
368///
369/// # Example
370///
371/// ```ignore
372/// fn handle_get_resource(args: &Value) -> Result<Value, String> {
373///     Ok(resource_content(
374///         "https://example.com/docs",
375///         Some("text/html".to_string()),
376///         None
377///     ))
378/// }
379/// ```
380pub fn resource_content(
381    uri: impl Into<String>,
382    mime_type: Option<String>,
383    text: Option<String>,
384) -> Value {
385    let mut res = serde_json::json!({
386        "type": "resource",
387        "uri": uri.into()
388    });
389
390    if let Some(mt) = mime_type {
391        res["mimeType"] = serde_json::json!(mt);
392    }
393    if let Some(t) = text {
394        res["text"] = serde_json::json!(t);
395    }
396
397    serde_json::json!({
398        "content": [res]
399    })
400}
401
402// ============================================================================
403// Content Helpers - MCP-compliant content construction (for tool results)
404// ============================================================================
405
406/// Helper to create a multi-content response
407///
408/// Creates a response with multiple content items (text, images, resources):
409/// ```json
410/// {
411///   "content": [
412///     {"type": "text", "text": "..."},
413///     {"type": "image", "data": "...", "mimeType": "..."},
414///     {"type": "resource", "uri": "..."}
415///   ]
416/// }
417/// ```
418///
419/// # Example
420///
421/// ```ignore
422/// fn handle_get_product(args: &Value) -> Result<Value, String> {
423///     Ok(multi_content(vec![
424///         serde_json::json!({"type": "text", "text": "Product info"}),
425///         serde_json::json!({
426///             "type": "image",
427///             "data": base64_image,
428///             "mimeType": "image/jpeg"
429///         })
430///     ]))
431/// }
432/// ```
433pub fn multi_content(items: Vec<Value>) -> Value {
434    serde_json::json!({
435        "content": items
436    })
437}