From 32d03ca6aa70c1425f4c8e4735c9cff30e9e9f97 Mon Sep 17 00:00:00 2001 From: Aashir Athar Date: Fri, 29 May 2026 09:10:44 +0500 Subject: [PATCH] feat(mcp): connection health toolbar with bulk Retry All / Disconnect All actions (#2641) Co-authored-by: Claude Opus 4.7 Co-authored-by: Steven Enamakel --- .../mcp/McpConnectionHealthToolbar.test.tsx | 329 ++++++++++++++++++ .../mcp/McpConnectionHealthToolbar.tsx | 250 +++++++++++++ .../channels/mcp/McpServersTab.test.tsx | 57 +++ .../components/channels/mcp/McpServersTab.tsx | 55 ++- app/src/lib/i18n/chunks/ar-1.ts | 17 + app/src/lib/i18n/chunks/bn-1.ts | 17 + app/src/lib/i18n/chunks/de-1.ts | 17 + app/src/lib/i18n/chunks/en-1.ts | 17 + app/src/lib/i18n/chunks/es-1.ts | 17 + app/src/lib/i18n/chunks/fr-1.ts | 17 + app/src/lib/i18n/chunks/hi-1.ts | 17 + app/src/lib/i18n/chunks/id-1.ts | 17 + app/src/lib/i18n/chunks/it-1.ts | 17 + app/src/lib/i18n/chunks/ko-1.ts | 17 + app/src/lib/i18n/chunks/pt-1.ts | 17 + app/src/lib/i18n/chunks/ru-1.ts | 17 + app/src/lib/i18n/chunks/zh-CN-1.ts | 17 + app/src/lib/i18n/en.ts | 17 + 18 files changed, 928 insertions(+), 1 deletion(-) create mode 100644 app/src/components/channels/mcp/McpConnectionHealthToolbar.test.tsx create mode 100644 app/src/components/channels/mcp/McpConnectionHealthToolbar.tsx diff --git a/app/src/components/channels/mcp/McpConnectionHealthToolbar.test.tsx b/app/src/components/channels/mcp/McpConnectionHealthToolbar.test.tsx new file mode 100644 index 000000000..39dd23a2d --- /dev/null +++ b/app/src/components/channels/mcp/McpConnectionHealthToolbar.test.tsx @@ -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( + {}} + onDisconnect={async () => {}} + /> + ); + expect(container.firstChild).toBeNull(); + }); + + it('always shows connected + disconnected counts (even when zero)', () => { + render( + {}} + 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( + {}} + onDisconnect={async () => {}} + /> + ); + expect(screen.queryByText(/connecting/)).not.toBeInTheDocument(); + rerender( + {}} + onDisconnect={async () => {}} + /> + ); + expect(screen.getByText('1 connecting')).toBeInTheDocument(); + }); + + it('only shows error count when there are errored servers', () => { + const { rerender } = render( + {}} + onDisconnect={async () => {}} + /> + ); + expect(screen.queryByText(/error/)).not.toBeInTheDocument(); + rerender( + {}} + onDisconnect={async () => {}} + /> + ); + expect(screen.getByText('1 error')).toBeInTheDocument(); + }); + + it('aggregates counts correctly across a mixed status set', () => { + render( + {}} + 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( + {}} + onDisconnect={async () => {}} + /> + ); + expect(screen.queryByRole('button', { name: /Retry all/i })).not.toBeInTheDocument(); + }); + + it('hides "Disconnect all" button when nothing is connected', () => { + render( + {}} + 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( + {}} + 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( + {}} + /> + ); + 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( + {}} + 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( + {}} + 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( + {}} + 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( + {}} + 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(res => { + resolveOp = res; + }) + ); + render( + {}} + /> + ); + 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( + {}} + /> + ); + 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( + {}} + /> + ); + 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( + {}} + 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( + {}} + 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(); + }); +}); diff --git a/app/src/components/channels/mcp/McpConnectionHealthToolbar.tsx b/app/src/components/channels/mcp/McpConnectionHealthToolbar.tsx new file mode 100644 index 000000000..d12bd03dd --- /dev/null +++ b/app/src/components/channels/mcp/McpConnectionHealthToolbar.tsx @@ -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; + /** Disconnect every currently-connected server. Caller resolves after refresh. */ + onDisconnect: (serverIds: string[]) => Promise; +} + +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(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 ( +
+
+ + {t('mcp.health.title')} + +
+ {counts.errorCount > 0 && ( + + )} + {counts.connectedCount > 0 && ( + + )} +
+
+
+ + + {counts.connectingCount > 0 && ( + + + )} + {counts.errorCount > 0 && ( + + + )} + + +
+ + {opError && ( +

+ {opError} +

+ )} + + {confirmDisconnect && ( +
+
+

+ {t('mcp.health.disconnectConfirm.title')} +

+

+ {t('mcp.health.disconnectConfirm.body').replace( + '{count}', + String(counts.connectedCount) + )} +

+
+ + +
+
+
+ )} +
+ ); +}; + +export default McpConnectionHealthToolbar; diff --git a/app/src/components/channels/mcp/McpServersTab.test.tsx b/app/src/components/channels/mcp/McpServersTab.test.tsx index 327977abe..bef365e46 100644 --- a/app/src/components/channels/mcp/McpServersTab.test.tsx +++ b/app/src/components/channels/mcp/McpServersTab.test.tsx @@ -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(); + 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(); + 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.'); + }); + }); }); diff --git a/app/src/components/channels/mcp/McpServersTab.tsx b/app/src/components/channels/mcp/McpServersTab.tsx index fbbe96b94..42de9f699 100644 --- a/app/src/components/channels/mcp/McpServersTab.tsx +++ b/app/src/components/channels/mcp/McpServersTab.tsx @@ -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[], 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 = () => {
- {/* Left pane: search + installed list */} + {/* Left pane: health toolbar + search + installed list */}
{loadError && (
{loadError}
)} + {servers.length > 0 && (
diff --git a/app/src/lib/i18n/chunks/ar-1.ts b/app/src/lib/i18n/chunks/ar-1.ts index 77e70cbd5..a76e07784 100644 --- a/app/src/lib/i18n/chunks/ar-1.ts +++ b/app/src/lib/i18n/chunks/ar-1.ts @@ -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…', diff --git a/app/src/lib/i18n/chunks/bn-1.ts b/app/src/lib/i18n/chunks/bn-1.ts index ca6f7a10a..adeb7b23f 100644 --- a/app/src/lib/i18n/chunks/bn-1.ts +++ b/app/src/lib/i18n/chunks/bn-1.ts @@ -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…', diff --git a/app/src/lib/i18n/chunks/de-1.ts b/app/src/lib/i18n/chunks/de-1.ts index 7bd42b1aa..79338ea94 100644 --- a/app/src/lib/i18n/chunks/de-1.ts +++ b/app/src/lib/i18n/chunks/de-1.ts @@ -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…', diff --git a/app/src/lib/i18n/chunks/en-1.ts b/app/src/lib/i18n/chunks/en-1.ts index 67522dbfb..7e84a8bb2 100644 --- a/app/src/lib/i18n/chunks/en-1.ts +++ b/app/src/lib/i18n/chunks/en-1.ts @@ -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…', diff --git a/app/src/lib/i18n/chunks/es-1.ts b/app/src/lib/i18n/chunks/es-1.ts index 6452c067b..ec4292562 100644 --- a/app/src/lib/i18n/chunks/es-1.ts +++ b/app/src/lib/i18n/chunks/es-1.ts @@ -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…', diff --git a/app/src/lib/i18n/chunks/fr-1.ts b/app/src/lib/i18n/chunks/fr-1.ts index e815876ad..0dde5cee9 100644 --- a/app/src/lib/i18n/chunks/fr-1.ts +++ b/app/src/lib/i18n/chunks/fr-1.ts @@ -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…', diff --git a/app/src/lib/i18n/chunks/hi-1.ts b/app/src/lib/i18n/chunks/hi-1.ts index 332696e9d..45d251eb9 100644 --- a/app/src/lib/i18n/chunks/hi-1.ts +++ b/app/src/lib/i18n/chunks/hi-1.ts @@ -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…', diff --git a/app/src/lib/i18n/chunks/id-1.ts b/app/src/lib/i18n/chunks/id-1.ts index 22ba4780a..190863de5 100644 --- a/app/src/lib/i18n/chunks/id-1.ts +++ b/app/src/lib/i18n/chunks/id-1.ts @@ -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…', diff --git a/app/src/lib/i18n/chunks/it-1.ts b/app/src/lib/i18n/chunks/it-1.ts index ac23404aa..17c26d8fb 100644 --- a/app/src/lib/i18n/chunks/it-1.ts +++ b/app/src/lib/i18n/chunks/it-1.ts @@ -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…', diff --git a/app/src/lib/i18n/chunks/ko-1.ts b/app/src/lib/i18n/chunks/ko-1.ts index c91ee8253..d4b23bb06 100644 --- a/app/src/lib/i18n/chunks/ko-1.ts +++ b/app/src/lib/i18n/chunks/ko-1.ts @@ -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…', diff --git a/app/src/lib/i18n/chunks/pt-1.ts b/app/src/lib/i18n/chunks/pt-1.ts index 8452cea24..96eac576c 100644 --- a/app/src/lib/i18n/chunks/pt-1.ts +++ b/app/src/lib/i18n/chunks/pt-1.ts @@ -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…', diff --git a/app/src/lib/i18n/chunks/ru-1.ts b/app/src/lib/i18n/chunks/ru-1.ts index 9e4bf998d..d5883bdf5 100644 --- a/app/src/lib/i18n/chunks/ru-1.ts +++ b/app/src/lib/i18n/chunks/ru-1.ts @@ -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…', diff --git a/app/src/lib/i18n/chunks/zh-CN-1.ts b/app/src/lib/i18n/chunks/zh-CN-1.ts index 289a96cef..6567060c3 100644 --- a/app/src/lib/i18n/chunks/zh-CN-1.ts +++ b/app/src/lib/i18n/chunks/zh-CN-1.ts @@ -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…', diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index 5bca264ed..d2f9092ed 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -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…',