Skip to main content

prism_mcp_rs/plugin/
loader.rs

1//! Dynamic plugin loader
2//!
3//! Module handles the low-level loading of plugin libraries from disk.
4
5use crate::plugin::{PluginError, PluginMetadata, PluginResult, ToolPlugin};
6use libloading::{Library, Symbol};
7use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use tokio::sync::RwLock;
11use tracing::{error, info};
12
13/// Plugin loader for dynamic libraries
14pub struct PluginLoader {
15    /// Loaded libraries
16    libraries: HashMap<String, LoadedPlugin>,
17
18    /// Plugin search paths
19    search_paths: Vec<PathBuf>,
20}
21
22/// Loaded plugin information
23struct LoadedPlugin {
24    /// The dynamic library
25    #[allow(dead_code)]
26    library: Library,
27
28    /// Plugin metadata
29    metadata: PluginMetadata,
30
31    /// Path to the plugin file
32    path: PathBuf,
33
34    /// Plugin instance wrapped in Arc<RwLock>
35    instance: Arc<RwLock<Box<dyn ToolPlugin>>>,
36}
37
38impl PluginLoader {
39    /// Create a new plugin loader
40    pub fn new() -> Self {
41        Self {
42            libraries: HashMap::new(),
43            search_paths: vec![
44                PathBuf::from("./plugins"),
45                PathBuf::from("/usr/local/lib/prism-mcp-plugins"),
46                PathBuf::from("~/.mcp/plugins"),
47            ],
48        }
49    }
50
51    /// Add a search path
52    pub fn add_search_path(&mut self, path: impl Into<PathBuf>) {
53        self.search_paths.push(path.into());
54    }
55
56    /// Load a plugin from a file
57    pub fn load_plugin(&mut self, path: &Path) -> PluginResult<Arc<RwLock<Box<dyn ToolPlugin>>>> {
58        info!("Loading plugin from: {:?}", path);
59
60        // Check if already loaded
61        let path_str = path.to_string_lossy().to_string();
62        if self.libraries.contains_key(&path_str) {
63            return Err(PluginError::AlreadyLoaded(path_str));
64        }
65
66        // Load the library
67        let library = unsafe {
68            Library::new(path).map_err(|e| {
69                error!("Failed to load library: {}", e);
70                PluginError::LoadFailed(e.to_string())
71            })?
72        };
73
74        // Get the plugin creation function with correct signature
75        let create_fn: Symbol<unsafe extern "C" fn() -> *mut Box<dyn ToolPlugin>> = unsafe {
76            library.get(b"_mcp_plugin_create\0").map_err(|e| {
77                error!("Plugin missing _mcp_plugin_create function: {}", e);
78                PluginError::InvalidPlugin("Missing _mcp_plugin_create export".to_string())
79            })?
80        };
81
82        // Create plugin instance safely
83        let instance = unsafe {
84            let raw_box = create_fn();
85            if raw_box.is_null() {
86                return Err(PluginError::InvalidPlugin(
87                    "Plugin creation returned null".to_string(),
88                ));
89            }
90            *Box::from_raw(raw_box)
91        };
92
93        // Skip initialization here - it will be done by the manager in an async context
94
95        // Get metadata
96        let metadata = instance.metadata();
97        info!("Loaded plugin: {} v{}", metadata.name, metadata.version);
98
99        // Store the loaded plugin
100        let instance_arc = Arc::new(RwLock::new(instance));
101        let loaded = LoadedPlugin {
102            library,
103            metadata: metadata.clone(),
104            path: path.to_path_buf(),
105            instance: instance_arc.clone(),
106        };
107
108        self.libraries.insert(path_str, loaded);
109        Ok(instance_arc)
110    }
111
112    /// Unload a plugin
113    pub fn unload_plugin(&mut self, plugin_id: &str) -> PluginResult<()> {
114        info!("Unloading plugin: {}", plugin_id);
115
116        // Find and remove the plugin
117        let path_to_remove = self
118            .libraries
119            .iter()
120            .find(|(_, p)| p.metadata.id == plugin_id)
121            .map(|(path, _)| path.clone());
122
123        if let Some(path) = path_to_remove {
124            // Remove from libraries map
125            if let Some(loaded) = self.libraries.remove(&path) {
126                // Note: The plugin will be properly shut down by the manager
127                // before this is called
128                info!("Plugin {} unloaded", loaded.metadata.id);
129                Ok(())
130            } else {
131                Err(PluginError::NotFound(plugin_id.to_string()))
132            }
133        } else {
134            Err(PluginError::NotFound(plugin_id.to_string()))
135        }
136    }
137
138    /// Reload a plugin - returns the existing instance if reload is not needed
139    pub fn reload_plugin(
140        &mut self,
141        plugin_id: &str,
142    ) -> PluginResult<Arc<RwLock<Box<dyn ToolPlugin>>>> {
143        info!("Reloading plugin: {}", plugin_id);
144
145        // Find the plugin path and get existing instance
146        let (_plugin_path, existing_instance) = self
147            .libraries
148            .iter()
149            .find(|(_, p)| p.metadata.id == plugin_id)
150            .map(|(_, p)| (p.path.clone(), p.instance.clone()))
151            .ok_or_else(|| PluginError::NotFound(plugin_id.to_string()))?;
152
153        // For now, return the existing instance to avoid runtime issues
154        // In production, you'd implement proper hot-reloading here
155        info!(
156            "Plugin reload requested for {}, returning existing instance",
157            plugin_id
158        );
159        Ok(existing_instance)
160    }
161
162    /// Find a plugin file by name
163    pub fn find_plugin(&self, name: &str) -> Option<PathBuf> {
164        let extensions = if cfg!(windows) {
165            vec!["dll"]
166        } else if cfg!(target_os = "macos") {
167            vec!["dylib"]
168        } else {
169            vec!["so"]
170        };
171
172        for search_path in &self.search_paths {
173            for ext in &extensions {
174                let path = search_path.join(format!("{name}.{ext}"));
175                if path.exists() {
176                    return Some(path);
177                }
178
179                // Also try with lib prefix on Unix
180                if !cfg!(windows) {
181                    let path = search_path.join(format!("lib{name}.{ext}"));
182                    if path.exists() {
183                        return Some(path);
184                    }
185                }
186            }
187        }
188
189        None
190    }
191
192    /// Get a plugin by ID
193    pub fn get_plugin(&self, plugin_id: &str) -> Option<Arc<RwLock<Box<dyn ToolPlugin>>>> {
194        self.libraries
195            .values()
196            .find(|p| p.metadata.id == plugin_id)
197            .map(|p| p.instance.clone())
198    }
199
200    /// List all loaded plugins
201    pub fn list_plugins(&self) -> Vec<PluginMetadata> {
202        self.libraries
203            .values()
204            .map(|p| p.metadata.clone())
205            .collect()
206    }
207
208    /// Get plugin by path
209    pub fn get_plugin_by_path(&self, path: &str) -> Option<Arc<RwLock<Box<dyn ToolPlugin>>>> {
210        self.libraries.get(path).map(|p| p.instance.clone())
211    }
212}
213
214impl Default for PluginLoader {
215    fn default() -> Self {
216        Self::new()
217    }
218}
219
220// Note: Drop is handled automatically by libloading
221// The Library type will unload the dynamic library when dropped