feat: add Plugin and MCP Server Management panels

Backend (Rust):
- Add plugin management commands: list, install, uninstall, enable, disable, update
- Add MCP server management commands: list, get details, remove
- Integrate with Claude CLI's 'plugin' and 'mcp' subcommands
- Export PluginInfo and McpServerInfo types

Frontend (Svelte):
- Create PluginManagementPanel component with full CRUD operations
- Create McpManagementPanel component with server listing and removal
- Add buttons to StatusBar for both panels
- Beautiful UI with lucide-svelte icons
- Theme-aware styling using CSS variables
- Real-time loading states and error handling

Closes #133
Closes #134
This commit is contained in:
2026-02-07 15:11:06 -08:00
committed by Naomi Carrigan
parent 70af6a6b72
commit cf20540a49
5 changed files with 956 additions and 0 deletions
+308
View File
@@ -1,4 +1,5 @@
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Manager, State};
use tauri_plugin_http::reqwest;
use tauri_plugin_store::StoreExt;
@@ -1257,6 +1258,313 @@ pub async fn get_claude_version() -> Result<String, String> {
}
}
// ==================== Plugin Management Commands ====================
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginInfo {
pub name: String,
pub version: String,
pub description: Option<String>,
pub enabled: bool,
}
#[tauri::command]
pub async fn list_plugins() -> Result<Vec<PluginInfo>, String> {
tracing::debug!("Listing Claude Code plugins");
let output = std::process::Command::new("claude")
.arg("plugin")
.arg("list")
.arg("--json")
.output();
match output {
Ok(output) => {
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
match serde_json::from_str::<Vec<PluginInfo>>(&stdout) {
Ok(plugins) => {
tracing::info!("Listed {} plugins", plugins.len());
Ok(plugins)
}
Err(e) => {
tracing::error!("Failed to parse plugin list: {}", e);
Err(format!("Failed to parse plugin list: {}", e))
}
}
} else {
let error = String::from_utf8_lossy(&output.stderr);
tracing::error!("Failed to list plugins: {}", error);
Err(format!("Failed to list plugins: {}", error))
}
}
Err(e) => {
tracing::error!("Failed to execute claude plugin list: {}", e);
Err(format!("Failed to execute claude plugin list: {}", e))
}
}
}
#[tauri::command]
pub async fn install_plugin(plugin_name: String) -> Result<String, String> {
tracing::debug!("Installing plugin: {}", plugin_name);
let output = std::process::Command::new("claude")
.arg("plugin")
.arg("install")
.arg(&plugin_name)
.output();
match output {
Ok(output) => {
if output.status.success() {
let message = String::from_utf8_lossy(&output.stdout).trim().to_string();
tracing::info!("Successfully installed plugin: {}", plugin_name);
Ok(message)
} else {
let error = String::from_utf8_lossy(&output.stderr);
tracing::error!("Failed to install plugin {}: {}", plugin_name, error);
Err(format!("Failed to install plugin: {}", error))
}
}
Err(e) => {
tracing::error!("Failed to execute claude plugin install: {}", e);
Err(format!("Failed to execute claude plugin install: {}", e))
}
}
}
#[tauri::command]
pub async fn uninstall_plugin(plugin_name: String) -> Result<String, String> {
tracing::debug!("Uninstalling plugin: {}", plugin_name);
let output = std::process::Command::new("claude")
.arg("plugin")
.arg("uninstall")
.arg(&plugin_name)
.output();
match output {
Ok(output) => {
if output.status.success() {
let message = String::from_utf8_lossy(&output.stdout).trim().to_string();
tracing::info!("Successfully uninstalled plugin: {}", plugin_name);
Ok(message)
} else {
let error = String::from_utf8_lossy(&output.stderr);
tracing::error!("Failed to uninstall plugin {}: {}", plugin_name, error);
Err(format!("Failed to uninstall plugin: {}", error))
}
}
Err(e) => {
tracing::error!("Failed to execute claude plugin uninstall: {}", e);
Err(format!("Failed to execute claude plugin uninstall: {}", e))
}
}
}
#[tauri::command]
pub async fn enable_plugin(plugin_name: String) -> Result<String, String> {
tracing::debug!("Enabling plugin: {}", plugin_name);
let output = std::process::Command::new("claude")
.arg("plugin")
.arg("enable")
.arg(&plugin_name)
.output();
match output {
Ok(output) => {
if output.status.success() {
let message = String::from_utf8_lossy(&output.stdout).trim().to_string();
tracing::info!("Successfully enabled plugin: {}", plugin_name);
Ok(message)
} else {
let error = String::from_utf8_lossy(&output.stderr);
tracing::error!("Failed to enable plugin {}: {}", plugin_name, error);
Err(format!("Failed to enable plugin: {}", error))
}
}
Err(e) => {
tracing::error!("Failed to execute claude plugin enable: {}", e);
Err(format!("Failed to execute claude plugin enable: {}", e))
}
}
}
#[tauri::command]
pub async fn disable_plugin(plugin_name: String) -> Result<String, String> {
tracing::debug!("Disabling plugin: {}", plugin_name);
let output = std::process::Command::new("claude")
.arg("plugin")
.arg("disable")
.arg(&plugin_name)
.output();
match output {
Ok(output) => {
if output.status.success() {
let message = String::from_utf8_lossy(&output.stdout).trim().to_string();
tracing::info!("Successfully disabled plugin: {}", plugin_name);
Ok(message)
} else {
let error = String::from_utf8_lossy(&output.stderr);
tracing::error!("Failed to disable plugin {}: {}", plugin_name, error);
Err(format!("Failed to disable plugin: {}", error))
}
}
Err(e) => {
tracing::error!("Failed to execute claude plugin disable: {}", e);
Err(format!("Failed to execute claude plugin disable: {}", e))
}
}
}
#[tauri::command]
pub async fn update_plugin(plugin_name: String) -> Result<String, String> {
tracing::debug!("Updating plugin: {}", plugin_name);
let output = std::process::Command::new("claude")
.arg("plugin")
.arg("update")
.arg(&plugin_name)
.output();
match output {
Ok(output) => {
if output.status.success() {
let message = String::from_utf8_lossy(&output.stdout).trim().to_string();
tracing::info!("Successfully updated plugin: {}", plugin_name);
Ok(message)
} else {
let error = String::from_utf8_lossy(&output.stderr);
tracing::error!("Failed to update plugin {}: {}", plugin_name, error);
Err(format!("Failed to update plugin: {}", error))
}
}
Err(e) => {
tracing::error!("Failed to execute claude plugin update: {}", e);
Err(format!("Failed to execute claude plugin update: {}", e))
}
}
}
// ==================== MCP Management Commands ====================
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpServerInfo {
pub name: String,
pub command: Option<String>,
pub url: Option<String>,
pub transport: String, // "stdio", "http", or "sse"
pub env: Option<serde_json::Value>,
}
#[tauri::command]
pub async fn list_mcp_servers() -> Result<Vec<McpServerInfo>, String> {
tracing::debug!("Listing MCP servers");
let output = std::process::Command::new("claude")
.arg("mcp")
.arg("list")
.arg("--json")
.output();
match output {
Ok(output) => {
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
match serde_json::from_str::<Vec<McpServerInfo>>(&stdout) {
Ok(servers) => {
tracing::info!("Listed {} MCP servers", servers.len());
Ok(servers)
}
Err(e) => {
tracing::error!("Failed to parse MCP server list: {}", e);
Err(format!("Failed to parse MCP server list: {}", e))
}
}
} else {
let error = String::from_utf8_lossy(&output.stderr);
tracing::error!("Failed to list MCP servers: {}", error);
Err(format!("Failed to list MCP servers: {}", error))
}
}
Err(e) => {
tracing::error!("Failed to execute claude mcp list: {}", e);
Err(format!("Failed to execute claude mcp list: {}", e))
}
}
}
#[tauri::command]
pub async fn get_mcp_server(name: String) -> Result<McpServerInfo, String> {
tracing::debug!("Getting MCP server details: {}", name);
let output = std::process::Command::new("claude")
.arg("mcp")
.arg("get")
.arg(&name)
.arg("--json")
.output();
match output {
Ok(output) => {
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
match serde_json::from_str::<McpServerInfo>(&stdout) {
Ok(server) => {
tracing::info!("Got MCP server details for: {}", name);
Ok(server)
}
Err(e) => {
tracing::error!("Failed to parse MCP server details: {}", e);
Err(format!("Failed to parse MCP server details: {}", e))
}
}
} else {
let error = String::from_utf8_lossy(&output.stderr);
tracing::error!("Failed to get MCP server {}: {}", name, error);
Err(format!("Failed to get MCP server: {}", error))
}
}
Err(e) => {
tracing::error!("Failed to execute claude mcp get: {}", e);
Err(format!("Failed to execute claude mcp get: {}", e))
}
}
}
#[tauri::command]
pub async fn remove_mcp_server(name: String) -> Result<String, String> {
tracing::debug!("Removing MCP server: {}", name);
let output = std::process::Command::new("claude")
.arg("mcp")
.arg("remove")
.arg(&name)
.output();
match output {
Ok(output) => {
if output.status.success() {
let message = String::from_utf8_lossy(&output.stdout).trim().to_string();
tracing::info!("Successfully removed MCP server: {}", name);
Ok(message)
} else {
let error = String::from_utf8_lossy(&output.stderr);
tracing::error!("Failed to remove MCP server {}: {}", name, error);
Err(format!("Failed to remove MCP server: {}", error))
}
}
Err(e) => {
tracing::error!("Failed to execute claude mcp remove: {}", e);
Err(format!("Failed to execute claude mcp remove: {}", e))
}
}
}
#[cfg(test)]
mod tests {
use super::*;