|
| 1 | +# MCP Server Plugin Implementation Plan for Tauri |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +This document outlines a concrete implementation plan for adding MCP (Model Context Protocol) server capabilities to the Tauri application, enabling AI agents to interact with all available plugins. |
| 6 | + |
| 7 | +## Architecture Design |
| 8 | + |
| 9 | +### Core Components |
| 10 | + |
| 11 | +1. **MCP Server Plugin** (`tauri-plugin-mcp-server`) |
| 12 | + - Implements MCP protocol using `jsonrpsee` or custom JSON-RPC |
| 13 | + - Manages client connections and authentication |
| 14 | + - Provides introspection and command routing |
| 15 | + |
| 16 | +2. **Plugin Discovery System** |
| 17 | + - Event-based plugin registration |
| 18 | + - Static configuration fallback |
| 19 | + - Runtime capability detection |
| 20 | + |
| 21 | +3. **Command Proxy Layer** |
| 22 | + - Translates MCP tool calls to Tauri commands |
| 23 | + - Handles async command execution |
| 24 | + - Manages response transformation |
| 25 | + |
| 26 | +## Implementation Steps |
| 27 | + |
| 28 | +### Phase 1: Basic MCP Server Infrastructure |
| 29 | + |
| 30 | +```rust |
| 31 | +// tauri-plugin-mcp-server/src/lib.rs |
| 32 | +pub struct McpServerPlugin<R: Runtime> { |
| 33 | + server: Arc<Mutex<Option<McpServer>>>, |
| 34 | + app_handle: AppHandle<R>, |
| 35 | + plugin_registry: Arc<Mutex<PluginRegistry>>, |
| 36 | +} |
| 37 | + |
| 38 | +pub struct PluginRegistry { |
| 39 | + plugins: HashMap<String, PluginMetadata>, |
| 40 | + command_map: HashMap<String, CommandInfo>, |
| 41 | +} |
| 42 | + |
| 43 | +#[derive(Serialize, Deserialize)] |
| 44 | +pub struct PluginMetadata { |
| 45 | + name: String, |
| 46 | + version: String, |
| 47 | + commands: Vec<String>, |
| 48 | + permissions: Vec<String>, |
| 49 | +} |
| 50 | +``` |
| 51 | + |
| 52 | +### Phase 2: Plugin Discovery Implementation |
| 53 | + |
| 54 | +#### Option A: Static Discovery (Simpler, More Secure) |
| 55 | +```rust |
| 56 | +impl McpServerPlugin { |
| 57 | + fn discover_static_plugins(&self) -> Result<()> { |
| 58 | + // Use the existing get_enabled_plugins() pattern |
| 59 | + let enabled = self.get_enabled_plugins(); |
| 60 | + |
| 61 | + // Hardcode known plugin commands based on permissions |
| 62 | + if enabled.workerd { |
| 63 | + self.register_plugin("workerd", vec![ |
| 64 | + "check_workerd_installed", |
| 65 | + "start_worker", |
| 66 | + "stop_worker", |
| 67 | + // ... other commands from permissions file |
| 68 | + ]); |
| 69 | + } |
| 70 | + |
| 71 | + if enabled.podman { |
| 72 | + self.register_plugin("podman", vec![ |
| 73 | + "check_podman_installed", |
| 74 | + "create_container", |
| 75 | + // ... |
| 76 | + ]); |
| 77 | + } |
| 78 | + |
| 79 | + Ok(()) |
| 80 | + } |
| 81 | +} |
| 82 | +``` |
| 83 | + |
| 84 | +#### Option B: Dynamic Discovery (More Flexible) |
| 85 | +```rust |
| 86 | +impl McpServerPlugin { |
| 87 | + fn setup_dynamic_discovery<R: Runtime>(&self, app: &AppHandle<R>) { |
| 88 | + // Emit discovery request |
| 89 | + app.emit("mcp:discover_plugins", json!({})); |
| 90 | + |
| 91 | + // Listen for plugin announcements |
| 92 | + app.listen("plugin:announce", |event| { |
| 93 | + if let Ok(metadata) = serde_json::from_str::<PluginMetadata>(&event.payload()) { |
| 94 | + self.plugin_registry.lock().unwrap().register(metadata); |
| 95 | + } |
| 96 | + }); |
| 97 | + } |
| 98 | +} |
| 99 | +``` |
| 100 | + |
| 101 | +### Phase 3: MCP Protocol Implementation |
| 102 | + |
| 103 | +```rust |
| 104 | +// MCP tool definition |
| 105 | +#[derive(Serialize, Deserialize)] |
| 106 | +struct McpTool { |
| 107 | + name: String, |
| 108 | + description: String, |
| 109 | + input_schema: serde_json::Value, |
| 110 | +} |
| 111 | + |
| 112 | +// Convert Tauri commands to MCP tools |
| 113 | +impl McpServerPlugin { |
| 114 | + fn commands_to_tools(&self) -> Vec<McpTool> { |
| 115 | + let registry = self.plugin_registry.lock().unwrap(); |
| 116 | + let mut tools = vec![]; |
| 117 | + |
| 118 | + for (plugin_name, metadata) in ®istry.plugins { |
| 119 | + for command in &metadata.commands { |
| 120 | + tools.push(McpTool { |
| 121 | + name: format!("{}:{}", plugin_name, command), |
| 122 | + description: format!("Execute {} command from {} plugin", command, plugin_name), |
| 123 | + input_schema: self.get_command_schema(plugin_name, command), |
| 124 | + }); |
| 125 | + } |
| 126 | + } |
| 127 | + |
| 128 | + tools |
| 129 | + } |
| 130 | +} |
| 131 | +``` |
| 132 | + |
| 133 | +### Phase 4: Command Routing and Execution |
| 134 | + |
| 135 | +```rust |
| 136 | +#[async_trait] |
| 137 | +impl McpHandler for McpServerPlugin { |
| 138 | + async fn handle_tool_call(&self, name: &str, args: Value) -> Result<Value> { |
| 139 | + // Parse plugin:command format |
| 140 | + let parts: Vec<&str> = name.split(':').collect(); |
| 141 | + if parts.len() != 2 { |
| 142 | + return Err(Error::InvalidToolName); |
| 143 | + } |
| 144 | + |
| 145 | + let (plugin, command) = (parts[0], parts[1]); |
| 146 | + |
| 147 | + // Route to frontend for execution |
| 148 | + let result = self.app_handle |
| 149 | + .emit("mcp:execute_command", json!({ |
| 150 | + "plugin": plugin, |
| 151 | + "command": command, |
| 152 | + "args": args |
| 153 | + })) |
| 154 | + .await?; |
| 155 | + |
| 156 | + // Wait for response via event listener |
| 157 | + let response = self.await_command_response(plugin, command).await?; |
| 158 | + |
| 159 | + Ok(response) |
| 160 | + } |
| 161 | +} |
| 162 | +``` |
| 163 | + |
| 164 | +### Phase 5: Frontend Integration |
| 165 | + |
| 166 | +```typescript |
| 167 | +// Frontend command router |
| 168 | +import { invoke } from '@tauri-apps/api/core'; |
| 169 | +import { listen } from '@tauri-apps/api/event'; |
| 170 | + |
| 171 | +class McpCommandRouter { |
| 172 | + async initialize() { |
| 173 | + // Listen for MCP command execution requests |
| 174 | + await listen('mcp:execute_command', async (event) => { |
| 175 | + const { plugin, command, args } = event.payload; |
| 176 | + |
| 177 | + try { |
| 178 | + // Dynamically invoke the command |
| 179 | + const result = await invoke(`plugin:${plugin}:${command}`, args); |
| 180 | + |
| 181 | + // Send response back to MCP server |
| 182 | + await emit('mcp:command_response', { |
| 183 | + plugin, |
| 184 | + command, |
| 185 | + result, |
| 186 | + success: true |
| 187 | + }); |
| 188 | + } catch (error) { |
| 189 | + await emit('mcp:command_response', { |
| 190 | + plugin, |
| 191 | + command, |
| 192 | + error: error.message, |
| 193 | + success: false |
| 194 | + }); |
| 195 | + } |
| 196 | + }); |
| 197 | + } |
| 198 | +} |
| 199 | +``` |
| 200 | + |
| 201 | +## Security Considerations |
| 202 | + |
| 203 | +### 1. Authentication and Authorization |
| 204 | +```rust |
| 205 | +pub struct McpAuthConfig { |
| 206 | + require_auth: bool, |
| 207 | + allowed_clients: Vec<String>, |
| 208 | + token_validation: Box<dyn Fn(&str) -> bool>, |
| 209 | +} |
| 210 | + |
| 211 | +impl McpServerPlugin { |
| 212 | + fn validate_client(&self, token: &str) -> Result<ClientContext> { |
| 213 | + // Implement token validation |
| 214 | + // Map client to allowed permissions |
| 215 | + Ok(ClientContext { |
| 216 | + client_id: "validated_client", |
| 217 | + allowed_plugins: vec!["workerd", "podman"], |
| 218 | + rate_limit: Some(100), // requests per minute |
| 219 | + }) |
| 220 | + } |
| 221 | +} |
| 222 | +``` |
| 223 | + |
| 224 | +### 2. Permission Enforcement |
| 225 | +```rust |
| 226 | +impl McpServerPlugin { |
| 227 | + fn check_permission(&self, client: &ClientContext, plugin: &str, command: &str) -> Result<()> { |
| 228 | + // Check if client can access this plugin |
| 229 | + if !client.allowed_plugins.contains(&plugin.to_string()) { |
| 230 | + return Err(Error::PermissionDenied); |
| 231 | + } |
| 232 | + |
| 233 | + // Check specific command permission |
| 234 | + let permission = format!("{}:allow-{}", plugin, command.replace('_', "-")); |
| 235 | + if !self.has_permission(&permission) { |
| 236 | + return Err(Error::CommandNotAllowed); |
| 237 | + } |
| 238 | + |
| 239 | + Ok(()) |
| 240 | + } |
| 241 | +} |
| 242 | +``` |
| 243 | + |
| 244 | +### 3. Network Security |
| 245 | +```rust |
| 246 | +pub struct McpServerConfig { |
| 247 | + bind_address: String, |
| 248 | + tls_config: Option<TlsConfig>, |
| 249 | + max_connections: usize, |
| 250 | +} |
| 251 | + |
| 252 | +impl Default for McpServerConfig { |
| 253 | + fn default() -> Self { |
| 254 | + Self { |
| 255 | + bind_address: "127.0.0.1:0".to_string(), // Local only by default |
| 256 | + tls_config: None, // Require explicit TLS configuration |
| 257 | + max_connections: 10, |
| 258 | + } |
| 259 | + } |
| 260 | +} |
| 261 | +``` |
| 262 | + |
| 263 | +## Testing Strategy |
| 264 | + |
| 265 | +### 1. Unit Tests |
| 266 | +- Test plugin discovery mechanisms |
| 267 | +- Test command routing logic |
| 268 | +- Test permission enforcement |
| 269 | + |
| 270 | +### 2. Integration Tests |
| 271 | +```rust |
| 272 | +#[cfg(test)] |
| 273 | +mod tests { |
| 274 | + #[test] |
| 275 | + async fn test_mcp_tool_execution() { |
| 276 | + let app = create_test_app(); |
| 277 | + let mcp_plugin = McpServerPlugin::new(); |
| 278 | + |
| 279 | + // Register test plugin |
| 280 | + mcp_plugin.register_plugin("test", vec!["echo"]); |
| 281 | + |
| 282 | + // Execute via MCP |
| 283 | + let result = mcp_plugin.handle_tool_call("test:echo", json!({"message": "hello"})).await; |
| 284 | + |
| 285 | + assert_eq!(result.unwrap(), json!({"response": "hello"})); |
| 286 | + } |
| 287 | +} |
| 288 | +``` |
| 289 | + |
| 290 | +### 3. Security Tests |
| 291 | +- Test unauthorized access attempts |
| 292 | +- Test rate limiting |
| 293 | +- Test input validation |
| 294 | + |
| 295 | +## Deployment Considerations |
| 296 | + |
| 297 | +### 1. Feature Flag Integration |
| 298 | +```toml |
| 299 | +[features] |
| 300 | +plugin-mcp-server = ["jsonrpsee", "tokio"] |
| 301 | +mcp-dynamic-discovery = [] # Optional dynamic discovery |
| 302 | +mcp-tls = ["rustls"] # Optional TLS support |
| 303 | +``` |
| 304 | + |
| 305 | +### 2. Configuration |
| 306 | +```json |
| 307 | +{ |
| 308 | + "mcp_server": { |
| 309 | + "enabled": true, |
| 310 | + "bind_address": "127.0.0.1:8080", |
| 311 | + "require_auth": true, |
| 312 | + "allowed_origins": ["http://localhost:*"], |
| 313 | + "plugin_whitelist": ["workerd", "podman", "bun"] |
| 314 | + } |
| 315 | +} |
| 316 | +``` |
| 317 | + |
| 318 | +## Timeline |
| 319 | + |
| 320 | +- **Week 1**: Basic MCP server infrastructure and static plugin discovery |
| 321 | +- **Week 2**: Command routing and frontend integration |
| 322 | +- **Week 3**: Security implementation and testing |
| 323 | +- **Week 4**: Dynamic discovery (optional) and documentation |
| 324 | + |
| 325 | +## Risks and Mitigations |
| 326 | + |
| 327 | +| Risk | Impact | Mitigation | |
| 328 | +|------|--------|------------| |
| 329 | +| Security vulnerabilities | High | Mandatory auth, permission checks, audit logging | |
| 330 | +| Performance overhead | Medium | Async execution, connection pooling, caching | |
| 331 | +| Plugin compatibility | Low | Fallback to static discovery, version checking | |
| 332 | +| Debugging complexity | Medium | Comprehensive logging, trace IDs, error context | |
| 333 | + |
| 334 | +## Conclusion |
| 335 | + |
| 336 | +This implementation plan provides a secure, scalable approach to adding MCP server capabilities to Tauri applications. The phased approach allows for incremental development while maintaining security throughout. |
0 commit comments