diff --git a/client/src/components/Chat/Input/MCPSelect.tsx b/client/src/components/Chat/Input/MCPSelect.tsx index 667ba73866..aed85d5d59 100644 --- a/client/src/components/Chat/Input/MCPSelect.tsx +++ b/client/src/components/Chat/Input/MCPSelect.tsx @@ -1,17 +1,14 @@ -import React, { memo, useRef, useMemo, useEffect, useCallback, useState } from 'react'; -import { useRecoilState } from 'recoil'; +import React, { memo, useCallback, useState } from 'react'; import { Settings2 } from 'lucide-react'; import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query'; -import { Constants, EModelEndpoint, LocalStorageKeys } from 'librechat-data-provider'; +import { Constants, EModelEndpoint } from 'librechat-data-provider'; import type { TPlugin, TPluginAuthConfig, TUpdateUserPlugins } from 'librechat-data-provider'; import MCPConfigDialog, { type ConfigFieldDetail } from '~/components/ui/MCPConfigDialog'; import { useToastContext, useBadgeRowContext } from '~/Providers'; import { useAvailableToolsQuery } from '~/data-provider'; -import useLocalStorage from '~/hooks/useLocalStorageAlt'; +import { useLocalize, useMCPSelect } from '~/hooks'; import MultiSelect from '~/components/ui/MultiSelect'; -import { ephemeralAgentByConvoId } from '~/store'; import MCPIcon from '~/components/ui/MCPIcon'; -import { useLocalize } from '~/hooks'; interface McpServerInfo { name: string; @@ -26,26 +23,10 @@ const getBaseMCPPluginKey = (fullPluginKey: string): string => { return Constants.mcp_prefix + parts[parts.length - 1]; }; -const storageCondition = (value: unknown, rawCurrentValue?: string | null) => { - if (rawCurrentValue) { - try { - const currentValue = rawCurrentValue?.trim() ?? ''; - if (currentValue.length > 2) { - return true; - } - } catch (e) { - console.error(e); - } - } - return Array.isArray(value) && value.length > 0; -}; - function MCPSelect() { const localize = useLocalize(); const { showToast } = useToastContext(); const { conversationId } = useBadgeRowContext(); - const key = conversationId ?? Constants.NEW_CONVO; - const hasSetFetched = useRef(null); const [isConfigModalOpen, setIsConfigModalOpen] = useState(false); const [selectedToolForConfig, setSelectedToolForConfig] = useState(null); @@ -71,6 +52,12 @@ function MCPSelect() { }, }); + const { mcpValues, setMCPValues, mcpServerNames } = useMCPSelect({ + conversationId, + mcpToolDetails, + isFetched, + }); + const updateUserPluginsMutation = useUpdateUserPluginsMutation({ onSuccess: () => { setIsConfigModalOpen(false); @@ -85,48 +72,6 @@ function MCPSelect() { }, }); - const [ephemeralAgent, setEphemeralAgent] = useRecoilState(ephemeralAgentByConvoId(key)); - const mcpState = useMemo(() => { - return ephemeralAgent?.mcp ?? []; - }, [ephemeralAgent?.mcp]); - - const setSelectedValues = useCallback( - (values: string[] | null | undefined) => { - if (!values) { - return; - } - if (!Array.isArray(values)) { - return; - } - setEphemeralAgent((prev) => ({ - ...prev, - mcp: values, - })); - }, - [setEphemeralAgent], - ); - const [mcpValues, setMCPValues] = useLocalStorage( - `${LocalStorageKeys.LAST_MCP_}${key}`, - mcpState, - setSelectedValues, - storageCondition, - ); - - useEffect(() => { - if (hasSetFetched.current === key) { - return; - } - if (!isFetched) { - return; - } - hasSetFetched.current = key; - if ((mcpToolDetails?.length ?? 0) > 0) { - setMCPValues(mcpValues.filter((mcp) => mcpToolDetails?.some((tool) => tool.name === mcp))); - return; - } - setMCPValues([]); - }, [isFetched, setMCPValues, mcpToolDetails, key, mcpValues]); - const renderSelectedValues = useCallback( (values: string[], placeholder?: string) => { if (values.length === 0) { @@ -140,10 +85,6 @@ function MCPSelect() { [localize], ); - const mcpServerNames = useMemo(() => { - return (mcpToolDetails ?? []).map((tool) => tool.name); - }, [mcpToolDetails]); - const handleConfigSave = useCallback( (targetName: string, authData: Record) => { if (selectedToolForConfig && selectedToolForConfig.name === targetName) { diff --git a/client/src/hooks/Plugins/index.ts b/client/src/hooks/Plugins/index.ts index 93a2fd3d5d..85b6c7186e 100644 --- a/client/src/hooks/Plugins/index.ts +++ b/client/src/hooks/Plugins/index.ts @@ -1,6 +1,7 @@ +export * from './useMCPSelect'; +export * from './useToolToggle'; export { default as useAuthCodeTool } from './useAuthCodeTool'; export { default as usePluginInstall } from './usePluginInstall'; export { default as useCodeApiKeyForm } from './useCodeApiKeyForm'; export { default as useSearchApiKeyForm } from './useSearchApiKeyForm'; export { default as usePluginDialogHelpers } from './usePluginDialogHelpers'; -export { useToolToggle } from './useToolToggle'; diff --git a/client/src/hooks/Plugins/useMCPSelect.ts b/client/src/hooks/Plugins/useMCPSelect.ts new file mode 100644 index 0000000000..6845b3aa1e --- /dev/null +++ b/client/src/hooks/Plugins/useMCPSelect.ts @@ -0,0 +1,85 @@ +import { useRef, useEffect, useCallback, useMemo } from 'react'; +import { useRecoilState } from 'recoil'; +import { Constants, LocalStorageKeys } from 'librechat-data-provider'; +import useLocalStorage from '~/hooks/useLocalStorageAlt'; +import { ephemeralAgentByConvoId } from '~/store'; + +const storageCondition = (value: unknown, rawCurrentValue?: string | null) => { + if (rawCurrentValue) { + try { + const currentValue = rawCurrentValue?.trim() ?? ''; + if (currentValue.length > 2) { + return true; + } + } catch (e) { + console.error(e); + } + } + return Array.isArray(value) && value.length > 0; +}; + +interface UseMCPSelectOptions { + conversationId?: string | null; + mcpToolDetails?: Array<{ name: string }> | null; + isFetched: boolean; +} + +export function useMCPSelect({ conversationId, mcpToolDetails, isFetched }: UseMCPSelectOptions) { + const key = conversationId ?? Constants.NEW_CONVO; + const hasSetFetched = useRef(null); + const [ephemeralAgent, setEphemeralAgent] = useRecoilState(ephemeralAgentByConvoId(key)); + + const mcpState = useMemo(() => { + return ephemeralAgent?.mcp ?? []; + }, [ephemeralAgent?.mcp]); + + const setSelectedValues = useCallback( + (values: string[] | null | undefined) => { + if (!values) { + return; + } + if (!Array.isArray(values)) { + return; + } + setEphemeralAgent((prev) => ({ + ...prev, + mcp: values, + })); + }, + [setEphemeralAgent], + ); + + const [mcpValues, setMCPValues] = useLocalStorage( + `${LocalStorageKeys.LAST_MCP_}${key}`, + mcpState, + setSelectedValues, + storageCondition, + ); + + useEffect(() => { + if (hasSetFetched.current === key) { + return; + } + if (!isFetched) { + return; + } + hasSetFetched.current = key; + if ((mcpToolDetails?.length ?? 0) > 0) { + setMCPValues(mcpValues.filter((mcp) => mcpToolDetails?.some((tool) => tool.name === mcp))); + return; + } + setMCPValues([]); + }, [isFetched, setMCPValues, mcpToolDetails, key, mcpValues]); + + const mcpServerNames = useMemo(() => { + return (mcpToolDetails ?? []).map((tool) => tool.name); + }, [mcpToolDetails]); + + return { + mcpValues, + setMCPValues, + mcpServerNames, + ephemeralAgent, + setEphemeralAgent, + }; +}