"use client"; import { useState, useEffect, useCallback, useMemo } from "react"; import { X, Search, Filter, Package, Star, Eye, CheckCircle, XCircle, Upload, } from "lucide-react"; import { MCPProvider, MCPTestResult } from "@/lib/types/mcp"; import { MCPCard } from "./MCPCard"; import { MCPCategoryFilter } from "./MCPCategoryFilter"; import { MCPSearchBar } from "./MCPSearchBar"; import { MCPInstallConfigModal } from "./MCPInstallConfigModal"; import { MCPManualInstallModal } from "./MCPManualInstallModal"; // import { VirtualizedMCPGrid } from "./VirtualizedMCPGrid"; interface MCPMarketplaceModalProps { isOpen: boolean; onClose: () => void; } export function MCPMarketplaceModal({ isOpen, onClose, }: MCPMarketplaceModalProps) { const [mcps, setMcps] = useState([]); const [loading, setLoading] = useState(false); const [selectedCategory, setSelectedCategory] = useState("all"); const [searchTerm, setSearchTerm] = useState(""); const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(""); const [sortBy, setSortBy] = useState< "rating" | "name" | "recent" | "total_ratings" | "updated_at" >("rating"); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc"); const [selectedMCP, setSelectedMCP] = useState(null); const [configModalOpen, setConfigModalOpen] = useState(false); const [selectedConfigMCP, setSelectedConfigMCP] = useState(null); const [mcpConfig, setMcpConfig] = useState(null); const [loadingConfig, setLoadingConfig] = useState(false); const [syncStatus, setSyncStatus] = useState<{ isSyncing: boolean; lastSync: number | null; totalCached: number; cacheValid: boolean; } | null>(null); const [categories, setCategories] = useState<{ primary: Array<{ category: string; count: number }>; others: Array<{ category: string; count: number }>; }>({ primary: [], others: [] }); // Estados para controle de abas const [activeTab, setActiveTab] = useState<"marketplace" | "installed">( "marketplace" ); const [installedMcps, setInstalledMcps] = useState([]); const [loadingInstalled, setLoadingInstalled] = useState(false); const [isEditMode, setIsEditMode] = useState(false); // Estados para teste de servidor const [testingMCP, setTestingMCP] = useState(null); const [testResult, setTestResult] = useState(null); // Estados para tracking de instalação em progresso const [installingMcps, setInstallingMcps] = useState< Map< string, { status: string; message: string; percentage: number; } > >(new Map()); // Estado para modal de instalação manual const [manualInstallModalOpen, setManualInstallModalOpen] = useState(false); // Função helper para calcular porcentagem baseada no status const calculatePercentage = (status: string): number => { const statusMap: Record = { pending: 5, checking_dependencies: 10, downloading: 30, installing: 60, testing: 85, ready: 100, failed: 0, }; return statusMap[status] || 0; }; // Memoizar categorias processadas para evitar re-processamento desnecessário const processedCategories = useMemo( () => ({ primary: categories.primary.map((cat) => ({ id: cat.category.toLowerCase(), name: cat.category, icon: cat.category, count: cat.count, })), others: categories.others.map((cat) => ({ id: cat.category.toLowerCase(), name: cat.category, icon: cat.category, count: cat.count, })), }), [categories] ); const fetchMCPs = useCallback(async () => { console.log("Fetching MCPs with params:", { selectedCategory, debouncedSearchTerm, sortBy, sortOrder, }); setLoading(true); try { const params = new URLSearchParams(); if (selectedCategory !== "all") params.append("category", selectedCategory); if (debouncedSearchTerm) params.append("search", debouncedSearchTerm); params.append("sort", sortBy); params.append("order", sortOrder); params.append("limit", "2000"); // Aumentado para 2000 const response = await fetch(`/api/mcp/list?${params}`); const data = await response.json(); if (data.success) { setMcps(data.mcps); } } catch (error) { console.error("Error fetching MCPs:", error); } finally { setLoading(false); } }, [selectedCategory, debouncedSearchTerm, sortBy, sortOrder]); const fetchCategories = useCallback(async () => { try { const response = await fetch("/api/mcp/categories"); const data = await response.json(); if (data.success) { setCategories(data.categories); console.log("📊 Categories loaded:", data.categories); } else { console.error("Failed to fetch categories:", data.error); } } catch (error) { console.error("Error fetching categories:", error); } }, []); const fetchInstalledMCPs = useCallback(async () => { setLoadingInstalled(true); try { const response = await fetch("/api/mcp/installed"); const data = await response.json(); if (data.success) { setInstalledMcps(data.mcps); console.log("📦 Installed MCPs loaded:", data.mcps.length); } else { console.error("Failed to fetch installed MCPs:", data.error); } } catch (error) { console.error("Error fetching installed MCPs:", error); } finally { setLoadingInstalled(false); } }, []); const handleInstall = async (mcpId: string) => { try { // Limpar ID de possíveis barras no início/fim const cleanId = mcpId.replace(/^\/+|\/+$/g, ""); console.log( `[handleInstall] Received mcpId: "${mcpId}" -> cleaned: "${cleanId}"` ); // Buscar dados completos do MCP no cache const mcpFromCache = mcps.find((m) => m.id === cleanId || m.id === mcpId); if (!mcpFromCache) { console.error( "MCP not found in cache. Looking for:", cleanId, "Available IDs:", mcps.slice(0, 5).map((m) => m.id) ); return; } console.log("[handleInstall] Found MCP:", { id: mcpFromCache.id, owner: mcpFromCache.owner, repo: mcpFromCache.repo, name: mcpFromCache.name, }); // Construir ID no formato owner/repo let finalId = cleanId; if (mcpFromCache.owner && mcpFromCache.owner.trim() !== "") { finalId = `${mcpFromCache.owner}/${mcpFromCache.repo}`; } else if (mcpFromCache.repo) { finalId = mcpFromCache.repo; } console.log(`[handleInstall] Using final ID: "${finalId}"`); setLoadingConfig(true); // Abrir modal de configuração sempre // Se tivermos owner/repo, buscar config; senão, abrir com config vazio if (finalId.includes("/")) { const configResponse = await fetch( `/api/mcp/server-config?mcpId=${encodeURIComponent(finalId)}` ); const configData = await configResponse.json(); if (configData.success) { console.log( "Opening config modal with:", configData.config ? "provided config" : "empty config" ); setSelectedConfigMCP(mcpFromCache); setMcpConfig(configData.config); // Pode ser null setConfigModalOpen(true); } else { console.error("Error fetching server config:", configData.error); // Abrir modal mesmo com erro (usuário pode adicionar config) setSelectedConfigMCP(mcpFromCache); setMcpConfig(null); setConfigModalOpen(true); } } else { console.log("No owner/repo format, opening modal with empty config"); // Abrir modal com config vazio setSelectedConfigMCP(mcpFromCache); setMcpConfig(null); setConfigModalOpen(true); } } catch (error) { console.error("Error preparing MCP installation:", error); } finally { setLoadingConfig(false); } }; const handleEditConfig = async (mcp: any) => { try { setLoadingConfig(true); setIsEditMode(true); // Converter dados do MCP instalado para formato MCPProvider const mcpProvider: MCPProvider = { id: mcp.id, name: mcp.name, author: mcp.author || mcp.owner || "Unknown", description: mcp.description || `MCP instalado: ${mcp.name}`, version: "1.0.0", category: mcp.category || "other", tags: mcp.tags || [], rating: mcp.rating || 0, totalRatings: mcp.totalRatings || 0, installed: true, repository: mcp.repository || "", homepage: mcp.homepage || "", config: mcp.config, tools: mcp.tools, }; setSelectedConfigMCP(mcpProvider); setMcpConfig(mcp.config); setConfigModalOpen(true); } catch (error) { console.error("Error preparing MCP config edit:", error); } finally { setLoadingConfig(false); } }; const handleTestServer = async (mcpId: string) => { try { setTestingMCP(mcpId); setTestResult(null); console.log(`🧪 Testando servidor MCP: ${mcpId}`); const response = await fetch("/api/mcp/test-server", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ mcpId }), }); const data = await response.json(); if (data.success) { setTestResult(data.result); console.log("✅ Teste concluído:", data.result); } else { setTestResult({ success: false, message: data.error || "Erro desconhecido", error: data.error, }); console.error("❌ Erro no teste:", data.error); } } catch (error: any) { console.error("Erro ao testar servidor:", error); setTestResult({ success: false, message: "Erro de conexão", error: error.message, }); } finally { setTestingMCP(null); // Limpar resultado após 5 segundos setTimeout(() => { setTestResult(null); }, 5000); } }; const handleConfirmInstall = async (editedConfig: any) => { if (!selectedConfigMCP) return; try { if (isEditMode) { // Modo de edição - atualizar configuração existente const response = await fetch("/api/mcp/update-config", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ mcpId: selectedConfigMCP.id, newConfig: editedConfig, }), }); const data = await response.json(); if (data.success) { // Recarregar lista de MCPs instalados await fetchInstalledMCPs(); // Fechar modal setConfigModalOpen(false); setSelectedConfigMCP(null); setMcpConfig(null); setIsEditMode(false); } else { console.error("Error updating MCP config:", data.error); } } else { // Modo de instalação - comportamento original let finalId = selectedConfigMCP.id; if (!finalId.includes("/")) { if ( selectedConfigMCP.owner && selectedConfigMCP.owner.trim() !== "" ) { finalId = `${selectedConfigMCP.owner}/${selectedConfigMCP.repo}`; } else if (selectedConfigMCP.repo) { finalId = selectedConfigMCP.repo; } } const response = await fetch("/api/mcp/install", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ mcpId: finalId, customConfig: editedConfig, }), }); const data = await response.json(); if (data.success) { // Adicionar ao tracking de instalação setInstallingMcps((prev) => { const next = new Map(prev); next.set(finalId, { status: data.status || "pending", message: "Installation started...", percentage: 5, }); return next; }); // Atualizar estado local setMcps((prev) => prev.map((mcp) => mcp.id === selectedConfigMCP.id ? { ...mcp, installed: true } : mcp ) ); // Fechar modal setConfigModalOpen(false); setSelectedConfigMCP(null); setMcpConfig(null); console.log(`✅ Installation started for ${finalId}`); } else { console.error("Error installing MCP:", data.error); } } } catch (error) { console.error("Error processing MCP:", error); } }; const handleUninstall = async (mcpId: string) => { try { const response = await fetch("/api/mcp/uninstall", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ mcpId }), }); const data = await response.json(); if (data.success) { // Atualizar estado local setMcps((prev) => prev.map((mcp) => mcp.id === mcpId ? { ...mcp, installed: false } : mcp ) ); } } catch (error) { console.error("Error uninstalling MCP:", error); } }; const handleViewDetails = (mcp: MCPProvider) => { setSelectedMCP(mcp); }; // Carregar MCPs e categorias quando o modal abrir useEffect(() => { if (isOpen) { fetchMCPs(); fetchCategories(); } }, [isOpen]); // Removido fetchMCPs das dependências // Carregar MCPs instalados quando aba "installed" for ativada useEffect(() => { if (isOpen && activeTab === "installed") { fetchInstalledMCPs(); } }, [isOpen, activeTab]); // Debounce para searchTerm useEffect(() => { const timer = setTimeout(() => { setDebouncedSearchTerm(searchTerm); }, 500); return () => clearTimeout(timer); }, [searchTerm]); // Fetch sync status when modal opens useEffect(() => { if (isOpen) { fetch("/api/mcp/sync") .then((r) => r.json()) .then((data) => { setSyncStatus({ isSyncing: data.isSyncing, lastSync: data.lastSync, totalCached: data.totalCached, cacheValid: data.cacheValid, }); }) .catch(console.error); } }, [isOpen]); // Poll status de instalações em progresso useEffect(() => { if (installingMcps.size === 0) return; const interval = setInterval(async () => { for (const mcpId of installingMcps.keys()) { try { const response = await fetch( `/api/mcp/install-status?mcpId=${mcpId}` ); const data = await response.json(); if (data.success) { const status = data.status; const isComplete = status === "ready" || status === "failed"; if (isComplete) { // Remover do tracking setInstallingMcps((prev) => { const next = new Map(prev); next.delete(mcpId); return next; }); // Atualizar lista de MCPs await fetchMCPs(); if (activeTab === "installed") { await fetchInstalledMCPs(); } } else { // Atualizar progresso setInstallingMcps((prev) => { const next = new Map(prev); next.set(mcpId, { status: data.status, message: data.message || "", percentage: calculatePercentage(data.status), }); return next; }); } } } catch (error) { console.error(`Error polling status for ${mcpId}:`, error); } } }, 2000); // Poll a cada 2 segundos return () => clearInterval(interval); }, [installingMcps, activeTab]); // Recarregar MCPs quando filtros mudarem (só se modal estiver aberto) useEffect(() => { if (isOpen) { fetchMCPs(); } }, [selectedCategory, debouncedSearchTerm, sortBy, sortOrder, isOpen]); if (!isOpen) return null; return (
{/* Header */}

MCP Marketplace

{activeTab === "marketplace" ? `${mcps.length} MCPs available` : `${installedMcps.length} MCPs installed`} {loadingConfig && (
Carregando configuração...
)}
{/* Tabs */}
{/* Sync Status Banner */} {syncStatus?.isSyncing && (
Sincronizando marketplace... {syncStatus.totalCached} servidores carregados
)} {syncStatus?.lastSync && !syncStatus.isSyncing && (
Cache local ativo • {syncStatus.totalCached} servidores • Última atualização: {new Date(syncStatus.lastSync).toLocaleString()}
)} {/* Content */}
{/* Sidebar - apenas para marketplace */} {activeTab === "marketplace" && (
)} {/* Main Content */}
{/* Search and Filters - apenas para marketplace */} {activeTab === "marketplace" && (
{ // O useEffect já cuida de recarregar quando searchTerm muda }} />
)} {/* MCP Grid */}
{activeTab === "marketplace" && ( <> {loading ? (
) : (
{mcps.map((mcp) => ( handleInstall(mcp.id)} onUninstall={() => handleUninstall(mcp.id)} onViewDetails={() => handleViewDetails(mcp)} installingStatus={installingMcps.get(mcp.id)} /> ))}
)} {!loading && mcps.length === 0 && (

No MCPs found

Try adjusting your search or filters

)} )} {activeTab === "installed" && ( <> {loadingInstalled ? (
) : (
{installedMcps.map((mcp) => ( {}} // Não usado no modo installed onEditConfig={() => handleEditConfig(mcp)} onTestServer={() => handleTestServer(mcp.id)} isTesting={testingMCP === mcp.id} onUninstall={() => handleUninstall(mcp.id)} onViewDetails={() => handleViewDetails({ id: mcp.id, name: mcp.name, author: mcp.author || mcp.owner || "Unknown", description: mcp.description || `MCP instalado: ${mcp.name}`, version: "1.0.0", category: mcp.category || "other", tags: mcp.tags || [], rating: mcp.rating || 0, totalRatings: mcp.totalRatings || 0, installed: true, repository: mcp.repository || "", homepage: mcp.homepage || "", config: mcp.config, tools: mcp.tools, }) } installingStatus={installingMcps.get(mcp.id)} /> ))}
)} {!loadingInstalled && installedMcps.length === 0 && (

Nenhum MCP instalado

Instale MCPs na aba Marketplace

)} )}
{/* Manual Install Modal */} setManualInstallModalOpen(false)} onSuccess={async (mcpId) => { console.log(`✅ Manual installation started for ${mcpId}`); // Adicionar ao tracking de instalação setInstallingMcps((prev) => { const next = new Map(prev); next.set(mcpId, { status: "pending", message: "Installation started...", percentage: 5, }); return next; }); // Atualizar listas await fetchMCPs(); if (activeTab === "installed") { await fetchInstalledMCPs(); } }} /> {/* Config Modal */} {selectedConfigMCP && ( { setConfigModalOpen(false); setSelectedConfigMCP(null); setMcpConfig(null); setIsEditMode(false); }} onConfirm={handleConfirmInstall} isEditMode={isEditMode} /> )} {/* Toast de resultado do teste */} {testResult && (
{testResult.success ? ( ) : ( )}

{testResult.message}

{testResult.responseTime && (

Tempo de resposta: {testResult.responseTime}ms

)} {testResult.error && (

{testResult.error}

)}
)}
); }