diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index becf33a..389d8ac 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1275,23 +1275,60 @@ pub async fn list_plugins() -> Result, String> { 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)) + let mut plugins = Vec::new(); + + // Parse text output format: + // ❯ macrodata@macrodata + // Version: 0.1.3 + // Scope: user + // Status: ✔ enabled + + let lines: Vec<&str> = stdout.lines().collect(); + let mut i = 0; + while i < lines.len() { + let line = lines[i].trim(); + + // Look for plugin name line (starts with ❯) + if line.starts_with("❯") { + let name = line.trim_start_matches("❯").trim().to_string(); + let mut version = String::new(); + let mut enabled = false; + + // Parse following lines for metadata + i += 1; + while i < lines.len() { + let meta_line = lines[i].trim(); + if meta_line.is_empty() || meta_line.starts_with("❯") { + break; + } + + if meta_line.starts_with("Version:") { + version = meta_line.trim_start_matches("Version:").trim().to_string(); + } else if meta_line.starts_with("Status:") { + enabled = meta_line.contains("enabled"); + } + i += 1; + } + + plugins.push(PluginInfo { + name, + version, + description: None, + enabled, + }); + continue; } + i += 1; } + + tracing::info!("Listed {} plugins", plugins.len()); + Ok(plugins) } else { let error = String::from_utf8_lossy(&output.stderr); tracing::error!("Failed to list plugins: {}", error); @@ -1459,6 +1496,7 @@ pub struct McpServerInfo { pub url: Option, pub transport: String, // "stdio", "http", or "sse" pub env: Option, + pub status: Option, // "Connected" or "Failed to connect" } #[tauri::command] @@ -1468,23 +1506,77 @@ pub async fn list_mcp_servers() -> Result, String> { 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) + let mut servers = Vec::new(); + + // Parse text output format: + // asana: https://mcp.asana.com/sse (SSE) - ✓ Connected + // gitea: gitea-mcp -t stdio --host https://git.nhcarrigan.com - ✓ Connected + + for line in stdout.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with("Checking") { + continue; } - Err(e) => { - tracing::error!("Failed to parse MCP server list: {}", e); - Err(format!("Failed to parse MCP server list: {}", e)) + + // Split by colon to get name and rest + if let Some((name, rest)) = line.split_once(':') { + let name = name.trim().to_string(); + let rest = rest.trim(); + + // Determine if it's a URL or command + let (url, command, transport, status) = if rest.starts_with("http") { + // HTTP/SSE server: "https://mcp.asana.com/sse (SSE) - ✓ Connected" + let parts: Vec<&str> = rest.split('-').collect(); + let url_and_transport = parts[0].trim(); + let status = if parts.len() > 1 { + Some(parts[1].trim().trim_start_matches("✓").trim_start_matches("✗").trim().to_string()) + } else { + None + }; + + // Extract URL and transport type + let (url, transport) = if let Some((url_part, transport_part)) = url_and_transport.rsplit_once('(') { + let url = url_part.trim().to_string(); + let transport = transport_part.trim_end_matches(')').trim().to_lowercase(); + (Some(url), transport) + } else { + (Some(url_and_transport.to_string()), "http".to_string()) + }; + + (url, None, transport, status) + } else { + // stdio server: "gitea-mcp -t stdio --host https://git.nhcarrigan.com - ✓ Connected" + let parts: Vec<&str> = rest.split('-').collect(); + let command = parts[0].trim().to_string(); + let status = if parts.len() > 1 { + let status_part = parts[parts.len() - 1]; + Some(status_part.trim().trim_start_matches("✓").trim_start_matches("✗").trim().to_string()) + } else { + None + }; + + (None, Some(command), "stdio".to_string(), status) + }; + + servers.push(McpServerInfo { + name, + command, + url, + transport, + env: None, + status, + }); } } + + tracing::info!("Listed {} MCP servers", servers.len()); + Ok(servers) } else { let error = String::from_utf8_lossy(&output.stderr); tracing::error!("Failed to list MCP servers: {}", error); @@ -1502,38 +1594,13 @@ pub async fn list_mcp_servers() -> Result, String> { 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(); + // Get all servers and find the matching one + let servers = list_mcp_servers().await?; - 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)) - } - } + servers + .into_iter() + .find(|s| s.name == name) + .ok_or_else(|| format!("MCP server '{}' not found", name)) } #[tauri::command] diff --git a/src/lib/components/McpManagementPanel.svelte b/src/lib/components/McpManagementPanel.svelte index bd3b46e..fa0ab46 100644 --- a/src/lib/components/McpManagementPanel.svelte +++ b/src/lib/components/McpManagementPanel.svelte @@ -13,6 +13,7 @@ url: string | null; transport: string; // "stdio", "http", or "sse" env: any | null; + status: string | null; // "Connected" or "Failed to connect" } const { onClose }: Props = $props(); @@ -178,6 +179,21 @@ class="w-4 h-4 {getTransportColor(server.transport)}" /> {server.name} + {#if server.status} + {#if server.status.includes("Connected")} + + ✓ + + {:else} + + ✗ + + {/if} + {/if}

{server.transport.toUpperCase()}