prism_mcp_rs/plugin/
loader.rs1use 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
13pub struct PluginLoader {
15 libraries: HashMap<String, LoadedPlugin>,
17
18 search_paths: Vec<PathBuf>,
20}
21
22struct LoadedPlugin {
24 #[allow(dead_code)]
26 library: Library,
27
28 metadata: PluginMetadata,
30
31 path: PathBuf,
33
34 instance: Arc<RwLock<Box<dyn ToolPlugin>>>,
36}
37
38impl PluginLoader {
39 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 pub fn add_search_path(&mut self, path: impl Into<PathBuf>) {
53 self.search_paths.push(path.into());
54 }
55
56 pub fn load_plugin(&mut self, path: &Path) -> PluginResult<Arc<RwLock<Box<dyn ToolPlugin>>>> {
58 info!("Loading plugin from: {:?}", path);
59
60 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 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 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 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 let metadata = instance.metadata();
97 info!("Loaded plugin: {} v{}", metadata.name, metadata.version);
98
99 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 pub fn unload_plugin(&mut self, plugin_id: &str) -> PluginResult<()> {
114 info!("Unloading plugin: {}", plugin_id);
115
116 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 if let Some(loaded) = self.libraries.remove(&path) {
126 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 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 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 info!(
156 "Plugin reload requested for {}, returning existing instance",
157 plugin_id
158 );
159 Ok(existing_instance)
160 }
161
162 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 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 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 pub fn list_plugins(&self) -> Vec<PluginMetadata> {
202 self.libraries
203 .values()
204 .map(|p| p.metadata.clone())
205 .collect()
206 }
207
208 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