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}