Skip to main content

mcp_kit/plugin/
mod.rs

1//! Plugin system for dynamically loading and managing MCP tools, resources, and prompts.
2//!
3//! The plugin system allows you to:
4//! - Load tools/resources/prompts from external libraries
5//! - Hot reload plugins during development
6//! - Create a plugin ecosystem with shareable components
7//! - Sandbox untrusted plugins with WASM
8//!
9//! # Example
10//!
11//! ```rust,no_run
12//! use mcp_kit::prelude::*;
13//! use mcp_kit::plugin::{McpPlugin, PluginManager};
14//!
15//! #[tokio::main]
16//! async fn main() -> anyhow::Result<()> {
17//!     let mut plugin_manager = PluginManager::new();
18//!     
19//!     // Load plugins
20//!     plugin_manager.load_from_path("./plugins/weather.so")?;
21//!     
22//!     let server = McpServer::builder()
23//!         .name("plugin-server")
24//!         .version("1.0.0")
25//!         .with_plugin_manager(plugin_manager)
26//!         .build()
27//!         .serve_stdio()
28//!         .await?;
29//!     
30//!     Ok(())
31//! }
32//! ```
33
34use crate::error::{McpError, McpResult};
35use crate::types::prompt::Prompt;
36use crate::types::resource::Resource;
37use crate::types::tool::Tool;
38use serde::{Deserialize, Serialize};
39use std::collections::HashMap;
40use std::sync::Arc;
41
42#[cfg(feature = "plugin-native")]
43mod native;
44#[cfg(feature = "plugin-wasm")]
45pub mod wasm;
46
47pub mod registry;
48
49// ─── Plugin Trait ────────────────────────────────────────────────────────────
50
51/// Core trait that all MCP plugins must implement.
52///
53/// A plugin can provide tools, resources, and/or prompts that will be
54/// registered with the MCP server.
55pub trait McpPlugin: Send + Sync {
56    /// Plugin identifier (unique name)
57    fn name(&self) -> &str;
58
59    /// Plugin version (semver)
60    fn version(&self) -> &str;
61
62    /// Optional plugin description
63    fn description(&self) -> Option<&str> {
64        None
65    }
66
67    /// Plugin author/maintainer
68    fn author(&self) -> Option<&str> {
69        None
70    }
71
72    /// Minimum mcp-kit version required
73    fn min_mcp_version(&self) -> Option<&str> {
74        None
75    }
76
77    /// Register tools provided by this plugin
78    fn register_tools(&self) -> Vec<ToolDefinition> {
79        vec![]
80    }
81
82    /// Register resources provided by this plugin
83    fn register_resources(&self) -> Vec<ResourceDefinition> {
84        vec![]
85    }
86
87    /// Register prompts provided by this plugin
88    fn register_prompts(&self) -> Vec<PromptDefinition> {
89        vec![]
90    }
91
92    /// Called when plugin is loaded
93    fn on_load(&mut self, _config: &PluginConfig) -> McpResult<()> {
94        Ok(())
95    }
96
97    /// Called when plugin is unloaded
98    fn on_unload(&mut self) -> McpResult<()> {
99        Ok(())
100    }
101
102    /// Called to check if plugin can be safely unloaded
103    fn can_unload(&self) -> bool {
104        true
105    }
106}
107
108// ─── Plugin Definitions ──────────────────────────────────────────────────────
109
110// Type aliases for complex handler types
111type ToolHandlerFn = Arc<
112    dyn Fn(
113            crate::types::messages::CallToolRequest,
114        ) -> std::pin::Pin<
115            Box<
116                dyn std::future::Future<Output = McpResult<crate::types::tool::CallToolResult>>
117                    + Send
118                    + 'static,
119            >,
120        > + Send
121        + Sync
122        + 'static,
123>;
124
125type ResourceHandlerFn = Arc<
126    dyn Fn(
127            crate::types::messages::ReadResourceRequest,
128        ) -> std::pin::Pin<
129            Box<
130                dyn std::future::Future<
131                        Output = McpResult<crate::types::resource::ReadResourceResult>,
132                    > + Send
133                    + 'static,
134            >,
135        > + Send
136        + Sync
137        + 'static,
138>;
139
140type PromptHandlerFn = Arc<
141    dyn Fn(
142            crate::types::messages::GetPromptRequest,
143        ) -> std::pin::Pin<
144            Box<
145                dyn std::future::Future<Output = McpResult<crate::types::prompt::GetPromptResult>>
146                    + Send
147                    + 'static,
148            >,
149        > + Send
150        + Sync
151        + 'static,
152>;
153
154/// Tool definition from a plugin
155pub struct ToolDefinition {
156    pub tool: Tool,
157    pub handler: ToolHandlerFn,
158}
159
160impl ToolDefinition {
161    /// Create a tool definition with a typed handler
162    pub fn new<F, Fut, T>(tool: Tool, handler: F) -> Self
163    where
164        F: Fn(T) -> Fut + Send + Sync + Clone + 'static,
165        Fut: std::future::Future<Output = crate::types::tool::CallToolResult> + Send + 'static,
166        T: serde::de::DeserializeOwned + Send + 'static,
167    {
168        let handler = Arc::new(move |req: crate::types::messages::CallToolRequest| {
169            let f = handler.clone();
170            let args = req.arguments.clone();
171            Box::pin(async move {
172                let params: T = serde_json::from_value(args)
173                    .map_err(|e| McpError::InvalidParams(e.to_string()))?;
174                Ok(f(params).await)
175            })
176                as std::pin::Pin<
177                    Box<
178                        dyn std::future::Future<
179                                Output = McpResult<crate::types::tool::CallToolResult>,
180                            > + Send
181                            + 'static,
182                    >,
183                >
184        });
185
186        Self { tool, handler }
187    }
188
189    /// Create a tool definition with a raw handler that takes CallToolRequest
190    pub fn from_handler(tool: Tool, handler: ToolHandlerFn) -> Self {
191        Self { tool, handler }
192    }
193}
194
195/// Resource definition from a plugin
196pub struct ResourceDefinition {
197    pub resource: Resource,
198    pub handler: ResourceHandlerFn,
199}
200
201impl ResourceDefinition {
202    pub fn new<F, Fut>(resource: Resource, handler: F) -> Self
203    where
204        F: Fn(crate::types::messages::ReadResourceRequest) -> Fut + Send + Sync + Clone + 'static,
205        Fut: std::future::Future<Output = McpResult<crate::types::resource::ReadResourceResult>>
206            + Send
207            + 'static,
208    {
209        let handler = Arc::new(move |req| {
210            let f = handler.clone();
211            Box::pin(f(req))
212                as std::pin::Pin<
213                    Box<
214                        dyn std::future::Future<
215                                Output = McpResult<crate::types::resource::ReadResourceResult>,
216                            > + Send
217                            + 'static,
218                    >,
219                >
220        });
221
222        Self { resource, handler }
223    }
224}
225
226/// Prompt definition from a plugin
227pub struct PromptDefinition {
228    pub prompt: Prompt,
229    pub handler: PromptHandlerFn,
230}
231
232impl PromptDefinition {
233    pub fn new<F, Fut>(prompt: Prompt, handler: F) -> Self
234    where
235        F: Fn(crate::types::messages::GetPromptRequest) -> Fut + Send + Sync + Clone + 'static,
236        Fut: std::future::Future<Output = McpResult<crate::types::prompt::GetPromptResult>>
237            + Send
238            + 'static,
239    {
240        let handler = Arc::new(move |req| {
241            let f = handler.clone();
242            Box::pin(f(req))
243                as std::pin::Pin<
244                    Box<
245                        dyn std::future::Future<
246                                Output = McpResult<crate::types::prompt::GetPromptResult>,
247                            > + Send
248                            + 'static,
249                    >,
250                >
251        });
252
253        Self { prompt, handler }
254    }
255}
256
257// ─── Plugin Config ───────────────────────────────────────────────────────────
258
259/// Configuration for a plugin instance
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct PluginConfig {
262    /// Plugin-specific configuration (JSON)
263    #[serde(default)]
264    pub config: serde_json::Value,
265
266    /// Whether plugin is enabled
267    #[serde(default = "default_true")]
268    pub enabled: bool,
269
270    /// Plugin priority (higher = loaded first)
271    #[serde(default)]
272    pub priority: i32,
273
274    /// Plugin permissions
275    #[serde(default)]
276    pub permissions: PluginPermissions,
277}
278
279fn default_true() -> bool {
280    true
281}
282
283impl Default for PluginConfig {
284    fn default() -> Self {
285        Self {
286            config: serde_json::Value::Null,
287            enabled: true,
288            priority: 0,
289            permissions: PluginPermissions::default(),
290        }
291    }
292}
293
294/// Plugin permissions/capabilities
295#[derive(Debug, Clone, Default, Serialize, Deserialize)]
296pub struct PluginPermissions {
297    /// Can access network
298    #[serde(default)]
299    pub network: bool,
300
301    /// Can access filesystem
302    #[serde(default)]
303    pub filesystem: bool,
304
305    /// Can access environment variables
306    #[serde(default)]
307    pub env: bool,
308
309    /// Can spawn processes
310    #[serde(default)]
311    pub process: bool,
312
313    /// Custom permission flags
314    #[serde(default)]
315    pub custom: HashMap<String, bool>,
316}
317
318// ─── Plugin Metadata ─────────────────────────────────────────────────────────
319
320/// Metadata about a loaded plugin
321#[derive(Debug, Clone)]
322pub struct PluginMetadata {
323    pub name: String,
324    pub version: String,
325    pub description: Option<String>,
326    pub author: Option<String>,
327    pub min_mcp_version: Option<String>,
328    pub tool_count: usize,
329    pub resource_count: usize,
330    pub prompt_count: usize,
331}
332
333// ─── Plugin Manager ──────────────────────────────────────────────────────────
334
335/// Manages plugin lifecycle and registration
336pub struct PluginManager {
337    plugins: HashMap<String, Box<dyn McpPlugin>>,
338    configs: HashMap<String, PluginConfig>,
339    #[cfg(feature = "plugin-hot-reload")]
340    watcher: Option<notify::RecommendedWatcher>,
341}
342
343impl PluginManager {
344    /// Create a new plugin manager
345    pub fn new() -> Self {
346        Self {
347            plugins: HashMap::new(),
348            configs: HashMap::new(),
349            #[cfg(feature = "plugin-hot-reload")]
350            watcher: None,
351        }
352    }
353
354    /// Load a plugin from a dynamic library file (.so, .dylib, .dll)
355    #[cfg(feature = "plugin-native")]
356    pub fn load_from_path(&mut self, path: &str) -> McpResult<()> {
357        self.load_from_path_with_config(path, PluginConfig::default())
358    }
359
360    /// Load a plugin with custom configuration
361    #[cfg(feature = "plugin-native")]
362    pub fn load_from_path_with_config(
363        &mut self,
364        path: &str,
365        config: PluginConfig,
366    ) -> McpResult<()> {
367        if !config.enabled {
368            tracing::debug!("Plugin at {} is disabled, skipping", path);
369            return Ok(());
370        }
371
372        let mut plugin = native::load_plugin(path)?;
373        plugin.on_load(&config)?;
374
375        let name = plugin.name().to_string();
376        tracing::info!("Loaded plugin: {} v{}", name, plugin.version());
377
378        self.plugins.insert(name.clone(), plugin);
379        self.configs.insert(name, config);
380
381        Ok(())
382    }
383
384    /// Load a WASM plugin
385    #[cfg(feature = "plugin-wasm")]
386    pub fn load_wasm(&mut self, wasm_bytes: &[u8]) -> McpResult<()> {
387        self.load_wasm_with_config(wasm_bytes, PluginConfig::default())
388    }
389
390    /// Load a WASM plugin with custom configuration
391    #[cfg(feature = "plugin-wasm")]
392    pub fn load_wasm_with_config(
393        &mut self,
394        wasm_bytes: &[u8],
395        config: PluginConfig,
396    ) -> McpResult<()> {
397        if !config.enabled {
398            return Ok(());
399        }
400
401        let mut plugin = wasm::load_plugin(wasm_bytes)?;
402        plugin.on_load(&config)?;
403
404        let name = plugin.name().to_string();
405        tracing::info!("Loaded WASM plugin: {} v{}", name, plugin.version());
406
407        self.plugins.insert(name.clone(), plugin);
408        self.configs.insert(name, config);
409
410        Ok(())
411    }
412
413    /// Register a plugin directly (for in-process plugins)
414    pub fn register_plugin<P: McpPlugin + 'static>(
415        &mut self,
416        mut plugin: P,
417        config: PluginConfig,
418    ) -> McpResult<()> {
419        if !config.enabled {
420            return Ok(());
421        }
422
423        plugin.on_load(&config)?;
424
425        let name = plugin.name().to_string();
426        tracing::info!("Registered plugin: {} v{}", name, plugin.version());
427
428        self.plugins.insert(name.clone(), Box::new(plugin));
429        self.configs.insert(name, config);
430
431        Ok(())
432    }
433
434    /// Unload a plugin by name
435    pub fn unload(&mut self, name: &str) -> McpResult<()> {
436        if let Some(mut plugin) = self.plugins.remove(name) {
437            if !plugin.can_unload() {
438                // Put it back if can't unload
439                self.plugins.insert(name.to_string(), plugin);
440                return Err(McpError::InvalidRequest(format!(
441                    "Plugin {} cannot be unloaded",
442                    name
443                )));
444            }
445
446            plugin.on_unload()?;
447            self.configs.remove(name);
448            tracing::info!("Unloaded plugin: {}", name);
449        }
450
451        Ok(())
452    }
453
454    /// Get plugin metadata
455    pub fn get_metadata(&self, name: &str) -> Option<PluginMetadata> {
456        self.plugins.get(name).map(|plugin| {
457            let tools = plugin.register_tools();
458            let resources = plugin.register_resources();
459            let prompts = plugin.register_prompts();
460
461            PluginMetadata {
462                name: plugin.name().to_string(),
463                version: plugin.version().to_string(),
464                description: plugin.description().map(String::from),
465                author: plugin.author().map(String::from),
466                min_mcp_version: plugin.min_mcp_version().map(String::from),
467                tool_count: tools.len(),
468                resource_count: resources.len(),
469                prompt_count: prompts.len(),
470            }
471        })
472    }
473
474    /// List all loaded plugins
475    pub fn list_plugins(&self) -> Vec<PluginMetadata> {
476        self.plugins
477            .keys()
478            .filter_map(|name| self.get_metadata(name))
479            .collect()
480    }
481
482    /// Get all tool definitions from all plugins
483    pub(crate) fn collect_tools(&self) -> Vec<ToolDefinition> {
484        let mut tools = Vec::new();
485
486        // Sort plugins by priority
487        let mut plugins: Vec<_> = self.plugins.iter().collect();
488        plugins.sort_by_key(|(name, _)| {
489            self.configs
490                .get(*name)
491                .map(|c| -c.priority) // Negative for descending order
492                .unwrap_or(0)
493        });
494
495        for (_, plugin) in plugins {
496            tools.extend(plugin.register_tools());
497        }
498
499        tools
500    }
501
502    /// Get all resource definitions from all plugins
503    pub(crate) fn collect_resources(&self) -> Vec<ResourceDefinition> {
504        let mut resources = Vec::new();
505
506        let mut plugins: Vec<_> = self.plugins.iter().collect();
507        plugins.sort_by_key(|(name, _)| self.configs.get(*name).map(|c| -c.priority).unwrap_or(0));
508
509        for (_, plugin) in plugins {
510            resources.extend(plugin.register_resources());
511        }
512
513        resources
514    }
515
516    /// Get all prompt definitions from all plugins
517    pub(crate) fn collect_prompts(&self) -> Vec<PromptDefinition> {
518        let mut prompts = Vec::new();
519
520        let mut plugins: Vec<_> = self.plugins.iter().collect();
521        plugins.sort_by_key(|(name, _)| self.configs.get(*name).map(|c| -c.priority).unwrap_or(0));
522
523        for (_, plugin) in plugins {
524            prompts.extend(plugin.register_prompts());
525        }
526
527        prompts
528    }
529
530    /// Enable hot reload (watch for plugin file changes)
531    #[cfg(feature = "plugin-hot-reload")]
532    pub fn enable_hot_reload(&mut self) -> McpResult<()> {
533        use notify::{RecommendedWatcher, Watcher};
534
535        let (tx, _rx) = std::sync::mpsc::channel();
536
537        let watcher = RecommendedWatcher::new(tx, notify::Config::default())
538            .map_err(|e| McpError::internal(format!("Failed to create watcher: {}", e)))?;
539
540        // TODO: Watch plugin directories
541
542        self.watcher = Some(watcher);
543
544        tracing::info!("Hot reload enabled");
545        Ok(())
546    }
547}
548
549impl Default for PluginManager {
550    fn default() -> Self {
551        Self::new()
552    }
553}