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