From e53619959d1067f6be9c8a70511ee8c54abb1be4 Mon Sep 17 00:00:00 2001 From: Atef Bellaaj Date: Mon, 15 Dec 2025 23:06:13 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=90=20feat:=20MCP=20Server=20Auth=20UX?= =?UTF-8?q?=20with=20Dynamic=20Detection=20&=20Manual=20OAuth=20(#10978)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔐 feat: Improve MCP Server Auth UX with Dynamic Detection & Manual OAuth * 🔧 fix: Update OAuth input autocomplete and refine translation description for clarity --------- Co-authored-by: Atef Bellaaj Co-authored-by: Danny Avila --- .../src/components/SidePanel/Builder/MCP.tsx | 59 -------------- .../components/SidePanel/Builder/MCPAuth.tsx | 55 ------------- .../SidePanel/MCPBuilder/MCPAuth.tsx | 79 +++++++++++++------ client/src/locales/en/translation.json | 4 + 4 files changed, 61 insertions(+), 136 deletions(-) delete mode 100644 client/src/components/SidePanel/Builder/MCP.tsx delete mode 100644 client/src/components/SidePanel/Builder/MCPAuth.tsx diff --git a/client/src/components/SidePanel/Builder/MCP.tsx b/client/src/components/SidePanel/Builder/MCP.tsx deleted file mode 100644 index 64ea37c51b..0000000000 --- a/client/src/components/SidePanel/Builder/MCP.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useState } from 'react'; -import { GearIcon, MCPIcon } from '@librechat/client'; -import type { MCP } from 'librechat-data-provider'; -import { cn } from '~/utils'; - -type MCPProps = { - mcp: MCP; - onClick: () => void; -}; - -export default function MCP({ mcp, onClick }: MCPProps) { - const [isHovering, setIsHovering] = useState(false); - - return ( -
{ - if (e.key === 'Enter' || e.key === ' ') { - onClick(); - } - }} - className="group flex w-full rounded-lg border border-border-medium text-sm hover:cursor-pointer focus:outline-none focus:ring-2 focus:ring-text-primary" - onMouseEnter={() => setIsHovering(true)} - onMouseLeave={() => setIsHovering(false)} - aria-label={`MCP for ${mcp.metadata.name}`} - > -
- {mcp.metadata.icon ? ( - {`${mcp.metadata.name} - ) : ( -
- -
- )} -
- {mcp.metadata.name} -
-
-
-
-
- ); -} diff --git a/client/src/components/SidePanel/Builder/MCPAuth.tsx b/client/src/components/SidePanel/Builder/MCPAuth.tsx deleted file mode 100644 index 2486f84d40..0000000000 --- a/client/src/components/SidePanel/Builder/MCPAuth.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { useEffect } from 'react'; -import { FormProvider, useForm } from 'react-hook-form'; -import { - AuthorizationTypeEnum, - TokenExchangeMethodEnum, - AuthTypeEnum, -} from 'librechat-data-provider'; -import ActionsAuth from '~/components/SidePanel/Builder/ActionsAuth'; - -export default function MCPAuth() { - // Create a separate form for auth - const authMethods = useForm({ - defaultValues: { - /* General */ - type: AuthTypeEnum.None, - saved_auth_fields: false, - /* API key */ - api_key: '', - authorization_type: AuthorizationTypeEnum.Basic, - custom_auth_header: '', - /* OAuth */ - oauth_client_id: '', - oauth_client_secret: '', - authorization_url: '', - client_url: '', - scope: '', - token_exchange_method: TokenExchangeMethodEnum.DefaultPost, - }, - }); - - const { watch, setValue } = authMethods; - const type = watch('type'); - - // Sync form state when auth type changes - useEffect(() => { - if (type === 'none') { - // Reset auth fields when type is none - setValue('api_key', ''); - setValue('authorization_type', AuthorizationTypeEnum.Basic); - setValue('custom_auth_header', ''); - setValue('oauth_client_id', ''); - setValue('oauth_client_secret', ''); - setValue('authorization_url', ''); - setValue('client_url', ''); - setValue('scope', ''); - setValue('token_exchange_method', TokenExchangeMethodEnum.DefaultPost); - } - }, [type, setValue]); - - return ( - - - - ); -} diff --git a/client/src/components/SidePanel/MCPBuilder/MCPAuth.tsx b/client/src/components/SidePanel/MCPBuilder/MCPAuth.tsx index 51f8347145..33fd89e8f3 100644 --- a/client/src/components/SidePanel/MCPBuilder/MCPAuth.tsx +++ b/client/src/components/SidePanel/MCPBuilder/MCPAuth.tsx @@ -50,9 +50,9 @@ function getAuthLocalizationKey(type: AuthTypeEnum): TranslationKeys { case AuthTypeEnum.ServiceHttp: return 'com_ui_api_key'; case AuthTypeEnum.OAuth: - return 'com_ui_oauth'; + return 'com_ui_manual_oauth'; default: - return 'com_ui_none'; + return 'com_ui_auto_detect'; } } @@ -156,12 +156,15 @@ export default function MCPAuth({ style={{ outline: 'none' }} >
-
@@ -190,12 +193,15 @@ export default function MCPAuth({
-
- {authType === AuthTypeEnum.None && null} + {authType === AuthTypeEnum.None && ( +
+

+ {localize('com_ui_auto_detect_description')} +

+
+ )} {authType === AuthTypeEnum.ServiceHttp && } {authType === AuthTypeEnum.OAuth && } @@ -384,8 +396,9 @@ const ApiKey = ({ inputClasses }: { inputClasses: string }) => { const OAuth = ({ inputClasses }: { inputClasses: string }) => { const localize = useLocalize(); const { showToast } = useToastContext(); - const { register, watch } = useFormContext(); + const { register, watch, formState } = useFormContext(); const [isCopying, setIsCopying] = useState(false); + const { errors } = formState; // Check if we're in edit mode (server exists with ID) const serverId = watch('server_id'); @@ -400,26 +413,48 @@ const OAuth = ({ inputClasses }: { inputClasses: string }) => { return ( <> - + + {errors.oauth_client_id && ( + {localize('com_ui_field_required')} + )} + + - + {errors.oauth_client_secret && ( + {localize('com_ui_field_required')} + )} + - - - - + {errors.oauth_authorization_url && ( + {localize('com_ui_field_required')} + )} + + + {errors.oauth_token_url && ( + {localize('com_ui_field_required')} + )} {/* Redirect URI - read-only in edit mode, info message in create mode */} diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 6ab0116f0a..44c04d39c3 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -739,6 +739,8 @@ "com_ui_authentication": "Authentication", "com_ui_authentication_type": "Authentication Type", "com_ui_auto": "Auto", + "com_ui_auto_detect": "Auto Detect", + "com_ui_auto_detect_description": "DCR will be attempted if auth is required. Choose this if your MCP server has no auth requirements or supports DCR.", "com_ui_avatar": "Avatar", "com_ui_azure": "Azure", "com_ui_azure_ad": "Entra ID", @@ -1036,6 +1038,7 @@ "com_ui_latest_footer": "Every AI for Everyone.", "com_ui_latest_production_version": "Latest production version", "com_ui_latest_version": "Latest version", + "com_ui_leave_blank_to_keep": "Leave blank to keep existing", "com_ui_librechat_code_api_key": "Get your LibreChat Code Interpreter API key", "com_ui_librechat_code_api_subtitle": "Secure. Multi-language. Input/Output Files.", "com_ui_librechat_code_api_title": "Run AI Code", @@ -1046,6 +1049,7 @@ "com_ui_logo": "{{0}} Logo", "com_ui_low": "Low", "com_ui_manage": "Manage", + "com_ui_manual_oauth": "Manual OAuth", "com_ui_marketplace": "Marketplace", "com_ui_marketplace_allow_use": "Allow using Marketplace", "com_ui_max_favorites_reached": "Maximum pinned items reached ({{0}}). Unpin an item to add more.",