import { useState, useEffect } from "react"; import type { McpServer, McpTransportType } from "../../lib/types"; interface Props { server: McpServer; onUpdate: (server: McpServer) => Promise; onRemove: (id: string) => Promise; } export default function McpServerCard({ server, onUpdate, onRemove }: Props) { const [expanded, setExpanded] = useState(false); const [name, setName] = useState(server.name); const [transportType, setTransportType] = useState(server.transport_type); const [command, setCommand] = useState(server.command ?? ""); const [args, setArgs] = useState(server.args.join(" ")); const [envPairs, setEnvPairs] = useState<[string, string][]>(Object.entries(server.env)); const [url, setUrl] = useState(server.url ?? ""); const [headerPairs, setHeaderPairs] = useState<[string, string][]>(Object.entries(server.headers)); const [dockerImage, setDockerImage] = useState(server.docker_image ?? ""); const [containerPort, setContainerPort] = useState(server.container_port?.toString() ?? "3000"); useEffect(() => { setName(server.name); setTransportType(server.transport_type); setCommand(server.command ?? ""); setArgs(server.args.join(" ")); setEnvPairs(Object.entries(server.env)); setUrl(server.url ?? ""); setHeaderPairs(Object.entries(server.headers)); setDockerImage(server.docker_image ?? ""); setContainerPort(server.container_port?.toString() ?? "3000"); }, [server]); const saveServer = async (patch: Partial) => { try { await onUpdate({ ...server, ...patch }); } catch (err) { console.error("Failed to update MCP server:", err); } }; const handleNameBlur = () => { if (name !== server.name) saveServer({ name }); }; const handleTransportChange = (t: McpTransportType) => { setTransportType(t); saveServer({ transport_type: t }); }; const handleCommandBlur = () => { saveServer({ command: command || null }); }; const handleArgsBlur = () => { const parsed = args.trim() ? args.trim().split(/\s+/) : []; saveServer({ args: parsed }); }; const handleUrlBlur = () => { saveServer({ url: url || null }); }; const handleDockerImageBlur = () => { saveServer({ docker_image: dockerImage || null }); }; const handleContainerPortBlur = () => { const port = parseInt(containerPort, 10); saveServer({ container_port: isNaN(port) ? null : port }); }; const saveEnv = (pairs: [string, string][]) => { const env: Record = {}; for (const [k, v] of pairs) { if (k.trim()) env[k.trim()] = v; } saveServer({ env }); }; const saveHeaders = (pairs: [string, string][]) => { const headers: Record = {}; for (const [k, v] of pairs) { if (k.trim()) headers[k.trim()] = v; } saveServer({ headers }); }; const inputCls = "w-full px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)]"; const isDocker = !!dockerImage; const transportBadge = { stdio: "Stdio", http: "HTTP", }[transportType]; const modeBadge = isDocker ? "Docker" : "Manual"; return (
{/* Header */}
{/* Expanded config */} {expanded && (
{/* Name */}
setName(e.target.value)} onBlur={handleNameBlur} className={inputCls} />
{/* Docker Image (primary field — determines Docker vs Manual mode) */}
setDockerImage(e.target.value)} onBlur={handleDockerImageBlur} placeholder="e.g. mcp/filesystem:latest (leave empty for manual mode)" className={inputCls} />

Set a Docker image to run this MCP server as a container. Leave empty for manual mode.

{/* Transport type */}
{(["stdio", "http"] as McpTransportType[]).map((t) => ( ))}
{/* Container Port (HTTP+Docker only) */} {transportType === "http" && isDocker && (
setContainerPort(e.target.value)} onBlur={handleContainerPortBlur} placeholder="3000" className={inputCls} />

Port inside the MCP container (default: 3000)

)} {/* Stdio fields */} {transportType === "stdio" && ( <>
setCommand(e.target.value)} onBlur={handleCommandBlur} placeholder={isDocker ? "Command inside container" : "npx"} className={inputCls} />
setArgs(e.target.value)} onBlur={handleArgsBlur} placeholder="-y @modelcontextprotocol/server-filesystem /path" className={inputCls} />
{ setEnvPairs(pairs); }} onSave={saveEnv} /> )} {/* HTTP fields (only for manual mode — Docker mode auto-generates URL) */} {transportType === "http" && !isDocker && ( <>
setUrl(e.target.value)} onBlur={handleUrlBlur} placeholder="http://localhost:3000/mcp" className={inputCls} />
{ setHeaderPairs(pairs); }} onSave={saveHeaders} /> )} {/* Environment variables for HTTP+Docker */} {transportType === "http" && isDocker && ( { setEnvPairs(pairs); }} onSave={saveEnv} /> )}
)}
); } function KeyValueEditor({ label, pairs, onChange, onSave, }: { label: string; pairs: [string, string][]; onChange: (pairs: [string, string][]) => void; onSave: (pairs: [string, string][]) => void; }) { const inputCls = "flex-1 min-w-0 px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)]"; return (
{pairs.map(([key, value], i) => (
{ const updated = [...pairs] as [string, string][]; updated[i] = [e.target.value, value]; onChange(updated); }} onBlur={() => onSave(pairs)} placeholder="KEY" className={inputCls} /> = { const updated = [...pairs] as [string, string][]; updated[i] = [key, e.target.value]; onChange(updated); }} onBlur={() => onSave(pairs)} placeholder="value" className={inputCls} />
))}
); }