/** * McpServersPanel — search and browse the MCP server catalog. */ import { useEffect, useMemo, useState } from 'react'; import { Badge } from '@renderer/components/ui/badge'; import { Button } from '@renderer/components/ui/button'; import { Checkbox } from '@renderer/components/ui/checkbox'; import { Label } from '@renderer/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@renderer/components/ui/select'; import { useStore } from '@renderer/store'; import { formatRelativeTime } from '@renderer/utils/formatters'; import { AlertTriangle, RefreshCw, Search, Server } from 'lucide-react'; import { SearchInput } from '../common/SearchInput'; import { McpServerCard } from './McpServerCard'; import { McpServerDetailDialog } from './McpServerDetailDialog'; import type { InstalledMcpEntry, McpCatalogItem, McpServerDiagnostic, } from '@shared/types/extensions'; import { sanitizeMcpServerName } from '@shared/utils/extensionNormalizers'; type McpSortValue = 'name-asc' | 'name-desc' | 'tools-desc'; const MCP_SORT_OPTIONS: { value: McpSortValue; label: string }[] = [ { value: 'name-asc', label: 'Name A→Z' }, { value: 'name-desc', label: 'Name Z→A' }, { value: 'tools-desc', label: 'Most tools' }, ]; function sortMcpServers(servers: McpCatalogItem[], sort: McpSortValue): McpCatalogItem[] { return [...servers].sort((a, b) => { switch (sort) { case 'name-asc': return a.name.localeCompare(b.name); case 'name-desc': return b.name.localeCompare(a.name); case 'tools-desc': return b.tools.length - a.tools.length; default: return 0; } }); } interface McpServersPanelProps { mcpSearchQuery: string; mcpSearch: (query: string) => void; mcpSearchResults: McpCatalogItem[]; mcpSearchLoading: boolean; mcpSearchWarnings: string[]; selectedMcpServerId: string | null; setSelectedMcpServerId: (id: string | null) => void; } export const McpServersPanel = ({ mcpSearchQuery, mcpSearch, mcpSearchResults, mcpSearchLoading, mcpSearchWarnings, selectedMcpServerId, setSelectedMcpServerId, }: McpServersPanelProps): React.JSX.Element => { const browseCatalog = useStore((s) => s.mcpBrowseCatalog); const browseNextCursor = useStore((s) => s.mcpBrowseNextCursor); const browseLoading = useStore((s) => s.mcpBrowseLoading); const browseError = useStore((s) => s.mcpBrowseError); const mcpBrowse = useStore((s) => s.mcpBrowse); const installedServers = useStore((s) => s.mcpInstalledServers); const fetchMcpGitHubStars = useStore((s) => s.fetchMcpGitHubStars); const mcpDiagnostics = useStore((s) => s.mcpDiagnostics); const mcpDiagnosticsLoading = useStore((s) => s.mcpDiagnosticsLoading); const mcpDiagnosticsError = useStore((s) => s.mcpDiagnosticsError); const mcpDiagnosticsLastCheckedAt = useStore((s) => s.mcpDiagnosticsLastCheckedAt); const runMcpDiagnostics = useStore((s) => s.runMcpDiagnostics); const [mcpSort, setMcpSort] = useState('name-asc'); const [mcpInstalledOnly, setMcpInstalledOnly] = useState(false); // Load initial browse data useEffect(() => { if (browseCatalog.length === 0 && !browseLoading) { void mcpBrowse(); } }, [browseCatalog.length, browseLoading, mcpBrowse]); useEffect(() => { void runMcpDiagnostics(); }, [runMcpDiagnostics]); // Fetch GitHub stars after catalog loads (fire-and-forget) useEffect(() => { const urls = browseCatalog.map((s) => s.repositoryUrl).filter((u): u is string => !!u); if (urls.length > 0) { fetchMcpGitHubStars(urls); } }, [browseCatalog, fetchMcpGitHubStars]); // Decide which list to show: search results or browse const isSearching = mcpSearchQuery.trim().length > 0; const rawServers = isSearching ? mcpSearchResults : browseCatalog; const isLoading = isSearching ? mcpSearchLoading : browseLoading; const warnings = isSearching ? mcpSearchWarnings : []; // Installed lookup set (lowercase CLI names) const installedNames = useMemo( () => new Set(installedServers.map((s) => s.name.toLowerCase())), [installedServers] ); const installedEntriesByName = useMemo( () => new Map(installedServers.map((entry) => [entry.name.toLowerCase(), entry] as const)), [installedServers] ); /** Check if a catalog server is installed by comparing sanitized names */ const isServerInstalled = (server: McpCatalogItem): boolean => installedNames.has(sanitizeMcpServerName(server.name)); const getInstalledEntry = (server: McpCatalogItem): InstalledMcpEntry | null => installedEntriesByName.get(sanitizeMcpServerName(server.name)) ?? null; const getDiagnostic = (server: McpCatalogItem): McpServerDiagnostic | null => { const installedEntry = getInstalledEntry(server); return installedEntry ? (mcpDiagnostics[installedEntry.name] ?? null) : null; }; const allDiagnostics = useMemo( () => Object.values(mcpDiagnostics).sort((a, b) => a.name.localeCompare(b.name)), [mcpDiagnostics] ); const getDiagnosticBadgeClass = (status: McpServerDiagnostic['status']): string => { switch (status) { case 'connected': return 'border-emerald-500/30 bg-emerald-500/10 text-emerald-400'; case 'needs-authentication': return 'border-amber-500/30 bg-amber-500/10 text-amber-400'; case 'failed': return 'border-red-500/30 bg-red-500/10 text-red-400'; default: return 'border-border bg-surface-raised text-text-muted'; } }; // Sort + filter const displayServers = useMemo(() => { let result = rawServers; if (mcpInstalledOnly) { result = result.filter(isServerInstalled); } return sortMcpServers(result, mcpSort); }, [rawServers, mcpSort, mcpInstalledOnly, installedNames]); // Find selected server (search in both lists to avoid losing selection during search toggle) const selectedServer = useMemo(() => { if (!selectedMcpServerId) return null; return ( displayServers.find((s) => s.id === selectedMcpServerId) ?? browseCatalog.find((s) => s.id === selectedMcpServerId) ?? mcpSearchResults.find((s) => s.id === selectedMcpServerId) ?? null ); }, [displayServers, browseCatalog, mcpSearchResults, selectedMcpServerId]); return (

MCP Health Status

{mcpDiagnosticsLoading ? ( <> Checking installed MCP servers via Claude CLI (claude mcp list) ... ) : mcpDiagnosticsLastCheckedAt ? ( `Last checked ${formatRelativeTime(new Date(mcpDiagnosticsLastCheckedAt).toISOString())}` ) : ( <> Run diagnostics (claude mcp list) to verify installed MCP connectivity. )}

{(mcpDiagnosticsLoading || allDiagnostics.length > 0) && (

Claude MCP List Results

{allDiagnostics.length > 0 && ( {allDiagnostics.length} servers )}
{allDiagnostics.length > 0 ? (
{allDiagnostics.map((diagnostic) => (

{diagnostic.name}

{diagnostic.target}

{diagnostic.statusLabel}
))}
) : (

Waiting for `claude mcp list` results...

)}
)}
{/* Search + Sort + Installed only row */}
setMcpInstalledOnly(!mcpInstalledOnly)} />
{/* Warnings */} {warnings.length > 0 && (
{warnings.map((w, i) => (
{w}
))}
)} {/* Skeleton loading */} {isLoading && displayServers.length === 0 && (
{Array.from({ length: 6 }, (_, i) => (
))}
)} {browseError && !isSearching && (
{browseError}
)} {mcpDiagnosticsError && (
{mcpDiagnosticsError}
)} {/* Empty state */} {!isLoading && displayServers.length === 0 && (
{isSearching || mcpInstalledOnly ? ( ) : ( )}

{isSearching ? 'No servers found' : mcpInstalledOnly ? 'No installed servers' : 'No MCP servers available'}

{isSearching ? 'Try a different search term' : mcpInstalledOnly ? 'Install servers from the catalog to see them here' : 'Check back later for new servers'}

)} {displayServers.length > 0 && (
{displayServers.map((server) => ( ))}
)} {/* Load more for browse */} {!isSearching && browseNextCursor && (
)} {/* Detail dialog */} setSelectedMcpServerId(null)} />
); };