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