From cf20540a49e6f9336d25533715f9dc5db5a2aab6 Mon Sep 17 00:00:00 2001 From: Hikari Date: Sat, 7 Feb 2026 15:11:06 -0800 Subject: [PATCH] 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 --- src-tauri/src/commands.rs | 308 ++++++++++++++++++ src-tauri/src/lib.rs | 9 + src/lib/components/McpManagementPanel.svelte | 300 +++++++++++++++++ .../components/PluginManagementPanel.svelte | 299 +++++++++++++++++ src/lib/components/StatusBar.svelte | 40 +++ 5 files changed, 956 insertions(+) create mode 100644 src/lib/components/McpManagementPanel.svelte create mode 100644 src/lib/components/PluginManagementPanel.svelte diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 00da09a..becf33a 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -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 { } } +// ==================== Plugin Management Commands ==================== + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PluginInfo { + pub name: String, + pub version: String, + pub description: Option, + pub enabled: bool, +} + +#[tauri::command] +pub async fn list_plugins() -> Result, 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::>(&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 { + 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 { + 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 { + 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 { + 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 { + 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, + pub url: Option, + pub transport: String, // "stdio", "http", or "sse" + pub env: Option, +} + +#[tauri::command] +pub async fn list_mcp_servers() -> Result, 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::>(&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 { + 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::(&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 { + 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::*; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1637f23..6394a21 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -195,6 +195,15 @@ pub fn run() { close_application, list_memory_files, get_claude_version, + list_plugins, + install_plugin, + uninstall_plugin, + enable_plugin, + disable_plugin, + update_plugin, + list_mcp_servers, + get_mcp_server, + remove_mcp_server, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/lib/components/McpManagementPanel.svelte b/src/lib/components/McpManagementPanel.svelte new file mode 100644 index 0000000..bd3b46e --- /dev/null +++ b/src/lib/components/McpManagementPanel.svelte @@ -0,0 +1,300 @@ + + +
+ +
+
+
+ +
+
+

MCP Server Management

+

+ {servers.length} server{servers.length !== 1 ? "s" : ""} configured +

+
+
+ +
+ + +
+

+ 💡 To add new MCP servers, use the Settings panel or edit your configuration directly. +

+
+ + + {#if error} +
+

{error}

+
+ {/if} + + +
+ +
+ {#if isLoading} +
+ +
+ {:else if servers.length === 0} +
+ +

No MCP servers configured

+

Add servers via Settings

+
+ {:else} +
+ {#each servers as server (server.name)} + + {/each} +
+ {/if} +
+ + + {#if selectedServer} +
+

Server Details

+ + {#if isLoadingDetails} +
+ +
+ {:else} +
+ +
+ +

{selectedServer.name}

+
+ + +
+ +

+ + {selectedServer.transport.toUpperCase()} +

+
+ + + {#if selectedServer.url} +
+ +

+ {selectedServer.url} +

+
+ {/if} + + {#if selectedServer.command} +
+ +

+ {selectedServer.command} +

+
+ {/if} + + + {#if selectedServer.env} +
+ +
{JSON.stringify(selectedServer.env, null, 2)}
+
+ {/if} + + +
+ +
+
+ {/if} +
+ {/if} +
+
+ + diff --git a/src/lib/components/PluginManagementPanel.svelte b/src/lib/components/PluginManagementPanel.svelte new file mode 100644 index 0000000..6ad9c19 --- /dev/null +++ b/src/lib/components/PluginManagementPanel.svelte @@ -0,0 +1,299 @@ + + +
+ +
+
+
+ + + +
+
+

Plugin Management

+

+ {plugins.length} plugin{plugins.length !== 1 ? "s" : ""} installed +

+
+
+ +
+ + +
+

Install New Plugin

+
+ e.key === "Enter" && installPlugin()} + disabled={isInstalling} + /> + +
+
+ + + {#if error} +
+

{error}

+
+ {/if} + + +
+ {#if isLoading} +
+ +
+ {:else if plugins.length === 0} +
+ + + +

No plugins installed

+

Install a plugin using the form above

+
+ {:else} +
+ {#each plugins as plugin (plugin.name)} +
+
+
+

+ {plugin.name} + {#if plugin.enabled} + + Enabled + + {:else} + + Disabled + + {/if} +

+

v{plugin.version}

+ {#if plugin.description} +

{plugin.description}

+ {/if} +
+
+ +
+ + + + + +
+
+ {/each} +
+ {/if} +
+
+ + diff --git a/src/lib/components/StatusBar.svelte b/src/lib/components/StatusBar.svelte index 50f3366..79e27f5 100644 --- a/src/lib/components/StatusBar.svelte +++ b/src/lib/components/StatusBar.svelte @@ -27,6 +27,8 @@ import GitPanel from "./GitPanel.svelte"; import ProfilePanel from "./ProfilePanel.svelte"; import AgentMonitorPanel from "./AgentMonitorPanel.svelte"; + import PluginManagementPanel from "./PluginManagementPanel.svelte"; + import McpManagementPanel from "./McpManagementPanel.svelte"; import { conversationsStore } from "$lib/stores/conversations"; import { generateContextInjection, @@ -54,6 +56,8 @@ let showGitPanel = $state(false); let showProfile = $state(false); let showAgentMonitor = $state(false); + let showPluginPanel = $state(false); + let showMcpPanel = $state(false); let isSummarising = $state(false); const progress = $derived($achievementProgress); const activeAgentCount = $derived($runningAgentCount); @@ -468,6 +472,34 @@ /> + +