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
@@ -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} />