feat: enhance plugin and MCP management panels

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~ 🌸
This commit is contained in:
2026-02-07 17:22:49 -08:00
committed by Naomi Carrigan
parent f5bc9a5016
commit 4c67380859
4 changed files with 487 additions and 6 deletions
+219
View File
@@ -1487,6 +1487,142 @@ pub async fn update_plugin(plugin_name: String) -> Result<String, String> {
}
}
// ==================== 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<Vec<MarketplaceInfo>, 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<String> = 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<String, String> {
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<String, String> {
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<String, String> {
}
}
#[tauri::command]
pub async fn add_mcp_server(
name: String,
command_or_url: String,
transport: String,
env_vars: Option<Vec<String>>,
headers: Option<Vec<String>>,
) -> Result<String, String> {
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<String, String> {
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::*;
+5
View File
@@ -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");