# MCP Config Dialog
URL: /docs/ui/mcp-config

Drop-in shadcn dialog that lists MCP connectors and custom servers, with inline OAuth/bearer auth controls and an add form.

`McpConfigDialog` is a shadcn template that wraps the

- href

  /docs/integrations/tools/react-mcp

`@assistant-ui/react-mcp`

primitives. Render it anywhere inside an `McpManagerResource`-mounted client and your users get a styled MCP server panel — no manual primitive composition.

- as

  h2

Getting Started

- as

  h3

Install the package and the component

- packages

  - @assistant-ui/react-mcp

* items

  - CLI
  - Manual

- urls

  - https\://r.assistant-ui.com/mcp-config.json

#### Main Component

- packages

  - @assistant-ui/react-mcp
  - @assistant-ui/store

* code

  "use client"; import { type FC, type ReactNode, useState } from "react"; import { useAuiState } from "@assistant-ui/store"; import { McpAddFormPrimitive, McpManagerPrimitive, McpServerPrimitive, type MCPConnectionState, } from "@assistant-ui/react-mcp"; import { Loader2Icon, PlugIcon, PlugZapIcon, PlusIcon, ServerIcon, ShieldAlertIcon, Trash2Icon, XIcon, } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button, buttonVariants } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; import { cn } from "@/lib/utils"; export namespace McpConfigDialog { export type Props = { /\*\* Trigger element. Defaults to a ghost button with a plug icon. \*/ children?: ReactNode; }; } /\*\* \* Drop-in MCP server configuration dialog. Lists app-defined connectors and \* user-added custom servers, with inline auth controls and an add form. \* \* Mount the manager once at the root of your app: \* \`\`\`tsx \* useAui({ mcp: McpManagerResource({ connectors }) }); \* \`\`\` \* then render \`\<McpConfigDialog />\` anywhere inside the provider. \*/ export const McpConfigDialog: FC\<McpConfigDialog.Props> = ({ children }) => { return ( \<Dialog> \<DialogTrigger asChild> {children ?? ( \<Button variant="outline" size="sm" className="aui-mcp-config-trigger gap-2" > \<PlugIcon className="size-4" /> MCP servers \</Button> )} \</DialogTrigger> \<DialogContent className="aui-mcp-config-content sm:max-w-lg"> \<DialogHeader> \<DialogTitle>MCP servers\</DialogTitle> \<DialogDescription> Connect to Model Context Protocol servers to expose their tools to this assistant. \</DialogDescription> \</DialogHeader> \<McpManagerPrimitive.Root> \<div className="flex flex-col gap-4"> \<ConnectorsSection /> \<Separator /> \<CustomServersSection /> \</div> \</McpManagerPrimitive.Root> \</DialogContent> \</Dialog> ); }; McpConfigDialog.displayName = "McpConfigDialog"; const ConnectorsSection: FC = () => { return ( \<section className="aui-mcp-connectors flex flex-col gap-2"> \<SectionTitle>Connectors\</SectionTitle> \<div className="flex flex-col gap-2"> \<McpManagerPrimitive.Connectors> {() => \<ServerCard />} \</McpManagerPrimitive.Connectors> \</div> \</section> ); }; const CustomServersSection: FC = () => { const \[showForm, setShowForm] = useState(false); return ( \<section className="aui-mcp-custom-servers flex flex-col gap-2"> \<SectionTitle>Custom servers\</SectionTitle> \<div className="flex flex-col gap-2"> \<McpManagerPrimitive.CustomServers> {() => \<ServerCard />} \</McpManagerPrimitive.CustomServers> \</div> {!showForm && ( \<McpManagerPrimitive.AddCustomTrigger asChild> \<Button variant="outline" className="aui-mcp-add-trigger h-9 justify-start gap-2 rounded-lg px-3 text-sm" onClick={() => setShowForm(true)} > \<PlusIcon className="size-4" /> Add server \</Button> \</McpManagerPrimitive.AddCustomTrigger> )} {showForm && \<AddServerForm onClose={() => setShowForm(false)} />} \</section> ); }; const SectionTitle: FC<{ children: ReactNode }> = ({ children }) => ( \<h3 className="font-medium text-muted-foreground text-xs uppercase tracking-wide"> {children} \</h3> ); const ServerCard: FC = () => { return ( \<McpServerPrimitive.Root className={cn( "aui-mcp-server-card flex flex-col gap-2 rounded-lg border p-3", "data-\[connection-state=error]:border-destructive/40", )} > \<div className="flex items-center gap-3"> \<ServerAvatar /> \<div className="flex min-w-0 flex-1 flex-col"> \<span className="truncate font-medium text-sm"> \<McpServerPrimitive.Name /> \</span> \<StatusLine /> \</div> \<div className="flex items-center gap-1"> \<ServerActions /> \<McpServerPrimitive.RemoveButton asChild> \<Button variant="ghost" size="icon" className="aui-mcp-server-remove size-7 text-muted-foreground hover:text-destructive" > \<Trash2Icon className="size-4" /> \<span className="sr-only">Remove\</span> \</Button> \</McpServerPrimitive.RemoveButton> \</div> \</div> \<ServerError /> \</McpServerPrimitive.Root> ); }; const ServerAvatar: FC = () => { const icon = useAuiState((s) => s.mcpServer.icon ?? null); const name = useAuiState((s) => s.mcpServer.name); return ( \<div className="flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-md border bg-muted text-muted-foreground"> {icon ? ( \<img src={icon} alt={name} className="size-full object-cover" /> ) : ( \<ServerIcon className="size-4" /> )} \</div> ); }; const STATUS\_VARIANT: Record< MCPConnectionState, "default" | "secondary" | "destructive" > = { connected: "default", connecting: "secondary", authRequired: "secondary", authPending: "secondary", error: "destructive", disconnected: "secondary", }; const STATUS\_LABEL: Record\<MCPConnectionState, string> = { connected: "Connected", connecting: "Connecting…", authRequired: "Auth required", authPending: "Authorizing…", error: "Error", disconnected: "Disconnected", }; const StatusLine: FC = () => { const status = useAuiState((s) => s.mcpServer.connectionState); const variant = STATUS\_VARIANT\[status]; const label = STATUS\_LABEL\[status]; return ( \<div className="flex items-center gap-1.5 text-muted-foreground text-xs"> \<Badge variant={variant}> {status === "connecting" && ( \<Loader2Icon className="size-3 animate-spin" /> )} {label} \</Badge> \</div> ); }; const ServerError: FC = () => { const message = useAuiState((s) => s.mcpServer.lastError?.message ?? null); if (!message) return null; return ( \<div className="flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/5 px-2 py-1.5 text-destructive text-xs"> \<ShieldAlertIcon className="mt-0.5 size-3.5 shrink-0" /> \<span className="break-words">{message}\</span> \</div> ); }; const ServerActions: FC = () => ( \<div className="flex flex-wrap gap-2"> \<McpServerPrimitive.ConnectButton asChild> \<Button size="sm" variant="default" className="aui-mcp-server-connect h-8 gap-2 text-xs" > \<PlugZapIcon className="size-3.5" /> Connect \</Button> \</McpServerPrimitive.ConnectButton> \<McpServerPrimitive.OAuthLink className={cn( buttonVariants({ variant: "default", size: "sm" }), "aui-mcp-server-authorize h-8 gap-2 text-xs", )} > Authorize \</McpServerPrimitive.OAuthLink> \<McpServerPrimitive.DisconnectButton asChild> \<Button size="sm" variant="outline" className="aui-mcp-server-disconnect h-8 text-xs" > Disconnect \</Button> \</McpServerPrimitive.DisconnectButton> \</div> ); const AddServerForm: FC<{ onClose: () => void }> = ({ onClose }) => { return ( \<McpAddFormPrimitive.Root onSubmitted={onClose} onCancel={onClose}> \<div className="aui-mcp-add-form flex flex-col gap-3 rounded-lg border p-3"> \<div className="flex items-center justify-between"> \<h4 className="font-medium text-sm">New server\</h4> \<McpAddFormPrimitive.Cancel asChild> \<Button type="button" variant="ghost" size="icon" className="size-7 text-muted-foreground" > \<XIcon className="size-4" /> \<span className="sr-only">Close\</span> \</Button> \</McpAddFormPrimitive.Cancel> \</div> \<FormRow label="Name"> \<McpAddFormPrimitive.NameField asChild> \<Input placeholder="My MCP server" /> \</McpAddFormPrimitive.NameField> \</FormRow> \<FormRow label="URL"> \<McpAddFormPrimitive.UrlField asChild> \<Input placeholder="https\://example.com/mcp" /> \</McpAddFormPrimitive.UrlField> \</FormRow> \<FormRow label="Auth"> \<McpAddFormPrimitive.AuthSelect className="aui-mcp-auth-select h-9 w-full rounded-md border bg-background px-2 text-sm" /> \<div className={cn( // Style the default \`\<input>\` inside AuthFields without // needing to thread useAddForm out of the primitive. Mirrors // the shadcn \<Input> look. "empty:hidden \[&\_input]:flex \[&\_input]:h-9 \[&\_input]:w-full \[&\_input]:rounded-md \[&\_input]:border \[&\_input]:border-input \[&\_input]:bg-transparent \[&\_input]:px-3 \[&\_input]:py-1 \[&\_input]:text-sm \[&\_input]:shadow-xs \[&\_input]:outline-none \[&\_input]:transition-\[color,box-shadow]", "\[&\_input:focus-visible]:border-ring \[&\_input:focus-visible]:ring-\[3px] \[&\_input:focus-visible]:ring-ring/50", "\[&\_input::placeholder]:text-muted-foreground", )} > \<McpAddFormPrimitive.AuthFields /> \</div> \</FormRow> \<McpAddFormPrimitive.Error className="text-destructive text-xs" /> \<div className="flex justify-end gap-2"> \<McpAddFormPrimitive.Cancel asChild> \<Button type="button" variant="ghost" size="sm"> Cancel \</Button> \</McpAddFormPrimitive.Cancel> \<McpAddFormPrimitive.Submit asChild> \<Button type="submit" size="sm"> Add server \</Button> \</McpAddFormPrimitive.Submit> \</div> \</div> \</McpAddFormPrimitive.Root> ); }; const FormRow: FC<{ label: string; children: ReactNode }> = ({ label, children, }) => ( \<div className="flex flex-col gap-1.5"> \<Label className="text-xs">{label}\</Label> \<div className="flex flex-col gap-2">{children}\</div> \</div> );

- lang

  tsx

- code

  "use client"; import { type FC, type ReactNode, useState } from "react"; import { useAuiState } from "@assistant-ui/store"; import { McpAddFormPrimitive, McpManagerPrimitive, McpServerPrimitive, type MCPConnectionState, } from "@assistant-ui/react-mcp"; import { Loader2Icon, PlugIcon, PlugZapIcon, PlusIcon, ServerIcon, ShieldAlertIcon, Trash2Icon, XIcon, } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button, buttonVariants } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; import { cn } from "@/lib/utils"; export namespace McpConfigDialog { export type Props = { /\*\* Trigger element. Defaults to a ghost button with a plug icon. \*/ children?: ReactNode; }; } /\*\* \* Drop-in MCP server configuration dialog. Lists app-defined connectors and \* user-added custom servers, with inline auth controls and an add form. \* \* Mount the manager once at the root of your app: \* \`\`\`tsx \* useAui({ mcp: McpManagerResource({ connectors }) }); \* \`\`\` \* then render \`\<McpConfigDialog />\` anywhere inside the provider. \*/ export const McpConfigDialog: FC\<McpConfigDialog.Props> = ({ children }) => { return ( \<Dialog> \<DialogTrigger asChild> {children ?? ( \<Button variant="outline" size="sm" className="aui-mcp-config-trigger gap-2" > \<PlugIcon className="size-4" /> MCP servers \</Button> )} \</DialogTrigger> \<DialogContent className="aui-mcp-config-content sm:max-w-lg"> \<DialogHeader> \<DialogTitle>MCP servers\</DialogTitle> \<DialogDescription> Connect to Model Context Protocol servers to expose their tools to this assistant. \</DialogDescription> \</DialogHeader> \<McpManagerPrimitive.Root> \<div className="flex flex-col gap-4"> \<ConnectorsSection /> \<Separator /> \<CustomServersSection /> \</div> \</McpManagerPrimitive.Root> \</DialogContent> \</Dialog> ); }; McpConfigDialog.displayName = "McpConfigDialog"; const ConnectorsSection: FC = () => { return ( \<section className="aui-mcp-connectors flex flex-col gap-2"> \<SectionTitle>Connectors\</SectionTitle> \<div className="flex flex-col gap-2"> \<McpManagerPrimitive.Connectors> {() => \<ServerCard />} \</McpManagerPrimitive.Connectors> \</div> \</section> ); }; const CustomServersSection: FC = () => { const \[showForm, setShowForm] = useState(false); return ( \<section className="aui-mcp-custom-servers flex flex-col gap-2"> \<SectionTitle>Custom servers\</SectionTitle> \<div className="flex flex-col gap-2"> \<McpManagerPrimitive.CustomServers> {() => \<ServerCard />} \</McpManagerPrimitive.CustomServers> \</div> {!showForm && ( \<McpManagerPrimitive.AddCustomTrigger asChild> \<Button variant="outline" className="aui-mcp-add-trigger h-9 justify-start gap-2 rounded-lg px-3 text-sm" onClick={() => setShowForm(true)} > \<PlusIcon className="size-4" /> Add server \</Button> \</McpManagerPrimitive.AddCustomTrigger> )} {showForm && \<AddServerForm onClose={() => setShowForm(false)} />} \</section> ); }; const SectionTitle: FC<{ children: ReactNode }> = ({ children }) => ( \<h3 className="font-medium text-muted-foreground text-xs uppercase tracking-wide"> {children} \</h3> ); const ServerCard: FC = () => { return ( \<McpServerPrimitive.Root className={cn( "aui-mcp-server-card flex flex-col gap-2 rounded-lg border p-3", "data-\[connection-state=error]:border-destructive/40", )} > \<div className="flex items-center gap-3"> \<ServerAvatar /> \<div className="flex min-w-0 flex-1 flex-col"> \<span className="truncate font-medium text-sm"> \<McpServerPrimitive.Name /> \</span> \<StatusLine /> \</div> \<div className="flex items-center gap-1"> \<ServerActions /> \<McpServerPrimitive.RemoveButton asChild> \<Button variant="ghost" size="icon" className="aui-mcp-server-remove size-7 text-muted-foreground hover:text-destructive" > \<Trash2Icon className="size-4" /> \<span className="sr-only">Remove\</span> \</Button> \</McpServerPrimitive.RemoveButton> \</div> \</div> \<ServerError /> \</McpServerPrimitive.Root> ); }; const ServerAvatar: FC = () => { const icon = useAuiState((s) => s.mcpServer.icon ?? null); const name = useAuiState((s) => s.mcpServer.name); return ( \<div className="flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-md border bg-muted text-muted-foreground"> {icon ? ( \<img src={icon} alt={name} className="size-full object-cover" /> ) : ( \<ServerIcon className="size-4" /> )} \</div> ); }; const STATUS\_VARIANT: Record< MCPConnectionState, "default" | "secondary" | "destructive" > = { connected: "default", connecting: "secondary", authRequired: "secondary", authPending: "secondary", error: "destructive", disconnected: "secondary", }; const STATUS\_LABEL: Record\<MCPConnectionState, string> = { connected: "Connected", connecting: "Connecting…", authRequired: "Auth required", authPending: "Authorizing…", error: "Error", disconnected: "Disconnected", }; const StatusLine: FC = () => { const status = useAuiState((s) => s.mcpServer.connectionState); const variant = STATUS\_VARIANT\[status]; const label = STATUS\_LABEL\[status]; return ( \<div className="flex items-center gap-1.5 text-muted-foreground text-xs"> \<Badge variant={variant}> {status === "connecting" && ( \<Loader2Icon className="size-3 animate-spin" /> )} {label} \</Badge> \</div> ); }; const ServerError: FC = () => { const message = useAuiState((s) => s.mcpServer.lastError?.message ?? null); if (!message) return null; return ( \<div className="flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/5 px-2 py-1.5 text-destructive text-xs"> \<ShieldAlertIcon className="mt-0.5 size-3.5 shrink-0" /> \<span className="break-words">{message}\</span> \</div> ); }; const ServerActions: FC = () => ( \<div className="flex flex-wrap gap-2"> \<McpServerPrimitive.ConnectButton asChild> \<Button size="sm" variant="default" className="aui-mcp-server-connect h-8 gap-2 text-xs" > \<PlugZapIcon className="size-3.5" /> Connect \</Button> \</McpServerPrimitive.ConnectButton> \<McpServerPrimitive.OAuthLink className={cn( buttonVariants({ variant: "default", size: "sm" }), "aui-mcp-server-authorize h-8 gap-2 text-xs", )} > Authorize \</McpServerPrimitive.OAuthLink> \<McpServerPrimitive.DisconnectButton asChild> \<Button size="sm" variant="outline" className="aui-mcp-server-disconnect h-8 text-xs" > Disconnect \</Button> \</McpServerPrimitive.DisconnectButton> \</div> ); const AddServerForm: FC<{ onClose: () => void }> = ({ onClose }) => { return ( \<McpAddFormPrimitive.Root onSubmitted={onClose} onCancel={onClose}> \<div className="aui-mcp-add-form flex flex-col gap-3 rounded-lg border p-3"> \<div className="flex items-center justify-between"> \<h4 className="font-medium text-sm">New server\</h4> \<McpAddFormPrimitive.Cancel asChild> \<Button type="button" variant="ghost" size="icon" className="size-7 text-muted-foreground" > \<XIcon className="size-4" /> \<span className="sr-only">Close\</span> \</Button> \</McpAddFormPrimitive.Cancel> \</div> \<FormRow label="Name"> \<McpAddFormPrimitive.NameField asChild> \<Input placeholder="My MCP server" /> \</McpAddFormPrimitive.NameField> \</FormRow> \<FormRow label="URL"> \<McpAddFormPrimitive.UrlField asChild> \<Input placeholder="https\://example.com/mcp" /> \</McpAddFormPrimitive.UrlField> \</FormRow> \<FormRow label="Auth"> \<McpAddFormPrimitive.AuthSelect className="aui-mcp-auth-select h-9 w-full rounded-md border bg-background px-2 text-sm" /> \<div className={cn( // Style the default \`\<input>\` inside AuthFields without // needing to thread useAddForm out of the primitive. Mirrors // the shadcn \<Input> look. "empty:hidden \[&\_input]:flex \[&\_input]:h-9 \[&\_input]:w-full \[&\_input]:rounded-md \[&\_input]:border \[&\_input]:border-input \[&\_input]:bg-transparent \[&\_input]:px-3 \[&\_input]:py-1 \[&\_input]:text-sm \[&\_input]:shadow-xs \[&\_input]:outline-none \[&\_input]:transition-\[color,box-shadow]", "\[&\_input:focus-visible]:border-ring \[&\_input:focus-visible]:ring-\[3px] \[&\_input:focus-visible]:ring-ring/50", "\[&\_input::placeholder]:text-muted-foreground", )} > \<McpAddFormPrimitive.AuthFields /> \</div> \</FormRow> \<McpAddFormPrimitive.Error className="text-destructive text-xs" /> \<div className="flex justify-end gap-2"> \<McpAddFormPrimitive.Cancel asChild> \<Button type="button" variant="ghost" size="sm"> Cancel \</Button> \</McpAddFormPrimitive.Cancel> \<McpAddFormPrimitive.Submit asChild> \<Button type="submit" size="sm"> Add server \</Button> \</McpAddFormPrimitive.Submit> \</div> \</div> \</McpAddFormPrimitive.Root> ); }; const FormRow: FC<{ label: string; children: ReactNode }> = ({ label, children, }) => ( \<div className="flex flex-col gap-1.5"> \<Label className="text-xs">{label}\</Label> \<div className="flex flex-col gap-2">{children}\</div> \</div> );

* as

  h3

Mount the manager

- title

  app/providers.tsx

`"use client"; import { AuiProvider, useAui } from "@assistant-ui/react"; import { McpManagerResource, defineConnector } from "@assistant-ui/react-mcp"; const connectors = [ defineConnector({ id: "linear", name: "Linear", url: "https://mcp.linear.app", auth: { type: "oauth", scopes: ["read"] }, }), ]; export function Providers({ children }: { children: React.ReactNode }) { const aui = useAui({ mcp: McpManagerResource({ connectors }) }); return <AuiProvider value={aui}>{children}</AuiProvider>; }`

- as

  h3

Drop the dialog in

- title

  app/page.tsx

`import { McpConfigDialog } from "@/components/assistant-ui/mcp-config"; export default function Page() { return ( <header className="flex items-center justify-between"> <h1>My app</h1> <McpConfigDialog /> </header> ); }`

That's it. The default trigger is an outline `"🔌 MCP servers"` button. Click it to open the dialog, which renders a **Connectors** list (your app-defined presets with connect/authorize/disconnect controls), a **Custom servers** list (user-added entries with the same controls plus a remove button), and an inline **Add server** form for URL + name + auth selection.

- as

  h3

Override the trigger (optional)

Pass children to swap the default button for your own:

`<McpConfigDialog> <Button variant="ghost">Servers</Button> </McpConfigDialog>`

- as

  h3

Wire up the OAuth callback

Required for any OAuth-enabled connector or custom server. See the

- href

  /docs/integrations/tools/react-mcp#handle-the-oauth-callback

OAuth callback section in the react-mcp guide

for the route handler.

- as

  h2

Related

- href

  /docs/integrations/tools/react-mcp

User-managed MCP serversFull @assistant-ui/react-mcp guide — connectors, custom servers, OAuth, storage, custom primitives.

- href

  /docs/integrations/tools/mcp

Server-side MCPApp-developer-controlled MCP servers wired into the API route.