mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
🔐 feat: MCP Server Auth UX with Dynamic Detection & Manual OAuth (#10978)
* 🔐 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 <slalom.bellaaj@external.daimlertruck.com> Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
03ced7a894
commit
e53619959d
4 changed files with 61 additions and 136 deletions
|
|
@ -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 (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={onClick}
|
||||
onKeyDown={(e) => {
|
||||
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}`}
|
||||
>
|
||||
<div className="flex h-9 items-center gap-2 px-3">
|
||||
{mcp.metadata.icon ? (
|
||||
<img
|
||||
src={mcp.metadata.icon}
|
||||
alt={`${mcp.metadata.name} icon`}
|
||||
className="h-6 w-6 rounded-md object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-md bg-surface-secondary">
|
||||
<MCPIcon />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="grow overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
style={{ wordBreak: 'break-all' }}
|
||||
>
|
||||
{mcp.metadata.name}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'ml-auto h-9 w-9 min-w-9 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-tertiary focus:outline-none focus:ring-2 focus:ring-text-primary group-focus:flex',
|
||||
isHovering ? 'flex' : 'hidden',
|
||||
)}
|
||||
aria-label="Settings"
|
||||
>
|
||||
<GearIcon className="icon-sm" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<FormProvider {...authMethods}>
|
||||
<ActionsAuth />
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
|
|
@ -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' }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor="auth-none" className="flex cursor-pointer items-center gap-1">
|
||||
<label
|
||||
htmlFor="auth-auto-detect"
|
||||
className="flex cursor-pointer items-center gap-1"
|
||||
>
|
||||
<RadioGroup.Item
|
||||
type="button"
|
||||
role="radio"
|
||||
value={AuthTypeEnum.None}
|
||||
id="auth-none"
|
||||
id="auth-auto-detect"
|
||||
className={cn(
|
||||
'mr-1 flex h-5 w-5 items-center justify-center rounded-full border',
|
||||
'border-border-heavy bg-surface-primary',
|
||||
|
|
@ -169,7 +172,7 @@ export default function MCPAuth({
|
|||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-text-primary" />
|
||||
</RadioGroup.Item>
|
||||
{localize('com_ui_none')}
|
||||
{localize('com_ui_auto_detect')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -190,12 +193,15 @@ export default function MCPAuth({
|
|||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor="auth-oauth" className="flex cursor-pointer items-center gap-1">
|
||||
<label
|
||||
htmlFor="auth-manual-oauth"
|
||||
className="flex cursor-pointer items-center gap-1"
|
||||
>
|
||||
<RadioGroup.Item
|
||||
type="button"
|
||||
role="radio"
|
||||
value={AuthTypeEnum.OAuth}
|
||||
id="auth-oauth"
|
||||
id="auth-manual-oauth"
|
||||
className={cn(
|
||||
'mr-1 flex h-5 w-5 items-center justify-center rounded-full border',
|
||||
'border-border-heavy bg-surface-primary',
|
||||
|
|
@ -203,12 +209,18 @@ export default function MCPAuth({
|
|||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-text-primary" />
|
||||
</RadioGroup.Item>
|
||||
{localize('com_ui_oauth')}
|
||||
{localize('com_ui_manual_oauth')}
|
||||
</label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
</div>
|
||||
{authType === AuthTypeEnum.None && null}
|
||||
{authType === AuthTypeEnum.None && (
|
||||
<div className="rounded-lg border border-border-medium bg-surface-secondary p-3">
|
||||
<p className="text-sm text-text-secondary">
|
||||
{localize('com_ui_auto_detect_description')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{authType === AuthTypeEnum.ServiceHttp && <ApiKey inputClasses={inputClasses} />}
|
||||
{authType === AuthTypeEnum.OAuth && <OAuth inputClasses={inputClasses} />}
|
||||
</div>
|
||||
|
|
@ -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 (
|
||||
<>
|
||||
<label className="mb-1 block text-sm font-medium">{localize('com_ui_client_id')}</label>
|
||||
<label className="mb-1 block text-sm font-medium">
|
||||
{localize('com_ui_client_id')} {!isEditMode && <span className="text-red-500">*</span>}
|
||||
</label>
|
||||
<input
|
||||
placeholder="<HIDDEN>"
|
||||
placeholder={isEditMode ? localize('com_ui_leave_blank_to_keep') : ''}
|
||||
autoComplete="off"
|
||||
className={inputClasses}
|
||||
{...register('oauth_client_id', { required: !isEditMode })}
|
||||
/>
|
||||
{errors.oauth_client_id && (
|
||||
<span className="text-xs text-red-500">{localize('com_ui_field_required')}</span>
|
||||
)}
|
||||
<label className="mb-1 block text-sm font-medium">
|
||||
{localize('com_ui_client_secret')} {!isEditMode && <span className="text-red-500">*</span>}
|
||||
</label>
|
||||
<input
|
||||
placeholder={isEditMode ? localize('com_ui_leave_blank_to_keep') : ''}
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
className={inputClasses}
|
||||
{...register('oauth_client_id')}
|
||||
{...register('oauth_client_secret', { required: !isEditMode })}
|
||||
/>
|
||||
<label className="mb-1 block text-sm font-medium">{localize('com_ui_client_secret')}</label>
|
||||
{errors.oauth_client_secret && (
|
||||
<span className="text-xs text-red-500">{localize('com_ui_field_required')}</span>
|
||||
)}
|
||||
<label className="mb-1 block text-sm font-medium">
|
||||
{localize('com_ui_auth_url')} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
placeholder="<HIDDEN>"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
className={inputClasses}
|
||||
{...register('oauth_client_secret')}
|
||||
{...register('oauth_authorization_url', { required: true })}
|
||||
/>
|
||||
<label className="mb-1 block text-sm font-medium">{localize('com_ui_auth_url')}</label>
|
||||
<input className={inputClasses} {...register('oauth_authorization_url')} />
|
||||
<label className="mb-1 block text-sm font-medium">{localize('com_ui_token_url')}</label>
|
||||
<input className={inputClasses} {...register('oauth_token_url')} />
|
||||
{errors.oauth_authorization_url && (
|
||||
<span className="text-xs text-red-500">{localize('com_ui_field_required')}</span>
|
||||
)}
|
||||
<label className="mb-1 block text-sm font-medium">
|
||||
{localize('com_ui_token_url')} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input className={inputClasses} {...register('oauth_token_url', { required: true })} />
|
||||
{errors.oauth_token_url && (
|
||||
<span className="text-xs text-red-500">{localize('com_ui_field_required')}</span>
|
||||
)}
|
||||
|
||||
{/* Redirect URI - read-only in edit mode, info message in create mode */}
|
||||
<label className="mb-1 block text-sm font-medium">{localize('com_ui_redirect_uri')}</label>
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue