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}