McpServerList
List configured MCP servers with their transport, status, command or URL, connection error and tool count. Search matches the name, command and URL; the filter narrows by connected, needs attention, disabled or scope, and withSearch={false} or withFilter={false} hides either control; servers are grouped by scope when there is more than one. The actions menu offers Authenticate, Reconnect, Enable or Disable depending on the status, and Remove asks for confirmation. Handles loading, error and empty states.
Getting Started
$pnpm add @sinups/ai-kit @mantine/core @mantine/hooks @tabler/icons-reactExamples
Servers
Loading, error and empty
Source
The component source as it ships in the package. Useful as a reference, or as a starting point if you want to fork a piece.
mcp/McpServerList.tsx
import React, { memo, useMemo, useState } from 'react';
import { Alert, Button, Stack } from '@mantine/core';
import {
IconAlertCircle,
IconKey,
IconPlayerPause,
IconPlayerPlay,
IconPlus,
IconRefresh,
IconServer,
IconTrash,
} from '@tabler/icons-react';
import { usePendingActions } from '../hooks/use-pending-actions';
import { ConfirmDialog } from '../primitives/ConfirmDialog/ConfirmDialog';
import { EntityList } from '../primitives/EntityList/EntityList';
import { EntityListItem, type EntityListItemAction } from '../primitives/EntityList/EntityListItem';
import { StatusBadge } from '../primitives/StatusBadge/StatusBadge';
import {
getMcpAgentUiStatus,
getMcpServerFilterOptions,
getMcpServerTarget,
getMcpToolCount,
matchesMcpServerFilter,
matchesMcpServerQuery,
MCP_SCOPE_LABELS,
MCP_SCOPE_ORDER,
MCP_STATUS_LABELS,
type McpServerFilter,
} from './mcp-server';
import { McpTransportIcon } from './McpTransportIcon';
import type { McpServer, McpServerScope, McpServerStatus } from './types';
import { formatTemplate } from '../utils/format-template';
export type McpServerListLabels = {
addServer: string;
list: string;
search: string;
filter: string;
noResults: string;
emptyTitle: string;
emptyDescription: string;
retry: string;
actions: string;
reconnect: string;
authenticate: string;
enable: string;
disable: string;
remove: string;
/** `{count}` is replaced with the number of tools */
tools: string;
error: string;
dismiss: string;
removeTitle: string;
/** `{name}` is replaced with the server name */
removeMessage: string;
cancel: string;
filterAll: string;
filterConnected: string;
filterAttention: string;
filterDisabled: string;
status: Record<McpServerStatus, string>;
scope: Record<McpServerScope, string>;
};
export const DEFAULT_MCP_SERVER_LIST_LABELS: McpServerListLabels = {
addServer: 'Add server',
list: 'MCP servers',
search: 'Search servers',
filter: 'Filter servers',
noResults: 'No servers match',
emptyTitle: 'No MCP servers',
emptyDescription: 'Connect a server to give the agent new tools',
retry: 'Retry',
actions: 'Server actions',
reconnect: 'Reconnect',
authenticate: 'Authenticate',
enable: 'Enable',
disable: 'Disable',
remove: 'Remove',
tools: '{count} tools',
error: 'Action failed',
dismiss: 'Dismiss',
removeTitle: 'Remove server',
removeMessage: '{name} and its tools will no longer be available to the agent.',
cancel: 'Cancel',
filterAll: 'All',
filterConnected: 'Connected',
filterAttention: 'Needs attention',
filterDisabled: 'Disabled',
status: MCP_STATUS_LABELS,
scope: MCP_SCOPE_LABELS,
};
const SERVER_ACTION_KEYS = ['authenticate', 'enable', 'reconnect', 'disable'];
type ServerAction = (server: McpServer) => void | Promise<void>;
export interface McpServerListProps {
/** Configured servers */
servers: McpServer[];
/** Id of the highlighted server */
selectedId?: string | null;
/** Called when a server row is clicked */
onSelect?: (server: McpServer) => void;
/** Shows skeleton rows */
loading?: boolean;
/** Error message shown instead of the list */
error?: React.ReactNode;
/** Called by the retry button of the error alert */
onRetry?: () => void;
/** Renders the "Add server" button when set */
onAdd?: () => void;
/** Adds a "Reconnect" action to servers that are not disabled */
onReconnect?: ServerAction;
/** Adds an "Authenticate" action to servers that need auth */
onAuthenticate?: ServerAction;
/** Adds an "Enable" action to disabled servers */
onEnable?: ServerAction;
/** Adds a "Disable" action to enabled servers */
onDisable?: ServerAction;
/** Adds a "Remove" action that asks for confirmation first */
onRemove?: ServerAction;
/** Groups servers by scope when they use more than one, `true` by default */
groupByScope?: boolean;
/** Shows the search input, `true` by default */
withSearch?: boolean;
/** Shows the status and scope filter, `true` by default */
withFilter?: boolean;
/** Button and message overrides */
labels?: Partial<McpServerListLabels>;
/** Class name added to the root element */
className?: string;
/** Inline styles added to the root element */
style?: React.CSSProperties;
}
function getFilterLabel(value: McpServerFilter, labels: McpServerListLabels): string {
if (value.startsWith('scope:')) {
return labels.scope[value.slice('scope:'.length) as McpServerScope];
}
const map: Record<string, string> = {
all: labels.filterAll,
connected: labels.filterConnected,
attention: labels.filterAttention,
disabled: labels.filterDisabled,
};
return map[value];
}
/** Searchable list of MCP servers with status, scope groups and per-server actions */
export const McpServerList = memo(function McpServerList({
servers,
selectedId,
onSelect,
loading,
error,
onRetry,
onAdd,
onReconnect,
onAuthenticate,
onEnable,
onDisable,
onRemove,
groupByScope = true,
withSearch = true,
withFilter = true,
labels: labelsOverride,
className,
style,
}: McpServerListProps) {
const labels = { ...DEFAULT_MCP_SERVER_LIST_LABELS, ...labelsOverride };
const [query, setQuery] = useState('');
const [filter, setFilter] = useState<string>('all');
const action = usePendingActions(labels.error);
const [removeTarget, setRemoveTarget] = useState<McpServer | null>(null);
const filterOptions = useMemo(() => getMcpServerFilterOptions(servers), [servers]);
const scopes = new Set(servers.map((server) => server.scope ?? 'user'));
const scopeGroupOrder = MCP_SCOPE_ORDER.map((scope) => labels.scope[scope]);
const activeFilter = filterOptions.some((option) => option.value === filter) ? filter : 'all';
const getActions = (server: McpServer): EntityListItemAction[] => {
const items: EntityListItemAction[] = [];
const add = (
key: string,
label: string,
icon: React.ReactNode,
callback: ServerAction | undefined,
color?: string
) => {
if (callback) {
items.push({
label,
icon,
color,
disabled: SERVER_ACTION_KEYS.some((name) => action.isPending(`${name}:${server.id}`)),
onClick: () => action.run(`${key}:${server.id}`, () => callback(server)),
});
}
};
if (server.status === 'needs-auth') {
add('authenticate', labels.authenticate, <IconKey size={14} />, onAuthenticate);
}
if (server.status === 'disabled') {
add('enable', labels.enable, <IconPlayerPlay size={14} />, onEnable);
} else {
add('reconnect', labels.reconnect, <IconRefresh size={14} />, onReconnect);
add('disable', labels.disable, <IconPlayerPause size={14} />, onDisable);
}
if (onRemove) {
items.push({
label: labels.remove,
icon: <IconTrash size={14} />,
color: 'red',
onClick: () => setRemoveTarget(server),
});
}
return items;
};
return (
<Stack gap="sm" className={className} style={style}>
{action.error && (
<Alert
color="red"
variant="light"
icon={<IconAlertCircle size={16} />}
withCloseButton
closeButtonLabel={labels.dismiss}
onClose={() => action.clearError()}
>
{action.error}
</Alert>
)}
<EntityList<McpServer>
items={servers}
getId={(server) => server.id}
selectedId={selectedId}
onSelect={onSelect}
loading={loading}
error={error}
onRetry={onRetry}
labels={{ retry: labels.retry, noResults: labels.noResults }}
ariaLabel={labels.list}
empty={{
title: labels.emptyTitle,
description: labels.emptyDescription,
icon: <IconServer />,
action: onAdd ? (
<Button size="xs" leftSection={<IconPlus size={14} />} onClick={onAdd}>
{labels.addServer}
</Button>
) : undefined,
}}
search={
withSearch && servers.length > 0
? {
value: query,
onChange: setQuery,
placeholder: labels.search,
filter: matchesMcpServerQuery,
}
: undefined
}
filters={
withFilter && servers.length > 0
? {
value: activeFilter,
onChange: setFilter,
label: labels.filter,
filter: matchesMcpServerFilter,
options: filterOptions.map((option) => ({
value: option.value,
label: getFilterLabel(option.value, labels),
count: option.count,
})),
}
: undefined
}
groupBy={
groupByScope && scopes.size > 1
? (server) => labels.scope[server.scope ?? 'user']
: undefined
}
groupOrder={scopeGroupOrder}
toolbar={
onAdd && servers.length > 0 ? (
<Button size="sm" variant="light" leftSection={<IconPlus size={14} />} onClick={onAdd}>
{labels.addServer}
</Button>
) : undefined
}
renderItem={(server, { selected }) => {
const toolCount = getMcpToolCount(server);
return (
<EntityListItem
title={server.name}
icon={<McpTransportIcon transport={server.transport} />}
status={
<StatusBadge
status={getMcpAgentUiStatus(server.status)}
label={labels.status[server.status]}
size="xs"
/>
}
description={
server.status === 'error' && server.error
? server.error
: getMcpServerTarget(server)
}
descriptionLines={1}
meta={
toolCount !== undefined && server.status !== 'disabled'
? formatTemplate(labels.tools, { count: toolCount })
: undefined
}
actions={getActions(server)}
labels={{ actions: `${labels.actions}: ${server.name}` }}
selected={selected}
/>
);
}}
/>
{onRemove && (
<ConfirmDialog
opened={removeTarget !== null}
danger
title={labels.removeTitle}
message={
removeTarget ? formatTemplate(labels.removeMessage, { name: removeTarget.name }) : null
}
labels={{ confirm: labels.remove, cancel: labels.cancel, error: labels.error }}
onConfirm={() => (removeTarget ? onRemove(removeTarget) : undefined)}
onClose={() => setRemoveTarget(null)}
/>
)}
</Stack>
);
});
McpServerList.displayName = 'McpServerList';
Depends on:
API reference
Prop
Type
Required
servers
McpServer[]
Yes
Configured servers
selectedId
string | null
No
Id of the highlighted server
onSelect
(server: McpServer) => void
No
Called when a server row is clicked
loading
boolean
No
Shows skeleton rows
error
React.ReactNode
No
Error message shown instead of the list
onRetry
() => void
No
Called by the retry button of the error alert
onAdd
() => void
No
Renders the "Add server" button when set
onReconnect
ServerAction
No
Adds a "Reconnect" action to servers that are not disabled
onAuthenticate
ServerAction
No
Adds an "Authenticate" action to servers that need auth
onEnable
ServerAction
No
Adds an "Enable" action to disabled servers
onDisable
ServerAction
No
Adds a "Disable" action to enabled servers
onRemove
ServerAction
No
Adds a "Remove" action that asks for confirmation first
groupByScope
boolean
No
Groups servers by scope when they use more than one, `true` by default
withSearch
boolean
No
Shows the search input, `true` by default
withFilter
boolean
No
Shows the status and scope filter, `true` by default
labels
Partial<McpServerListLabels>
No
Button and message overrides
className
string
No
Class name added to the root element
style
React.CSSProperties
No
Inline styles added to the root element