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}