/** * 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@renderer/components/ui/select'; import { useStore } from '@renderer/store'; import { formatRelativeTime } from '@renderer/utils/formatters'; import { CLI_NOT_FOUND_MARKER } from '@shared/constants/cli'; import { getMcpDiagnosticKey, getMcpProjectStateKey, getPreferredMcpInstallationEntry, sanitizeMcpServerName, } from '@shared/utils/extensionNormalizers'; import { AlertTriangle, RefreshCw, Search, Server } from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; import { SearchInput } from '../common/SearchInput'; import { McpServerCard } from './McpServerCard'; import { McpServerDetailDialog } from './McpServerDetailDialog'; import type { InstalledMcpEntry, McpCatalogItem, McpServerDiagnostic, } from '@shared/types/extensions'; 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 { projectPath: string | null; mcpSearchQuery: string; mcpSearch: (query: string) => void; mcpSearchResults: McpCatalogItem[]; mcpSearchLoading: boolean; mcpSearchWarnings: string[]; selectedMcpServerId: string | null; setSelectedMcpServerId: (id: string | null) => void; } export const McpServersPanel = ({ projectPath, mcpSearchQuery, mcpSearch, mcpSearchResults, mcpSearchLoading, mcpSearchWarnings, selectedMcpServerId, setSelectedMcpServerId, }: McpServersPanelProps): React.JSX.Element => { const projectStateKey = getMcpProjectStateKey(projectPath); const { browseCatalog, browseNextCursor, browseLoading, browseError, mcpBrowse, installedServersByProjectPath, installedServersFallback, fetchMcpGitHubStars, mcpDiagnosticsByProjectPath, mcpDiagnosticsFallback, mcpDiagnosticsLoadingByProjectPath, mcpDiagnosticsLoadingFallback, mcpDiagnosticsErrorByProjectPath, mcpDiagnosticsErrorFallback, mcpDiagnosticsLastCheckedAtByProjectPath, mcpDiagnosticsLastCheckedAtFallback, runMcpDiagnostics, cliStatus, } = useStore( useShallow((s) => ({ browseCatalog: s.mcpBrowseCatalog, browseNextCursor: s.mcpBrowseNextCursor, browseLoading: s.mcpBrowseLoading, browseError: s.mcpBrowseError, mcpBrowse: s.mcpBrowse, installedServersByProjectPath: s.mcpInstalledServersByProjectPath, installedServersFallback: s.mcpInstalledServers, fetchMcpGitHubStars: s.fetchMcpGitHubStars, mcpDiagnosticsByProjectPath: s.mcpDiagnosticsByProjectPath, mcpDiagnosticsFallback: s.mcpDiagnostics, mcpDiagnosticsLoadingByProjectPath: s.mcpDiagnosticsLoadingByProjectPath, mcpDiagnosticsLoadingFallback: s.mcpDiagnosticsLoading, mcpDiagnosticsErrorByProjectPath: s.mcpDiagnosticsErrorByProjectPath, mcpDiagnosticsErrorFallback: s.mcpDiagnosticsError, mcpDiagnosticsLastCheckedAtByProjectPath: s.mcpDiagnosticsLastCheckedAtByProjectPath, mcpDiagnosticsLastCheckedAtFallback: s.mcpDiagnosticsLastCheckedAt, runMcpDiagnostics: s.runMcpDiagnostics, cliStatus: s.cliStatus, })) ); const installedServers = installedServersByProjectPath?.[projectStateKey] ?? installedServersFallback ?? []; const mcpDiagnostics = mcpDiagnosticsByProjectPath?.[projectStateKey] ?? mcpDiagnosticsFallback ?? {}; const mcpDiagnosticsLoading = mcpDiagnosticsLoadingByProjectPath?.[projectStateKey] ?? mcpDiagnosticsLoadingFallback ?? false; const mcpDiagnosticsError = mcpDiagnosticsErrorByProjectPath?.[projectStateKey] ?? mcpDiagnosticsErrorFallback ?? null; const mcpDiagnosticsLastCheckedAt = mcpDiagnosticsLastCheckedAtByProjectPath?.[projectStateKey] ?? mcpDiagnosticsLastCheckedAtFallback ?? null; const [mcpSort, setMcpSort] = useState('name-asc'); // Load initial browse data useEffect(() => { if (browseCatalog.length === 0 && !browseLoading && !browseError) { void mcpBrowse(); } }, [browseCatalog.length, browseError, browseLoading, mcpBrowse]); useEffect(() => { void runMcpDiagnostics(projectPath ?? undefined); }, [projectPath, 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(() => { const entriesByName = new Map(); for (const entry of installedServers) { const key = entry.name.toLowerCase(); entriesByName.set(key, [...(entriesByName.get(key) ?? []), entry]); } return entriesByName; }, [installedServers]); /** Check if a catalog server is installed by comparing sanitized names */ const isServerInstalled = (server: McpCatalogItem): boolean => installedNames.has(sanitizeMcpServerName(server.name)); const getInstalledEntries = (server: McpCatalogItem): InstalledMcpEntry[] => installedEntriesByName.get(sanitizeMcpServerName(server.name)) ?? []; const getInstalledEntry = (server: McpCatalogItem): InstalledMcpEntry | null => getPreferredMcpInstallationEntry(getInstalledEntries(server)); const getDiagnostic = (server: McpCatalogItem): McpServerDiagnostic | null => { const installedEntry = getInstalledEntry(server); return installedEntry ? (mcpDiagnostics[getMcpDiagnosticKey(installedEntry.name, installedEntry.scope)] ?? mcpDiagnostics[getMcpDiagnosticKey(installedEntry.name)] ?? 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 displayed servers const displayServers = useMemo(() => sortMcpServers(rawServers, mcpSort), [rawServers, mcpSort]); const runtimeLabel = cliStatus?.flavor === 'agent_teams_orchestrator' ? 'multimodel runtime' : 'Claude CLI'; // 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 {runtimeLabel} ... ) : mcpDiagnosticsLastCheckedAt ? ( `Last checked ${formatRelativeTime(new Date(mcpDiagnosticsLastCheckedAt).toISOString())}` ) : ( <>Run diagnostics from this page to verify installed MCP connectivity. )}

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

Runtime MCP Diagnostics

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

{diagnostic.name}

{diagnostic.scope && ( {diagnostic.scope} )}

{diagnostic.target}

{diagnostic.statusLabel}
))}
) : (

Waiting for {diagnosticsCommand} results...

)}
)}
{/* Search + sort row */}
{/* 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.includes(CLI_NOT_FOUND_MARKER) ? (

Claude CLI not installed

MCP health checks require Claude CLI. Go to the Dashboard to install it automatically.

) : (
{mcpDiagnosticsError}
))} {/* Empty state */} {!isLoading && displayServers.length === 0 && (
{isSearching ? ( ) : ( )}

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

{isSearching ? 'Try a different search term' : 'Check back later for new servers'}

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