generated from nhcarrigan/template
fix: parse text output instead of JSON for plugin/MCP commands
- Remove --json flag from CLI commands (not supported) - Parse plugin list text output format - Parse MCP server list text output format - Add status field to McpServerInfo for connection status - Display connection status in MCP panel (✓/✗ badges) - Simplify get_mcp_server to reuse list_mcp_servers
This commit is contained in:
+115
-48
@@ -1275,23 +1275,60 @@ pub async fn list_plugins() -> Result<Vec<PluginInfo>, String> {
|
|||||||
let output = std::process::Command::new("claude")
|
let output = std::process::Command::new("claude")
|
||||||
.arg("plugin")
|
.arg("plugin")
|
||||||
.arg("list")
|
.arg("list")
|
||||||
.arg("--json")
|
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
match output {
|
match output {
|
||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
match serde_json::from_str::<Vec<PluginInfo>>(&stdout) {
|
let mut plugins = Vec::new();
|
||||||
Ok(plugins) => {
|
|
||||||
tracing::info!("Listed {} plugins", plugins.len());
|
// Parse text output format:
|
||||||
Ok(plugins)
|
// ❯ macrodata@macrodata
|
||||||
}
|
// Version: 0.1.3
|
||||||
Err(e) => {
|
// Scope: user
|
||||||
tracing::error!("Failed to parse plugin list: {}", e);
|
// Status: ✔ enabled
|
||||||
Err(format!("Failed to parse plugin list: {}", e))
|
|
||||||
|
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 {
|
} else {
|
||||||
let error = String::from_utf8_lossy(&output.stderr);
|
let error = String::from_utf8_lossy(&output.stderr);
|
||||||
tracing::error!("Failed to list plugins: {}", error);
|
tracing::error!("Failed to list plugins: {}", error);
|
||||||
@@ -1459,6 +1496,7 @@ pub struct McpServerInfo {
|
|||||||
pub url: Option<String>,
|
pub url: Option<String>,
|
||||||
pub transport: String, // "stdio", "http", or "sse"
|
pub transport: String, // "stdio", "http", or "sse"
|
||||||
pub env: Option<serde_json::Value>,
|
pub env: Option<serde_json::Value>,
|
||||||
|
pub status: Option<String>, // "Connected" or "Failed to connect"
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -1468,23 +1506,77 @@ pub async fn list_mcp_servers() -> Result<Vec<McpServerInfo>, String> {
|
|||||||
let output = std::process::Command::new("claude")
|
let output = std::process::Command::new("claude")
|
||||||
.arg("mcp")
|
.arg("mcp")
|
||||||
.arg("list")
|
.arg("list")
|
||||||
.arg("--json")
|
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
match output {
|
match output {
|
||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
match serde_json::from_str::<Vec<McpServerInfo>>(&stdout) {
|
let mut servers = Vec::new();
|
||||||
Ok(servers) => {
|
|
||||||
tracing::info!("Listed {} MCP servers", servers.len());
|
// Parse text output format:
|
||||||
Ok(servers)
|
// 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);
|
// Split by colon to get name and rest
|
||||||
Err(format!("Failed to parse MCP server list: {}", e))
|
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 {
|
} else {
|
||||||
let error = String::from_utf8_lossy(&output.stderr);
|
let error = String::from_utf8_lossy(&output.stderr);
|
||||||
tracing::error!("Failed to list MCP servers: {}", error);
|
tracing::error!("Failed to list MCP servers: {}", error);
|
||||||
@@ -1502,38 +1594,13 @@ pub async fn list_mcp_servers() -> Result<Vec<McpServerInfo>, String> {
|
|||||||
pub async fn get_mcp_server(name: String) -> Result<McpServerInfo, String> {
|
pub async fn get_mcp_server(name: String) -> Result<McpServerInfo, String> {
|
||||||
tracing::debug!("Getting MCP server details: {}", name);
|
tracing::debug!("Getting MCP server details: {}", name);
|
||||||
|
|
||||||
let output = std::process::Command::new("claude")
|
// Get all servers and find the matching one
|
||||||
.arg("mcp")
|
let servers = list_mcp_servers().await?;
|
||||||
.arg("get")
|
|
||||||
.arg(&name)
|
|
||||||
.arg("--json")
|
|
||||||
.output();
|
|
||||||
|
|
||||||
match output {
|
servers
|
||||||
Ok(output) => {
|
.into_iter()
|
||||||
if output.status.success() {
|
.find(|s| s.name == name)
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
.ok_or_else(|| format!("MCP server '{}' not found", name))
|
||||||
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]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
url: string | null;
|
url: string | null;
|
||||||
transport: string; // "stdio", "http", or "sse"
|
transport: string; // "stdio", "http", or "sse"
|
||||||
env: any | null;
|
env: any | null;
|
||||||
|
status: string | null; // "Connected" or "Failed to connect"
|
||||||
}
|
}
|
||||||
|
|
||||||
const { onClose }: Props = $props();
|
const { onClose }: Props = $props();
|
||||||
@@ -178,6 +179,21 @@
|
|||||||
class="w-4 h-4 {getTransportColor(server.transport)}"
|
class="w-4 h-4 {getTransportColor(server.transport)}"
|
||||||
/>
|
/>
|
||||||
{server.name}
|
{server.name}
|
||||||
|
{#if server.status}
|
||||||
|
{#if server.status.includes("Connected")}
|
||||||
|
<span
|
||||||
|
class="px-2 py-0.5 bg-[var(--success-color)]/20 text-[var(--success-color)] text-xs rounded border border-[var(--success-color)]/30"
|
||||||
|
>
|
||||||
|
✓
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
|
<span
|
||||||
|
class="px-2 py-0.5 bg-red-500/20 text-red-400 text-xs rounded border border-red-500/30"
|
||||||
|
>
|
||||||
|
✗
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
</h4>
|
</h4>
|
||||||
<p class="text-xs text-[var(--text-secondary)] mt-1">
|
<p class="text-xs text-[var(--text-secondary)] mt-1">
|
||||||
{server.transport.toUpperCase()}
|
{server.transport.toUpperCase()}
|
||||||
|
|||||||
Reference in New Issue
Block a user