import React, { useState, useEffect, useRef, useCallback } from 'react'; import { serverRegistry } from '@/lib/serverRegistry'; import type { ServerRegistryEntry, ServerRegistryFilter } from '@dexto/registry'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog'; import { Button } from './ui/button'; import { Input } from './ui/input'; import { Badge } from './ui/badge'; import { Alert, AlertDescription } from './ui/alert'; import { Card, CardContent, CardHeader, CardTitle } from './ui/card'; import { Search, CheckCircle, ExternalLink, Star, Server, Grid3X3, List, ChevronDown, ChevronUp, Users, Plus, PlusCircle, X, } from 'lucide-react'; import { cn } from '@/lib/utils'; interface ServerRegistryModalProps { isOpen: boolean; onClose: () => void; onInstallServer: (entry: ServerRegistryEntry) => Promise<'connected' | 'requires-input'>; onOpenConnectModal?: () => void; refreshTrigger?: number; disableClose?: boolean; } export default function ServerRegistryModal({ isOpen, onClose, onInstallServer, onOpenConnectModal, refreshTrigger, disableClose = false, }: ServerRegistryModalProps) { const [entries, setEntries] = useState([]); const [filteredEntries, setFilteredEntries] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [installing, setInstalling] = useState(null); // Filter state const [filter] = useState({}); const [searchInput, setSearchInput] = useState(''); // View state const [viewMode, setViewMode] = useState<'list' | 'grid'>('grid'); const [expandedEntry, setExpandedEntry] = useState(null); // Ref for debouncing const debounceTimerRef = useRef | null>(null); // Track if component is mounted to prevent state updates after unmount const isMountedRef = useRef(true); const abortControllerRef = useRef(null); // Cleanup effect to handle unmounting useEffect(() => { isMountedRef.current = true; return () => { isMountedRef.current = false; // Clear debounce timer if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); } // Abort any ongoing requests if (abortControllerRef.current) { abortControllerRef.current.abort(); } }; }, []); // Load entries when modal opens useEffect(() => { if (!isOpen) return; const loadEntries = async () => { // Cancel any ongoing request if (abortControllerRef.current) { abortControllerRef.current.abort(); } // Create new AbortController for this request const abortController = new AbortController(); abortControllerRef.current = abortController; if (!isMountedRef.current) return; setIsLoading(true); setError(null); try { // Sync registry with current server status first await serverRegistry.syncWithServerStatus(); const registryEntries = await serverRegistry.getEntries(); // Check if component is still mounted and request wasn't aborted if (isMountedRef.current && !abortController.signal.aborted) { setEntries(registryEntries); setFilteredEntries(registryEntries); } } catch (err: unknown) { // Only set error if component is still mounted and request wasn't aborted if (isMountedRef.current && !abortController.signal.aborted) { const errorMessage = err instanceof Error ? err.message : 'Failed to load server registry'; setError(errorMessage); } } finally { // Only update loading state if component is still mounted and request wasn't aborted if (isMountedRef.current && !abortController.signal.aborted) { setIsLoading(false); } } }; loadEntries(); }, [isOpen, refreshTrigger]); // Debounced filter function const debouncedApplyFilters = useCallback( async (currentFilter: ServerRegistryFilter, currentSearchInput: string) => { if (!isMountedRef.current) return; try { const filtered = await serverRegistry.getEntries({ ...currentFilter, search: currentSearchInput || undefined, }); if (isMountedRef.current) setFilteredEntries(filtered); } catch (err: unknown) { if (isMountedRef.current) { const errorMessage = err instanceof Error ? err.message : 'Failed to filter entries'; setError(errorMessage); } } }, [] ); // Apply filters with debouncing useEffect(() => { // Clear the previous timer if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); } // Set a new timer to debounce the filter operation debounceTimerRef.current = setTimeout(() => { debouncedApplyFilters(filter, searchInput); }, 300); // 300ms delay // Cleanup function to clear timer on unmount return () => { if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); } }; }, [filter, searchInput, entries, debouncedApplyFilters]); // TODO: consolidate registry connection flows so modal + panels share a single state machine. const handleInstall = async (entry: ServerRegistryEntry) => { if (!isMountedRef.current) return; setInstalling(entry.id); try { const result = await onInstallServer(entry); if (result === 'connected') { await serverRegistry.setInstalled(entry.id, true); if (isMountedRef.current) { setEntries((prev) => prev.map((e) => (e.id === entry.id ? { ...e, isInstalled: true } : e)) ); setFilteredEntries((prev) => prev.map((e) => (e.id === entry.id ? { ...e, isInstalled: true } : e)) ); } } else if (isMountedRef.current) { setEntries((prev) => prev.map((e) => (e.id === entry.id ? { ...e, isInstalled: false } : e)) ); setFilteredEntries((prev) => prev.map((e) => (e.id === entry.id ? { ...e, isInstalled: false } : e)) ); } } catch (err: unknown) { if (isMountedRef.current) { const errorMessage = err instanceof Error ? err.message : 'Failed to install server'; setError(errorMessage); } } finally { if (isMountedRef.current) { setInstalling(null); } } }; // Theme spec: Subtle, professional badge colors with soft backgrounds const getCategoryColor = (category: string) => { const colors = { productivity: 'bg-blue-50 text-blue-700 border-blue-200/60', development: 'bg-emerald-50 text-emerald-700 border-emerald-200/60', research: 'bg-purple-50 text-purple-700 border-purple-200/60', creative: 'bg-pink-50 text-pink-700 border-pink-200/60', data: 'bg-amber-50 text-amber-700 border-amber-200/60', communication: 'bg-indigo-50 text-indigo-700 border-indigo-200/60', custom: 'bg-slate-50 text-slate-700 border-slate-200/60', }; return colors[category as keyof typeof colors] || colors.custom; }; const isCloseBlocked = disableClose || Boolean(installing); const handleDialogOpenChange = useCallback( (open: boolean) => { if (!open && !isCloseBlocked) { onClose(); } }, [isCloseBlocked, onClose] ); const preventCloseInteraction = isCloseBlocked ? (event: Event) => { event.preventDefault(); } : undefined; return ( event.preventDefault() : undefined} onInteractOutside={preventCloseInteraction} > {/* Theme: Compact header with refined typography and subtle background */}
MCP Server Registry Discover and add integrations to your AI assistant
{/* Theme: Clean search with subtle focus state and refined controls */}
setSearchInput(e.target.value)} className="pl-10 h-10 border focus:border-primary/40 bg-background text-sm placeholder:text-muted-foreground/50 focus:ring-1 focus:ring-primary/20 transition-all" />
{error && ( {error} )} {isLoading ? (
Discovering servers...
Loading registry entries
) : filteredEntries.length === 0 ? (
{entries.length === 0 ? 'No servers available in the registry' : 'No servers match your search'}
{entries.length === 0 ? 'The registry is currently empty or failed to load' : 'Try adjusting your search terms or browse all categories'}
{searchInput && ( )}
) : (
{filteredEntries.map((entry, index) => { const isExpanded = expandedEntry === entry.id; const hasLongDescription = entry.description && entry.description.length > 100; return viewMode === 'grid' ? (
setExpandedEntry(isExpanded ? null : entry.id) } > {/* Theme: Balanced header with medium emphasis icon and refined typography */}
{entry.icon || '⚡'}
{entry.isInstalled && (
)}
{entry.name}
{entry.isOfficial && ( Official )} {entry.category && ( {entry.category} )}
{/* Theme: Readable description with proper line height */}

{entry.description}

{isExpanded && (
{entry.author && (
by{' '} {entry.author}
)} {entry.tags && entry.tags.length > 0 && (
{entry.tags .slice(0, 6) .map((tag) => ( {tag} ))} {entry.tags.length > 6 && ( + {entry.tags.length - 6} )}
)} {entry.homepage && ( e.stopPropagation() } > Documentation )}
)}
{/* Theme: Clean footer with primary CTA emphasis */}
{hasLongDescription && ( )}
) : ( setExpandedEntry(isExpanded ? null : entry.id) } >
{entry.icon || '⚡'}
{entry.isInstalled && (
)}

{entry.name}

{hasLongDescription && ( )}
{entry.isOfficial && ( Official )} {entry.category && ( {entry.category} )}

{entry.description}

{isExpanded && (
{entry.author && (
by{' '} {entry.author}
)} {entry.tags && entry.tags.length > 0 && (
{entry.tags.map((tag) => ( {tag} ))}
)} {entry.homepage && ( e.stopPropagation() } > Documentation )}
)}
); })}
)}
); }