Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions app/src/components/channels/mcp/InstalledServerDetail.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<InstalledServerDetail
server={BASE_SERVER}
connStatus={{
server_id: 'srv-1',
qualified_name: 'acme/test-server',
display_name: 'Test Server',
status: 'unauthorized',
tool_count: 0,
// Core sends no raw error for a 401 (avoids leaking the OAuth URL).
last_error: undefined,
}}
onUninstalled={() => {}}
/>
);
// 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)
// ----------------------------------------------------------------------
Expand Down
23 changes: 20 additions & 3 deletions app/src/components/channels/mcp/InstalledServerDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,21 @@ const InstalledServerDetail = ({
</div>
</div>

{/* 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' && (
<div className="rounded-lg border border-amber-200 dark:border-amber-500/30 bg-amber-50 dark:bg-amber-500/10 px-4 py-3 text-sm text-amber-700 dark:text-amber-300">
{t('mcp.detail.authRequired')}
</div>
)}

{/* 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) && (
<div className="rounded-lg border border-coral-200 dark:border-coral-500/30 bg-coral-50 dark:bg-coral-500/10 px-4 py-3 text-sm text-coral-700 dark:text-coral-300">
{error ?? connStatus?.last_error}
</div>
Expand All @@ -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')}
</button>
) : (
<button
Expand Down
2 changes: 2 additions & 0 deletions app/src/components/channels/mcp/InstalledServerList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const STATUS_DOT: Record<ServerStatus, string> = {
connected: 'bg-sage-500',
connecting: 'bg-amber-400',
disconnected: 'bg-stone-300 dark:bg-neutral-600',
unauthorized: 'bg-amber-500',
error: 'bg-coral-500',
disabled: 'bg-stone-200 dark:bg-neutral-700',
};
Expand All @@ -40,6 +41,7 @@ const STATUS_I18N_KEYS: Record<ServerStatus, string> = {
connected: 'channels.status.connected',
connecting: 'channels.status.connecting',
disconnected: 'channels.status.disconnected',
unauthorized: 'mcp.status.unauthorized',
error: 'channels.status.error',
disabled: 'mcp.status.disabled',
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ const computeHealthCounts = (statuses: ConnStatus[]): HealthCounts => {
case 'connecting':
connectingCount += 1;
break;
// An `unauthorized` server is not connected, but it must NOT join the
// `errorIds` set — "Retry all" blindly reconnects those, which would just
// 401 again. Re-auth is a deliberate per-server action (sign in / token),
// so we surface it under the disconnected tally rather than as an error.
case 'unauthorized':
case 'disconnected':
disconnectedCount += 1;
break;
Expand Down
15 changes: 13 additions & 2 deletions app/src/components/channels/mcp/McpServersTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const STATUS_DOT: Record<ServerStatus, string> = {
connected: 'bg-sage-500',
connecting: 'bg-amber-400',
disconnected: 'bg-stone-300 dark:bg-neutral-600',
unauthorized: 'bg-amber-500',
error: 'bg-coral-500',
disabled: 'bg-stone-200 dark:bg-neutral-700',
};
Expand Down Expand Up @@ -119,8 +120,18 @@ const McpServersTab = () => {

// Poll status
useEffect(() => {
const hasConnected = statuses.some(s => s.status === 'connected');
if (!hasConnected) {
// Poll while anything is in a non-terminal state — not just `connected`.
// An `unauthorized`/`error`/`connecting` server can transition (the
// background reconnect supervisor, a completed OAuth sign-in, an expiring
// token) and the UI must reflect that without a manual refresh (#3719 RC5).
const hasActive = statuses.some(
s =>
s.status === 'connected' ||
s.status === 'connecting' ||
s.status === 'unauthorized' ||
s.status === 'error'
);
if (!hasActive) {
if (pollTimerRef.current) {
clearTimeout(pollTimerRef.current);
pollTimerRef.current = null;
Expand Down
1 change: 1 addition & 0 deletions app/src/components/channels/mcp/McpStatusBadge.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ describe('McpStatusBadge', () => {
['connected', 'Connected'],
['connecting', 'Connecting'],
['disconnected', 'Disconnected'],
['unauthorized', 'Sign in needed'],
['error', 'Error'],
])('renders i18n label for status=%s', (status, expectedLabel) => {
render(<McpStatusBadge status={status} />);
Expand Down
4 changes: 4 additions & 0 deletions app/src/components/channels/mcp/McpStatusBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ const STATUS_META: Record<ServerStatus, { i18nKey: string; className: string }>
className:
'bg-stone-100 dark:bg-neutral-800 text-stone-500 dark:text-neutral-400 border-stone-200 dark:border-neutral-700',
},
unauthorized: {
i18nKey: 'mcp.status.unauthorized',
className: 'bg-amber-500/10 text-amber-700 border-amber-500/30 dark:text-amber-300',
},
error: {
i18nKey: 'channels.status.error',
className: 'bg-coral-500/10 text-coral-700 border-coral-500/30 dark:text-coral-300',
Expand Down
10 changes: 9 additions & 1 deletion app/src/components/channels/mcp/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,15 @@ export type InstalledServer = {

export type McpTool = { name: string; description?: string; input_schema: unknown };

export type ServerStatus = 'disconnected' | 'connecting' | 'connected' | 'error' | 'disabled';
export type ServerStatus =
| 'disconnected'
| 'connecting'
| 'connected'
// Server reachable but rejected the connect with HTTP 401 — needs sign-in or
// an access token. Distinct from `error` so the UI offers a re-auth path.
| 'unauthorized'
| 'error'
| 'disabled';

export type ConnStatus = {
server_id: string;
Expand Down
4 changes: 4 additions & 0 deletions app/src/lib/i18n/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1305,6 +1305,9 @@ const messages: TranslationMap = {
'mcp.detail.suggestedEnvBody': 'أعِد تثبيت هذا الخادم بالقيم المقترحة لتطبيقها: {keys}',
'mcp.detail.connect': 'اتصال',
'mcp.detail.connecting': 'جارٍ الاتصال...',
'mcp.detail.authenticate': 'تسجيل الدخول',
'mcp.detail.authRequired':
'يتطلب هذا الخادم تسجيل الدخول أو إضافة رمز وصول قبل أن يتمكن من الاتصال. انقر على “تسجيل الدخول” للمصادقة.',
'mcp.detail.disconnect': 'قطع الاتصال',
'mcp.detail.hideAssistant': 'إخفاء المساعد',
'mcp.detail.helpConfigure': 'ساعدني في التهيئة',
Expand All @@ -1322,6 +1325,7 @@ const messages: TranslationMap = {
'mcp.detail.enable': 'تفعيل',
'mcp.detail.disable': 'تعطيل',
'mcp.status.disabled': 'معطّل',
'mcp.status.unauthorized': 'يلزم تسجيل الدخول',
'mcp.detail.tools': 'الأدوات',
'mcp.connectAuth.title': 'الاتصال بـ {name}',
'mcp.connectAuth.hint':
Expand Down
4 changes: 4 additions & 0 deletions app/src/lib/i18n/bn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1330,6 +1330,9 @@ const messages: TranslationMap = {
'এই সার্ভারে-ইনস্টল করার জন্য পরামর্শ দেওয়া হল: xqxq0x ব্যবহার করুন',
'mcp.detail.connect': 'সংযোগ করুন',
'mcp.detail.connecting': 'সংযুক্ত হচ্ছে...',
'mcp.detail.authenticate': 'সাইন ইন করুন',
'mcp.detail.authRequired':
'এই সার্ভারটি সংযোগ করার আগে আপনাকে সাইন ইন করতে বা একটি অ্যাক্সেস টোকেন যোগ করতে হবে। প্রমাণীকরণ করতে “সাইন ইন করুন”-এ ক্লিক করুন।',
'mcp.detail.disconnect': 'সংযোগ বিচ্ছিন্ন করুন',
'mcp.detail.hideAssistant': 'সহকারী লুকান',
'mcp.detail.helpConfigure': 'কনফিগার করতে আমাকে সাহায্য করুন',
Expand All @@ -1348,6 +1351,7 @@ const messages: TranslationMap = {
'mcp.detail.enable': 'সক্রিয় করুন',
'mcp.detail.disable': 'নিষ্ক্রিয় করুন',
'mcp.status.disabled': 'নিষ্ক্রিয়',
'mcp.status.unauthorized': 'সাইন ইন প্রয়োজন',
'mcp.detail.tools': 'টুলস',
'mcp.connectAuth.title': '{name} সংযুক্ত করুন',
'mcp.connectAuth.hint':
Expand Down
4 changes: 4 additions & 0 deletions app/src/lib/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1368,6 +1368,9 @@ const messages: TranslationMap = {
'Installieren Sie diesen Server mit den vorgeschlagenen Werten neu, um sie anzuwenden: {keys}',
'mcp.detail.connect': 'Verbinden',
'mcp.detail.connecting': 'Verbinden ...',
'mcp.detail.authenticate': 'Anmelden',
'mcp.detail.authRequired':
'Dieser Server erfordert, dass Sie sich anmelden oder ein Zugriffstoken hinzufügen, bevor er eine Verbindung herstellen kann. Klicken Sie auf „Anmelden“, um sich zu authentifizieren.',
'mcp.detail.disconnect': 'Trennen',
'mcp.detail.hideAssistant': 'Ausblenden Assistent',
'mcp.detail.helpConfigure': 'Helfen Sie mir bei der Konfiguration',
Expand All @@ -1386,6 +1389,7 @@ const messages: TranslationMap = {
'mcp.detail.enable': 'Aktivieren',
'mcp.detail.disable': 'Deaktivieren',
'mcp.status.disabled': 'Deaktiviert',
'mcp.status.unauthorized': 'Anmeldung erforderlich',
'mcp.detail.tools': 'Werkzeuge',
'mcp.connectAuth.title': '{name} verbinden',
'mcp.connectAuth.hint':
Expand Down
4 changes: 4 additions & 0 deletions app/src/lib/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1685,6 +1685,9 @@ const en: TranslationMap = {
'Re-install this server with the suggested values to apply them: {keys}',
'mcp.detail.connect': 'Connect',
'mcp.detail.connecting': 'Connecting...',
'mcp.detail.authenticate': 'Sign in',
'mcp.detail.authRequired':
'This server needs you to sign in or add an access token before it can connect. Click “Sign in” to authenticate.',
'mcp.detail.disconnect': 'Disconnect',
'mcp.detail.hideAssistant': 'Hide assistant',
'mcp.detail.helpConfigure': 'Help me configure',
Expand Down Expand Up @@ -1726,6 +1729,7 @@ const en: TranslationMap = {
'mcp.detail.enable': 'Enable',
'mcp.detail.disable': 'Disable',
'mcp.status.disabled': 'Disabled',
'mcp.status.unauthorized': 'Sign in needed',
'mcp.detail.tools': 'Tools',
'onboarding.skipForNow': 'Skip for Now',
'onboarding.localAI.continueWithCloud': 'Continue with Cloud',
Expand Down
4 changes: 4 additions & 0 deletions app/src/lib/i18n/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1364,6 +1364,9 @@ const messages: TranslationMap = {
'Reinstale este servidor con los valores sugeridos para aplicarlos: {keys}',
'mcp.detail.connect': 'Conectar',
'mcp.detail.connecting': 'Conectando...',
'mcp.detail.authenticate': 'Iniciar sesión',
'mcp.detail.authRequired':
'Este servidor necesita que inicies sesión o agregues un token de acceso antes de poder conectarse. Haz clic en “Iniciar sesión” para autenticarte.',
'mcp.detail.disconnect': 'Desconectar',
'mcp.detail.hideAssistant': 'Ocultar asistente',
'mcp.detail.helpConfigure': 'Ayúdame a configurar',
Expand All @@ -1382,6 +1385,7 @@ const messages: TranslationMap = {
'mcp.detail.enable': 'Habilitar',
'mcp.detail.disable': 'Deshabilitar',
'mcp.status.disabled': 'Deshabilitado',
'mcp.status.unauthorized': 'Inicio de sesión necesario',
'mcp.detail.tools': 'Herramientas',
'mcp.connectAuth.title': 'Conectar {name}',
'mcp.connectAuth.hint':
Expand Down
4 changes: 4 additions & 0 deletions app/src/lib/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1370,6 +1370,9 @@ const messages: TranslationMap = {
'Réinstallez ce serveur avec les valeurs suggérées pour les appliquer: {keys}',
'mcp.detail.connect': 'Connecter',
'mcp.detail.connecting': 'Connexion...',
'mcp.detail.authenticate': 'Se connecter',
'mcp.detail.authRequired':
"Ce serveur nécessite que vous vous connectiez ou ajoutiez un jeton d'accès avant de pouvoir se connecter. Cliquez sur « Se connecter » pour vous authentifier.",
'mcp.detail.disconnect': 'Déconnecter',
'mcp.detail.hideAssistant': "Cacher l'assistant",
'mcp.detail.helpConfigure': 'Aidez-moi à configurer',
Expand All @@ -1388,6 +1391,7 @@ const messages: TranslationMap = {
'mcp.detail.enable': 'Activer',
'mcp.detail.disable': 'Désactiver',
'mcp.status.disabled': 'Désactivé',
'mcp.status.unauthorized': 'Connexion requise',
'mcp.detail.tools': 'Outils',
'mcp.connectAuth.title': 'Connecter {name}',
'mcp.connectAuth.hint':
Expand Down
4 changes: 4 additions & 0 deletions app/src/lib/i18n/hi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1328,6 +1328,9 @@ const messages: TranslationMap = {
'इस सर्वर को फिर से स्थापित करने के लिए सुझाए गए मूल्यों के साथ उन्हें लागू करने के लिए: {keys}',
'mcp.detail.connect': 'कनेक्ट करें',
'mcp.detail.connecting': 'कनेक्ट हो रहा है...',
'mcp.detail.authenticate': 'साइन इन करें',
'mcp.detail.authRequired':
'कनेक्ट होने से पहले इस सर्वर के लिए आपको साइन इन करना होगा या एक एक्सेस टोकन जोड़ना होगा। प्रमाणित करने के लिए “साइन इन करें” पर क्लिक करें।',
'mcp.detail.disconnect': 'डिस्कनेक्ट करें',
'mcp.detail.hideAssistant': 'सहायक छिपाएँ',
'mcp.detail.helpConfigure': 'कॉन्फ़िगर करने में मेरी सहायता करें',
Expand All @@ -1346,6 +1349,7 @@ const messages: TranslationMap = {
'mcp.detail.enable': 'सक्षम करें',
'mcp.detail.disable': 'अक्षम करें',
'mcp.status.disabled': 'अक्षम',
'mcp.status.unauthorized': 'साइन इन आवश्यक',
'mcp.detail.tools': 'उपकरण',
'mcp.connectAuth.title': '{name} कनेक्ट करें',
'mcp.connectAuth.hint':
Expand Down
4 changes: 4 additions & 0 deletions app/src/lib/i18n/id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1338,6 +1338,9 @@ const messages: TranslationMap = {
'Instal ulang server ini dengan nilai yang disarankan untuk diterapkan: {keys}',
'mcp.detail.connect': 'Sambungkan',
'mcp.detail.connecting': 'Sambungan...',
'mcp.detail.authenticate': 'Masuk',
'mcp.detail.authRequired':
'Server ini mengharuskan Anda masuk atau menambahkan token akses sebelum dapat tersambung. Klik “Masuk” untuk mengautentikasi.',
'mcp.detail.disconnect': 'Putuskan sambungan',
'mcp.detail.hideAssistant': 'Sembunyikan asisten',
'mcp.detail.helpConfigure': 'Bantu saya mengonfigurasi',
Expand All @@ -1356,6 +1359,7 @@ const messages: TranslationMap = {
'mcp.detail.enable': 'Aktifkan',
'mcp.detail.disable': 'Nonaktifkan',
'mcp.status.disabled': 'Dinonaktifkan',
'mcp.status.unauthorized': 'Perlu masuk',
'mcp.detail.tools': 'Alat',
'mcp.connectAuth.title': 'Hubungkan {name}',
'mcp.connectAuth.hint':
Expand Down
4 changes: 4 additions & 0 deletions app/src/lib/i18n/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1360,6 +1360,9 @@ const messages: TranslationMap = {
'Reinstalla questo server con i valori suggeriti per applicarli: {keys}',
'mcp.detail.connect': 'Connetti',
'mcp.detail.connecting': 'Connessione in corso...',
'mcp.detail.authenticate': 'Accedi',
'mcp.detail.authRequired':
'Questo server richiede di accedere o di aggiungere un token di accesso prima di potersi connettere. Fai clic su “Accedi” per autenticarti.',
'mcp.detail.disconnect': 'Disconnetti',
'mcp.detail.hideAssistant': 'Nascondi assistente',
'mcp.detail.helpConfigure': 'Aiutami a configurare',
Expand All @@ -1378,6 +1381,7 @@ const messages: TranslationMap = {
'mcp.detail.enable': 'Abilita',
'mcp.detail.disable': 'Disabilita',
'mcp.status.disabled': 'Disabilitato',
'mcp.status.unauthorized': 'Accesso necessario',
'mcp.detail.tools': 'Strumenti',
'mcp.connectAuth.title': 'Connetti {name}',
'mcp.connectAuth.hint':
Expand Down
4 changes: 4 additions & 0 deletions app/src/lib/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1325,6 +1325,9 @@ const messages: TranslationMap = {
'제안 값을 적용하려면 이 서버를 해당 값으로 다시 설치하세요: {keys}',
'mcp.detail.connect': '연결',
'mcp.detail.connecting': '연결 중...',
'mcp.detail.authenticate': '로그인',
'mcp.detail.authRequired':
'이 서버에 연결하려면 먼저 로그인하거나 액세스 토큰을 추가해야 합니다. 인증하려면 “로그인”을 클릭하세요.',
'mcp.detail.disconnect': '연결 끊기',
'mcp.detail.hideAssistant': '보조 숨기기',
'mcp.detail.helpConfigure': '구성 도와주세요',
Expand All @@ -1342,6 +1345,7 @@ const messages: TranslationMap = {
'mcp.detail.enable': '활성화',
'mcp.detail.disable': '비활성화',
'mcp.status.disabled': '비활성화됨',
'mcp.status.unauthorized': '로그인 필요',
'mcp.detail.tools': '도구',
'mcp.connectAuth.title': '{name} 연결',
'mcp.connectAuth.hint':
Expand Down
4 changes: 4 additions & 0 deletions app/src/lib/i18n/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1350,6 +1350,9 @@ const messages: TranslationMap = {
'Zainstaluj ten serwer ponownie z sugerowanymi wartościami, aby je zastosować: {keys}',
'mcp.detail.connect': 'Połącz',
'mcp.detail.connecting': 'Łączenie...',
'mcp.detail.authenticate': 'Zaloguj się',
'mcp.detail.authRequired':
'Ten serwer wymaga zalogowania się lub dodania tokena dostępu, zanim będzie mógł się połączyć. Kliknij „Zaloguj się”, aby uwierzytelnić.',
'mcp.detail.disconnect': 'Rozłącz',
'mcp.detail.hideAssistant': 'Ukryj asystenta',
'mcp.detail.helpConfigure': 'Pomóż skonfigurować',
Expand All @@ -1368,6 +1371,7 @@ const messages: TranslationMap = {
'mcp.detail.enable': 'Włącz',
'mcp.detail.disable': 'Wyłącz',
'mcp.status.disabled': 'Wyłączony',
'mcp.status.unauthorized': 'Wymagane logowanie',
'mcp.detail.tools': 'Narzędzia',
'mcp.connectAuth.title': 'Połącz {name}',
'mcp.connectAuth.hint':
Expand Down
4 changes: 4 additions & 0 deletions app/src/lib/i18n/pt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1366,6 +1366,9 @@ const messages: TranslationMap = {
'Reinstale este servidor com os valores sugeridos para aplicá-los: {keys}',
'mcp.detail.connect': 'Conectar',
'mcp.detail.connecting': 'Conectando...',
'mcp.detail.authenticate': 'Entrar',
'mcp.detail.authRequired':
'Este servidor exige que você entre ou adicione um token de acesso antes de poder se conectar. Clique em “Entrar” para autenticar.',
'mcp.detail.disconnect': 'Desconectar',
'mcp.detail.hideAssistant': 'Ocultar assistente',
'mcp.detail.helpConfigure': 'Ajude-me a configurar',
Expand All @@ -1383,6 +1386,7 @@ const messages: TranslationMap = {
'mcp.detail.enable': 'Ativar',
'mcp.detail.disable': 'Desativar',
'mcp.status.disabled': 'Desativado',
'mcp.status.unauthorized': 'É necessário entrar',
'mcp.detail.tools': 'Ferramentas',
'mcp.connectAuth.title': 'Conectar {name}',
'mcp.connectAuth.hint':
Expand Down
Loading
Loading