/** * McpServerDetailDialog — full detail view for a single MCP server with install controls. * Uses Radix UI Kit for all form elements. */ import { useEffect, useState } from 'react'; import { api } from '@renderer/api'; import { Badge } from '@renderer/components/ui/badge'; import { Button } from '@renderer/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from '@renderer/components/ui/dialog'; import { Input } from '@renderer/components/ui/input'; import { Label } from '@renderer/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@renderer/components/ui/select'; import { useStore } from '@renderer/store'; import { sanitizeMcpServerName } from '@shared/utils/extensionNormalizers'; import { ExternalLink, Lock, Plus, Star, Trash2, Wrench } from 'lucide-react'; import { InstallButton } from '../common/InstallButton'; import { SourceBadge } from '../common/SourceBadge'; import type { InstalledMcpEntry, McpCatalogItem, McpHeaderDef, McpServerDiagnostic, } from '@shared/types/extensions'; interface McpServerDetailDialogProps { server: McpCatalogItem | null; isInstalled: boolean; installedEntry?: InstalledMcpEntry | null; diagnostic?: McpServerDiagnostic | null; diagnosticsLoading?: boolean; open: boolean; onClose: () => void; } type Scope = 'local' | 'user'; const SCOPE_OPTIONS: { value: Scope; label: string }[] = [ { value: 'user', label: 'User (global)' }, { value: 'local', label: 'Local' }, ]; export const McpServerDetailDialog = ({ server, isInstalled, installedEntry, diagnostic, diagnosticsLoading, open, onClose, }: McpServerDetailDialogProps): React.JSX.Element => { const installProgress = useStore( (s) => (server ? s.mcpInstallProgress[server.id] : undefined) ?? 'idle' ); const installMcpServer = useStore((s) => s.installMcpServer); const uninstallMcpServer = useStore((s) => s.uninstallMcpServer); const installError = useStore((s) => (server ? s.installErrors[server.id] : undefined)); const stars = useStore((s) => server?.repositoryUrl ? s.mcpGitHubStars[server.repositoryUrl] : undefined ); const [scope, setScope] = useState('user'); const [serverName, setServerName] = useState(''); const [envValues, setEnvValues] = useState>({}); const [headers, setHeaders] = useState([]); const [imgError, setImgError] = useState(false); const [autoFilledFields, setAutoFilledFields] = useState>(new Set()); // Initialize form when dialog opens or server changes useEffect(() => { if (!server || !open) { return; } setEnvValues(Object.fromEntries(server.envVars.map((env) => [env.name, '']))); setHeaders( (server.authHeaders ?? []).map((header) => ({ key: header.key, value: '', secret: header.isSecret, description: header.description, isRequired: header.isRequired, valueTemplate: header.valueTemplate, locked: true, })) ); setServerName(installedEntry?.name ?? sanitizeMcpServerName(server.name)); setScope(installedEntry?.scope === 'local' ? 'local' : 'user'); setImgError(false); setAutoFilledFields(new Set()); }, [installedEntry?.name, installedEntry?.scope, open, server?.id]); // Auto-fill env values from saved API keys useEffect(() => { if (!server || !open || server.envVars.length === 0 || !api.apiKeys) return; const envVarNames = server.envVars.map((e) => e.name); void api.apiKeys.lookup(envVarNames).then( (results) => { if (results.length === 0) return; const filled = new Set(); const values: Record = {}; for (const r of results) { values[r.envVarName] = r.value; filled.add(r.envVarName); } setEnvValues((prev) => ({ ...prev, ...values })); setAutoFilledFields(filled); }, () => { // Silently fail — auto-fill is supplementary } ); }, [server?.id, open]); // eslint-disable-line react-hooks/exhaustive-deps // Auto-fill env vars from saved API keys useEffect(() => { if (!server || server.envVars.length === 0 || !api.apiKeys) return; const envVarNames = server.envVars.map((env) => env.name); void api.apiKeys.lookup(envVarNames).then( (results) => { if (results.length === 0) return; const filled = new Set(); const updates: Record = {}; for (const r of results) { updates[r.envVarName] = r.value; filled.add(r.envVarName); } setEnvValues((prev) => { const next = { ...prev }; for (const [k, v] of Object.entries(updates)) { // Only auto-fill if the field is empty if (!next[k]) { next[k] = v; } } return next; }); setAutoFilledFields(filled); }, () => { // Silently ignore lookup failures } ); }, [server?.id]); // eslint-disable-line react-hooks/exhaustive-deps if (!server) return <>; const canAutoInstall = !!server.installSpec; const isHttp = server.installSpec?.type === 'http'; const hasIcon = !!server.iconUrl && !imgError; const npmPackageUrl = server.installSpec?.type === 'stdio' ? `https://www.npmjs.com/package/${server.installSpec.npmPackage}` : null; const hasSuggestedHeaders = headers.some((header) => header.locked); const missingRequiredEnvVars = server.envVars.some( (env) => env.isRequired && !envValues[env.name]?.trim() ); const missingRequiredHeaders = headers.some( (header) => header.isRequired && !header.value.trim() ); const installDisabled = !serverName.trim() || missingRequiredEnvVars || missingRequiredHeaders; const uninstallServerName = installedEntry?.name ?? serverName; const uninstallScope = installedEntry?.scope ?? scope; 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'; const handleInstall = () => { installMcpServer({ registryId: server.id, serverName, scope, envValues, headers, }); }; const handleUninstall = () => { uninstallMcpServer(server.id, uninstallServerName, uninstallScope); }; const addHeader = () => { setHeaders((prev) => [...prev, { key: '', value: '' }]); }; const removeHeader = (index: number) => { setHeaders((prev) => prev.filter((_, i) => i !== index)); }; const updateHeader = (index: number, field: 'key' | 'value', value: string) => { setHeaders((prev) => prev.map((h, i) => (i === index ? { ...h, [field]: value } : h))); }; return ( !o && onClose()}>
{/* Server icon (only when available) */} {hasIcon && (
setImgError(true)} />
)}
{server.name} {server.description}
{isInstalled && ( Installed )} {server.source !== 'official' && }
{/* Metadata grid */}
Source

{server.source}

{stars != null && (
GitHub Stars

{stars.toLocaleString()}

)} {server.version && (
Version

{server.version}

)} {server.license && (
License

{server.license}

)}
Install Type {server.installSpec?.type === 'stdio' ? ( ) : (

{server.installSpec ? `HTTP: ${server.installSpec.transportType}` : 'Manual setup required'}

)}
{server.author && (
Author

{server.author}

)} {server.hostingType && (
Hosting

{server.hostingType}

)} {server.publishedAt && (
Published

{new Date(server.publishedAt).toLocaleDateString()}

)} {server.updatedAt && (
Updated

{new Date(server.updatedAt).toLocaleDateString()}

)}
{/* Auth indicator */} {server.requiresAuth && (
This server requires authentication
)} {isHttp && !server.requiresAuth && (server.authHeaders?.length ?? 0) === 0 && (
Remote MCP servers may still require custom headers or API keys even when the registry does not describe them. If connection fails after install, check the provider docs.
)} {(isInstalled || diagnosticsLoading) && (
Claude Status {diagnosticsLoading && !diagnostic ? ( Checking... ) : diagnostic ? ( {diagnostic.statusLabel} ) : ( Not checked )}
{diagnostic?.target && (

Launch Target

{diagnostic.target}
)}
)} {/* Install form */} {canAutoInstall && (

{isInstalled ? 'Manage Installation' : 'Install Server'}

{/* Server name */}
setServerName(e.target.value)} placeholder="my-server" className="h-8 text-sm" disabled={isInstalled} />
{/* Scope */}
{/* Environment variables */} {server.envVars.length > 0 && (
{server.envVars.map((env) => (
{env.name} setEnvValues((prev) => ({ ...prev, [env.name]: e.target.value })) } className="h-7 flex-1 text-xs" placeholder={env.description ?? env.name} /> {autoFilledFields.has(env.name) && envValues[env.name] && ( Auto-filled )}
))}
)} {/* Headers (for HTTP/SSE servers) */} {isHttp && (
{headers.length > 0 && (
{headers.map((header, index) => (
{header.locked ? ( {header.key} ) : ( updateHeader(index, 'key', e.target.value)} className="h-7 w-32 text-xs" placeholder="Header-Name" /> )} updateHeader(index, 'value', e.target.value)} className="h-7 flex-1 text-xs" placeholder={header.valueTemplate ?? header.description ?? 'value'} />
{(header.description || header.valueTemplate || header.isRequired) && (

{[ header.isRequired ? 'Required' : null, header.description, header.valueTemplate, ] .filter(Boolean) .join(' • ')}

)}
))}
)}
)} {/* Install/Uninstall button */}
)} {!canAutoInstall && (
This server requires manual setup. Check the repository for installation instructions.
)} {/* Tools */} {server.tools.length > 0 && (

Tools ({server.tools.length})

{server.tools.map((tool) => (
{tool.name} {tool.description &&

{tool.description}

}
))}
)} {/* Links */}
{server.repositoryUrl && ( )} {server.glamaUrl && ( )} {server.websiteUrl && ( )}
); };