feat(mcp): connection health toolbar with bulk Retry All / Disconnect All actions (#2641)

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
Aashir Athar
2026-05-28 21:10:44 -07:00
committed by GitHub
co-authored by Claude Opus 4.7 Steven Enamakel
parent 5a5cd43ea9
commit 32d03ca6aa
18 changed files with 928 additions and 1 deletions
@@ -0,0 +1,329 @@
/**
* Tests for McpConnectionHealthToolbar — aggregate status counts +
* Retry All / Disconnect All bulk-action surface with confirmation
* dialog.
*/
import { act, fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import McpConnectionHealthToolbar from './McpConnectionHealthToolbar';
import type { ConnStatus, ServerStatus } from './types';
const statusFor = (server_id: string, status: ServerStatus): ConnStatus => ({
server_id,
qualified_name: `acme/${server_id}`,
display_name: server_id,
status,
tool_count: status === 'connected' ? 3 : 0,
});
describe('McpConnectionHealthToolbar', () => {
it('renders nothing when statuses array is empty', () => {
const { container } = render(
<McpConnectionHealthToolbar
statuses={[]}
onReconnect={async () => {}}
onDisconnect={async () => {}}
/>
);
expect(container.firstChild).toBeNull();
});
it('always shows connected + disconnected counts (even when zero)', () => {
render(
<McpConnectionHealthToolbar
statuses={[statusFor('a', 'disconnected')]}
onReconnect={async () => {}}
onDisconnect={async () => {}}
/>
);
expect(screen.getByText('0 connected')).toBeInTheDocument();
expect(screen.getByText('1 idle')).toBeInTheDocument();
});
it('only shows connecting count when there are connecting servers', () => {
const { rerender } = render(
<McpConnectionHealthToolbar
statuses={[statusFor('a', 'connected'), statusFor('b', 'error')]}
onReconnect={async () => {}}
onDisconnect={async () => {}}
/>
);
expect(screen.queryByText(/connecting/)).not.toBeInTheDocument();
rerender(
<McpConnectionHealthToolbar
statuses={[statusFor('a', 'connected'), statusFor('b', 'connecting')]}
onReconnect={async () => {}}
onDisconnect={async () => {}}
/>
);
expect(screen.getByText('1 connecting')).toBeInTheDocument();
});
it('only shows error count when there are errored servers', () => {
const { rerender } = render(
<McpConnectionHealthToolbar
statuses={[statusFor('a', 'connected')]}
onReconnect={async () => {}}
onDisconnect={async () => {}}
/>
);
expect(screen.queryByText(/error/)).not.toBeInTheDocument();
rerender(
<McpConnectionHealthToolbar
statuses={[statusFor('a', 'connected'), statusFor('b', 'error')]}
onReconnect={async () => {}}
onDisconnect={async () => {}}
/>
);
expect(screen.getByText('1 error')).toBeInTheDocument();
});
it('aggregates counts correctly across a mixed status set', () => {
render(
<McpConnectionHealthToolbar
statuses={[
statusFor('a', 'connected'),
statusFor('b', 'connected'),
statusFor('c', 'error'),
statusFor('d', 'connecting'),
statusFor('e', 'disconnected'),
statusFor('f', 'disconnected'),
]}
onReconnect={async () => {}}
onDisconnect={async () => {}}
/>
);
expect(screen.getByText('2 connected')).toBeInTheDocument();
expect(screen.getByText('1 connecting')).toBeInTheDocument();
expect(screen.getByText('1 error')).toBeInTheDocument();
expect(screen.getByText('2 idle')).toBeInTheDocument();
});
it('hides "Retry all" button when there are no errors', () => {
render(
<McpConnectionHealthToolbar
statuses={[statusFor('a', 'connected'), statusFor('b', 'disconnected')]}
onReconnect={async () => {}}
onDisconnect={async () => {}}
/>
);
expect(screen.queryByRole('button', { name: /Retry all/i })).not.toBeInTheDocument();
});
it('hides "Disconnect all" button when nothing is connected', () => {
render(
<McpConnectionHealthToolbar
statuses={[statusFor('a', 'error'), statusFor('b', 'disconnected')]}
onReconnect={async () => {}}
onDisconnect={async () => {}}
/>
);
expect(screen.queryByRole('button', { name: /Disconnect all/i })).not.toBeInTheDocument();
});
it('shows "Retry all (N)" button with the correct error count when errors exist', () => {
render(
<McpConnectionHealthToolbar
statuses={[statusFor('a', 'error'), statusFor('b', 'error'), statusFor('c', 'connected')]}
onReconnect={async () => {}}
onDisconnect={async () => {}}
/>
);
expect(
screen.getByRole('button', { name: 'Retry all 2 errored MCP servers' })
).toBeInTheDocument();
expect(screen.getByText('Retry all (2)')).toBeInTheDocument();
});
it('calls onReconnect with the errored server IDs when "Retry all" is clicked', async () => {
const onReconnect = vi.fn().mockResolvedValue(undefined);
render(
<McpConnectionHealthToolbar
statuses={[
statusFor('srv-1', 'error'),
statusFor('srv-2', 'connected'),
statusFor('srv-3', 'error'),
]}
onReconnect={onReconnect}
onDisconnect={async () => {}}
/>
);
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: /Retry all/i }));
});
expect(onReconnect).toHaveBeenCalledTimes(1);
expect(onReconnect).toHaveBeenCalledWith(['srv-1', 'srv-3']);
});
it('does NOT call onDisconnect directly — opens confirm dialog first', () => {
const onDisconnect = vi.fn();
render(
<McpConnectionHealthToolbar
statuses={[statusFor('a', 'connected')]}
onReconnect={async () => {}}
onDisconnect={onDisconnect}
/>
);
fireEvent.click(screen.getByRole('button', { name: /Disconnect all/i }));
expect(onDisconnect).not.toHaveBeenCalled();
// Confirm dialog appears with accessible structure
const dialog = screen.getByRole('dialog');
expect(dialog).toHaveAttribute('aria-modal', 'true');
expect(screen.getByText('Disconnect all MCP servers?')).toBeInTheDocument();
});
it('cancel in the dialog closes it without calling onDisconnect', () => {
const onDisconnect = vi.fn();
render(
<McpConnectionHealthToolbar
statuses={[statusFor('a', 'connected')]}
onReconnect={async () => {}}
onDisconnect={onDisconnect}
/>
);
fireEvent.click(
screen.getByRole('button', { name: /Disconnect all \d+ connected MCP servers/i })
);
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
expect(onDisconnect).not.toHaveBeenCalled();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
it('Escape closes the confirm dialog without calling onDisconnect', () => {
const onDisconnect = vi.fn();
render(
<McpConnectionHealthToolbar
statuses={[statusFor('a', 'connected')]}
onReconnect={async () => {}}
onDisconnect={onDisconnect}
/>
);
fireEvent.click(
screen.getByRole('button', { name: /Disconnect all \d+ connected MCP servers/i })
);
expect(screen.getByRole('dialog')).toBeInTheDocument();
act(() => {
fireEvent.keyDown(document, { key: 'Escape' });
});
expect(onDisconnect).not.toHaveBeenCalled();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
it('confirm in the dialog fires onDisconnect with connected IDs and closes the dialog', async () => {
const onDisconnect = vi.fn().mockResolvedValue(undefined);
render(
<McpConnectionHealthToolbar
statuses={[
statusFor('srv-1', 'connected'),
statusFor('srv-2', 'error'),
statusFor('srv-3', 'connected'),
]}
onReconnect={async () => {}}
onDisconnect={onDisconnect}
/>
);
fireEvent.click(
screen.getByRole('button', { name: /Disconnect all \d+ connected MCP servers/i })
);
// Confirm button inside dialog
const dialogConfirm = screen.getAllByRole('button', { name: 'Disconnect all' })[0];
await act(async () => {
fireEvent.click(dialogConfirm);
});
expect(onDisconnect).toHaveBeenCalledTimes(1);
expect(onDisconnect).toHaveBeenCalledWith(['srv-1', 'srv-3']);
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
it('disables both action buttons while a bulk operation is pending', async () => {
let resolveOp: (() => void) | undefined;
const onReconnect = vi.fn(
() =>
new Promise<void>(res => {
resolveOp = res;
})
);
render(
<McpConnectionHealthToolbar
statuses={[statusFor('a', 'error'), statusFor('b', 'connected')]}
onReconnect={onReconnect}
onDisconnect={async () => {}}
/>
);
fireEvent.click(screen.getByRole('button', { name: /Retry all/i }));
// While the promise is pending, both buttons should be disabled.
expect(screen.getByRole('button', { name: /Retry all/i })).toBeDisabled();
expect(
screen.getByRole('button', { name: /Disconnect all \d+ connected MCP servers/i })
).toBeDisabled();
// Resolve and re-render — buttons re-enable.
await act(async () => {
resolveOp?.();
});
expect(screen.getByRole('button', { name: /Retry all/i })).not.toBeDisabled();
});
it('surfaces a thrown error from onReconnect via role="alert"', async () => {
const onReconnect = vi.fn().mockRejectedValue(new Error('upstream RPC died'));
render(
<McpConnectionHealthToolbar
statuses={[statusFor('a', 'error')]}
onReconnect={onReconnect}
onDisconnect={async () => {}}
/>
);
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: /Retry all/i }));
});
const alert = screen.getByRole('alert');
expect(alert).toHaveTextContent('upstream RPC died');
});
it('falls back to a generic error message when the thrown value is not an Error instance', async () => {
const onReconnect = vi.fn().mockRejectedValue('not-an-error-object');
render(
<McpConnectionHealthToolbar
statuses={[statusFor('a', 'error')]}
onReconnect={onReconnect}
onDisconnect={async () => {}}
/>
);
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: /Retry all/i }));
});
expect(screen.getByRole('alert')).toHaveTextContent('Bulk operation failed. See logs.');
});
it('the summary region is a polite live region with an accessible label', () => {
render(
<McpConnectionHealthToolbar
statuses={[statusFor('a', 'connected')]}
onReconnect={async () => {}}
onDisconnect={async () => {}}
/>
);
const status = screen.getByRole('status', { name: 'MCP connection health summary' });
expect(status).toHaveAttribute('aria-live', 'polite');
});
it('confirm dialog body interpolates the connected count', () => {
render(
<McpConnectionHealthToolbar
statuses={[
statusFor('a', 'connected'),
statusFor('b', 'connected'),
statusFor('c', 'connected'),
]}
onReconnect={async () => {}}
onDisconnect={async () => {}}
/>
);
fireEvent.click(
screen.getByRole('button', { name: /Disconnect all \d+ connected MCP servers/i })
);
expect(
screen.getByText(/This will disconnect 3 currently-connected MCP servers/)
).toBeInTheDocument();
});
});
@@ -0,0 +1,250 @@
/**
* Aggregate health summary + bulk actions for the MCP server list.
*
* Lives in the left pane of `McpServersTab` above the list. Reads the
* polled `statuses` array (no extra fetches) and surfaces:
*
* - Live counts per state (connected / connecting / error / disconnected),
* announced through a `role="status" aria-live="polite"` region so
* screen readers hear updates as the polling loop refreshes.
* - `Retry all` button — visible only when there are servers in error
* state; iterates through them and calls `onReconnect` once.
* - `Disconnect all` button — visible only when there are connected
* servers; opens a confirm dialog (`role="dialog" aria-modal`) before
* firing `onDisconnect`.
*
* Parent (`McpServersTab`) owns the actual `mcpClientsApi` calls and the
* subsequent `fetchStatuses()` refresh; this component only orchestrates
* the user intent.
*/
import { useEffect, useMemo, useState } from 'react';
import { useT } from '../../../lib/i18n/I18nContext';
import type { ConnStatus } from './types';
interface McpConnectionHealthToolbarProps {
statuses: ConnStatus[];
/** Reconnect every server in error state. Caller resolves after refresh. */
onReconnect: (serverIds: string[]) => Promise<void>;
/** Disconnect every currently-connected server. Caller resolves after refresh. */
onDisconnect: (serverIds: string[]) => Promise<void>;
}
interface HealthCounts {
connectedIds: string[];
errorIds: string[];
connectedCount: number;
connectingCount: number;
errorCount: number;
disconnectedCount: number;
}
const computeHealthCounts = (statuses: ConnStatus[]): HealthCounts => {
const connectedIds: string[] = [];
const errorIds: string[] = [];
let connectingCount = 0;
let disconnectedCount = 0;
for (const s of statuses) {
switch (s.status) {
case 'connected':
connectedIds.push(s.server_id);
break;
case 'error':
errorIds.push(s.server_id);
break;
case 'connecting':
connectingCount += 1;
break;
case 'disconnected':
disconnectedCount += 1;
break;
}
}
return {
connectedIds,
errorIds,
connectedCount: connectedIds.length,
connectingCount,
errorCount: errorIds.length,
disconnectedCount,
};
};
const McpConnectionHealthToolbar = ({
statuses,
onReconnect,
onDisconnect,
}: McpConnectionHealthToolbarProps) => {
const { t } = useT();
const [isOperating, setIsOperating] = useState(false);
const [confirmDisconnect, setConfirmDisconnect] = useState(false);
const [opError, setOpError] = useState<string | null>(null);
const counts = useMemo(() => computeHealthCounts(statuses), [statuses]);
// Escape closes the "Disconnect all" confirmation WITHOUT firing the bulk
// RPC — the standard modal-dismiss affordance, matching the other MCP
// dialogs. The listener is only attached while the dialog is open. (Must
// be declared before the early return below to satisfy the rules of hooks.)
useEffect(() => {
if (!confirmDisconnect) return;
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') setConfirmDisconnect(false);
};
document.addEventListener('keydown', onKeyDown);
return () => document.removeEventListener('keydown', onKeyDown);
}, [confirmDisconnect]);
// Nothing to summarise — match the parent's existing "hide chrome when
// there's nothing installed" pattern.
if (statuses.length === 0) return null;
const runRetryAll = async () => {
if (counts.errorIds.length === 0 || isOperating) return;
setIsOperating(true);
setOpError(null);
try {
await onReconnect(counts.errorIds);
} catch (err) {
setOpError(err instanceof Error ? err.message : t('mcp.health.opErrorGeneric'));
} finally {
setIsOperating(false);
}
};
const runDisconnectAll = async () => {
if (counts.connectedIds.length === 0 || isOperating) return;
setConfirmDisconnect(false);
setIsOperating(true);
setOpError(null);
try {
await onDisconnect(counts.connectedIds);
} catch (err) {
setOpError(err instanceof Error ? err.message : t('mcp.health.opErrorGeneric'));
} finally {
setIsOperating(false);
}
};
return (
<div className="mb-2 rounded-lg border border-stone-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 px-2.5 py-2">
<div className="flex items-center justify-between gap-2 mb-1.5">
<span className="text-[10px] font-semibold text-stone-500 dark:text-neutral-400 uppercase tracking-wide">
{t('mcp.health.title')}
</span>
<div className="flex items-center gap-2">
{counts.errorCount > 0 && (
<button
type="button"
onClick={() => void runRetryAll()}
disabled={isOperating}
aria-label={t('mcp.health.retryAllAria').replace(
'{count}',
String(counts.errorCount)
)}
className="text-[10px] font-medium text-amber-700 dark:text-amber-300 hover:underline disabled:opacity-50 disabled:no-underline">
{t('mcp.health.retryAll').replace('{count}', String(counts.errorCount))}
</button>
)}
{counts.connectedCount > 0 && (
<button
type="button"
onClick={() => setConfirmDisconnect(true)}
disabled={isOperating}
aria-label={t('mcp.health.disconnectAllAria').replace(
'{count}',
String(counts.connectedCount)
)}
className="text-[10px] font-medium text-stone-600 dark:text-neutral-300 hover:underline disabled:opacity-50 disabled:no-underline">
{t('mcp.health.disconnectAll').replace('{count}', String(counts.connectedCount))}
</button>
)}
</div>
</div>
<div
role="status"
aria-live="polite"
aria-label={t('mcp.health.summaryAria')}
className="flex flex-wrap items-center gap-x-2 gap-y-1 text-[10px] text-stone-600 dark:text-neutral-300">
<span className="inline-flex items-center gap-1">
<span className="w-1.5 h-1.5 rounded-full bg-sage-500" aria-hidden="true" />
<span>
{t('mcp.health.connectedCount').replace('{count}', String(counts.connectedCount))}
</span>
</span>
{counts.connectingCount > 0 && (
<span className="inline-flex items-center gap-1">
<span className="w-1.5 h-1.5 rounded-full bg-amber-400" aria-hidden="true" />
<span>
{t('mcp.health.connectingCount').replace('{count}', String(counts.connectingCount))}
</span>
</span>
)}
{counts.errorCount > 0 && (
<span className="inline-flex items-center gap-1">
<span className="w-1.5 h-1.5 rounded-full bg-coral-500" aria-hidden="true" />
<span>{t('mcp.health.errorCount').replace('{count}', String(counts.errorCount))}</span>
</span>
)}
<span className="inline-flex items-center gap-1">
<span
className="w-1.5 h-1.5 rounded-full bg-stone-300 dark:bg-neutral-600"
aria-hidden="true"
/>
<span>
{t('mcp.health.disconnectedCount').replace('{count}', String(counts.disconnectedCount))}
</span>
</span>
</div>
{opError && (
<p
role="alert"
className="mt-1.5 text-[10px] text-coral-700 dark:text-coral-300 break-words">
{opError}
</p>
)}
{confirmDisconnect && (
<div
role="dialog"
aria-modal="true"
aria-labelledby="mcp-disconnect-all-title"
aria-describedby="mcp-disconnect-all-body"
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4">
<div className="bg-white dark:bg-neutral-900 rounded-xl shadow-xl max-w-sm w-full p-4">
<h2
id="mcp-disconnect-all-title"
className="text-sm font-semibold text-stone-900 dark:text-neutral-100 mb-2">
{t('mcp.health.disconnectConfirm.title')}
</h2>
<p
id="mcp-disconnect-all-body"
className="text-xs text-stone-600 dark:text-neutral-300 mb-4">
{t('mcp.health.disconnectConfirm.body').replace(
'{count}',
String(counts.connectedCount)
)}
</p>
<div className="flex justify-end gap-2">
<button
type="button"
onClick={() => setConfirmDisconnect(false)}
className="px-3 py-1.5 text-xs rounded-lg border border-stone-200 dark:border-neutral-700 text-stone-700 dark:text-neutral-200 hover:bg-stone-50 dark:hover:bg-neutral-800">
{t('mcp.health.disconnectConfirm.cancel')}
</button>
<button
type="button"
onClick={() => void runDisconnectAll()}
className="px-3 py-1.5 text-xs rounded-lg bg-coral-500 text-white hover:bg-coral-600">
{t('mcp.health.disconnectConfirm.confirm')}
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default McpConnectionHealthToolbar;
@@ -66,6 +66,16 @@ const STATUSES_CONNECTED = [
},
];
const STATUSES_ERROR = [
{
server_id: 'srv-1',
qualified_name: 'acme/fs-server',
display_name: 'File Server',
status: 'error' as const,
tool_count: 0,
},
];
describe('McpServersTab', () => {
beforeEach(() => {
vi.useFakeTimers();
@@ -437,4 +447,51 @@ describe('McpServersTab', () => {
expect(mockInstall).toHaveBeenCalled();
});
});
it('surfaces a partial-failure alert when bulk Retry All has rejections', async () => {
mockInstalledList.mockResolvedValue(SERVERS);
mockStatus.mockResolvedValue(STATUSES_ERROR);
// The single errored server fails to reconnect.
mockConnect.mockRejectedValue(new Error('connect refused'));
render(<McpServersTab />);
vi.useRealTimers();
const retryBtn = await screen.findByRole('button', { name: 'Retry all 1 errored MCP servers' });
await act(async () => {
fireEvent.click(retryBtn);
});
// allSettled never rejects, so the connect was attempted...
await waitFor(() => expect(mockConnect).toHaveBeenCalledWith('srv-1'));
// ...and the failure is surfaced through the alert region, not swallowed.
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent('1 of 1 servers failed. See logs.');
});
});
it('surfaces a partial-failure alert when bulk Disconnect All has rejections', async () => {
mockInstalledList.mockResolvedValue(SERVERS);
mockStatus.mockResolvedValue(STATUSES_CONNECTED);
mockDisconnect.mockRejectedValue(new Error('disconnect failed'));
render(<McpServersTab />);
vi.useRealTimers();
const disconnectBtn = await screen.findByRole('button', {
name: 'Disconnect all 1 connected MCP servers',
});
fireEvent.click(disconnectBtn);
// Confirm dialog gates the bulk RPC; confirm it.
const confirmBtn = await screen.findByRole('button', { name: 'Disconnect all' });
await act(async () => {
fireEvent.click(confirmBtn);
});
await waitFor(() => expect(mockDisconnect).toHaveBeenCalledWith('srv-1'));
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent('1 of 1 servers failed. See logs.');
});
});
});
@@ -13,6 +13,7 @@ import InstallDialog from './InstallDialog';
import InstalledServerDetail from './InstalledServerDetail';
import InstalledServerList from './InstalledServerList';
import McpCatalogBrowser from './McpCatalogBrowser';
import McpConnectionHealthToolbar from './McpConnectionHealthToolbar';
import McpInventoryPanel from './McpInventoryPanel';
import McpServerSearch from './McpServerSearch';
import type { ConnStatus, InstalledServer } from './types';
@@ -139,6 +140,53 @@ const McpServersTab = () => {
[loadInstalled, fetchStatuses]
);
// Count rejected settlements and, if any, throw a descriptive error so the
// toolbar surfaces it through its `role="alert"` region — otherwise a bulk
// action that partially (or wholly) fails looks identical to success and
// the user is left re-scanning the status dots. The status refresh still
// runs first so the dots reconcile regardless of the partial failure.
const reportBulkFailures = useCallback(
(results: PromiseSettledResult<unknown>[], total: number) => {
const failed = results.filter(r => r.status === 'rejected').length;
if (failed > 0) {
log('bulk op partial failure: %d/%d failed', failed, total);
throw new Error(
t('mcp.health.bulkPartialFailure')
.replace('{failed}', String(failed))
.replace('{total}', String(total))
);
}
},
[t]
);
// Bulk Retry — iterate through errored servers, collect per-server outcomes
// via `Promise.allSettled` so one bad apple doesn't abort the batch, then
// refresh statuses once at the end. The toolbar shows its own disabled state
// during the await; the next poll tick reconciles any drift. Partial/total
// failures are surfaced via `reportBulkFailures`.
const handleBulkReconnect = useCallback(
async (serverIds: string[]) => {
log('bulk reconnect ids=%o', serverIds);
const results = await Promise.allSettled(serverIds.map(id => mcpClientsApi.connect(id)));
await fetchStatuses();
reportBulkFailures(results, serverIds.length);
},
[fetchStatuses, reportBulkFailures]
);
// Bulk Disconnect — same shape as bulk reconnect. The toolbar gates this
// behind a confirmation dialog before we get here.
const handleBulkDisconnect = useCallback(
async (serverIds: string[]) => {
log('bulk disconnect ids=%o', serverIds);
const results = await Promise.allSettled(serverIds.map(id => mcpClientsApi.disconnect(id)));
await fetchStatuses();
reportBulkFailures(results, serverIds.length);
},
[fetchStatuses, reportBulkFailures]
);
const selectedServerId = rightPane.mode === 'detail' ? rightPane.serverId : null;
const selectedServer = servers.find(s => s.server_id === selectedServerId) ?? null;
const selectedConnStatus = statuses.find(s => s.server_id === selectedServerId);
@@ -171,13 +219,18 @@ const McpServersTab = () => {
</button>
</div>
<div className="flex gap-4 flex-1 min-h-0">
{/* Left pane: search + installed list */}
{/* Left pane: health toolbar + search + installed list */}
<div className="w-56 shrink-0 flex flex-col">
{loadError && (
<div className="mb-2 rounded-lg border border-coral-200 dark:border-coral-500/30 bg-coral-50 dark:bg-coral-500/10 px-3 py-2 text-xs text-coral-700 dark:text-coral-300">
{loadError}
</div>
)}
<McpConnectionHealthToolbar
statuses={statuses}
onReconnect={handleBulkReconnect}
onDisconnect={handleBulkDisconnect}
/>
{servers.length > 0 && (
<div className="mb-2">
<McpServerSearch value={searchFilter} onChange={setSearchFilter} />
+17
View File
@@ -1343,6 +1343,23 @@ const ar1: TranslationMap = {
'mcp.installed.empty': 'لم يتم تثبيت خوادم MCP حتى الآن.',
'mcp.installed.toolSingular': 'أداة {count}',
'mcp.installed.toolPlural': 'أدوات {count}',
'mcp.health.title': 'Health',
'mcp.health.summaryAria': 'MCP connection health summary',
'mcp.health.connectedCount': '{count} connected',
'mcp.health.connectingCount': '{count} connecting',
'mcp.health.errorCount': '{count} error',
'mcp.health.disconnectedCount': '{count} idle',
'mcp.health.retryAll': 'Retry all ({count})',
'mcp.health.retryAllAria': 'Retry all {count} errored MCP servers',
'mcp.health.disconnectAll': 'Disconnect all ({count})',
'mcp.health.disconnectAllAria': 'Disconnect all {count} connected MCP servers',
'mcp.health.disconnectConfirm.title': 'Disconnect all MCP servers?',
'mcp.health.disconnectConfirm.body':
'This will disconnect {count} currently-connected MCP servers. Installed configurations and secrets are kept; you can reconnect any server later.',
'mcp.health.disconnectConfirm.cancel': 'Cancel',
'mcp.health.disconnectConfirm.confirm': 'Disconnect all',
'mcp.health.opErrorGeneric': 'Bulk operation failed. See logs.',
'mcp.health.bulkPartialFailure': '{failed} of {total} servers failed. See logs.',
'mcp.installed.search.landmarkAria': 'Search installed MCP servers',
'mcp.installed.search.inputAria': 'Filter installed MCP servers by name',
'mcp.installed.search.placeholder': 'Filter servers…',
+17
View File
@@ -1352,6 +1352,23 @@ const bn1: TranslationMap = {
'mcp.installed.empty': 'এখনো কোনো MCP সার্ভার ইনস্টল করা হয়নি।',
'mcp.installed.toolSingular': '{count} টুল',
'mcp.installed.toolPlural': '{count} টুল',
'mcp.health.title': 'Health',
'mcp.health.summaryAria': 'MCP connection health summary',
'mcp.health.connectedCount': '{count} connected',
'mcp.health.connectingCount': '{count} connecting',
'mcp.health.errorCount': '{count} error',
'mcp.health.disconnectedCount': '{count} idle',
'mcp.health.retryAll': 'Retry all ({count})',
'mcp.health.retryAllAria': 'Retry all {count} errored MCP servers',
'mcp.health.disconnectAll': 'Disconnect all ({count})',
'mcp.health.disconnectAllAria': 'Disconnect all {count} connected MCP servers',
'mcp.health.disconnectConfirm.title': 'Disconnect all MCP servers?',
'mcp.health.disconnectConfirm.body':
'This will disconnect {count} currently-connected MCP servers. Installed configurations and secrets are kept; you can reconnect any server later.',
'mcp.health.disconnectConfirm.cancel': 'Cancel',
'mcp.health.disconnectConfirm.confirm': 'Disconnect all',
'mcp.health.opErrorGeneric': 'Bulk operation failed. See logs.',
'mcp.health.bulkPartialFailure': '{failed} of {total} servers failed. See logs.',
'mcp.installed.search.landmarkAria': 'Search installed MCP servers',
'mcp.installed.search.inputAria': 'Filter installed MCP servers by name',
'mcp.installed.search.placeholder': 'Filter servers…',
+17
View File
@@ -1370,6 +1370,23 @@ const de1: TranslationMap = {
'mcp.installed.empty': 'Noch keine MCP-Server installiert.',
'mcp.installed.toolSingular': '{count}-Tool',
'mcp.installed.toolPlural': '{count}-Tools',
'mcp.health.title': 'Health',
'mcp.health.summaryAria': 'MCP connection health summary',
'mcp.health.connectedCount': '{count} connected',
'mcp.health.connectingCount': '{count} connecting',
'mcp.health.errorCount': '{count} error',
'mcp.health.disconnectedCount': '{count} idle',
'mcp.health.retryAll': 'Retry all ({count})',
'mcp.health.retryAllAria': 'Retry all {count} errored MCP servers',
'mcp.health.disconnectAll': 'Disconnect all ({count})',
'mcp.health.disconnectAllAria': 'Disconnect all {count} connected MCP servers',
'mcp.health.disconnectConfirm.title': 'Disconnect all MCP servers?',
'mcp.health.disconnectConfirm.body':
'This will disconnect {count} currently-connected MCP servers. Installed configurations and secrets are kept; you can reconnect any server later.',
'mcp.health.disconnectConfirm.cancel': 'Cancel',
'mcp.health.disconnectConfirm.confirm': 'Disconnect all',
'mcp.health.opErrorGeneric': 'Bulk operation failed. See logs.',
'mcp.health.bulkPartialFailure': '{failed} of {total} servers failed. See logs.',
'mcp.installed.search.landmarkAria': 'Search installed MCP servers',
'mcp.installed.search.inputAria': 'Filter installed MCP servers by name',
'mcp.installed.search.placeholder': 'Filter servers…',
+17
View File
@@ -1328,6 +1328,23 @@ const en1: TranslationMap = {
'mcp.installed.empty': 'No MCP servers installed yet.',
'mcp.installed.toolSingular': '{count} tool',
'mcp.installed.toolPlural': '{count} tools',
'mcp.health.title': 'Health',
'mcp.health.summaryAria': 'MCP connection health summary',
'mcp.health.connectedCount': '{count} connected',
'mcp.health.connectingCount': '{count} connecting',
'mcp.health.errorCount': '{count} error',
'mcp.health.disconnectedCount': '{count} idle',
'mcp.health.retryAll': 'Retry all ({count})',
'mcp.health.retryAllAria': 'Retry all {count} errored MCP servers',
'mcp.health.disconnectAll': 'Disconnect all ({count})',
'mcp.health.disconnectAllAria': 'Disconnect all {count} connected MCP servers',
'mcp.health.disconnectConfirm.title': 'Disconnect all MCP servers?',
'mcp.health.disconnectConfirm.body':
'This will disconnect {count} currently-connected MCP servers. Installed configurations and secrets are kept; you can reconnect any server later.',
'mcp.health.disconnectConfirm.cancel': 'Cancel',
'mcp.health.disconnectConfirm.confirm': 'Disconnect all',
'mcp.health.opErrorGeneric': 'Bulk operation failed. See logs.',
'mcp.health.bulkPartialFailure': '{failed} of {total} servers failed. See logs.',
'mcp.installed.search.landmarkAria': 'Search installed MCP servers',
'mcp.installed.search.inputAria': 'Filter installed MCP servers by name',
'mcp.installed.search.placeholder': 'Filter servers…',
+17
View File
@@ -1367,6 +1367,23 @@ const es1: TranslationMap = {
'mcp.installed.empty': 'Aún no hay servidores MCP instalados.',
'mcp.installed.toolSingular': 'herramienta {count}',
'mcp.installed.toolPlural': '{count} herramientas',
'mcp.health.title': 'Health',
'mcp.health.summaryAria': 'MCP connection health summary',
'mcp.health.connectedCount': '{count} connected',
'mcp.health.connectingCount': '{count} connecting',
'mcp.health.errorCount': '{count} error',
'mcp.health.disconnectedCount': '{count} idle',
'mcp.health.retryAll': 'Retry all ({count})',
'mcp.health.retryAllAria': 'Retry all {count} errored MCP servers',
'mcp.health.disconnectAll': 'Disconnect all ({count})',
'mcp.health.disconnectAllAria': 'Disconnect all {count} connected MCP servers',
'mcp.health.disconnectConfirm.title': 'Disconnect all MCP servers?',
'mcp.health.disconnectConfirm.body':
'This will disconnect {count} currently-connected MCP servers. Installed configurations and secrets are kept; you can reconnect any server later.',
'mcp.health.disconnectConfirm.cancel': 'Cancel',
'mcp.health.disconnectConfirm.confirm': 'Disconnect all',
'mcp.health.opErrorGeneric': 'Bulk operation failed. See logs.',
'mcp.health.bulkPartialFailure': '{failed} of {total} servers failed. See logs.',
'mcp.installed.search.landmarkAria': 'Search installed MCP servers',
'mcp.installed.search.inputAria': 'Filter installed MCP servers by name',
'mcp.installed.search.placeholder': 'Filter servers…',
+17
View File
@@ -1372,6 +1372,23 @@ const fr1: TranslationMap = {
'mcp.installed.empty': 'No MCP servers installed yet.',
'mcp.installed.toolSingular': 'Outil {count}',
'mcp.installed.toolPlural': 'Outils {count}',
'mcp.health.title': 'Health',
'mcp.health.summaryAria': 'MCP connection health summary',
'mcp.health.connectedCount': '{count} connected',
'mcp.health.connectingCount': '{count} connecting',
'mcp.health.errorCount': '{count} error',
'mcp.health.disconnectedCount': '{count} idle',
'mcp.health.retryAll': 'Retry all ({count})',
'mcp.health.retryAllAria': 'Retry all {count} errored MCP servers',
'mcp.health.disconnectAll': 'Disconnect all ({count})',
'mcp.health.disconnectAllAria': 'Disconnect all {count} connected MCP servers',
'mcp.health.disconnectConfirm.title': 'Disconnect all MCP servers?',
'mcp.health.disconnectConfirm.body':
'This will disconnect {count} currently-connected MCP servers. Installed configurations and secrets are kept; you can reconnect any server later.',
'mcp.health.disconnectConfirm.cancel': 'Cancel',
'mcp.health.disconnectConfirm.confirm': 'Disconnect all',
'mcp.health.opErrorGeneric': 'Bulk operation failed. See logs.',
'mcp.health.bulkPartialFailure': '{failed} of {total} servers failed. See logs.',
'mcp.installed.search.landmarkAria': 'Search installed MCP servers',
'mcp.installed.search.inputAria': 'Filter installed MCP servers by name',
'mcp.installed.search.placeholder': 'Filter servers…',
+17
View File
@@ -1351,6 +1351,23 @@ const hi1: TranslationMap = {
'mcp.installed.empty': 'अभी तक कोई MCP सर्वर स्थापित नहीं है।',
'mcp.installed.toolSingular': '{count} उपकरण',
'mcp.installed.toolPlural': '{count} उपकरण',
'mcp.health.title': 'Health',
'mcp.health.summaryAria': 'MCP connection health summary',
'mcp.health.connectedCount': '{count} connected',
'mcp.health.connectingCount': '{count} connecting',
'mcp.health.errorCount': '{count} error',
'mcp.health.disconnectedCount': '{count} idle',
'mcp.health.retryAll': 'Retry all ({count})',
'mcp.health.retryAllAria': 'Retry all {count} errored MCP servers',
'mcp.health.disconnectAll': 'Disconnect all ({count})',
'mcp.health.disconnectAllAria': 'Disconnect all {count} connected MCP servers',
'mcp.health.disconnectConfirm.title': 'Disconnect all MCP servers?',
'mcp.health.disconnectConfirm.body':
'This will disconnect {count} currently-connected MCP servers. Installed configurations and secrets are kept; you can reconnect any server later.',
'mcp.health.disconnectConfirm.cancel': 'Cancel',
'mcp.health.disconnectConfirm.confirm': 'Disconnect all',
'mcp.health.opErrorGeneric': 'Bulk operation failed. See logs.',
'mcp.health.bulkPartialFailure': '{failed} of {total} servers failed. See logs.',
'mcp.installed.search.landmarkAria': 'Search installed MCP servers',
'mcp.installed.search.inputAria': 'Filter installed MCP servers by name',
'mcp.installed.search.placeholder': 'Filter servers…',
+17
View File
@@ -1355,6 +1355,23 @@ const id1: TranslationMap = {
'mcp.installed.empty': 'Belum ada server MCP yang terinstal.',
'mcp.installed.toolSingular': '{count} alat',
'mcp.installed.toolPlural': '{count} alat',
'mcp.health.title': 'Health',
'mcp.health.summaryAria': 'MCP connection health summary',
'mcp.health.connectedCount': '{count} connected',
'mcp.health.connectingCount': '{count} connecting',
'mcp.health.errorCount': '{count} error',
'mcp.health.disconnectedCount': '{count} idle',
'mcp.health.retryAll': 'Retry all ({count})',
'mcp.health.retryAllAria': 'Retry all {count} errored MCP servers',
'mcp.health.disconnectAll': 'Disconnect all ({count})',
'mcp.health.disconnectAllAria': 'Disconnect all {count} connected MCP servers',
'mcp.health.disconnectConfirm.title': 'Disconnect all MCP servers?',
'mcp.health.disconnectConfirm.body':
'This will disconnect {count} currently-connected MCP servers. Installed configurations and secrets are kept; you can reconnect any server later.',
'mcp.health.disconnectConfirm.cancel': 'Cancel',
'mcp.health.disconnectConfirm.confirm': 'Disconnect all',
'mcp.health.opErrorGeneric': 'Bulk operation failed. See logs.',
'mcp.health.bulkPartialFailure': '{failed} of {total} servers failed. See logs.',
'mcp.installed.search.landmarkAria': 'Search installed MCP servers',
'mcp.installed.search.inputAria': 'Filter installed MCP servers by name',
'mcp.installed.search.placeholder': 'Filter servers…',
+17
View File
@@ -1360,6 +1360,23 @@ const it1: TranslationMap = {
'mcp.installed.empty': 'Nessun server MCP ancora installato.',
'mcp.installed.toolSingular': '{count} strumento',
'mcp.installed.toolPlural': '{count} strumenti',
'mcp.health.title': 'Health',
'mcp.health.summaryAria': 'MCP connection health summary',
'mcp.health.connectedCount': '{count} connected',
'mcp.health.connectingCount': '{count} connecting',
'mcp.health.errorCount': '{count} error',
'mcp.health.disconnectedCount': '{count} idle',
'mcp.health.retryAll': 'Retry all ({count})',
'mcp.health.retryAllAria': 'Retry all {count} errored MCP servers',
'mcp.health.disconnectAll': 'Disconnect all ({count})',
'mcp.health.disconnectAllAria': 'Disconnect all {count} connected MCP servers',
'mcp.health.disconnectConfirm.title': 'Disconnect all MCP servers?',
'mcp.health.disconnectConfirm.body':
'This will disconnect {count} currently-connected MCP servers. Installed configurations and secrets are kept; you can reconnect any server later.',
'mcp.health.disconnectConfirm.cancel': 'Cancel',
'mcp.health.disconnectConfirm.confirm': 'Disconnect all',
'mcp.health.opErrorGeneric': 'Bulk operation failed. See logs.',
'mcp.health.bulkPartialFailure': '{failed} of {total} servers failed. See logs.',
'mcp.installed.search.landmarkAria': 'Search installed MCP servers',
'mcp.installed.search.inputAria': 'Filter installed MCP servers by name',
'mcp.installed.search.placeholder': 'Filter servers…',
+17
View File
@@ -1352,6 +1352,23 @@ const ko1: TranslationMap = {
'mcp.installed.empty': '아직 MCP 서버가 설치되지 않았습니다.',
'mcp.installed.toolSingular': '{count} 도구',
'mcp.installed.toolPlural': '{count} 도구',
'mcp.health.title': 'Health',
'mcp.health.summaryAria': 'MCP connection health summary',
'mcp.health.connectedCount': '{count} connected',
'mcp.health.connectingCount': '{count} connecting',
'mcp.health.errorCount': '{count} error',
'mcp.health.disconnectedCount': '{count} idle',
'mcp.health.retryAll': 'Retry all ({count})',
'mcp.health.retryAllAria': 'Retry all {count} errored MCP servers',
'mcp.health.disconnectAll': 'Disconnect all ({count})',
'mcp.health.disconnectAllAria': 'Disconnect all {count} connected MCP servers',
'mcp.health.disconnectConfirm.title': 'Disconnect all MCP servers?',
'mcp.health.disconnectConfirm.body':
'This will disconnect {count} currently-connected MCP servers. Installed configurations and secrets are kept; you can reconnect any server later.',
'mcp.health.disconnectConfirm.cancel': 'Cancel',
'mcp.health.disconnectConfirm.confirm': 'Disconnect all',
'mcp.health.opErrorGeneric': 'Bulk operation failed. See logs.',
'mcp.health.bulkPartialFailure': '{failed} of {total} servers failed. See logs.',
'mcp.installed.search.landmarkAria': 'Search installed MCP servers',
'mcp.installed.search.inputAria': 'Filter installed MCP servers by name',
'mcp.installed.search.placeholder': 'Filter servers…',
+17
View File
@@ -1365,6 +1365,23 @@ const pt1: TranslationMap = {
'mcp.installed.empty': 'Nenhum servidor MCP instalado ainda.',
'mcp.installed.toolSingular': 'Ferramenta {count}',
'mcp.installed.toolPlural': 'Ferramentas {count}',
'mcp.health.title': 'Health',
'mcp.health.summaryAria': 'MCP connection health summary',
'mcp.health.connectedCount': '{count} connected',
'mcp.health.connectingCount': '{count} connecting',
'mcp.health.errorCount': '{count} error',
'mcp.health.disconnectedCount': '{count} idle',
'mcp.health.retryAll': 'Retry all ({count})',
'mcp.health.retryAllAria': 'Retry all {count} errored MCP servers',
'mcp.health.disconnectAll': 'Disconnect all ({count})',
'mcp.health.disconnectAllAria': 'Disconnect all {count} connected MCP servers',
'mcp.health.disconnectConfirm.title': 'Disconnect all MCP servers?',
'mcp.health.disconnectConfirm.body':
'This will disconnect {count} currently-connected MCP servers. Installed configurations and secrets are kept; you can reconnect any server later.',
'mcp.health.disconnectConfirm.cancel': 'Cancel',
'mcp.health.disconnectConfirm.confirm': 'Disconnect all',
'mcp.health.opErrorGeneric': 'Bulk operation failed. See logs.',
'mcp.health.bulkPartialFailure': '{failed} of {total} servers failed. See logs.',
'mcp.installed.search.landmarkAria': 'Search installed MCP servers',
'mcp.installed.search.inputAria': 'Filter installed MCP servers by name',
'mcp.installed.search.placeholder': 'Filter servers…',
+17
View File
@@ -1355,6 +1355,23 @@ const ru1: TranslationMap = {
'mcp.installed.empty': 'Серверы MCP пока не установлены.',
'mcp.installed.toolSingular': '{count} инструмент',
'mcp.installed.toolPlural': '{count} инструменты',
'mcp.health.title': 'Health',
'mcp.health.summaryAria': 'MCP connection health summary',
'mcp.health.connectedCount': '{count} connected',
'mcp.health.connectingCount': '{count} connecting',
'mcp.health.errorCount': '{count} error',
'mcp.health.disconnectedCount': '{count} idle',
'mcp.health.retryAll': 'Retry all ({count})',
'mcp.health.retryAllAria': 'Retry all {count} errored MCP servers',
'mcp.health.disconnectAll': 'Disconnect all ({count})',
'mcp.health.disconnectAllAria': 'Disconnect all {count} connected MCP servers',
'mcp.health.disconnectConfirm.title': 'Disconnect all MCP servers?',
'mcp.health.disconnectConfirm.body':
'This will disconnect {count} currently-connected MCP servers. Installed configurations and secrets are kept; you can reconnect any server later.',
'mcp.health.disconnectConfirm.cancel': 'Cancel',
'mcp.health.disconnectConfirm.confirm': 'Disconnect all',
'mcp.health.opErrorGeneric': 'Bulk operation failed. See logs.',
'mcp.health.bulkPartialFailure': '{failed} of {total} servers failed. See logs.',
'mcp.installed.search.landmarkAria': 'Search installed MCP servers',
'mcp.installed.search.inputAria': 'Filter installed MCP servers by name',
'mcp.installed.search.placeholder': 'Filter servers…',
+17
View File
@@ -1336,6 +1336,23 @@ const zhCN1: TranslationMap = {
'mcp.installed.empty': '尚未安装 MCP 服务器。',
'mcp.installed.toolSingular': '{count} 工具',
'mcp.installed.toolPlural': '{count} 工具',
'mcp.health.title': 'Health',
'mcp.health.summaryAria': 'MCP connection health summary',
'mcp.health.connectedCount': '{count} connected',
'mcp.health.connectingCount': '{count} connecting',
'mcp.health.errorCount': '{count} error',
'mcp.health.disconnectedCount': '{count} idle',
'mcp.health.retryAll': 'Retry all ({count})',
'mcp.health.retryAllAria': 'Retry all {count} errored MCP servers',
'mcp.health.disconnectAll': 'Disconnect all ({count})',
'mcp.health.disconnectAllAria': 'Disconnect all {count} connected MCP servers',
'mcp.health.disconnectConfirm.title': 'Disconnect all MCP servers?',
'mcp.health.disconnectConfirm.body':
'This will disconnect {count} currently-connected MCP servers. Installed configurations and secrets are kept; you can reconnect any server later.',
'mcp.health.disconnectConfirm.cancel': 'Cancel',
'mcp.health.disconnectConfirm.confirm': 'Disconnect all',
'mcp.health.opErrorGeneric': 'Bulk operation failed. See logs.',
'mcp.health.bulkPartialFailure': '{failed} of {total} servers failed. See logs.',
'mcp.installed.search.landmarkAria': 'Search installed MCP servers',
'mcp.installed.search.inputAria': 'Filter installed MCP servers by name',
'mcp.installed.search.placeholder': 'Filter servers…',
+17
View File
@@ -952,6 +952,23 @@ const en: TranslationMap = {
'mcp.installed.empty': 'No MCP servers installed yet.',
'mcp.installed.toolSingular': '{count} tool',
'mcp.installed.toolPlural': '{count} tools',
'mcp.health.title': 'Health',
'mcp.health.summaryAria': 'MCP connection health summary',
'mcp.health.connectedCount': '{count} connected',
'mcp.health.connectingCount': '{count} connecting',
'mcp.health.errorCount': '{count} error',
'mcp.health.disconnectedCount': '{count} idle',
'mcp.health.retryAll': 'Retry all ({count})',
'mcp.health.retryAllAria': 'Retry all {count} errored MCP servers',
'mcp.health.disconnectAll': 'Disconnect all ({count})',
'mcp.health.disconnectAllAria': 'Disconnect all {count} connected MCP servers',
'mcp.health.disconnectConfirm.title': 'Disconnect all MCP servers?',
'mcp.health.disconnectConfirm.body':
'This will disconnect {count} currently-connected MCP servers. Installed configurations and secrets are kept; you can reconnect any server later.',
'mcp.health.disconnectConfirm.cancel': 'Cancel',
'mcp.health.disconnectConfirm.confirm': 'Disconnect all',
'mcp.health.opErrorGeneric': 'Bulk operation failed. See logs.',
'mcp.health.bulkPartialFailure': '{failed} of {total} servers failed. See logs.',
'mcp.installed.search.landmarkAria': 'Search installed MCP servers',
'mcp.installed.search.inputAria': 'Filter installed MCP servers by name',
'mcp.installed.search.placeholder': 'Filter servers…',