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 (