mcp_plugin_api/lib.rs
1//! MCP Plugin API - Interface definitions for plugin development
2//!
3//! This crate defines the C ABI interface between the MCP framework
4//! and plugins. Both the framework and plugins depend on this crate.
5//!
6//! ## Overview
7//!
8//! This crate provides two ways to create plugins:
9//!
10//! ### 1. High-Level API (Recommended)
11//!
12//! Use the `Tool` builder and `declare_tools!` macro for a clean, type-safe API:
13//!
14//! ```ignore
15//! use mcp_plugin_api::*;
16//! use serde_json::{json, Value};
17//!
18//! fn handle_hello(args: &Value) -> Result<Value, String> {
19//! let name = args["name"].as_str().unwrap_or("World");
20//! Ok(json!({ "message": format!("Hello, {}!", name) }))
21//! }
22//!
23//! declare_tools! {
24//! tools: [
25//! Tool::new("hello", "Say hello")
26//! .param_string("name", "Name to greet", false)
27//! .handler(handle_hello),
28//! ]
29//! }
30//!
31//! declare_plugin! {
32//! list_tools: generated_list_tools,
33//! execute_tool: generated_execute_tool,
34//! free_string: mcp_plugin_api::utils::standard_free_string
35//! }
36//! ```
37//!
38//! ### 2. Low-Level API
39//!
40//! Manually implement the three C functions for maximum control:
41//! - `list_tools`: Returns JSON array of available tools
42//! - `execute_tool`: Executes a tool by name
43//! - `free_string`: Deallocates plugin-allocated memory
44//!
45//! ## Memory Management
46//!
47//! The `utils` module provides safe wrappers for memory management:
48//! - `return_success`: Return a success result
49//! - `return_error`: Return an error result
50//! - `standard_free_string`: Standard deallocation function
51//!
52//! ## Thread Safety
53//!
54//! The `execute_tool` function will be called concurrently from multiple
55//! threads. Implementations must be thread-safe.
56
57use std::os::raw::c_char;
58
59// Re-export serde_json for use in macros
60pub use serde_json;
61
62// Re-export once_cell for configuration
63pub use once_cell;
64
65// Export sub-modules
66pub mod tool;
67pub mod utils;
68
69// Don't make macros a public module - macros are exported at crate root
70#[macro_use]
71mod macros;
72
73// Re-export commonly used items
74pub use tool::{ParamType, Tool, ToolBuilder, ToolHandler, ToolParam};
75
76// ============================================================================
77// ABI Type Aliases - Single Source of Truth
78// ============================================================================
79
80/// Function signature for listing available tools
81///
82/// Returns a JSON array of tool definitions.
83///
84/// # Parameters
85/// - `result_buf`: Output pointer for JSON array (allocated by plugin)
86/// - `result_len`: Output capacity of buffer
87///
88/// # Returns
89/// - 0 on success
90/// - Non-zero error code on failure
91pub type ListToolsFn = unsafe extern "C" fn(*mut *mut u8, *mut usize) -> i32;
92
93/// Function signature for executing a tool by name
94///
95/// # Parameters
96/// - `tool_name`: Null-terminated C string with tool name
97/// - `args_json`: JSON arguments as byte array
98/// - `args_len`: Length of args_json
99/// - `result_buf`: Output pointer for result (allocated by plugin)
100/// - `result_len`: Output capacity of result buffer
101///
102/// # Returns
103/// - 0 on success
104/// - Non-zero error code on failure
105pub type ExecuteToolFn = unsafe extern "C" fn(
106 *const c_char, // tool name
107 *const u8, // args JSON
108 usize, // args length
109 *mut *mut u8, // result buffer (allocated by plugin)
110 *mut usize, // result capacity
111) -> i32;
112
113/// Function signature for freeing memory allocated by the plugin
114///
115/// # Parameters
116/// - `ptr`: Pointer to memory to free
117/// - `capacity`: Capacity of the allocation (from the original allocation)
118pub type FreeStringFn = unsafe extern "C" fn(*mut u8, usize);
119
120/// Function signature for plugin configuration
121///
122/// # Parameters
123/// - `config_json`: JSON configuration as byte array
124/// - `config_len`: Length of config_json
125///
126/// # Returns
127/// - 0 on success
128/// - Non-zero error code on failure
129pub type ConfigureFn = unsafe extern "C" fn(*const u8, usize) -> i32;
130
131/// Function signature for plugin initialization
132///
133/// Called by the framework at the end of `handle_initialize`, after:
134/// - Plugin library is loaded
135/// - Configuration is set (via configure function if present)
136/// - But before any tools are registered or called
137///
138/// The plugin should use this to:
139/// - Validate configuration
140/// - Initialize resources (database connections, caches, etc.)
141/// - Perform any expensive setup operations
142/// - Report initialization errors
143///
144/// # Parameters
145/// - `error_msg_ptr`: Output pointer for error message (on failure)
146/// - `error_msg_len`: Output length of error message (on failure)
147///
148/// # Returns
149/// - 0 on success
150/// - Non-zero error code on failure
151///
152/// If initialization fails, the plugin should allocate an error message,
153/// write the pointer and length to the output parameters, and return non-zero.
154/// The framework will call `free_string` to deallocate the error message.
155pub type InitFn =
156 unsafe extern "C" fn(error_msg_ptr: *mut *mut u8, error_msg_len: *mut usize) -> i32;
157
158/// Function signature for getting plugin configuration schema
159///
160/// This function returns a JSON Schema describing the plugin's configuration structure.
161/// It's used by clients to:
162/// - Validate configuration before sending
163/// - Generate UI for configuration
164/// - Document configuration requirements
165///
166/// The schema should follow JSON Schema Draft 7 format.
167///
168/// # Parameters
169/// - `schema_ptr`: Output pointer for schema JSON string
170/// - `schema_len`: Output length of schema JSON string
171///
172/// # Returns
173/// - 0 on success
174/// - Non-zero if schema generation fails
175///
176/// The framework will call `free_string` to deallocate the schema string.
177pub type GetConfigSchemaFn = unsafe extern "C" fn(
178 schema_ptr: *mut *mut u8,
179 schema_len: *mut usize,
180) -> i32;
181
182// ============================================================================
183// Plugin Declaration
184// ============================================================================
185
186/// Plugin declaration exported by each plugin
187///
188/// This structure must be exported as a static with the name `plugin_declaration`.
189/// Use the `declare_plugin!` macro for automatic version management.
190#[repr(C)]
191pub struct PluginDeclaration {
192 /// MCP Plugin API version the plugin was built against (e.g., "0.1.0")
193 ///
194 /// This is automatically set from the mcp-plugin-api crate version.
195 /// The C ABI is stable across Rust compiler versions, so only the API
196 /// version matters for compatibility checking.
197 pub api_version: *const u8,
198
199 /// Returns list of tools as JSON array
200 ///
201 /// See [`ListToolsFn`] for details.
202 pub list_tools: ListToolsFn,
203
204 /// Execute a tool by name
205 ///
206 /// See [`ExecuteToolFn`] for details.
207 pub execute_tool: ExecuteToolFn,
208
209 /// Function to free memory allocated by the plugin
210 ///
211 /// See [`FreeStringFn`] for details.
212 pub free_string: FreeStringFn,
213
214 /// Optional configuration function called after plugin is loaded
215 ///
216 /// See [`ConfigureFn`] for details.
217 pub configure: Option<ConfigureFn>,
218
219 /// Optional initialization function called after configuration
220 ///
221 /// See [`InitFn`] for details.
222 pub init: Option<InitFn>,
223
224 /// Optional function to get configuration schema
225 ///
226 /// See [`GetConfigSchemaFn`] for details.
227 pub get_config_schema: Option<GetConfigSchemaFn>,
228}
229
230// Safety: The static is initialized with constant values and never modified
231unsafe impl Sync for PluginDeclaration {}
232
233/// Current MCP Plugin API version (from Cargo.toml at compile time)
234pub const API_VERSION: &str = env!("CARGO_PKG_VERSION");
235
236/// API version as a null-terminated C string (for PluginDeclaration)
237pub const API_VERSION_CSTR: &[u8] = concat!(env!("CARGO_PKG_VERSION"), "\0").as_bytes();
238
239/// Helper macro to declare a plugin with automatic version management
240///
241/// # Example
242///
243/// ```ignore
244/// use mcp_plugin_api::*;
245///
246/// // Minimal (no configuration, no init)
247/// declare_plugin! {
248/// list_tools: my_list_tools,
249/// execute_tool: my_execute_tool,
250/// free_string: my_free_string
251/// }
252///
253/// // With configuration
254/// declare_plugin! {
255/// list_tools: my_list_tools,
256/// execute_tool: my_execute_tool,
257/// free_string: my_free_string,
258/// configure: my_configure
259/// }
260///
261/// // With configuration and init
262/// declare_plugin! {
263/// list_tools: my_list_tools,
264/// execute_tool: my_execute_tool,
265/// free_string: my_free_string,
266/// configure: my_configure,
267/// init: my_init
268/// }
269/// ```
270#[macro_export]
271macro_rules! declare_plugin {
272 (
273 list_tools: $list_fn:expr,
274 execute_tool: $execute_fn:expr,
275 free_string: $free_fn:expr
276 $(, configure: $configure_fn:expr)?
277 $(, init: $init_fn:expr)?
278 $(, get_config_schema: $schema_fn:expr)?
279 ) => {
280 #[no_mangle]
281 pub static plugin_declaration: $crate::PluginDeclaration = $crate::PluginDeclaration {
282 api_version: $crate::API_VERSION_CSTR.as_ptr(),
283 list_tools: $list_fn,
284 execute_tool: $execute_fn,
285 free_string: $free_fn,
286 configure: $crate::__declare_plugin_option!($($configure_fn)?),
287 init: $crate::__declare_plugin_option!($($init_fn)?),
288 get_config_schema: $crate::__declare_plugin_option!($($schema_fn)?),
289 };
290 };
291}
292
293/// Helper macro for optional parameters in declare_plugin!
294#[doc(hidden)]
295#[macro_export]
296macro_rules! __declare_plugin_option {
297 ($value:expr) => {
298 Some($value)
299 };
300 () => {
301 None
302 };
303}
304
305/// Declare a plugin initialization function with automatic wrapper generation
306///
307/// This macro takes a native Rust function and wraps it as an `extern "C"` function
308/// that can be used in `declare_plugin!`. The native function should have the signature:
309///
310/// ```ignore
311/// fn my_init() -> Result<(), String>
312/// ```
313///
314/// The macro generates a C ABI wrapper function named `plugin_init` that:
315/// - Calls your native init function
316/// - Handles the FFI error reporting
317/// - Returns appropriate error codes
318///
319/// # Example
320///
321/// ```ignore
322/// use mcp_plugin_api::*;
323/// use once_cell::sync::OnceCell;
324///
325/// static DB_POOL: OnceCell<DatabasePool> = OnceCell::new();
326///
327/// // Native Rust init function
328/// fn init() -> Result<(), String> {
329/// let config = get_config();
330///
331/// // Initialize database
332/// let pool = connect_to_db(&config.database_url)
333/// .map_err(|e| format!("Failed to connect: {}", e))?;
334///
335/// // Validate connection
336/// pool.ping()
337/// .map_err(|e| format!("DB ping failed: {}", e))?;
338///
339/// DB_POOL.set(pool)
340/// .map_err(|_| "DB already initialized".to_string())?;
341///
342/// Ok(())
343/// }
344///
345/// // Generate the C ABI wrapper
346/// declare_plugin_init!(init);
347///
348/// // Use in plugin declaration
349/// declare_plugin! {
350/// list_tools: generated_list_tools,
351/// execute_tool: generated_execute_tool,
352/// free_string: utils::standard_free_string,
353/// configure: plugin_configure,
354/// init: plugin_init // ← Generated by declare_plugin_init!
355/// }
356/// ```
357#[macro_export]
358macro_rules! declare_plugin_init {
359 ($native_fn:ident) => {
360 /// Auto-generated initialization function for plugin ABI
361 ///
362 /// This function is called by the framework after configuration
363 /// and before any tools are registered or called.
364 #[no_mangle]
365 pub unsafe extern "C" fn plugin_init(
366 error_msg_ptr: *mut *mut ::std::primitive::u8,
367 error_msg_len: *mut ::std::primitive::usize,
368 ) -> ::std::primitive::i32 {
369 match $native_fn() {
370 ::std::result::Result::Ok(_) => 0, // Success
371 ::std::result::Result::Err(e) => {
372 $crate::utils::return_error(&e, error_msg_ptr, error_msg_len)
373 }
374 }
375 }
376 };
377}
378
379/// Declare configuration schema export with automatic generation
380///
381/// This macro generates an `extern "C"` function that exports the plugin's
382/// configuration schema in JSON Schema format. It uses the `schemars` crate
383/// to automatically generate the schema from your configuration struct.
384///
385/// The config type must derive `JsonSchema` from the `schemars` crate.
386///
387/// # Example
388///
389/// ```ignore
390/// use mcp_plugin_api::*;
391/// use serde::Deserialize;
392/// use schemars::JsonSchema;
393///
394/// #[derive(Debug, Clone, Deserialize, JsonSchema)]
395/// struct PluginConfig {
396/// /// PostgreSQL connection URL
397/// #[schemars(example = "example_db_url")]
398/// database_url: String,
399///
400/// /// Maximum database connections
401/// #[schemars(range(min = 1, max = 100))]
402/// #[serde(default = "default_max_connections")]
403/// max_connections: u32,
404/// }
405///
406/// fn example_db_url() -> &'static str {
407/// "postgresql://user:pass@localhost:5432/dbname"
408/// }
409///
410/// declare_plugin_config!(PluginConfig);
411/// declare_config_schema!(PluginConfig); // ← Generates plugin_get_config_schema
412///
413/// declare_plugin! {
414/// list_tools: generated_list_tools,
415/// execute_tool: generated_execute_tool,
416/// free_string: utils::standard_free_string,
417/// configure: plugin_configure,
418/// get_config_schema: plugin_get_config_schema // ← Use generated function
419/// }
420/// ```
421#[macro_export]
422macro_rules! declare_config_schema {
423 ($config_type:ty) => {
424 /// Auto-generated function to export configuration schema
425 ///
426 /// This function is called by the framework (via --get-plugin-schema)
427 /// to retrieve the JSON Schema for this plugin's configuration.
428 #[no_mangle]
429 pub unsafe extern "C" fn plugin_get_config_schema(
430 schema_ptr: *mut *mut ::std::primitive::u8,
431 schema_len: *mut ::std::primitive::usize,
432 ) -> ::std::primitive::i32 {
433 use schemars::schema_for;
434
435 let schema = schema_for!($config_type);
436 let schema_json = match $crate::serde_json::to_string(&schema) {
437 ::std::result::Result::Ok(s) => s,
438 ::std::result::Result::Err(e) => {
439 ::std::eprintln!("Failed to serialize schema: {}", e);
440 return 1;
441 }
442 };
443
444 // Convert to bytes and return using standard pattern
445 let mut vec = schema_json.into_bytes();
446 vec.shrink_to_fit();
447
448 *schema_len = vec.capacity();
449 *schema_ptr = vec.as_mut_ptr();
450 let _ = ::std::mem::ManuallyDrop::new(vec);
451
452 0 // Success
453 }
454 };
455}
456
457/// Declare plugin configuration with automatic boilerplate generation
458///
459/// This macro generates:
460/// - Static storage for the configuration (`OnceCell`)
461/// - `get_config()` function to access the configuration
462/// - `try_get_config()` function for optional access
463/// - `plugin_configure()` C ABI function for the framework
464///
465/// # Example
466///
467/// ```ignore
468/// use mcp_plugin_api::*;
469/// use serde::Deserialize;
470///
471/// #[derive(Debug, Clone, Deserialize)]
472/// struct PluginConfig {
473/// database_url: String,
474/// max_connections: u32,
475/// }
476///
477/// // Generate all configuration boilerplate
478/// declare_plugin_config!(PluginConfig);
479///
480/// // Use in handlers
481/// fn my_handler(args: &Value) -> Result<Value, String> {
482/// let config = get_config();
483/// // Use config.database_url, etc.
484/// Ok(json!({"status": "ok"}))
485/// }
486///
487/// declare_plugin! {
488/// list_tools: generated_list_tools,
489/// execute_tool: generated_execute_tool,
490/// free_string: mcp_plugin_api::utils::standard_free_string,
491/// configure: plugin_configure // Auto-generated by declare_plugin_config!
492/// }
493/// ```
494#[macro_export]
495macro_rules! declare_plugin_config {
496 ($config_type:ty) => {
497 // Generate static storage
498 static __PLUGIN_CONFIG: $crate::once_cell::sync::OnceCell<$config_type> =
499 $crate::once_cell::sync::OnceCell::new();
500
501 /// Get plugin configuration
502 ///
503 /// # Panics
504 ///
505 /// Panics if the plugin has not been configured yet. The framework calls
506 /// `plugin_configure()` during plugin loading, so this should only panic
507 /// if called before the plugin is fully loaded.
508 pub fn get_config() -> &'static $config_type {
509 __PLUGIN_CONFIG
510 .get()
511 .expect("Plugin not configured - configure() must be called first")
512 }
513
514 /// Try to get plugin configuration
515 ///
516 /// Returns `None` if the plugin has not been configured yet.
517 /// Use this if you need to check configuration availability.
518 pub fn try_get_config() -> ::std::option::Option<&'static $config_type> {
519 __PLUGIN_CONFIG.get()
520 }
521
522 /// Auto-generated configuration function
523 ///
524 /// This function is called by the framework during plugin loading.
525 /// It parses the JSON configuration and stores it in a static.
526 ///
527 /// # Returns
528 /// - 0 on success
529 /// - 1 on JSON parsing error
530 /// - 2 if plugin is already configured
531 #[no_mangle]
532 pub unsafe extern "C" fn plugin_configure(
533 config_json: *const ::std::primitive::u8,
534 config_len: ::std::primitive::usize,
535 ) -> ::std::primitive::i32 {
536 // Parse configuration
537 let config_slice = ::std::slice::from_raw_parts(config_json, config_len);
538 let config: $config_type = match $crate::serde_json::from_slice(config_slice) {
539 ::std::result::Result::Ok(c) => c,
540 ::std::result::Result::Err(e) => {
541 ::std::eprintln!("Failed to parse plugin config: {}", e);
542 return 1; // Error code
543 }
544 };
545
546 // Store globally
547 if __PLUGIN_CONFIG.set(config).is_err() {
548 ::std::eprintln!("Plugin already configured");
549 return 2;
550 }
551
552 0 // Success
553 }
554 };
555}