From 6474c8456d7939c20279858f7f8f7359b48a12ad Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Sat, 30 May 2026 22:41:00 +0200 Subject: [PATCH] feat: add optional sensitive flag to MCP customUserVars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dynamic MCP credential fields all rendered as masked SecretInputs, which also hid non-secret setup values like usernames, project keys, and URLs. Add an optional `sensitive` flag to customUserVars and the plugin auth config. It defaults to masked when omitted, so existing configs keep the safe-by-default behavior; set `sensitive: false` to render a field as plain text. The flag is display-only — values remain encrypted at rest. --- api/server/controllers/mcp.js | 1 + client/src/common/types.ts | 2 + .../components/Chat/Input/MCPConfigDialog.tsx | 42 ++++++++---- .../components/MCP/CustomUserVarsSection.tsx | 51 ++++++++------- .../__tests__/CustomUserVarsSection.test.tsx | 17 +++++ .../Plugins/Store/PluginAuthForm.tsx | 65 ++++++++++--------- .../Store/__tests__/PluginAuthForm.spec.tsx | 21 ++++++ client/src/hooks/MCP/useMCPServerManager.ts | 2 + packages/data-provider/src/mcp.ts | 6 ++ packages/data-provider/src/schemas.ts | 2 + 10 files changed, 143 insertions(+), 66 deletions(-) diff --git a/api/server/controllers/mcp.js b/api/server/controllers/mcp.js index a50680f3d1..d58e1feacf 100644 --- a/api/server/controllers/mcp.js +++ b/api/server/controllers/mcp.js @@ -146,6 +146,7 @@ const getMCPTools = async (req, res) => { authField: key, label: value.title || key, description: value.description || '', + sensitive: value.sensitive, })); server.authenticated = false; } diff --git a/client/src/common/types.ts b/client/src/common/types.ts index d8f86f23f4..7be7bef749 100644 --- a/client/src/common/types.ts +++ b/client/src/common/types.ts @@ -16,6 +16,8 @@ export function isEphemeralAgent(agentId: string | null | undefined): boolean { export interface ConfigFieldDetail { title: string; description: string; + /** Whether the field holds a secret and should be masked (defaults to masked when omitted). */ + sensitive?: boolean; } export type CodeBarProps = { diff --git a/client/src/components/Chat/Input/MCPConfigDialog.tsx b/client/src/components/Chat/Input/MCPConfigDialog.tsx index c34685431c..58fcf4b636 100644 --- a/client/src/components/Chat/Input/MCPConfigDialog.tsx +++ b/client/src/components/Chat/Input/MCPConfigDialog.tsx @@ -1,7 +1,7 @@ import DOMPurify from 'dompurify'; import React, { useEffect, useMemo } from 'react'; import { useForm, Controller } from 'react-hook-form'; -import { Button, Label, SecretInput, OGDialog, OGDialogTemplate } from '@librechat/client'; +import { Button, Input, Label, SecretInput, OGDialog, OGDialogTemplate } from '@librechat/client'; import type { ConfigFieldDetail } from '~/common'; import { useLocalize } from '~/hooks'; @@ -90,18 +90,34 @@ export default function MCPConfigDialog({ name={key} control={control} defaultValue={initialValues[key] || ''} - render={({ field }) => ( - - )} + render={({ field }) => { + const placeholder = localize('com_ui_mcp_enter_var', { 0: details.title }); + const className = + 'w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white sm:text-sm'; + if (details.sensitive === false) { + return ( + + ); + } + return ( + + ); + }} /> {details.description && (

( - - )} + render={({ field }) => { + const placeholder = hasValue + ? localize('com_ui_mcp_update_var', { 0: config.title }) + : localize('com_ui_mcp_enter_var', { 0: config.title }); + const className = + 'w-full rounded border border-border-medium bg-transparent px-2 py-1 text-text-primary placeholder:text-text-secondary focus:outline-none sm:text-sm'; + // Prevent autofill: browser DOM mutations bypass React's synthetic + // onChange, silently emptying react-hook-form state on submit. + const sharedProps = { + id: name, + 'data-lpignore': 'true', + 'data-1p-ignore': 'true', + /* autoFocus is generally disorienting, but here the required field is navigated to + * anyway, and the section emulates a modal opening where users expect focus to shift. */ + autoFocus, + ...field, + placeholder, + className, + }; + if (config.sensitive === false) { + return ; + } + return ; + }} /> {sanitizedDescription && (

{ expect(input).toHaveAttribute('data-lpignore', 'true'); expect(input).toHaveAttribute('data-1p-ignore', 'true'); }); + + it('renders non-sensitive fields as unmasked text while keeping secrets masked', () => { + render( + , + ); + + expect(screen.getByLabelText(/My API Key/)).toHaveAttribute('type', 'password'); + expect(screen.getByLabelText(/Project Key/)).toHaveAttribute('type', 'text'); + }); }); diff --git a/client/src/components/Plugins/Store/PluginAuthForm.tsx b/client/src/components/Plugins/Store/PluginAuthForm.tsx index 28c433200b..effc3643c3 100644 --- a/client/src/components/Plugins/Store/PluginAuthForm.tsx +++ b/client/src/components/Plugins/Store/PluginAuthForm.tsx @@ -40,6 +40,31 @@ function PluginAuthForm({ plugin, onSubmit, isEntityTool }: TPluginAuthFormProps {authConfig.map((config: TPluginAuthConfig, i: number) => { const authField = config.authField.split('||')[0]; const isOptional = config.optional === true; + const inputClassName = + 'flex h-10 max-h-10 w-full resize-none rounded-md border border-gray-200 bg-transparent px-3 py-2 text-sm text-gray-700 shadow-[0_0_10px_rgba(0,0,0,0.05)] outline-none placeholder:text-gray-400 focus:border-gray-400 focus:bg-gray-50 focus:outline-none focus:ring-0 focus:ring-gray-400 focus:ring-opacity-0 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-500 dark:bg-gray-700 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-400 focus:dark:bg-gray-600 dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0'; + const sharedProps = { + id: authField, + 'aria-invalid': !!errors[authField], + 'aria-describedby': `${authField}-error`, + 'aria-label': config.label, + 'aria-required': !isOptional, + /* autoFocus is generally disorienting, but here the required field must be navigated to + * anyway, and the form emulates a modal opening where users expect focus to shift. */ + autoFocus: i === 0, + className: inputClassName, + ...register( + authField, + isOptional + ? {} + : { + required: `${config.label} is required.`, + minLength: { + value: 1, + message: `${config.label} must be at least 1 character long`, + }, + }, + ), + }; return (