import { useState, useEffect, useCallback, useRef } from 'react'; import { useAppStore } from '@/stores/app'; import { useAuthStore } from '@/stores/auth'; import { useI18n } from '@/i18n'; import { mcpApi, type McpServerFull, type McpServerPayload, type McpToolDef } from '@/api/endpoints/mcp'; import { Plug, Plus, Pencil, Trash2, Check, X, Loader2, ChevronRight, Power, PowerOff, Search, Wrench, Terminal, Globe, FileCode2, ChevronDown, ChevronUp, } from 'lucide-react'; /* ── Msg ── */ function Msg({ type, text }: { type: 's' | 'e' | 'i'; text: string }) { const bg = type === 's' ? 'var(--gnl)' : type === 'e' ? 'var(--rdl)' : 'var(--bg2)'; const fg = type === 's' ? 'var(--gn)' : type === 'e' ? 'var(--rd)' : 'var(--t2)'; return (
{type === 's' && } {type === 'e' && } {type === 'i' && } {text}
); } /* ── Type badge ── */ function TypeBadge({ type }: { type: string }) { const cfg: Record = { stdio: { icon: , bg: 'color-mix(in srgb, var(--ac) 12%, transparent)', fg: 'var(--ac)' }, sse: { icon: , bg: 'color-mix(in srgb, var(--gn) 12%, transparent)', fg: 'var(--gn)' }, module: { icon: , bg: 'color-mix(in srgb, #e67e22 12%, transparent)', fg: '#e67e22' }, }; const c = cfg[type] || cfg.stdio; return ( {c.icon} {type} ); } /* ── Main ── */ export default function McpServersPage() { const { adbCfg } = useAppStore(); const isAdmin = useAuthStore((s) => s.user?.role === 'admin'); const { t } = useI18n(); const [servers, setServers] = useState([]); const [loading, setLoading] = useState(true); // Form const [formOpen, setFormOpen] = useState(false); const [editing, setEditing] = useState(null); const [saving, setSaving] = useState(false); const [formMsg, setFormMsg] = useState<{ type: 's' | 'e' | 'i'; text: string } | null>(null); const formRef = useRef(null); // Form fields const [sName, setSName] = useState(''); const [sDesc, setSDesc] = useState(''); const [sType, setSType] = useState<'stdio' | 'sse' | 'module'>('stdio'); const [sCmd, setSCmd] = useState(''); const [sArgs, setSArgs] = useState(''); const [sUrl, setSUrl] = useState(''); const [sAdb, setSAdb] = useState(''); // Table const [tableMsg, setTableMsg] = useState<{ type: 's' | 'e' | 'i'; text: string } | null>(null); const [confirmDelete, setConfirmDelete] = useState(null); const [toggling, setToggling] = useState>({}); const [discovering, setDiscovering] = useState>({}); const [expandedTools, setExpandedTools] = useState>({}); const fetchServers = useCallback(async () => { try { const data = await mcpApi.list(); setServers(data); } catch {} finally { setLoading(false); } }, []); useEffect(() => { fetchServers(); }, [fetchServers]); const resetForm = () => { setEditing(null); setFormOpen(false); setSName(''); setSDesc(''); setSType('stdio'); setSCmd(''); setSArgs(''); setSUrl(''); setSAdb(''); setFormMsg(null); setSaving(false); }; const startEdit = (srv: McpServerFull) => { setEditing(srv); setSName(srv.name); setSDesc(srv.description || ''); setSType((srv.server_type || 'stdio') as 'stdio' | 'sse' | 'module'); setSCmd(srv.command || ''); setSArgs(srv.args ? JSON.stringify(srv.args) : ''); setSUrl(srv.url || ''); setSAdb(srv.linked_adb_id || ''); setFormOpen(true); setFormMsg(null); setTimeout(() => formRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }), 100); }; const handleSave = async () => { if (!sName.trim()) { setFormMsg({ type: 'e', text: t('mcp.fillName') }); return; } let args: string[] | null = null; if (sArgs.trim()) { try { args = JSON.parse(sArgs); } catch { setFormMsg({ type: 'e', text: t('mcp.invalidArgs') }); return; } } const body: McpServerPayload = { name: sName.trim(), description: sDesc.trim() || null, server_type: sType, command: sType !== 'sse' ? (sCmd.trim() || null) : null, url: sType === 'sse' ? (sUrl.trim() || null) : null, args, linked_adb_id: sAdb || null, }; setSaving(true); setFormMsg({ type: 'i', text: editing ? t('mcp.updating') : t('mcp.saving') }); try { if (editing) { await mcpApi.update(editing.id, body); } else { await mcpApi.create(body); } setFormMsg({ type: 's', text: editing ? t('mcp.updatedSuccess') : t('mcp.savedSuccess') }); await fetchServers(); useAppStore.getState().loadData(); setTimeout(resetForm, 1200); } catch (err) { setFormMsg({ type: 'e', text: err instanceof Error ? err.message : t('common.errorSave') }); setSaving(false); } }; const handleDelete = async (id: string) => { setConfirmDelete(null); setTableMsg({ type: 'i', text: t('mcp.deletingServer') }); try { await mcpApi.remove(id); await fetchServers(); useAppStore.getState().loadData(); setTableMsg({ type: 's', text: t('mcp.deletedSuccess') }); if (editing?.id === id) resetForm(); setTimeout(() => setTableMsg(null), 3000); } catch (err) { setTableMsg({ type: 'e', text: err instanceof Error ? err.message : t('common.errorDelete') }); } }; const handleToggle = async (id: string) => { setToggling((p) => ({ ...p, [id]: true })); try { await mcpApi.toggle(id); await fetchServers(); useAppStore.getState().loadData(); } catch (err) { setTableMsg({ type: 'e', text: err instanceof Error ? err.message : 'Erro' }); } finally { setToggling((p) => ({ ...p, [id]: false })); } }; const handleDiscover = async (id: string) => { setDiscovering((p) => ({ ...p, [id]: true })); setTableMsg({ type: 'i', text: t('mcp.discovering') }); try { const res = await mcpApi.discoverTools(id); await fetchServers(); useAppStore.getState().loadData(); setTableMsg({ type: 's', text: t('mcp.discoveredTools').replace('{0}', String(res.discovered)).replace('{1}', String(res.total)) }); setExpandedTools((p) => ({ ...p, [id]: true })); setTimeout(() => setTableMsg(null), 4000); } catch (err) { setTableMsg({ type: 'e', text: err instanceof Error ? err.message : t('mcp.errorDiscoverTools') }); } finally { setDiscovering((p) => ({ ...p, [id]: false })); } }; const handleRemoveTool = async (srvId: string, idx: number) => { const srv = servers.find((s) => s.id === srvId); if (!srv?.tools) return; const tool = srv.tools[idx]; const toolName = typeof tool === 'string' ? tool : (tool as McpToolDef).name; if (!confirm(t('mcp.removeToolConfirm').replace('{0}', toolName))) return; const newTools = [...srv.tools]; newTools.splice(idx, 1); try { await mcpApi.updateTools(srvId, newTools); await fetchServers(); useAppStore.getState().loadData(); } catch (err) { setTableMsg({ type: 'e', text: err instanceof Error ? err.message : 'Erro' }); } }; const isFormVisible = formOpen || !!editing; const inputCls = "w-full px-3 py-2 rounded-lg text-[.78rem] outline-none transition-colors focus:ring-1"; const inputStyle = { background: 'var(--bg2)', border: '1.5px solid var(--bd)', color: 'var(--t1)' }; const labelCls = "block"; const labelStyle = {}; const hintCls = "text-[.64rem] mt-0.5"; const hintStyle = { color: 'var(--t4)' }; return (
{/* Header */}

{t('mcp.title')}

{t('mcp.subtitle')}
{servers.length} {servers.length === 1 ? 'server' : 'servers'}
{/* Server cards */}

{t('mcp.registered')}

{tableMsg &&
} {loading ? (
) : servers.length === 0 ? (

{t('mcp.noServers')}

) : (
{servers.map((srv) => { const tools: McpToolDef[] = Array.isArray(srv.tools) ? srv.tools : []; const isExpanded = expandedTools[srv.id]; const adb = srv.linked_adb_id ? adbCfg.find((a) => a.id === srv.linked_adb_id) : null; return (
{/* Header row */}
{srv.name} {(srv as any).is_global === 1 && ( GLOBAL )} {srv.is_active ? : } {srv.is_active ? t('mcp.active') : t('mcp.inactive')}
{(isAdmin || !(srv as any).is_global) && ( <> {confirmDelete === srv.id ? (
) : ( )} )}
{/* Description */} {srv.description && (
{srv.description}
)} {/* Details row */}
{srv.command && (
{t('mcp.command')}{' '} {srv.command}
)} {srv.url && (
{t('mcp.url')}{' '} {srv.url}
)} {srv.module_path && (
{t('mcp.module')}{' '} {srv.module_path}
)}
{t('mcp.adb')}{' '} {adb ? ( {adb.config_name} ) : t('mcp.noAdb')}
{/* Tools section */}
setExpandedTools((p) => ({ ...p, [srv.id]: !p[srv.id] }))} > {t('mcp.tools')} ({tools.length}) {isExpanded ? : }
{isExpanded && tools.length > 0 && (
{tools.map((tool, i) => (
{typeof tool === 'string' ? tool : tool.name} {typeof tool === 'object' && tool.description && ( — {tool.description} )} {typeof tool === 'object' && tool.input_schema && (tool.input_schema as { properties?: Record }).properties && (
Params: {Object.keys((tool.input_schema as { properties: Record }).properties).join(', ')}
)}
))}
)} {isExpanded && tools.length === 0 && (
{t('mcp.noTools')}
)}
{/* Action buttons */}
); })}
)}
{/* Form */}
{ if (!editing) setFormOpen(!formOpen); }}>
{editing ? : } {editing ? `${t('mcp.editServer')} — ${editing.name}` : t('mcp.newServer')}
{isFormVisible && (

{editing ? <>{t('mcp.editingLabel')} {editing.name} : t('mcp.configDesc')}

{formMsg && } {/* Name + description */}
setSName(e.target.value)} placeholder="CIS Benchmark Server" className={inputCls} style={inputStyle} />
setSDesc(e.target.value)} placeholder="Executa checks CIS 3.0" className={inputCls} style={inputStyle} />
{/* Server type */}
{(['stdio', 'sse', 'module'] as const).map((tp) => { const icons = { stdio: , sse: , module: }; const labels = { stdio: 'stdio', sse: 'SSE (HTTP)', module: 'Python Module' }; const selected = sType === tp; return ( ); })}
{/* Conditional fields */} {sType !== 'sse' && (

{t('mcp.commandHint')}

setSCmd(e.target.value)} placeholder="python3 server.py" className={inputCls} style={inputStyle} />

{t('mcp.argsHint')}

setSArgs(e.target.value)} placeholder='["--config", "/path/to/config"]' className={inputCls} style={{ ...inputStyle, fontFamily: 'var(--fm)', fontSize: '.72rem' }} />
)} {sType === 'sse' && (

{t('mcp.urlHint')}

setSUrl(e.target.value)} placeholder="http://localhost:8001/sse" className={inputCls} style={inputStyle} />
)} {/* ADB link */}

{t('mcp.linkAdbHint')}

{/* Buttons */}
{editing && ( )}
)}
); }