From c818f026cbf8e4e901560051b393a910dbefd884 Mon Sep 17 00:00:00 2001 From: sanil-23 Date: Wed, 17 Jun 2026 13:11:28 +0530 Subject: [PATCH] fix(mcp): surface 401s as an actionable "needs sign-in" state (#3719) Closes #3719. MCP HTTP 401s now report a distinct `Unauthorized` state (typed McpUnauthorizedError + atomic ConnectFailure snapshot) with a localized 'Sign in' notice instead of a raw-error dead-end; recovery polling + i18n across 14 locales. --- .../mcp/InstalledServerDetail.test.tsx | 24 +++ .../channels/mcp/InstalledServerDetail.tsx | 23 ++- .../channels/mcp/InstalledServerList.tsx | 2 + .../mcp/McpConnectionHealthToolbar.tsx | 5 + .../components/channels/mcp/McpServersTab.tsx | 15 +- .../channels/mcp/McpStatusBadge.test.tsx | 1 + .../channels/mcp/McpStatusBadge.tsx | 4 + app/src/components/channels/mcp/types.ts | 10 +- app/src/lib/i18n/ar.ts | 4 + app/src/lib/i18n/bn.ts | 4 + app/src/lib/i18n/de.ts | 4 + app/src/lib/i18n/en.ts | 4 + app/src/lib/i18n/es.ts | 4 + app/src/lib/i18n/fr.ts | 4 + app/src/lib/i18n/hi.ts | 4 + app/src/lib/i18n/id.ts | 4 + app/src/lib/i18n/it.ts | 4 + app/src/lib/i18n/ko.ts | 4 + app/src/lib/i18n/pl.ts | 4 + app/src/lib/i18n/pt.ts | 4 + app/src/lib/i18n/ru.ts | 4 + app/src/lib/i18n/zh-CN.ts | 3 + src/openhuman/mcp_client/client.rs | 55 +++-- src/openhuman/mcp_client/mod.rs | 2 +- src/openhuman/mcp_registry/connections.rs | 195 +++++++++++++++--- src/openhuman/mcp_registry/types.rs | 7 + 26 files changed, 353 insertions(+), 45 deletions(-) diff --git a/app/src/components/channels/mcp/InstalledServerDetail.test.tsx b/app/src/components/channels/mcp/InstalledServerDetail.test.tsx index dafabb5c0..c999230f2 100644 --- a/app/src/components/channels/mcp/InstalledServerDetail.test.tsx +++ b/app/src/components/channels/mcp/InstalledServerDetail.test.tsx @@ -262,6 +262,30 @@ describe('InstalledServerDetail', () => { expect(screen.getByText('Timed out')).toBeInTheDocument(); }); + it('renders a graceful auth notice (not a raw error) for unauthorized status', () => { + render( + {}} + /> + ); + // Friendly "sign in needed" badge + actionable notice, no raw HTTP string. + expect(screen.getByText('Sign in needed')).toBeInTheDocument(); + expect(screen.getByText(/needs you to sign in or add an access token/i)).toBeInTheDocument(); + expect(screen.queryByText(/HTTP 401/i)).not.toBeInTheDocument(); + // The primary action is relabelled "Sign in" (it opens the auth modal). + expect(screen.getByRole('button', { name: 'Sign in' })).toBeInTheDocument(); + }); + // ---------------------------------------------------------------------- // Env reconfiguration (issue #3039) // ---------------------------------------------------------------------- diff --git a/app/src/components/channels/mcp/InstalledServerDetail.tsx b/app/src/components/channels/mcp/InstalledServerDetail.tsx index e0103b6d3..90eaaaf9a 100644 --- a/app/src/components/channels/mcp/InstalledServerDetail.tsx +++ b/app/src/components/channels/mcp/InstalledServerDetail.tsx @@ -245,8 +245,21 @@ const InstalledServerDetail = ({ - {/* Error */} - {(error || connStatus?.last_error) && ( + {/* Auth required — a graceful, actionable notice rather than the raw HTTP + 401 string. The core reports `unauthorized` (no raw error) for a 401; + the Connect button below opens the auth modal, which probes the server + and offers browser sign-in or a token field as appropriate (#3719). */} + {status === 'unauthorized' && ( +
+ {t('mcp.detail.authRequired')} +
+ )} + + {/* Error — a genuine (non-auth) connect failure. Suppressed entirely while + `unauthorized`: the amber notice above is the only message shown, so a + local action error (e.g. a reconfigure reconnect that re-hits the 401) + can't re-expose raw transport/auth text in this state (#3719). */} + {status !== 'unauthorized' && (error || connStatus?.last_error) && (
{error ?? connStatus?.last_error}
@@ -270,7 +283,11 @@ const InstalledServerDetail = ({ disabled={busy || status === 'connecting'} onClick={handleConnect} className="rounded-lg bg-primary-500 px-3 py-1.5 text-xs font-medium text-white hover:bg-primary-600 disabled:opacity-50 transition-colors"> - {status === 'connecting' ? t('mcp.detail.connecting') : t('mcp.detail.connect')} + {status === 'connecting' + ? t('mcp.detail.connecting') + : status === 'unauthorized' + ? t('mcp.detail.authenticate') + : t('mcp.detail.connect')} ) : (