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 */}
{tableMsg &&
}
{loading ? (
) : servers.length === 0 ? (
) : (
{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 */}
{/* 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' && (
)}
{sType === 'sse' && (
)}
{/* ADB link */}
{t('mcp.linkAdbHint')}
{/* Buttons */}
{editing && (
)}
)}
);
}