From 4c67380859018d33b81ebdcd364624ba1da2a2f7 Mon Sep 17 00:00:00 2001 From: Hikari Date: Sat, 7 Feb 2026 17:22:49 -0800 Subject: [PATCH] feat: enhance plugin and MCP management panels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive management capabilities to both panels: MCP Management Panel: - Add "Add New Server" form with transport selection (STDIO/HTTP/SSE) - Add enhanced server details view showing full CLI output - Dynamic placeholder text based on transport type - Support for environment variables and headers (backend) Plugin Management Panel: - Add collapsible marketplace management section - Add/remove marketplaces from GitHub repos - List all configured marketplaces with sources - Enhanced plugin installation with marketplace syntax support Backend Commands: - add_mcp_server: Add MCP servers with transport/env/headers - get_mcp_server_details: Fetch full server details from CLI - list_marketplaces: Parse and list plugin marketplaces - add_marketplace: Add marketplace from GitHub source - remove_marketplace: Remove marketplace by name All operations use Claude CLI directly, avoiding cross-platform path issues by letting the CLI handle file system operations internally. ✨ This enhancement was built by Hikari~ 🌸 --- src-tauri/src/commands.rs | 219 ++++++++++++++++++ src-tauri/src/lib.rs | 5 + src/lib/components/McpManagementPanel.svelte | 119 +++++++++- .../components/PluginManagementPanel.svelte | 150 +++++++++++- 4 files changed, 487 insertions(+), 6 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 389d8ac..76a541f 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1487,6 +1487,142 @@ pub async fn update_plugin(plugin_name: String) -> Result { } } +// ==================== Plugin Marketplace Commands ==================== + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MarketplaceInfo { + pub name: String, + pub source: String, +} + +#[tauri::command] +pub async fn list_marketplaces() -> Result, String> { + tracing::debug!("Listing plugin marketplaces"); + + let output = std::process::Command::new("claude") + .arg("plugin") + .arg("marketplace") + .arg("list") + .output(); + + match output { + Ok(output) => { + if !output.status.success() { + let error = String::from_utf8_lossy(&output.stderr); + tracing::error!("Failed to list marketplaces: {}", error); + return Err(format!("Failed to list marketplaces: {}", error)); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let mut marketplaces = Vec::new(); + + // Parse format: + // Configured marketplaces: + // + // ❯ claude-plugins-official + // Source: GitHub (anthropics/claude-plugins-official) + // + // ❯ macrodata + // Source: GitHub (ascorbic/macrodata) + + let mut current_name: Option = None; + + for line in stdout.lines() { + let trimmed = line.trim(); + + // Look for marketplace names starting with ❯ + if trimmed.starts_with("❯ ") { + current_name = Some(trimmed[2..].trim().to_string()); + } + // Look for Source line + else if trimmed.starts_with("Source: ") && current_name.is_some() { + let source = trimmed[8..].trim().to_string(); + marketplaces.push(MarketplaceInfo { + name: current_name.take().unwrap(), + source, + }); + } + } + + tracing::info!("Found {} marketplaces", marketplaces.len()); + Ok(marketplaces) + } + Err(e) => { + tracing::error!("Failed to execute claude plugin marketplace list: {}", e); + Err(format!( + "Failed to execute claude plugin marketplace list: {}", + e + )) + } + } +} + +#[tauri::command] +pub async fn add_marketplace(source: String) -> Result { + tracing::debug!("Adding marketplace: {}", source); + + let output = std::process::Command::new("claude") + .arg("plugin") + .arg("marketplace") + .arg("add") + .arg(&source) + .output(); + + match output { + Ok(output) => { + if output.status.success() { + let message = String::from_utf8_lossy(&output.stdout).trim().to_string(); + tracing::info!("Successfully added marketplace: {}", source); + Ok(message) + } else { + let error = String::from_utf8_lossy(&output.stderr); + tracing::error!("Failed to add marketplace {}: {}", source, error); + Err(format!("Failed to add marketplace: {}", error)) + } + } + Err(e) => { + tracing::error!("Failed to execute claude plugin marketplace add: {}", e); + Err(format!( + "Failed to execute claude plugin marketplace add: {}", + e + )) + } + } +} + +#[tauri::command] +pub async fn remove_marketplace(name: String) -> Result { + tracing::debug!("Removing marketplace: {}", name); + + let output = std::process::Command::new("claude") + .arg("plugin") + .arg("marketplace") + .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 marketplace: {}", name); + Ok(message) + } else { + let error = String::from_utf8_lossy(&output.stderr); + tracing::error!("Failed to remove marketplace {}: {}", name, error); + Err(format!("Failed to remove marketplace: {}", error)) + } + } + Err(e) => { + tracing::error!("Failed to execute claude plugin marketplace remove: {}", e); + Err(format!( + "Failed to execute claude plugin marketplace remove: {}", + e + )) + } + } +} + // ==================== MCP Management Commands ==================== #[derive(Debug, Clone, Serialize, Deserialize)] @@ -1632,6 +1768,89 @@ pub async fn remove_mcp_server(name: String) -> Result { } } +#[tauri::command] +pub async fn add_mcp_server( + name: String, + command_or_url: String, + transport: String, + env_vars: Option>, + headers: Option>, +) -> Result { + tracing::debug!("Adding MCP server: {} with transport {}", name, transport); + + let mut cmd = std::process::Command::new("claude"); + cmd.arg("mcp").arg("add"); + + // Add transport flag + cmd.arg("--transport").arg(&transport); + + // Add environment variables if provided + if let Some(env_vars) = env_vars { + for env_var in env_vars { + cmd.arg("-e").arg(env_var); + } + } + + // Add headers if provided (for HTTP/SSE) + if let Some(headers) = headers { + for header in headers { + cmd.arg("-H").arg(header); + } + } + + // Add name and command/URL + cmd.arg(&name).arg(&command_or_url); + + let output = cmd.output(); + + match output { + Ok(output) => { + if output.status.success() { + let message = String::from_utf8_lossy(&output.stdout).trim().to_string(); + tracing::info!("Successfully added MCP server: {}", name); + Ok(message) + } else { + let error = String::from_utf8_lossy(&output.stderr); + tracing::error!("Failed to add MCP server {}: {}", name, error); + Err(format!("Failed to add MCP server: {}", error)) + } + } + Err(e) => { + tracing::error!("Failed to execute claude mcp add: {}", e); + Err(format!("Failed to execute claude mcp add: {}", e)) + } + } +} + +#[tauri::command] +pub async fn get_mcp_server_details(name: String) -> Result { + tracing::debug!("Getting detailed info for MCP server: {}", name); + + let output = std::process::Command::new("claude") + .arg("mcp") + .arg("get") + .arg(&name) + .output(); + + match output { + Ok(output) => { + if output.status.success() { + let details = String::from_utf8_lossy(&output.stdout).trim().to_string(); + tracing::debug!("Got MCP server details: {}", details); + Ok(details) + } else { + let error = String::from_utf8_lossy(&output.stderr); + tracing::error!("Failed to get MCP server details for {}: {}", name, error); + Err(format!("Failed to get server details: {}", error)) + } + } + Err(e) => { + tracing::error!("Failed to execute claude mcp get: {}", e); + Err(format!("Failed to execute claude mcp get: {}", e)) + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6394a21..6c9c1a8 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -201,9 +201,14 @@ pub fn run() { enable_plugin, disable_plugin, update_plugin, + list_marketplaces, + add_marketplace, + remove_marketplace, list_mcp_servers, get_mcp_server, remove_mcp_server, + add_mcp_server, + get_mcp_server_details, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/lib/components/McpManagementPanel.svelte b/src/lib/components/McpManagementPanel.svelte index fa0ab46..753a22d 100644 --- a/src/lib/components/McpManagementPanel.svelte +++ b/src/lib/components/McpManagementPanel.svelte @@ -24,6 +24,14 @@ let selectedServer = $state(null); let isLoadingDetails = $state(false); let actionInProgress = $state(null); + let showAddForm = $state(false); + let serverDetails = $state(""); + + // Add server form fields + let newServerName = $state(""); + let newServerUrl = $state(""); + let newServerTransport = $state("stdio"); + let isAdding = $state(false); async function loadServers(): Promise { try { @@ -43,6 +51,7 @@ isLoadingDetails = true; error = null; selectedServer = await invoke("get_mcp_server", { name }); + serverDetails = await invoke("get_mcp_server_details", { name }); } catch (e) { error = `Failed to load server details: ${e}`; console.error(error); @@ -58,6 +67,7 @@ await invoke("remove_mcp_server", { name }); if (selectedServer?.name === name) { selectedServer = null; + serverDetails = ""; } await loadServers(); } catch (e) { @@ -68,6 +78,32 @@ } } + async function addServer(): Promise { + if (!newServerName.trim() || !newServerUrl.trim()) return; + + try { + isAdding = true; + error = null; + await invoke("add_mcp_server", { + name: newServerName.trim(), + commandOrUrl: newServerUrl.trim(), + transport: newServerTransport, + envVars: null, + headers: null, + }); + newServerName = ""; + newServerUrl = ""; + newServerTransport = "stdio"; + showAddForm = false; + await loadServers(); + } catch (e) { + error = `Failed to add server: ${e}`; + console.error(error); + } finally { + isAdding = false; + } + } + function getTransportIcon(transport: string) { switch (transport) { case "http": @@ -135,13 +171,75 @@ - -
-

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

+ +
+
+ + {#if showAddForm} +
+

Add MCP Server

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ {/if} + {#if error}
@@ -282,6 +380,17 @@
{/if} + + {#if serverDetails} +
+ +
{serverDetails}
+
+ {/if} +
+ + {#if showMarketplaces} +
+ +
+

+ Add a marketplace from GitHub (e.g., "ascorbic/macrodata") +

+
+ e.key === "Enter" && addMarketplace()} + disabled={isAddingMarketplace} + /> + +
+
+ + + {#if isLoadingMarketplaces} +
+ +
+ {:else if marketplaces.length > 0} +
+ {#each marketplaces as marketplace (marketplace.name)} +
+
+
+

{marketplace.name}

+

{marketplace.source}

+
+ +
+
+ {/each} +
+ {:else} +

+ No marketplaces configured +

+ {/if} +
+ {/if} +
+ {#if error}