/** * McpServerCard — grid card for a single MCP server in the catalog. * Shows server icon from registry when available. */ import { useState } from 'react'; import { api } from '@renderer/api'; import { Badge } from '@renderer/components/ui/badge'; import { Button } from '@renderer/components/ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { useStore } from '@renderer/store'; import { formatCompactNumber, formatRelativeTime } from '@renderer/utils/formatters'; import { getMcpInstallationSummaryLabel, getMcpOperationKey, sanitizeMcpServerName, } from '@shared/utils/extensionNormalizers'; import { Clock, Cloud, Globe, KeyRound, Lock, Monitor, Star, Tag, Wrench } from 'lucide-react'; import { Github as GithubIcon } from 'lucide-react'; import { InstallButton } from '../common/InstallButton'; import { SourceBadge } from '../common/SourceBadge'; import type { InstalledMcpEntry, McpCatalogItem, McpServerDiagnostic, } from '@shared/types/extensions'; interface McpServerCardProps { server: McpCatalogItem; isInstalled: boolean; installedEntry?: InstalledMcpEntry | null; installedEntries?: InstalledMcpEntry[]; diagnostic?: McpServerDiagnostic | null; diagnosticsLoading?: boolean; onClick: (serverId: string) => void; } export const McpServerCard = ({ server, isInstalled, installedEntry, installedEntries = [], diagnostic, diagnosticsLoading, onClick, }: McpServerCardProps): React.JSX.Element => { const operationKey = getMcpOperationKey(server.id, 'user'); const installProgress = useStore((s) => s.mcpInstallProgress[operationKey] ?? 'idle'); const installMcpServer = useStore((s) => s.installMcpServer); const uninstallMcpServer = useStore((s) => s.uninstallMcpServer); const installError = useStore((s) => s.installErrors[operationKey]); const stars = useStore((s) => server.repositoryUrl ? s.mcpGitHubStars[server.repositoryUrl] : undefined ); const canAutoInstall = !!server.installSpec; const normalizedInstalledEntries = installedEntries.length ? installedEntries : installedEntry ? [installedEntry] : []; const requiresConfiguration = server.installSpec?.type === 'http' || server.envVars.length > 0 || server.requiresAuth || (server.authHeaders?.length ?? 0) > 0; const defaultServerName = sanitizeMcpServerName(server.name); const userInstallEntry = normalizedInstalledEntries.find((entry) => entry.scope === 'user') ?? null; const installSummaryLabel = getMcpInstallationSummaryLabel(normalizedInstalledEntries); const supportsDirectInstalledAction = isInstalled && normalizedInstalledEntries.length === 1 && userInstallEntry?.name === defaultServerName && !requiresConfiguration; const shouldShowDirectInstallButton = canAutoInstall && (!isInstalled ? !requiresConfiguration : supportsDirectInstalledAction); const [imgError, setImgError] = useState(false); const hasIcon = !!server.iconUrl && !imgError; const diagnosticBadgeClass = diagnostic?.status === 'connected' ? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-400' : diagnostic?.status === 'needs-authentication' ? 'border-amber-500/30 bg-amber-500/10 text-amber-400' : diagnostic?.status === 'failed' ? 'border-red-500/30 bg-red-500/10 text-red-400' : 'border-border bg-surface-raised text-text-muted'; return (
onClick(server.id)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick(server.id); } }} className={`relative flex w-full cursor-pointer flex-col gap-2 overflow-hidden rounded-lg border p-4 text-left transition-all duration-200 hover:border-border-emphasis hover:bg-surface-raised hover:shadow-[0_0_12px_rgba(255,255,255,0.02)] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--color-border-emphasis)] ${ isInstalled ? 'border-l-2 border-border border-l-emerald-500/30' : 'border-border' }`} > {/* Header: icon + name */}
{/* Server icon (only when available) */} {hasIcon && (
setImgError(true)} />
)}

{server.name}

{server.source !== 'official' && (
)}
{isInstalled && ( {installSummaryLabel ?? 'Installed'} )} {isInstalled && diagnosticsLoading && !diagnostic && ( Checking... )} {diagnostic && ( {diagnostic.statusLabel} )}
{/* Description */}

{server.description}

{diagnostic?.target && (

{diagnostic.target}

)} {/* Footer indicators + install button */}
{server.tools.length > 0 && ( {server.tools.length} {server.tools.length === 1 ? 'tool' : 'tools'} )} {server.envVars.length > 0 && ( {server.envVars.length} {server.envVars.length === 1 ? 'env' : 'envs'} )} {server.requiresAuth && ( Auth )} {server.version && ( {server.version} )} {server.updatedAt && ( {formatRelativeTime(server.updatedAt)} )} {server.author && by {server.author}} {server.hostingType === 'remote' && ( Remote )} {server.hostingType === 'local' && ( Local )} {server.hostingType === 'both' && ( Both )} {/* External links + stars */} {server.repositoryUrl && ( Repository )} {server.websiteUrl && ( Website )}
{shouldShowDirectInstallButton && (
installMcpServer({ registryId: server.id, serverName: defaultServerName, scope: 'user', envValues: {}, headers: [], }) } onUninstall={() => uninstallMcpServer(server.id, userInstallEntry?.name ?? defaultServerName, 'user') } size="sm" errorMessage={installError} />
)} {canAutoInstall && (!shouldShowDirectInstallButton || requiresConfiguration) && (
)}
); };