mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
✨ feat: Configurable MCP Dropdown Placeholder (#7988)
* new env variable for mcp label * 🔄 refactor: Update MCPSelect placeholderText to draw from interface section of librechat.yaml rather than .env * 🧹 chore: extract mcpServers schema for better maintainability * 🔄 refactor: Update MCPSelect and useMCPSelect to utilize TPlugin type for better type consistency * 🔄 refactor: Pass placeholder from startupConfig to MCPSubMenu for improved localization * 🔄 refactor: Integrate startupConfig into BadgeRowContext and related components for enhanced configuration management --------- Co-authored-by: mwbrandao <mariana.brandao@nos.pt> Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
a058963a9f
commit
2b2f7fe289
9 changed files with 46 additions and 21 deletions
|
|
@ -41,6 +41,7 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol
|
||||||
sidePanel: interfaceConfig?.sidePanel ?? defaults.sidePanel,
|
sidePanel: interfaceConfig?.sidePanel ?? defaults.sidePanel,
|
||||||
privacyPolicy: interfaceConfig?.privacyPolicy ?? defaults.privacyPolicy,
|
privacyPolicy: interfaceConfig?.privacyPolicy ?? defaults.privacyPolicy,
|
||||||
termsOfService: interfaceConfig?.termsOfService ?? defaults.termsOfService,
|
termsOfService: interfaceConfig?.termsOfService ?? defaults.termsOfService,
|
||||||
|
mcpServers: interfaceConfig?.mcpServers ?? defaults.mcpServers,
|
||||||
bookmarks: interfaceConfig?.bookmarks ?? defaults.bookmarks,
|
bookmarks: interfaceConfig?.bookmarks ?? defaults.bookmarks,
|
||||||
memories: shouldDisableMemories ? false : (interfaceConfig?.memories ?? defaults.memories),
|
memories: shouldDisableMemories ? false : (interfaceConfig?.memories ?? defaults.memories),
|
||||||
prompts: interfaceConfig?.prompts ?? defaults.prompts,
|
prompts: interfaceConfig?.prompts ?? defaults.prompts,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { createContext, useContext } from 'react';
|
import React, { createContext, useContext } from 'react';
|
||||||
import { Tools, LocalStorageKeys } from 'librechat-data-provider';
|
import { Tools, LocalStorageKeys } from 'librechat-data-provider';
|
||||||
import { useMCPSelect, useToolToggle, useCodeApiKeyForm, useSearchApiKeyForm } from '~/hooks';
|
import { useMCPSelect, useToolToggle, useCodeApiKeyForm, useSearchApiKeyForm } from '~/hooks';
|
||||||
|
import { useGetStartupConfig } from '~/data-provider';
|
||||||
|
|
||||||
interface BadgeRowContextType {
|
interface BadgeRowContextType {
|
||||||
conversationId?: string | null;
|
conversationId?: string | null;
|
||||||
|
|
@ -10,6 +11,7 @@ interface BadgeRowContextType {
|
||||||
fileSearch: ReturnType<typeof useToolToggle>;
|
fileSearch: ReturnType<typeof useToolToggle>;
|
||||||
codeApiKeyForm: ReturnType<typeof useCodeApiKeyForm>;
|
codeApiKeyForm: ReturnType<typeof useCodeApiKeyForm>;
|
||||||
searchApiKeyForm: ReturnType<typeof useSearchApiKeyForm>;
|
searchApiKeyForm: ReturnType<typeof useSearchApiKeyForm>;
|
||||||
|
startupConfig: ReturnType<typeof useGetStartupConfig>['data'];
|
||||||
}
|
}
|
||||||
|
|
||||||
const BadgeRowContext = createContext<BadgeRowContextType | undefined>(undefined);
|
const BadgeRowContext = createContext<BadgeRowContextType | undefined>(undefined);
|
||||||
|
|
@ -28,6 +30,9 @@ interface BadgeRowProviderProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BadgeRowProvider({ children, conversationId }: BadgeRowProviderProps) {
|
export default function BadgeRowProvider({ children, conversationId }: BadgeRowProviderProps) {
|
||||||
|
/** Startup config */
|
||||||
|
const { data: startupConfig } = useGetStartupConfig();
|
||||||
|
|
||||||
/** MCPSelect hook */
|
/** MCPSelect hook */
|
||||||
const mcpSelect = useMCPSelect({ conversationId });
|
const mcpSelect = useMCPSelect({ conversationId });
|
||||||
|
|
||||||
|
|
@ -73,6 +78,7 @@ export default function BadgeRowProvider({ children, conversationId }: BadgeRowP
|
||||||
mcpSelect,
|
mcpSelect,
|
||||||
webSearch,
|
webSearch,
|
||||||
fileSearch,
|
fileSearch,
|
||||||
|
startupConfig,
|
||||||
conversationId,
|
conversationId,
|
||||||
codeApiKeyForm,
|
codeApiKeyForm,
|
||||||
codeInterpreter,
|
codeInterpreter,
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,7 @@ import React, { memo, useCallback, useState } from 'react';
|
||||||
import { SettingsIcon } from 'lucide-react';
|
import { SettingsIcon } from 'lucide-react';
|
||||||
import { Constants } from 'librechat-data-provider';
|
import { Constants } from 'librechat-data-provider';
|
||||||
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
|
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
|
||||||
import type { TUpdateUserPlugins } from 'librechat-data-provider';
|
import type { TUpdateUserPlugins, TPlugin } from 'librechat-data-provider';
|
||||||
import type { McpServerInfo } from '~/hooks/Plugins/useMCPSelect';
|
|
||||||
import MCPConfigDialog, { type ConfigFieldDetail } from '~/components/ui/MCPConfigDialog';
|
import MCPConfigDialog, { type ConfigFieldDetail } from '~/components/ui/MCPConfigDialog';
|
||||||
import { useToastContext, useBadgeRowContext } from '~/Providers';
|
import { useToastContext, useBadgeRowContext } from '~/Providers';
|
||||||
import MultiSelect from '~/components/ui/MultiSelect';
|
import MultiSelect from '~/components/ui/MultiSelect';
|
||||||
|
|
@ -18,11 +17,11 @@ const getBaseMCPPluginKey = (fullPluginKey: string): string => {
|
||||||
function MCPSelect() {
|
function MCPSelect() {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { showToast } = useToastContext();
|
const { showToast } = useToastContext();
|
||||||
const { mcpSelect } = useBadgeRowContext();
|
const { mcpSelect, startupConfig } = useBadgeRowContext();
|
||||||
const { mcpValues, setMCPValues, mcpServerNames, mcpToolDetails, isPinned } = mcpSelect;
|
const { mcpValues, setMCPValues, mcpServerNames, mcpToolDetails, isPinned } = mcpSelect;
|
||||||
|
|
||||||
const [isConfigModalOpen, setIsConfigModalOpen] = useState(false);
|
const [isConfigModalOpen, setIsConfigModalOpen] = useState(false);
|
||||||
const [selectedToolForConfig, setSelectedToolForConfig] = useState<McpServerInfo | null>(null);
|
const [selectedToolForConfig, setSelectedToolForConfig] = useState<TPlugin | null>(null);
|
||||||
|
|
||||||
const updateUserPluginsMutation = useUpdateUserPluginsMutation({
|
const updateUserPluginsMutation = useUpdateUserPluginsMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|
@ -129,6 +128,8 @@ function MCPSelect() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const placeholderText =
|
||||||
|
startupConfig?.interface?.mcpServers?.placeholder || localize('com_ui_mcp_servers');
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
|
|
@ -138,7 +139,7 @@ function MCPSelect() {
|
||||||
defaultSelectedValues={mcpValues ?? []}
|
defaultSelectedValues={mcpValues ?? []}
|
||||||
renderSelectedValues={renderSelectedValues}
|
renderSelectedValues={renderSelectedValues}
|
||||||
renderItemContent={renderItemContent}
|
renderItemContent={renderItemContent}
|
||||||
placeholder={localize('com_ui_mcp_servers')}
|
placeholder={placeholderText}
|
||||||
popoverClassName="min-w-fit"
|
popoverClassName="min-w-fit"
|
||||||
className="badge-icon min-w-fit"
|
className="badge-icon min-w-fit"
|
||||||
selectIcon={<MCPIcon className="icon-md text-text-primary" />}
|
selectIcon={<MCPIcon className="icon-md text-text-primary" />}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ interface MCPSubMenuProps {
|
||||||
mcpValues?: string[];
|
mcpValues?: string[];
|
||||||
mcpServerNames: string[];
|
mcpServerNames: string[];
|
||||||
handleMCPToggle: (serverName: string) => void;
|
handleMCPToggle: (serverName: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MCPSubMenu = ({
|
const MCPSubMenu = ({
|
||||||
|
|
@ -19,6 +20,7 @@ const MCPSubMenu = ({
|
||||||
mcpServerNames,
|
mcpServerNames,
|
||||||
setIsMCPPinned,
|
setIsMCPPinned,
|
||||||
handleMCPToggle,
|
handleMCPToggle,
|
||||||
|
placeholder,
|
||||||
...props
|
...props
|
||||||
}: MCPSubMenuProps) => {
|
}: MCPSubMenuProps) => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
@ -38,7 +40,7 @@ const MCPSubMenu = ({
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<MCPIcon className="icon-md" />
|
<MCPIcon className="icon-md" />
|
||||||
<span>{localize('com_ui_mcp_servers')}</span>
|
<span>{placeholder || localize('com_ui_mcp_servers')}</span>
|
||||||
<ChevronRight className="ml-auto h-3 w-3" />
|
<ChevronRight className="ml-auto h-3 w-3" />
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,15 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const isDisabled = disabled ?? false;
|
const isDisabled = disabled ?? false;
|
||||||
const [isPopoverActive, setIsPopoverActive] = useState(false);
|
const [isPopoverActive, setIsPopoverActive] = useState(false);
|
||||||
const { webSearch, codeInterpreter, fileSearch, mcpSelect, searchApiKeyForm, codeApiKeyForm } =
|
const {
|
||||||
useBadgeRowContext();
|
webSearch,
|
||||||
|
mcpSelect,
|
||||||
|
fileSearch,
|
||||||
|
startupConfig,
|
||||||
|
codeApiKeyForm,
|
||||||
|
codeInterpreter,
|
||||||
|
searchApiKeyForm,
|
||||||
|
} = useBadgeRowContext();
|
||||||
const { setIsDialogOpen: setIsCodeDialogOpen, menuTriggerRef: codeMenuTriggerRef } =
|
const { setIsDialogOpen: setIsCodeDialogOpen, menuTriggerRef: codeMenuTriggerRef } =
|
||||||
codeApiKeyForm;
|
codeApiKeyForm;
|
||||||
const { setIsDialogOpen: setIsSearchDialogOpen, menuTriggerRef: searchMenuTriggerRef } =
|
const { setIsDialogOpen: setIsSearchDialogOpen, menuTriggerRef: searchMenuTriggerRef } =
|
||||||
|
|
@ -89,6 +96,8 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
|
||||||
[mcpSelect],
|
[mcpSelect],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const mcpPlaceholder = startupConfig?.interface?.mcpServers?.placeholder;
|
||||||
|
|
||||||
const dropdownItems = useMemo(() => {
|
const dropdownItems = useMemo(() => {
|
||||||
const items: MenuItemProps[] = [
|
const items: MenuItemProps[] = [
|
||||||
{
|
{
|
||||||
|
|
@ -246,8 +255,9 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
|
||||||
<MCPSubMenu
|
<MCPSubMenu
|
||||||
{...props}
|
{...props}
|
||||||
mcpValues={mcpValues}
|
mcpValues={mcpValues}
|
||||||
mcpServerNames={mcpServerNames}
|
|
||||||
isMCPPinned={isMCPPinned}
|
isMCPPinned={isMCPPinned}
|
||||||
|
placeholder={mcpPlaceholder}
|
||||||
|
mcpServerNames={mcpServerNames}
|
||||||
setIsMCPPinned={setIsMCPPinned}
|
setIsMCPPinned={setIsMCPPinned}
|
||||||
handleMCPToggle={handleMCPToggle}
|
handleMCPToggle={handleMCPToggle}
|
||||||
/>
|
/>
|
||||||
|
|
@ -262,6 +272,7 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
|
||||||
canRunCode,
|
canRunCode,
|
||||||
isMCPPinned,
|
isMCPPinned,
|
||||||
isCodePinned,
|
isCodePinned,
|
||||||
|
mcpPlaceholder,
|
||||||
mcpServerNames,
|
mcpServerNames,
|
||||||
isSearchPinned,
|
isSearchPinned,
|
||||||
setIsMCPPinned,
|
setIsMCPPinned,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useRef, useEffect, useCallback, useMemo } from 'react';
|
import { useRef, useEffect, useCallback, useMemo } from 'react';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
import { Constants, LocalStorageKeys, EModelEndpoint } from 'librechat-data-provider';
|
import { Constants, LocalStorageKeys, EModelEndpoint } from 'librechat-data-provider';
|
||||||
import type { TPlugin, TPluginAuthConfig } from 'librechat-data-provider';
|
import type { TPlugin } from 'librechat-data-provider';
|
||||||
import { useAvailableToolsQuery } from '~/data-provider';
|
import { useAvailableToolsQuery } from '~/data-provider';
|
||||||
import useLocalStorage from '~/hooks/useLocalStorageAlt';
|
import useLocalStorage from '~/hooks/useLocalStorageAlt';
|
||||||
import { ephemeralAgentByConvoId } from '~/store';
|
import { ephemeralAgentByConvoId } from '~/store';
|
||||||
|
|
@ -24,20 +24,13 @@ interface UseMCPSelectOptions {
|
||||||
conversationId?: string | null;
|
conversationId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface McpServerInfo {
|
|
||||||
name: string;
|
|
||||||
pluginKey: string;
|
|
||||||
authConfig?: TPluginAuthConfig[];
|
|
||||||
authenticated?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useMCPSelect({ conversationId }: UseMCPSelectOptions) {
|
export function useMCPSelect({ conversationId }: UseMCPSelectOptions) {
|
||||||
const key = conversationId ?? Constants.NEW_CONVO;
|
const key = conversationId ?? Constants.NEW_CONVO;
|
||||||
const hasSetFetched = useRef<string | null>(null);
|
const hasSetFetched = useRef<string | null>(null);
|
||||||
const [ephemeralAgent, setEphemeralAgent] = useRecoilState(ephemeralAgentByConvoId(key));
|
const [ephemeralAgent, setEphemeralAgent] = useRecoilState(ephemeralAgentByConvoId(key));
|
||||||
const { data: mcpToolDetails, isFetched } = useAvailableToolsQuery(EModelEndpoint.agents, {
|
const { data: mcpToolDetails, isFetched } = useAvailableToolsQuery(EModelEndpoint.agents, {
|
||||||
select: (data: TPlugin[]) => {
|
select: (data: TPlugin[]) => {
|
||||||
const mcpToolsMap = new Map<string, McpServerInfo>();
|
const mcpToolsMap = new Map<string, TPlugin>();
|
||||||
data.forEach((tool) => {
|
data.forEach((tool) => {
|
||||||
const isMCP = tool.pluginKey.includes(Constants.mcp_delimiter);
|
const isMCP = tool.pluginKey.includes(Constants.mcp_delimiter);
|
||||||
if (isMCP && tool.chatMenu !== false) {
|
if (isMCP && tool.chatMenu !== false) {
|
||||||
|
|
@ -109,13 +102,13 @@ export function useMCPSelect({ conversationId }: UseMCPSelectOptions) {
|
||||||
}, [mcpToolDetails]);
|
}, [mcpToolDetails]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
isPinned,
|
||||||
mcpValues,
|
mcpValues,
|
||||||
|
setIsPinned,
|
||||||
setMCPValues,
|
setMCPValues,
|
||||||
mcpServerNames,
|
mcpServerNames,
|
||||||
ephemeralAgent,
|
ephemeralAgent,
|
||||||
mcpToolDetails,
|
mcpToolDetails,
|
||||||
setEphemeralAgent,
|
setEphemeralAgent,
|
||||||
isPinned,
|
|
||||||
setIsPinned,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@ cache: true
|
||||||
# Custom interface configuration
|
# Custom interface configuration
|
||||||
interface:
|
interface:
|
||||||
customWelcome: "Welcome to LibreChat! Enjoy your experience."
|
customWelcome: "Welcome to LibreChat! Enjoy your experience."
|
||||||
|
# MCP Servers UI configuration
|
||||||
|
mcpServers:
|
||||||
|
placeholder: 'MCP Servers'
|
||||||
# Privacy policy settings
|
# Privacy policy settings
|
||||||
privacyPolicy:
|
privacyPolicy:
|
||||||
externalUrl: 'https://librechat.ai/privacy-policy'
|
externalUrl: 'https://librechat.ai/privacy-policy'
|
||||||
|
|
|
||||||
|
|
@ -482,6 +482,12 @@ const termsOfServiceSchema = z.object({
|
||||||
|
|
||||||
export type TTermsOfService = z.infer<typeof termsOfServiceSchema>;
|
export type TTermsOfService = z.infer<typeof termsOfServiceSchema>;
|
||||||
|
|
||||||
|
const mcpServersSchema = z.object({
|
||||||
|
placeholder: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TMcpServersConfig = z.infer<typeof mcpServersSchema>;
|
||||||
|
|
||||||
export const intefaceSchema = z
|
export const intefaceSchema = z
|
||||||
.object({
|
.object({
|
||||||
privacyPolicy: z
|
privacyPolicy: z
|
||||||
|
|
@ -492,6 +498,7 @@ export const intefaceSchema = z
|
||||||
.optional(),
|
.optional(),
|
||||||
termsOfService: termsOfServiceSchema.optional(),
|
termsOfService: termsOfServiceSchema.optional(),
|
||||||
customWelcome: z.string().optional(),
|
customWelcome: z.string().optional(),
|
||||||
|
mcpServers: mcpServersSchema.optional(),
|
||||||
endpointsMenu: z.boolean().optional(),
|
endpointsMenu: z.boolean().optional(),
|
||||||
modelSelect: z.boolean().optional(),
|
modelSelect: z.boolean().optional(),
|
||||||
parameters: z.boolean().optional(),
|
parameters: z.boolean().optional(),
|
||||||
|
|
@ -600,6 +607,7 @@ export type TStartupConfig = {
|
||||||
>;
|
>;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
mcpPlaceholder?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum OCRStrategy {
|
export enum OCRStrategy {
|
||||||
|
|
|
||||||
|
|
@ -400,7 +400,7 @@ export type TPluginAuthConfig = z.infer<typeof tPluginAuthConfigSchema>;
|
||||||
export const tPluginSchema = z.object({
|
export const tPluginSchema = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
pluginKey: z.string(),
|
pluginKey: z.string(),
|
||||||
description: z.string(),
|
description: z.string().optional(),
|
||||||
icon: z.string().optional(),
|
icon: z.string().optional(),
|
||||||
authConfig: z.array(tPluginAuthConfigSchema).optional(),
|
authConfig: z.array(tPluginAuthConfigSchema).optional(),
|
||||||
authenticated: z.boolean().optional(),
|
authenticated: z.boolean().optional(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue