🔧 fix: MCP Selection Persist and UI Flicker Issues (#9324)

* refactor: useMCPSelect

    - Add useGetMCPTools to use in useMCPSelect and elsewhere hooks for fetching MCP tools
    - remove memoized key
    - remove use of `useChatContext` and require conversationId as prop

* feat: Add MCPPanelContext and integrate conversationId as prop for useMCPSelect across components

- Introduced MCPPanelContext to manage conversationId state.
- Updated MCPSelect, MCPSubMenu, and MCPConfigDialog to accept conversationId as a prop.
- Modified ToolsDropdown and BadgeRow to pass conversationId to relevant components.
- Refactored MCPPanel to utilize MCPPanelProvider for context management.

* fix: remove nested ternary in ServerInitializationSection

- Replaced conditional operator with if-else statements for better readability in determining button text based on server initialization state and reinitialization status.

* refactor: wrap setValueWrap in useCallback for performance optimization

* refactor: streamline useMCPSelect by consolidating storageKey definition

* fix: prevent clearing selections on page refresh by tracking initial load completion

* refactor: simplify concern of useMCPSelect hook

* refactor: move ConfigFieldDetail interface to common types for better reusability, isolate usage of `useGetMCPTools`

* refactor: integrate mcpServerNames into BadgeRowContext and update ToolsDropdown and MCPSelect components
This commit is contained in:
Danny Avila 2025-08-28 00:44:49 -04:00 committed by GitHub
parent 2483623c88
commit c0511b9a5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 270 additions and 206 deletions

View file

@ -368,7 +368,7 @@ function BadgeRow({
<CodeInterpreter />
<FileSearch />
<Artifacts />
<MCPSelect />
<MCPSelect conversationId={conversationId} />
</>
)}
{ghostBadge && (

View file

@ -1,13 +1,9 @@
import React, { useEffect } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { Button, Input, Label, OGDialog, OGDialogTemplate } from '@librechat/client';
import type { ConfigFieldDetail } from '~/common';
import { useLocalize } from '~/hooks';
export interface ConfigFieldDetail {
title: string;
description: string;
}
interface MCPConfigDialogProps {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
@ -34,7 +30,7 @@ export default function MCPConfigDialog({
control,
handleSubmit,
reset,
formState: { errors, _ },
formState: { errors },
} = useForm<Record<string, string>>({
defaultValues: initialValues,
});
@ -56,14 +52,12 @@ export default function MCPConfigDialog({
};
const dialogTitle = localize('com_ui_configure_mcp_variables_for', { 0: serverName });
const dialogDescription = localize('com_ui_mcp_dialog_desc');
return (
<OGDialog open={isOpen} onOpenChange={onOpenChange}>
<OGDialogTemplate
className="sm:max-w-lg"
title={dialogTitle}
description={dialogDescription}
headerClassName="px-6 pt-6 pb-4"
main={
<form onSubmit={handleSubmit(onFormSubmit)} className="space-y-4 px-6 pb-2">

View file

@ -3,8 +3,11 @@ import { MultiSelect, MCPIcon } from '@librechat/client';
import MCPServerStatusIcon from '~/components/MCP/MCPServerStatusIcon';
import { useMCPServerManager } from '~/hooks/MCP/useMCPServerManager';
import MCPConfigDialog from '~/components/MCP/MCPConfigDialog';
import { useBadgeRowContext } from '~/Providers';
function MCPSelect() {
type MCPSelectProps = { conversationId?: string | null };
function MCPSelectContent({ conversationId }: MCPSelectProps) {
const {
configuredServers,
mcpValues,
@ -15,7 +18,7 @@ function MCPSelect() {
getConfigDialogProps,
isInitializing,
localize,
} = useMCPServerManager();
} = useMCPServerManager({ conversationId });
const renderSelectedValues = useCallback(
(values: string[], placeholder?: string) => {
@ -93,9 +96,17 @@ function MCPSelect() {
selectItemsClassName="border border-blue-600/50 bg-blue-500/10 hover:bg-blue-700/10"
selectClassName="group relative inline-flex items-center justify-center md:justify-start gap-1.5 rounded-full border border-border-medium text-sm font-medium transition-all md:w-full size-9 p-2 md:p-3 bg-transparent shadow-sm hover:bg-surface-hover hover:shadow-md active:shadow-inner"
/>
{configDialogProps && <MCPConfigDialog {...configDialogProps} />}
{configDialogProps && (
<MCPConfigDialog {...configDialogProps} conversationId={conversationId} />
)}
</>
);
}
function MCPSelect(props: MCPSelectProps) {
const { mcpServerNames } = useBadgeRowContext();
if ((mcpServerNames?.length ?? 0) === 0) return null;
return <MCPSelectContent {...props} />;
}
export default memo(MCPSelect);

View file

@ -9,10 +9,11 @@ import { cn } from '~/utils';
interface MCPSubMenuProps {
placeholder?: string;
conversationId?: string | null;
}
const MCPSubMenu = React.forwardRef<HTMLDivElement, MCPSubMenuProps>(
({ placeholder, ...props }, ref) => {
({ placeholder, conversationId, ...props }, ref) => {
const {
configuredServers,
mcpValues,
@ -23,7 +24,7 @@ const MCPSubMenu = React.forwardRef<HTMLDivElement, MCPSubMenuProps>(
getServerStatusIconProps,
getConfigDialogProps,
isInitializing,
} = useMCPServerManager();
} = useMCPServerManager({ conversationId });
const menuStore = Ariakit.useMenuStore({
focusLoop: true,

View file

@ -10,12 +10,12 @@ import {
PermissionTypes,
defaultAgentCapabilities,
} from 'librechat-data-provider';
import { useLocalize, useHasAccess, useAgentCapabilities, useMCPSelect } from '~/hooks';
import { useLocalize, useHasAccess, useAgentCapabilities } from '~/hooks';
import ArtifactsSubMenu from '~/components/Chat/Input/ArtifactsSubMenu';
import MCPSubMenu from '~/components/Chat/Input/MCPSubMenu';
import { useGetStartupConfig } from '~/data-provider';
import { useBadgeRowContext } from '~/Providers';
import { cn } from '~/utils';
import { useGetStartupConfig } from '~/data-provider';
interface ToolsDropdownProps {
disabled?: boolean;
@ -30,11 +30,12 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
artifacts,
fileSearch,
agentsConfig,
mcpServerNames,
conversationId,
codeApiKeyForm,
codeInterpreter,
searchApiKeyForm,
} = useBadgeRowContext();
const mcpSelect = useMCPSelect();
const { data: startupConfig } = useGetStartupConfig();
const { codeEnabled, webSearchEnabled, artifactsEnabled, fileSearchEnabled } =
@ -56,7 +57,6 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
} = codeInterpreter;
const { isPinned: isFileSearchPinned, setIsPinned: setIsFileSearchPinned } = fileSearch;
const { isPinned: isArtifactsPinned, setIsPinned: setIsArtifactsPinned } = artifacts;
const { mcpServerNames } = mcpSelect;
const canUseWebSearch = useHasAccess({
permissionType: PermissionTypes.WEB_SEARCH,
@ -290,7 +290,9 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
if (mcpServerNames && mcpServerNames.length > 0) {
dropdownItems.push({
hideOnClick: false,
render: (props) => <MCPSubMenu {...props} placeholder={mcpPlaceholder} />,
render: (props) => (
<MCPSubMenu {...props} placeholder={mcpPlaceholder} conversationId={conversationId} />
),
});
}