import React, { useState, useEffect, useCallback } from "react"; import { Plus, Pencil, Trash2, RefreshCw, CheckCircle, XCircle, Clock, Plug } from "lucide-react"; import { Card, CardHeader, CardTitle, CardContent, Button, Input, Label, Badge, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Separator, RadioGroup, RadioGroupItem, } from "@/components/ui"; import { listMcpServersCmd, createMcpServerCmd, updateMcpServerCmd, deleteMcpServerCmd, toggleMcpServerCmd, discoverMcpServerCmd, getMcpServerStatusCmd, initiateMcpOauthCmd, type McpServer, type McpServerStatus, type CreateMcpServerRequest, type UpdateMcpServerRequest, } from "@/lib/tauriCommands"; function timeAgo(iso?: string): string { if (!iso) return "Never"; const diff = Date.now() - new Date(iso).getTime(); if (diff < 60_000) return "Just now"; const mins = Math.floor(diff / 60_000); if (mins < 60) return `${mins}m ago`; const hours = Math.floor(mins / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); return `${days}d ago`; } function parseTransportConfig(config: string): { command: string; args: string[] } | null { try { const parsed = JSON.parse(config); return { command: parsed.command ?? "", args: parsed.args ?? [] }; } catch { return null; } } function parseEnvVars(input: string): Record { const result: Record = {}; const pairs = input.trim().split(/\s+/).filter(Boolean); for (const pair of pairs) { const [key, ...valueParts] = pair.split("="); if (key) { result[key] = valueParts.join("=") || ""; } } return result; } function formatEnvVars(obj: Record): string { return Object.entries(obj) .map(([k, v]) => `${k}=${v}`) .join(" "); } function parseHeaders(input: string): Record { const result: Record = {}; const pairs = input.trim().split(/\s+/).filter(Boolean); for (const pair of pairs) { const [key, ...valueParts] = pair.split(":"); if (key) { result[key] = valueParts.join(":") || ""; } } return result; } function formatHeaders(obj: Record): string { return Object.entries(obj) .map(([k, v]) => `${k}:${v}`) .join(" "); } type StatusKey = McpServerStatus["status"]; const statusColors: Record = { connected: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200", pending: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200", error: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200", unreachable: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200", }; interface ServerForm { name: string; url: string; transport_type: "stdio" | "http"; command: string; args: string; auth_type: "none" | "api_key" | "bearer" | "oauth2"; auth_value: string; enabled: boolean; plaintext_env: string; encrypted_env: string; http_headers: string; } const emptyForm: ServerForm = { name: "", url: "", transport_type: "http", command: "", args: "", auth_type: "none", auth_value: "", enabled: true, plaintext_env: "", encrypted_env: "", http_headers: "", }; export default function MCPServers() { const [servers, setServers] = useState([]); const [statuses, setStatuses] = useState>({}); const [discovering, setDiscovering] = useState>({}); const [editServer, setEditServer] = useState(null); const [isAdding, setIsAdding] = useState(false); const [form, setForm] = useState({ ...emptyForm }); const [deleteConfirm, setDeleteConfirm] = useState(null); const loadServers = useCallback(async () => { try { const list = await listMcpServersCmd(); setServers(list); for (const server of list) { getMcpServerStatusCmd(server.id) .then((s) => setStatuses((prev) => ({ ...prev, [server.id]: s }))) .catch(() => {}); } } catch (err) { console.error("Failed to load MCP servers:", err); } }, []); useEffect(() => { loadServers(); }, [loadServers]); const handleDiscover = async (id: string) => { setDiscovering((prev) => ({ ...prev, [id]: true })); try { const status = await discoverMcpServerCmd(id); setStatuses((prev) => ({ ...prev, [id]: status })); const updated = await listMcpServersCmd(); setServers(updated); } catch (err) { console.error("Discovery failed:", err); } finally { setDiscovering((prev) => ({ ...prev, [id]: false })); } }; const handleToggle = async (server: McpServer) => { try { await toggleMcpServerCmd(server.id, !server.enabled); setServers((prev) => prev.map((s) => (s.id === server.id ? { ...s, enabled: !s.enabled } : s)) ); } catch (err) { console.error("Toggle failed:", err); } }; const handleDelete = async (id: string) => { try { await deleteMcpServerCmd(id); setServers((prev) => prev.filter((s) => s.id !== id)); setDeleteConfirm(null); } catch (err) { console.error("Delete failed:", err); } }; const startAdd = () => { setForm({ ...emptyForm }); setEditServer(null); setIsAdding(true); }; const startEdit = (server: McpServer) => { const parsed = parseTransportConfig(server.transport_config); // Parse plaintext env from transport_config.env let plaintextEnv = ""; let httpHeaders = ""; try { const config = JSON.parse(server.transport_config); if (server.transport_type === "stdio" && config.env) { plaintextEnv = formatEnvVars(config.env); } else if (server.transport_type === "http" && config.headers) { httpHeaders = formatHeaders(config.headers); } } catch { // Invalid JSON, ignore } setForm({ name: server.name, url: server.url, transport_type: server.transport_type, command: parsed?.command ?? "", args: parsed?.args.join(" ") ?? "", auth_type: server.auth_type, auth_value: "", enabled: server.enabled, plaintext_env: plaintextEnv, encrypted_env: "", // Never populate (security: don't show encrypted values) http_headers: httpHeaders, }); setEditServer(server); setIsAdding(true); }; const handleCancel = () => { setIsAdding(false); setEditServer(null); setForm({ ...emptyForm }); }; const handleSave = async () => { if (!form.name) return; if (form.transport_type === "http" && !form.url) return; if (form.transport_type === "stdio" && !form.command) return; // Build transport_config with env vars or headers const plaintextEnvObj = parseEnvVars(form.plaintext_env); const httpHeadersObj = parseHeaders(form.http_headers); const transportConfig = form.transport_type === "stdio" ? JSON.stringify({ command: form.command, args: form.args.split(/\s+/).filter(Boolean), env: plaintextEnvObj, }) : JSON.stringify({ headers: httpHeadersObj, }); // Build env_config (encrypted env) as JSON string const encryptedEnvObj = parseEnvVars(form.encrypted_env); const envConfig = Object.keys(encryptedEnvObj).length > 0 ? JSON.stringify(encryptedEnvObj) : undefined; const url = form.transport_type === "http" ? form.url : ""; try { if (editServer) { const request: UpdateMcpServerRequest = { name: form.name, url, transport_type: form.transport_type, transport_config: transportConfig, auth_type: form.auth_type, enabled: form.enabled, env_config: envConfig, }; if (form.auth_value) { request.auth_value = form.auth_value; } await updateMcpServerCmd(editServer.id, request); } else { const request: CreateMcpServerRequest = { name: form.name, url, transport_type: form.transport_type, transport_config: transportConfig, auth_type: form.auth_type, auth_value: form.auth_value || undefined, enabled: form.enabled, env_config: envConfig, }; await createMcpServerCmd(request); } handleCancel(); loadServers(); } catch (err) { console.error("Failed to save MCP server:", err); } }; const handleOAuth = async (id: string) => { try { await initiateMcpOauthCmd(id); } catch (err) { console.error("OAuth initiation failed:", err); } }; return (

MCP Servers

Manage Model Context Protocol servers to extend AI tool capabilities.

{!isAdding && ( )}
{servers.length === 0 && !isAdding && (

No MCP servers configured. Add one to extend AI tool capabilities.

)} {servers.map((server) => { const status = statuses[server.id]; const discoveryStatus = status?.status ?? server.discovery_status; const isDiscovering = discovering[server.id] ?? false; return (
{server.name} {server.transport_type} {discoveryStatus === "connected" && } {discoveryStatus === "pending" && } {(discoveryStatus === "error" || discoveryStatus === "unreachable") && ( )} {discoveryStatus} {!server.enabled && ( Disabled )}

{server.transport_type === "http" ? server.url : (() => { const parsed = parseTransportConfig(server.transport_config); return parsed ? `${parsed.command} ${parsed.args.join(" ")}` : server.transport_config; })()} {" | "} Last discovered: {timeAgo(status?.last_discovered_at ?? server.last_discovered_at)} {status && ` | Tools: ${status.tool_count} | Resources: ${status.resource_count}`}

{(status?.error || server.discovery_error) && (

{status?.error ?? server.discovery_error}

)}
{deleteConfirm === server.id ? (
) : ( )}
); })} {isAdding && ( {editServer ? "Edit Server" : "Add Server"}
setForm({ ...form, name: e.target.value })} placeholder="My MCP Server" />
setForm({ ...form, transport_type: v as "stdio" | "http" }) } className="flex gap-4" >
{form.transport_type === "stdio" && (
setForm({ ...form, command: e.target.value })} placeholder="/usr/local/bin/mcp-server" />
setForm({ ...form, args: e.target.value })} placeholder="--port 8080 --verbose" />

Space-separated arguments

)} {form.transport_type === "http" && (
setForm({ ...form, url: e.target.value })} placeholder="http://localhost:3001/mcp" />
)}
{(form.auth_type === "api_key" || form.auth_type === "bearer") && (
setForm({ ...form, auth_value: e.target.value })} placeholder={editServer ? "Leave blank to keep existing" : "Enter value"} />
)} {form.auth_type === "oauth2" && editServer && (

Opens a browser window to complete OAuth2 authentication.

)} {form.transport_type === "stdio" && ( <>

Space-separated KEY=value pairs for non-sensitive values (e.g., DEBUG=1 LOG_LEVEL=info)

setForm({ ...form, plaintext_env: e.target.value })} placeholder="KEY1=value1 KEY2=value2" />

For sensitive values like API keys. Space-separated KEY=value pairs.

setForm({ ...form, encrypted_env: e.target.value })} placeholder={editServer ? "Leave blank to keep existing values" : "API_KEY=secret TOKEN=xyz"} /> {editServer && (

Encrypted values are stored securely and never displayed. Leave blank to preserve existing values.

)}
)} {form.transport_type === "http" && ( <>

Space-separated KEY:value pairs for custom HTTP headers (e.g., X-API-Key:secret X-Custom:value)

setForm({ ...form, http_headers: e.target.value })} placeholder="X-API-Key:secret X-Custom-Header:value" />
)}
)}
); }