"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 [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); // Atualizar MCPs do marketplace para marcar como instalados setMcps((prevMcps) => prevMcps.map((mcp) => { const installedMatch = data.mcps.find( (installed: any) => installed.id === mcp.id || installed.repo === mcp.repo || installed.name === mcp.name ); if (installedMatch) { return { ...mcp, installed: true, tools: installedMatch.tools || mcp.tools, // Atualizar tools também }; } return { ...mcp, installed: false }; }) ); console.log( `📊 Marketplace updated: ${data.mcps.length} MCPs marked as installed` ); } 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) { console.log(`✅ Installation started for ${finalId}`); // 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; }); // Marcar como "instalando" no marketplace (não como instalado ainda) // O polling vai atualizar para installed: true quando status === "ready" setMcps((prev) => prev.map((mcp) => { // Verificar se é o mesmo MCP (por ID ou repo) const isSameMcp = mcp.id === selectedConfigMCP.id || mcp.id === finalId || mcp.repo === selectedConfigMCP.repo; return isSameMcp ? { ...mcp, installed: false } : mcp; }) ); // Fechar modal setConfigModalOpen(false); setSelectedConfigMCP(null); setMcpConfig(null); } 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) { console.log(`✅ MCP ${mcpId} uninstalled successfully`); // Remover do tracking de instalação (se estiver lá) setInstallingMcps((prev) => { const next = new Map(prev); next.delete(mcpId); return next; }); // Atualizar marketplace: marcar como não instalado setMcps((prev) => prev.map((mcp) => mcp.id === mcpId || mcp.repo === mcpId ? { ...mcp, installed: false } : mcp ) ); // Recarregar lista de instalados para sincronizar await fetchInstalledMCPs(); console.log(`📦 Lists updated after uninstalling ${mcpId}`); } else { console.error("Failed to uninstall MCP:", data.error); } } catch (error) { console.error("Error uninstalling MCP:", error); } }; // 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 // Carregar MCPs instalados sempre que o modal abrir (não apenas na aba instalados) useEffect(() => { if (isOpen) { fetchInstalledMCPs(); // Limpar MCPs órfãos no tracking (que estão instalados mas ainda no Map) // Isso previne polling infinito de MCPs que já completaram instalação setTimeout(async () => { if (installingMcps.size > 0) { console.log( `🔍 Checking for orphaned installations: ${installingMcps.size} tracked` ); const installedResponse = await fetch("/api/mcp/installed"); const installedData = await installedResponse.json(); if (installedData.success) { const installedIds = new Set( installedData.mcps.map((m: any) => m.id) ); setInstallingMcps((prev) => { const next = new Map(prev); let removed = 0; for (const mcpId of prev.keys()) { // Se já está instalado, remover do tracking if (installedIds.has(mcpId)) { next.delete(mcpId); removed++; console.log(`🧹 Removed orphaned tracking for ${mcpId}`); } } if (removed > 0) { console.log(`✅ Cleaned ${removed} orphaned installations`); } return next; }); } } }, 1000); // Aguardar 1 segundo após abrir modal } }, [isOpen, fetchInstalledMCPs]); // 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) { console.log( `🎉 Installation ${status} for ${mcpId}, updating lists...` ); // Remover do tracking setInstallingMcps((prev) => { const next = new Map(prev); next.delete(mcpId); return next; }); // Se instalação foi bem-sucedida, marcar como instalado imediatamente if (status === "ready") { const toolsCount = data.validationResult?.tools?.length || 0; console.log( `✨ Marking ${mcpId} as installed with ${toolsCount} tools` ); setMcps((prev) => prev.map((mcp) => mcp.id === mcpId || mcp.repo === mcpId || mcp.name === mcpId ? { ...mcp, installed: true, tools: data.validationResult?.tools || mcp.tools, } : mcp ) ); } // Recarregar lista de instalados sempre (não apenas na aba) await fetchInstalledMCPs(); console.log(`✅ Lists synchronized after ${status} for ${mcpId}`); } 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; }); } } else { // MCP não encontrado ou erro no servidor console.warn(`⚠️ MCP ${mcpId} not found or error: ${data.error}`); // Remover do tracking após erro (evita polling infinito) setInstallingMcps((prev) => { const next = new Map(prev); next.delete(mcpId); return next; }); } } catch (error) { console.error(`❌ Error polling status for ${mcpId}:`, error); // Remover do tracking após erro de rede (evita polling infinito) setInstallingMcps((prev) => { const next = new Map(prev); next.delete(mcpId); return next; }); } } }, 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)} 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)} 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}

)}
)}
); }