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; } } 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; } const emptyForm: ServerForm = { name: "", url: "", transport_type: "http", command: "", args: "", auth_type: "none", auth_value: "", enabled: true, }; 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); 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, }); 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; const transportConfig = form.transport_type === "stdio" ? JSON.stringify({ command: form.command, args: form.args.split(/\s+/).filter(Boolean) }) : "{}"; 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, }; 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, }; 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.

)}
)}
); }