From d6b6f191f705c066ecc2ca591127ca02601e83e5 Mon Sep 17 00:00:00 2001
From: Marco Beretta <81851188+berry-13@users.noreply.github.com>
Date: Thu, 12 Feb 2026 04:08:40 +0100
Subject: [PATCH] =?UTF-8?q?=E2=99=BF=20style(MCP):=20Enhance=20dialog=20ac?=
=?UTF-8?q?cessibility=20and=20styling=20consistency=20(#11585)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* style: update input IDs in BasicInfoSection for consistency and improve accessibility
* style: add border-destructive variable for improved design consistency
* style: update error border color for title input in BasicInfoSection
* style: update delete confirmation dialog title and description for MCP Server
* style: add text-destructive variable for improved design consistency
* style: update error message and border color for URL and trust fields for consistency
* style: reorder imports and update error message styling for consistency across sections
* style: enhance MCPServerDialog with copy link functionality and UI improvements
* style: enhance MCPServerDialog with improved accessibility and loading indicators
* style: bump @librechat/client to 0.4.51 and enhance OGDialogTemplate for improved selection handling
* a11y: enhance accessibility and error handling in MCPServerDialog sections
* style: enhance MCPServerDialog accessibility and improve resource name handling
* style: improve accessibility in MCPServerDialog and AuthSection, update translation for delete confirmation
* style: update aria-invalid attributes to use string values for improved accessibility in form sections
* style: enhance accessibility in AuthSection by updating aria attributes and adding error messages
* style: remove unnecessary aria-hidden attributes from Spinner components in MCPServerDialog
* style: simplify legacy selection check in OGDialogTemplate
---
.../MCPServerDialog/MCPServerForm.tsx | 4 +-
.../MCPBuilder/MCPServerDialog/index.tsx | 115 +++++++++++-------
.../MCPServerDialog/sections/AuthSection.tsx | 89 +++++++++++---
.../sections/BasicInfoSection.tsx | 38 +++---
.../sections/ConnectionSection.tsx | 16 ++-
.../sections/TransportSection.tsx | 11 +-
.../MCPServerDialog/sections/TrustSection.tsx | 22 ++--
client/src/locales/en/translation.json | 10 +-
client/src/style.css | 4 +
client/src/utils/resources.ts | 18 +--
client/tailwind.config.cjs | 2 +
.../src/components/OGDialogTemplate.tsx | 103 ++++++++++------
packages/client/src/components/Radio.tsx | 4 +
13 files changed, 295 insertions(+), 141 deletions(-)
diff --git a/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/MCPServerForm.tsx b/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/MCPServerForm.tsx
index 188c518597..d4096ea96a 100644
--- a/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/MCPServerForm.tsx
+++ b/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/MCPServerForm.tsx
@@ -1,10 +1,10 @@
import { FormProvider } from 'react-hook-form';
+import type { useMCPServerForm } from './hooks/useMCPServerForm';
import ConnectionSection from './sections/ConnectionSection';
import BasicInfoSection from './sections/BasicInfoSection';
import TransportSection from './sections/TransportSection';
-import AuthSection from './sections/AuthSection';
import TrustSection from './sections/TrustSection';
-import type { useMCPServerForm } from './hooks/useMCPServerForm';
+import AuthSection from './sections/AuthSection';
interface MCPServerFormProps {
formHook: ReturnType;
diff --git a/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/index.tsx b/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/index.tsx
index f86d3f8056..c9d3473d60 100644
--- a/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/index.tsx
+++ b/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/index.tsx
@@ -1,13 +1,18 @@
import React, { useState, useEffect } from 'react';
+import { Copy, CopyCheck } from 'lucide-react';
import {
- OGDialog,
- OGDialogTemplate,
- OGDialogContent,
- OGDialogHeader,
- OGDialogTitle,
+ Label,
+ Input,
Button,
- TrashIcon,
Spinner,
+ TrashIcon,
+ useToastContext,
+ OGDialog,
+ OGDialogTitle,
+ OGDialogHeader,
+ OGDialogFooter,
+ OGDialogContent,
+ OGDialogTemplate,
} from '@librechat/client';
import {
SystemRoles,
@@ -16,10 +21,10 @@ import {
PermissionBits,
PermissionTypes,
} from 'librechat-data-provider';
-import { GenericGrantAccessDialog } from '~/components/Sharing';
import { useAuthContext, useHasAccess, useResourcePermissions, MCPServerDefinition } from '~/hooks';
-import { useLocalize } from '~/hooks';
+import { GenericGrantAccessDialog } from '~/components/Sharing';
import { useMCPServerForm } from './hooks/useMCPServerForm';
+import { useLocalize, useCopyToClipboard } from '~/hooks';
import MCPServerForm from './MCPServerForm';
interface MCPServerDialogProps {
@@ -39,8 +44,10 @@ export default function MCPServerDialog({
}: MCPServerDialogProps) {
const localize = useLocalize();
const { user } = useAuthContext();
+ const { showToast } = useToastContext();
// State for dialogs
+ const [isCopying, setIsCopying] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [showRedirectUriDialog, setShowRedirectUriDialog] = useState(false);
const [createdServerId, setCreatedServerId] = useState(null);
@@ -99,20 +106,26 @@ export default function MCPServerDialog({
? `${window.location.origin}/api/mcp/${createdServerId}/oauth/callback`
: '';
+ const copyLink = useCopyToClipboard({ text: redirectUri });
+
return (
<>
{/* Delete confirmation dialog */}
setShowDeleteConfirm(isOpen)}>
{localize('com_ui_mcp_server_delete_confirm')}
}
- selection={{
- selectHandler: handleDelete,
- selectClasses:
- 'bg-destructive text-white transition-all duration-200 hover:bg-destructive/80',
- selectText: isDeleting ? : localize('com_ui_delete'),
- }}
+ title={localize('com_ui_delete_mcp_server')}
+ className="w-11/12 max-w-md"
+ description={localize('com_ui_mcp_server_delete_confirm', { 0: server?.serverName })}
+ selection={
+
+ }
/>
@@ -127,48 +140,53 @@ export default function MCPServerDialog({
}
}}
>
-
-
+
+
{localize('com_ui_mcp_server_created')}
-
-
- {localize('com_ui_redirect_uri_instructions')}
-
-
-
diff --git a/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/sections/ConnectionSection.tsx b/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/sections/ConnectionSection.tsx
index 5d7094fd83..ee77a54699 100644
--- a/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/sections/ConnectionSection.tsx
+++ b/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/sections/ConnectionSection.tsx
@@ -15,13 +15,19 @@ export default function ConnectionSection() {
return (
- {localize('com_ui_mcp_url')} *
+ {localize('com_ui_mcp_url')}{' '}
+
+ *
+
+ {localize('com_ui_field_required')}
{
@@ -29,9 +35,13 @@ export default function ConnectionSection() {
return isValidUrl(normalized) || localize('com_ui_mcp_invalid_url');
},
})}
- className={cn(errors.url && 'border-red-500 focus:border-red-500')}
+ className={cn(errors.url && 'border-border-destructive')}
/>
- {errors.url &&
{errors.url.message}
}
+ {errors.url && (
+
+ {errors.url.message}
+
+ )}
);
}
diff --git a/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/sections/TransportSection.tsx b/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/sections/TransportSection.tsx
index 80d4595719..5c7b610b70 100644
--- a/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/sections/TransportSection.tsx
+++ b/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/sections/TransportSection.tsx
@@ -25,14 +25,19 @@ export default function TransportSection() {
);
return (
-
- {localize('com_ui_mcp_transport')}
+
+
);
}
diff --git a/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/sections/TrustSection.tsx b/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/sections/TrustSection.tsx
index 854ac717b7..36d8d73a49 100644
--- a/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/sections/TrustSection.tsx
+++ b/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/sections/TrustSection.tsx
@@ -26,17 +26,17 @@ export default function TrustSection() {
checked={field.value}
onCheckedChange={field.onChange}
aria-labelledby="trust-label"
- aria-describedby="trust-description"
+ aria-describedby={
+ errors.trust ? 'trust-description trust-error' : 'trust-description'
+ }
+ aria-invalid={errors.trust ? 'true' : 'false'}
+ aria-required="true"
className="mt-0.5"
/>
)}
/>
-
-
+
+
{startupConfig?.interface?.mcpServers?.trustCheckbox?.label ? (
*
+
+ *
+
{startupConfig?.interface?.mcpServers?.trustCheckbox?.subLabel ? (
@@ -68,7 +70,9 @@ export default function TrustSection() {
{errors.trust && (
- {localize('com_ui_field_required')}
+
+ {localize('com_ui_field_required')}
+
)}
);
diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json
index e961e6cd3c..491bc2258b 100644
--- a/client/src/locales/en/translation.json
+++ b/client/src/locales/en/translation.json
@@ -857,8 +857,11 @@
"com_ui_copy_url_to_clipboard": "Copy URL to clipboard",
"com_ui_create": "Create",
"com_ui_create_api_key": "Create API Key",
+ "com_ui_created": "Created",
+ "com_ui_creating": "Creating...",
"com_ui_create_assistant": "Create Assistant",
"com_ui_create_link": "Create link",
+ "com_ui_create_mcp_server": "Create MCP server",
"com_ui_create_memory": "Create Memory",
"com_ui_create_new_agent": "Create New Agent",
"com_ui_create_prompt": "Create Prompt",
@@ -893,6 +896,7 @@
"com_ui_decline": "I do not accept",
"com_ui_default_post_request": "Default (POST request)",
"com_ui_delete": "Delete",
+ "com_ui_deleting": "Deleting...",
"com_ui_delete_action": "Delete Action",
"com_ui_delete_action_confirm": "Are you sure you want to delete this action?",
"com_ui_delete_agent": "Delete Agent",
@@ -915,6 +919,8 @@
"com_ui_delete_tool": "Delete Tool",
"com_ui_delete_tool_confirm": "Are you sure you want to delete this tool?",
"com_ui_delete_tool_save_reminder": "Tool removed. Save the agent to apply changes.",
+ "com_ui_delete_mcp_server": "Delete MCP Server?",
+ "com_ui_delete_mcp_server_name": "Delete MCP server {{0}}",
"com_ui_deleted": "Deleted",
"com_ui_deleting_file": "Deleting file...",
"com_ui_descending": "Desc",
@@ -1111,7 +1117,7 @@
"com_ui_mcp_server": "MCP Server",
"com_ui_mcp_server_connection_failed": "Connection attempt to the provided MCP server failed. Please make sure the URL, the server type, and any authentication configuration are correct, then try again. Also ensure the URL is reachable.",
"com_ui_mcp_server_created": "MCP server created successfully",
- "com_ui_mcp_server_delete_confirm": "Are you sure you want to delete this MCP server?",
+ "com_ui_mcp_server_delete_confirm": "Are you sure you want to delete the {{0}} MCP server?",
"com_ui_mcp_server_deleted": "MCP server deleted successfully",
"com_ui_mcp_server_role_editor": "MCP Server Editor",
"com_ui_mcp_server_role_editor_desc": "Can view, use, and edit MCP servers",
@@ -1438,6 +1444,8 @@
"com_ui_unset": "Unset",
"com_ui_untitled": "Untitled",
"com_ui_update": "Update",
+ "com_ui_updating": "Updating...",
+ "com_ui_update_mcp_server": "Update MCP server",
"com_ui_upload": "Upload",
"com_ui_upload_agent_avatar": "Successfully updated agent avatar",
"com_ui_upload_agent_avatar_label": "Upload agent avatar image",
diff --git a/client/src/style.css b/client/src/style.css
index 689c05423d..cf3ea50294 100644
--- a/client/src/style.css
+++ b/client/src/style.css
@@ -70,6 +70,7 @@ html {
--text-secondary-alt: var(--gray-500);
--text-tertiary: var(--gray-500);
--text-warning: var(--amber-500);
+ --text-destructive: var(--red-600);
--ring-primary: var(--gray-500);
--header-primary: var(--white);
--header-hover: var(--gray-50);
@@ -96,6 +97,7 @@ html {
--border-medium: var(--gray-300);
--border-heavy: var(--gray-400);
--border-xheavy: var(--gray-500);
+ --border-destructive: var(--red-600);
/* These are test styles */
--background: 0 0% 100%;
@@ -131,6 +133,7 @@ html {
--text-secondary-alt: var(--gray-400);
--text-tertiary: var(--gray-500);
--text-warning: var(--amber-500);
+ --text-destructive: var(--red-600);
--header-primary: var(--gray-700);
--header-hover: var(--gray-600);
--header-button-hover: var(--gray-700);
@@ -156,6 +159,7 @@ html {
--border-medium: var(--gray-600);
--border-heavy: var(--gray-500);
--border-xheavy: var(--gray-400);
+ --border-destructive: var(--red-500);
/* These are test styles */
--background: 0 0% 7%;
diff --git a/client/src/utils/resources.ts b/client/src/utils/resources.ts
index 9b68cef3f6..7a1e2b86c1 100644
--- a/client/src/utils/resources.ts
+++ b/client/src/utils/resources.ts
@@ -19,10 +19,10 @@ export const RESOURCE_CONFIGS: Record = {
defaultEditorRoleId: AccessRoleIds.AGENT_EDITOR,
defaultOwnerRoleId: AccessRoleIds.AGENT_OWNER,
getResourceUrl: (agentId: string) => `${window.location.origin}/c/new?agent_id=${agentId}`,
- getResourceName: (name?: string) => (name && name !== '' ? `"${name}"` : 'agent'),
- getShareMessage: (name?: string) => (name && name !== '' ? `"${name}"` : 'agent'),
+ getResourceName: (name?: string) => (name && name !== '' ? name : 'agent'),
+ getShareMessage: (name?: string) => (name && name !== '' ? name : 'agent'),
getManageMessage: (name?: string) =>
- `Manage permissions for ${name && name !== '' ? `"${name}"` : 'agent'}`,
+ `Manage permissions for ${name && name !== '' ? name : 'agent'}`,
getCopyUrlMessage: () => 'Agent URL copied',
},
[ResourceType.PROMPTGROUP]: {
@@ -30,10 +30,10 @@ export const RESOURCE_CONFIGS: Record = {
defaultViewerRoleId: AccessRoleIds.PROMPTGROUP_VIEWER,
defaultEditorRoleId: AccessRoleIds.PROMPTGROUP_EDITOR,
defaultOwnerRoleId: AccessRoleIds.PROMPTGROUP_OWNER,
- getResourceName: (name?: string) => (name && name !== '' ? `"${name}"` : 'prompt'),
- getShareMessage: (name?: string) => (name && name !== '' ? `"${name}"` : 'prompt'),
+ getResourceName: (name?: string) => (name && name !== '' ? name : 'prompt'),
+ getShareMessage: (name?: string) => (name && name !== '' ? name : 'prompt'),
getManageMessage: (name?: string) =>
- `Manage permissions for ${name && name !== '' ? `"${name}"` : 'prompt'}`,
+ `Manage permissions for ${name && name !== '' ? name : 'prompt'}`,
getCopyUrlMessage: () => 'Prompt URL copied',
},
[ResourceType.MCPSERVER]: {
@@ -41,10 +41,10 @@ export const RESOURCE_CONFIGS: Record = {
defaultViewerRoleId: AccessRoleIds.MCPSERVER_VIEWER,
defaultEditorRoleId: AccessRoleIds.MCPSERVER_EDITOR,
defaultOwnerRoleId: AccessRoleIds.MCPSERVER_OWNER,
- getResourceName: (name?: string) => (name && name !== '' ? `"${name}"` : 'MCP server'),
- getShareMessage: (name?: string) => (name && name !== '' ? `"${name}"` : 'MCP server'),
+ getResourceName: (name?: string) => (name && name !== '' ? name : 'MCP server'),
+ getShareMessage: (name?: string) => (name && name !== '' ? name : 'MCP server'),
getManageMessage: (name?: string) =>
- `Manage permissions for ${name && name !== '' ? `"${name}"` : 'MCP server'}`,
+ `Manage permissions for ${name && name !== '' ? name : 'MCP server'}`,
getCopyUrlMessage: () => 'MCP Server URL copied',
},
[ResourceType.REMOTE_AGENT]: {
diff --git a/client/tailwind.config.cjs b/client/tailwind.config.cjs
index c30d2ca703..624998e9d8 100644
--- a/client/tailwind.config.cjs
+++ b/client/tailwind.config.cjs
@@ -92,6 +92,7 @@ module.exports = {
'text-secondary-alt': 'var(--text-secondary-alt)',
'text-tertiary': 'var(--text-tertiary)',
'text-warning': 'var(--text-warning)',
+ 'text-destructive': 'var(--text-destructive)',
'ring-primary': 'var(--ring-primary)',
'header-primary': 'var(--header-primary)',
'header-hover': 'var(--header-hover)',
@@ -118,6 +119,7 @@ module.exports = {
'border-medium-alt': 'var(--border-medium-alt)',
'border-heavy': 'var(--border-heavy)',
'border-xheavy': 'var(--border-xheavy)',
+ 'border-destructive': 'var(--border-destructive)',
/* These are test styles */
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
diff --git a/packages/client/src/components/OGDialogTemplate.tsx b/packages/client/src/components/OGDialogTemplate.tsx
index 8bf2cea090..300ae5b194 100644
--- a/packages/client/src/components/OGDialogTemplate.tsx
+++ b/packages/client/src/components/OGDialogTemplate.tsx
@@ -1,4 +1,4 @@
-import { forwardRef, ReactNode, Ref } from 'react';
+import { forwardRef, isValidElement, ReactNode, Ref } from 'react';
import {
OGDialogTitle,
OGDialogClose,
@@ -19,13 +19,39 @@ type SelectionProps = {
isLoading?: boolean;
};
+/**
+ * Type guard to check if selection is a legacy SelectionProps object
+ */
+function isSelectionProps(selection: unknown): selection is SelectionProps {
+ return (
+ typeof selection === 'object' &&
+ selection !== null &&
+ !isValidElement(selection) &&
+ ('selectHandler' in selection ||
+ 'selectClasses' in selection ||
+ 'selectText' in selection ||
+ 'isLoading' in selection)
+ );
+}
+
type DialogTemplateProps = {
title: string;
description?: string;
main?: ReactNode;
buttons?: ReactNode;
leftButtons?: ReactNode;
- selection?: SelectionProps;
+ /**
+ * Selection button configuration. Can be either:
+ * - An object with selectHandler, selectClasses, selectText, isLoading (legacy)
+ * - A ReactNode for custom selection component
+ * @example
+ * // Legacy usage
+ * selection={{ selectHandler: () => {}, selectText: 'Confirm' }}
+ * @example
+ * // Custom component
+ * selection={}
+ */
+ selection?: SelectionProps | ReactNode;
className?: string;
overlayClassName?: string;
headerClassName?: string;
@@ -49,14 +75,39 @@ const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref
+ {isLoading === true ? (
+
+ ) : (
+ (selectText as React.JSX.Element)
+ )}
+
+ );
+ } else if (selection) {
+ selectionContent = selection;
+ }
+
return (
{main != null ? main : null}
-
- {leftButtons != null ? (
-
- {leftButtons}
-
- ) : null}
-
-
- {showCancelButton && (
-
-
-
- )}
- {buttons != null ? buttons : null}
- {selection ? (
-
- {isLoading === true ? (
-
- ) : (
- (selectText as React.JSX.Element)
- )}
-
- ) : null}
-
+ {leftButtons != null ? (
+ {leftButtons}
+ ) : null}
+ {showCancelButton && (
+
+
+
+ )}
+ {buttons != null ? buttons : null}
+ {selectionContent}
);
diff --git a/packages/client/src/components/Radio.tsx b/packages/client/src/components/Radio.tsx
index b4c9c21259..2f52387981 100644
--- a/packages/client/src/components/Radio.tsx
+++ b/packages/client/src/components/Radio.tsx
@@ -14,6 +14,7 @@ interface RadioProps {
disabled?: boolean;
className?: string;
fullWidth?: boolean;
+ 'aria-labelledby'?: string;
}
const Radio = memo(function Radio({
@@ -23,6 +24,7 @@ const Radio = memo(function Radio({
disabled = false,
className = '',
fullWidth = false,
+ 'aria-labelledby': ariaLabelledBy,
}: RadioProps) {
const localize = useLocalize();
const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);
@@ -79,6 +81,7 @@ const Radio = memo(function Radio({
{localize('com_ui_no_options')}
@@ -93,6 +96,7 @@ const Radio = memo(function Radio({
{selectedIndex >= 0 && isMounted && (