LibreChat/client/src/components/MCP/MCPConfigDialog.tsx
Dustin Healy 1a38e2a081
🗣 feat: MCP Status Accessibility Improvements (#10738)
* feat: make MultiSelect highlight same opacity as other focus highlights in app

* feat: add better screenreader announcements for mcp server and variable states

* feat: memoize fullTitle calculation
2025-12-11 16:37:12 -05:00

164 lines
5.2 KiB
TypeScript

import React, { useMemo } from 'react';
import { KeyRound, PlugZap, AlertTriangle } from 'lucide-react';
import {
Spinner,
OGDialog,
OGDialogTitle,
OGDialogHeader,
OGDialogContent,
} from '@librechat/client';
import type { MCPServerStatus } from 'librechat-data-provider';
import type { ConfigFieldDetail } from '~/common';
import ServerInitializationSection from './ServerInitializationSection';
import CustomUserVarsSection from './CustomUserVarsSection';
import { useLocalize } from '~/hooks';
interface MCPConfigDialogProps {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
fieldsSchema: Record<string, ConfigFieldDetail>;
initialValues: Record<string, string>;
onSave: (updatedValues: Record<string, string>) => void;
isSubmitting?: boolean;
onRevoke?: () => void;
serverName: string;
serverStatus?: MCPServerStatus;
conversationId?: string | null;
}
export default function MCPConfigDialog({
isOpen,
onOpenChange,
fieldsSchema,
onSave,
isSubmitting = false,
onRevoke,
serverName,
serverStatus,
conversationId,
}: MCPConfigDialogProps) {
const localize = useLocalize();
const hasFields = Object.keys(fieldsSchema).length > 0;
const dialogTitle = hasFields
? localize('com_ui_configure_mcp_variables_for', { 0: serverName })
: `${serverName} MCP Server`;
const fullTitle = useMemo(() => {
if (!serverStatus) {
return localize('com_ui_mcp_dialog_title', {
serverName,
status: '',
});
}
const { connectionState, requiresOAuth } = serverStatus;
let statusText = '';
if (connectionState === 'connecting') {
statusText = localize('com_ui_connecting');
} else if (connectionState === 'error') {
statusText = localize('com_ui_error');
} else if (connectionState === 'connected') {
statusText = localize('com_ui_active');
} else if (connectionState === 'disconnected') {
statusText = requiresOAuth ? localize('com_ui_oauth') : localize('com_ui_offline');
}
return localize('com_ui_mcp_dialog_title', {
serverName,
status: statusText,
});
}, [serverStatus, serverName, localize]);
// Helper function to render status badge based on connection state
const renderStatusBadge = () => {
if (!serverStatus) {
return null;
}
const { connectionState, requiresOAuth } = serverStatus;
if (connectionState === 'connecting') {
return (
<div className="flex items-center gap-2 rounded-full bg-blue-50 px-2 py-0.5 text-xs font-medium text-blue-600 dark:bg-blue-950 dark:text-blue-400">
<Spinner className="h-3 w-3" />
<span>{localize('com_ui_connecting')}</span>
</div>
);
}
if (connectionState === 'disconnected') {
if (requiresOAuth) {
return (
<div className="flex items-center gap-2 rounded-full bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-600 dark:bg-amber-950 dark:text-amber-400">
<KeyRound className="h-3 w-3" aria-hidden="true" />
<span>{localize('com_ui_oauth')}</span>
</div>
);
} else {
return (
<div className="flex items-center gap-2 rounded-full bg-orange-50 px-2 py-0.5 text-xs font-medium text-orange-600 dark:bg-orange-950 dark:text-orange-400">
<PlugZap className="h-3 w-3" aria-hidden="true" />
<span>{localize('com_ui_offline')}</span>
</div>
);
}
}
if (connectionState === 'error') {
return (
<div className="flex items-center gap-2 rounded-full bg-red-50 px-2 py-0.5 text-xs font-medium text-red-600 dark:bg-red-950 dark:text-red-400">
<AlertTriangle className="h-3 w-3" aria-hidden="true" />
<span>{localize('com_ui_error')}</span>
</div>
);
}
if (connectionState === 'connected') {
return (
<div className="flex items-center gap-2 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700 dark:bg-green-900 dark:text-green-300">
<div className="h-1.5 w-1.5 rounded-full bg-green-500" />
<span>{localize('com_ui_active')}</span>
</div>
);
}
return null;
};
return (
<OGDialog open={isOpen} onOpenChange={onOpenChange}>
<OGDialogContent
className="flex max-h-screen w-11/12 max-w-lg flex-col space-y-2"
title={fullTitle}
>
<OGDialogHeader>
<div className="flex items-center gap-3">
<OGDialogTitle className="text-xl">
{dialogTitle.charAt(0).toUpperCase() + dialogTitle.slice(1)}
</OGDialogTitle>
{renderStatusBadge()}
</div>
</OGDialogHeader>
{/* Custom User Variables Section */}
<CustomUserVarsSection
serverName={serverName}
fields={fieldsSchema}
onSave={onSave}
onRevoke={onRevoke || (() => {})}
isSubmitting={isSubmitting}
/>
{/* Server Initialization Section */}
<ServerInitializationSection
serverName={serverName}
conversationId={conversationId}
requiresOAuth={serverStatus?.requiresOAuth || false}
hasCustomUserVars={fieldsSchema && Object.keys(fieldsSchema).length > 0}
/>
</OGDialogContent>
</OGDialog>
);
}