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}