31 / 122 · Tool use
Server panel
Which servers are connected, what each one brought, and which is still waiting on you.
Servers2 of 3 connected
Installation
npx shadcn@latest add "@assistant-ui/elements-mcp-server-panel"
Usage
import { McpServerPanel } from "@/components/elements/mcp-server-panel";
<McpServerPanel
servers={servers}
expandedId={expandedId}
onToggle={setExpandedId}
onAuthorize={startOAuth}
/>Props
McpServerPanel
servers*McpServer[]Every configured server, connected or not. The header counts how many are live.
expandedIdstringWhich server is open. Only one expands at a time; omit for an all-collapsed list.
onToggle(id: string) => voidCalled when a server row is expanded or collapsed.
onAuthorize(id: string) => voidCalled from the Authorize button, shown only while a server needs auth.
classNamestringExtra classes merged onto the root.
McpServer
id*stringStable identity, reported back by onToggle and onAuthorize.
name*stringServer name as configured.
transport*stringHow the server is reached: stdio, sse, streamable-http.
status*"connected" | "connecting" | "needs-auth" | "failed"Drives the status dot and, for needs-auth, surfaces the authorize action.
tools*string[]Tool names the server contributed, counted collapsed and listed expanded.
Source
mcp-server-panel.tsx"use client";
import { ChevronRightIcon, PlugIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { field, mono, paper } from "./surfaces";
export type McpServerStatus =
| "connected"
| "connecting"
| "needs-auth"
| "failed";
export interface McpServer {
id: string;
name: string;
transport: string;
status: McpServerStatus;
tools: readonly string[];
}
const DOT: Record<McpServerStatus, string> = {
connected: "bg-emerald-500",
connecting: "bg-foreground/30 animate-pulse",
"needs-auth": "bg-amber-500",
failed: "bg-red-500",
};
const LABEL: Record<McpServerStatus, string> = {
connected: "connected",
connecting: "connecting",
"needs-auth": "needs auth",
failed: "failed",
};
export function McpServerPanel({
servers,
expandedId,
onToggle,
onAuthorize,
className,
}: {
servers: readonly McpServer[];
expandedId?: string;
onToggle?: (id: string) => void;
onAuthorize?: (id: string) => void;
className?: string;
}) {
const connected = servers.filter(
(server) => server.status === "connected",
).length;
return (
<div
className={cn(
paper,
"flex w-full max-w-sm flex-col gap-1 rounded-2xl p-3",
className,
)}
>
<div className="flex items-baseline justify-between px-1 pb-1">
<span className="text-[13.5px] font-medium">Servers</span>
<span className={cn(mono, "text-foreground/35 tabular-nums")}>
{connected} of {servers.length} connected
</span>
</div>
{servers.map((server) => {
const expanded = expandedId === server.id;
return (
<div key={server.id} className="flex flex-col">
<button
type="button"
aria-expanded={expanded}
onClick={() => onToggle?.(server.id)}
className="hover:bg-foreground/[0.04] flex items-center gap-2.5 rounded-xl px-1.5 py-2 text-start transition-colors"
>
<ChevronRightIcon
className={cn(
"text-foreground/25 size-3 shrink-0 transition-transform duration-200 motion-reduce:transition-none",
expanded && "rotate-90",
)}
/>
<PlugIcon className="text-foreground/35 size-3.5 shrink-0" />
<span className="min-w-0 flex-1 truncate text-[13.5px]">
{server.name}
</span>
<span className={cn(mono, "text-foreground/30 shrink-0")}>
{server.tools.length} tools
</span>
<span
aria-hidden
className={cn(
"size-1.5 shrink-0 rounded-full",
DOT[server.status],
)}
/>
</button>
{expanded && (
<div className="fade-in slide-in-from-top-1 animate-in flex flex-col gap-1.5 px-1.5 ps-8 pb-2 duration-200">
<div className="flex items-center gap-2">
<span className={cn(mono, "text-foreground/30")}>
{server.transport}
</span>
<span className={cn(mono, "text-foreground/30")}>
· {LABEL[server.status]}
</span>
{server.status === "needs-auth" && onAuthorize && (
<button
type="button"
onClick={() => onAuthorize?.(server.id)}
className="ms-auto rounded-full bg-amber-500/15 px-2 py-0.5 text-[11px] font-medium text-amber-700 transition-[background-color,scale] duration-150 hover:bg-amber-500/25 active:scale-[0.96] dark:text-amber-300"
>
Authorize
</button>
)}
</div>
<div className="flex flex-wrap gap-1">
{server.tools.map((tool) => (
<span
key={tool}
className={cn(
field,
mono,
"text-foreground/55 rounded-md px-1.5 py-0.5",
)}
>
{tool}
</span>
))}
</div>
</div>
)}
</div>
);
})}
</div>
);
}