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//!
6//! All returned buffers use `into_boxed_slice()` to guarantee `len == capacity`,
7//! so `standard_free_string(ptr, len)` can safely reconstruct and drop a `Vec`.
8
9use serde_json::Value;
10
11/// Return a success result to the framework
12///
13/// Serializes `data` to JSON, allocates a buffer with `len == capacity`, and
14/// writes the pointer and length to the output parameters.
15///
16/// # Safety
17///
18/// `result_buf` and `result_len` must point to valid, properly aligned,
19/// non-aliased memory that remains valid for the duration of the call.
20///
21/// # Example
22///
23/// ```ignore
24/// unsafe {
25///     let result = json!({"status": "ok"});
26///     return return_success(result, result_buf, result_len);
27/// }
28/// ```
29pub unsafe fn return_success(data: Value, result_buf: *mut *mut u8, result_len: *mut usize) -> i32 {
30    prepare_result(data, result_buf, result_len);
31    0
32}
33
34/// Return an error result to the framework
35///
36/// Wraps the error message in `{ "error": "..." }` and returns error code 1.
37///
38/// # Safety
39///
40/// Same requirements as [`return_success`].
41///
42/// # Example
43///
44/// ```ignore
45/// unsafe {
46///     return return_error("Product not found", result_buf, result_len);
47/// }
48/// ```
49pub unsafe fn return_error(error: &str, result_buf: *mut *mut u8, result_len: *mut usize) -> i32 {
50    let error_json = serde_json::json!({ "error": error });
51    prepare_result(error_json, result_buf, result_len);
52    1
53}
54
55/// Serialize a `Value` and hand the buffer to the framework.
56///
57/// Uses `into_boxed_slice()` so `len == capacity` is guaranteed — no reliance
58/// on `shrink_to_fit` (which is only a hint).
59///
60/// # Safety
61///
62/// Same requirements as [`return_success`].
63pub unsafe fn prepare_result(data: Value, result_buf: *mut *mut u8, result_len: *mut usize) {
64    let boxed = data.to_string().into_bytes().into_boxed_slice();
65    let len = boxed.len();
66    *result_buf = Box::into_raw(boxed) as *mut u8;
67    *result_len = len;
68}
69
70/// Copy pre-serialized bytes into a fresh allocation and hand it to the framework.
71///
72/// Used by `generated_list_*` functions that cache their JSON response in a
73/// `OnceLock<Vec<u8>>`. Each call does one `memcpy` instead of rebuilding the
74/// `Value` tree and re-serializing.
75///
76/// # Safety
77///
78/// Same requirements as [`return_success`].
79pub unsafe fn return_prebuilt(bytes: &[u8], result_buf: *mut *mut u8, result_len: *mut usize) -> i32 {
80    let boxed = bytes.to_vec().into_boxed_slice();
81    let len = boxed.len();
82    *result_buf = Box::into_raw(boxed) as *mut u8;
83    *result_len = len;
84    0
85}
86
87/// Build MCP resources/list response JSON
88///
89/// Creates `{ "resources": [...], "nextCursor"?: "..." }` per MCP spec.
90pub fn resource_list_response(
91    resources: Vec<Value>,
92    next_cursor: Option<&str>,
93) -> Value {
94    let mut obj = serde_json::Map::new();
95    obj.insert("resources".to_string(), Value::Array(resources));
96    if let Some(c) = next_cursor {
97        obj.insert("nextCursor".to_string(), serde_json::json!(c));
98    }
99    Value::Object(obj)
100}
101
102/// Build MCP resources/read response JSON
103///
104/// Creates `{ "contents": [...] }` per MCP spec.
105pub fn resource_read_response(contents: &[crate::resource::ResourceContent]) -> Value {
106    let items: Vec<Value> = contents.iter().map(|c| c.to_json()).collect();
107    serde_json::json!({ "contents": items })
108}
109
110/// Build MCP resources/templates/list response JSON
111///
112/// Creates `{ "resourceTemplates": [...], "nextCursor"?: "..." }` per MCP spec.
113pub fn resource_template_list_response(
114    resource_templates: Vec<Value>,
115    next_cursor: Option<&str>,
116) -> Value {
117    let mut obj = serde_json::Map::new();
118    obj.insert(
119        "resourceTemplates".to_string(),
120        Value::Array(resource_templates),
121    );
122    if let Some(c) = next_cursor {
123        obj.insert("nextCursor".to_string(), serde_json::json!(c));
124    }
125    Value::Object(obj)
126}
127
128/// Standard free_string implementation
129///
130/// Safely deallocates a buffer previously returned by [`return_success`],
131/// [`return_error`], or [`return_prebuilt`]. All of these guarantee
132/// `len == capacity` (via `into_boxed_slice()`), so the second parameter
133/// serves as both.
134///
135/// # Safety
136///
137/// `ptr` and `len` must exactly match values previously written to
138/// `result_buf` / `result_len` by this crate's return helpers.
139///
140/// # Example
141///
142/// ```ignore
143/// declare_plugin! {
144///     list_tools: generated_list_tools,
145///     execute_tool: generated_execute_tool,
146///     free_string: mcp_plugin_api::utils::standard_free_string
147/// }
148/// ```
149pub unsafe extern "C" fn standard_free_string(ptr: *mut u8, len: usize) {
150    if !ptr.is_null() && len > 0 {
151        let _ = Vec::from_raw_parts(ptr, len, len);
152    }
153}
154
155// ============================================================================
156// Content Helpers - MCP-compliant content construction
157// ============================================================================
158
159/// Helper to create a text content response
160///
161/// Creates a standard MCP text content response:
162/// ```json
163/// {
164///   "content": [{
165///     "type": "text",
166///     "text": "your text here"
167///   }]
168/// }
169/// ```
170///
171/// # Example
172///
173/// ```ignore
174/// fn handle_get_price(args: &Value) -> Result<Value, String> {
175///     let price = 29.99;
176///     Ok(text_content(format!("Price: ${:.2}", price)))
177/// }
178/// ```
179pub fn text_content(text: impl Into<String>) -> Value {
180    serde_json::json!({
181        "content": [{
182            "type": "text",
183            "text": text.into()
184        }]
185    })
186}
187
188/// Helper to create a JSON content response
189///
190/// Creates a standard MCP JSON content response with structured data:
191/// ```json
192/// {
193///   "content": [{
194///     "type": "json",
195///     "json": { ... }
196///   }]
197/// }
198/// ```
199///
200/// **Use case**: Structured data for programmatic clients
201///
202/// # Example
203///
204/// ```ignore
205/// fn handle_get_product(args: &Value) -> Result<Value, String> {
206///     let product = get_product_from_db()?;
207///     Ok(json_content(serde_json::to_value(product)?))
208/// }
209/// ```
210pub fn json_content(json: Value) -> Value {
211    serde_json::json!({
212        "content": [{
213            "type": "json",
214            "json": json
215        }]
216    })
217}
218
219/// Helper to create an HTML content response
220///
221/// Creates a standard MCP HTML content response:
222/// ```json
223/// {
224///   "content": [{
225///     "type": "html",
226///     "html": "<div>...</div>"
227///   }]
228/// }
229/// ```
230///
231/// **Use case**: Rich HTML content for UIs
232///
233/// # Example
234///
235/// ```ignore
236/// fn handle_get_formatted(args: &Value) -> Result<Value, String> {
237///     let html = format!("<div><h1>{}</h1><p>{}</p></div>", title, body);
238///     Ok(html_content(html))
239/// }
240/// ```
241pub fn html_content(html: impl Into<String>) -> Value {
242    serde_json::json!({
243        "content": [{
244            "type": "html",
245            "html": html.into()
246        }]
247    })
248}
249
250/// Helper to create a Markdown content response
251///
252/// Creates a standard MCP Markdown content response:
253/// ```json
254/// {
255///   "content": [{
256///     "type": "markdown",
257///     "markdown": "# Title\n\nContent..."
258///   }]
259/// }
260/// ```
261///
262/// **Use case**: Formatted text for chat clients
263///
264/// # Example
265///
266/// ```ignore
267/// fn handle_get_readme(args: &Value) -> Result<Value, String> {
268///     let markdown = format!("# {}\n\n{}", title, content);
269///     Ok(markdown_content(markdown))
270/// }
271/// ```
272pub fn markdown_content(markdown: impl Into<String>) -> Value {
273    serde_json::json!({
274        "content": [{
275            "type": "markdown",
276            "markdown": markdown.into()
277        }]
278    })
279}
280
281/// Helper to create an image content response with URL
282///
283/// Creates a standard MCP image content response with image URL:
284/// ```json
285/// {
286///   "content": [{
287///     "type": "image",
288///     "imageUrl": "https://example.com/image.png",
289///     "mimeType": "image/png"
290///   }]
291/// }
292/// ```
293///
294/// **Use case**: Return image by URL reference
295///
296/// # Example
297///
298/// ```ignore
299/// fn handle_get_product_image(args: &Value) -> Result<Value, String> {
300///     let url = format!("https://cdn.example.com/products/{}.jpg", product_id);
301///     Ok(image_url_content(url, Some("image/jpeg".to_string())))
302/// }
303/// ```
304pub fn image_url_content(url: impl Into<String>, mime_type: Option<String>) -> Value {
305    let mut img = serde_json::json!({
306        "type": "image",
307        "imageUrl": url.into()
308    });
309    
310    if let Some(mt) = mime_type {
311        img["mimeType"] = serde_json::json!(mt);
312    }
313    
314    serde_json::json!({
315        "content": [img]
316    })
317}
318
319/// Helper to create an image content response with base64 data
320///
321/// Creates a standard MCP image content response with embedded data:
322/// ```json
323/// {
324///   "content": [{
325///     "type": "image",
326///     "imageData": "base64-encoded-data",
327///     "mimeType": "image/png"
328///   }]
329/// }
330/// ```
331///
332/// **Use case**: Return embedded image data
333///
334/// # Example
335///
336/// ```ignore
337/// fn handle_get_chart(args: &Value) -> Result<Value, String> {
338///     let chart_bytes = generate_chart()?;
339///     let base64_data = base64::encode(chart_bytes);
340///     Ok(image_data_content(base64_data, Some("image/png".to_string())))
341/// }
342/// ```
343pub fn image_data_content(data: impl Into<String>, mime_type: Option<String>) -> Value {
344    let mut img = serde_json::json!({
345        "type": "image",
346        "imageData": data.into()
347    });
348    
349    if let Some(mt) = mime_type {
350        img["mimeType"] = serde_json::json!(mt);
351    }
352    
353    serde_json::json!({
354        "content": [img]
355    })
356}
357
358/// Helper to create an image content response (legacy)
359///
360/// **DEPRECATED**: Use `image_url_content` or `image_data_content` instead.
361///
362/// This function is kept for backward compatibility but uses the old field name.
363#[deprecated(since = "0.2.0", note = "Use image_url_content or image_data_content instead")]
364pub fn image_content(data: impl Into<String>, mime_type: impl Into<String>) -> Value {
365    image_data_content(data, Some(mime_type.into()))
366}
367
368/// Helper to create a resource content response
369///
370/// Creates a standard MCP resource content response:
371/// ```json
372/// {
373///   "content": [{
374///     "type": "resource",
375///     "uri": "https://example.com/resource",
376///     "mimeType": "text/html",  // optional
377///     "text": "content"         // optional
378///   }]
379/// }
380/// ```
381///
382/// # Example
383///
384/// ```ignore
385/// fn handle_get_resource(args: &Value) -> Result<Value, String> {
386///     Ok(resource_content(
387///         "https://example.com/docs",
388///         Some("text/html".to_string()),
389///         None
390///     ))
391/// }
392/// ```
393pub fn resource_content(
394    uri: impl Into<String>,
395    mime_type: Option<String>,
396    text: Option<String>,
397) -> Value {
398    let mut res = serde_json::json!({
399        "type": "resource",
400        "uri": uri.into()
401    });
402
403    if let Some(mt) = mime_type {
404        res["mimeType"] = serde_json::json!(mt);
405    }
406    if let Some(t) = text {
407        res["text"] = serde_json::json!(t);
408    }
409
410    serde_json::json!({
411        "content": [res]
412    })
413}
414
415// ============================================================================
416// Content Helpers - MCP-compliant content construction (for tool results)
417// ============================================================================
418
419/// Helper to create a multi-content response
420///
421/// Creates a response with multiple content items (text, images, resources):
422/// ```json
423/// {
424///   "content": [
425///     {"type": "text", "text": "..."},
426///     {"type": "image", "data": "...", "mimeType": "..."},
427///     {"type": "resource", "uri": "..."}
428///   ]
429/// }
430/// ```
431///
432/// # Example
433///
434/// ```ignore
435/// fn handle_get_product(args: &Value) -> Result<Value, String> {
436///     Ok(multi_content(vec![
437///         serde_json::json!({"type": "text", "text": "Product info"}),
438///         serde_json::json!({
439///             "type": "image",
440///             "data": base64_image,
441///             "mimeType": "image/jpeg"
442///         })
443///     ]))
444/// }
445/// ```
446pub fn multi_content(items: Vec<Value>) -> Value {
447    serde_json::json!({
448        "content": items
449    })
450}