Skip to main content

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}