import { useEffect, useState, useMemo } from 'react'; import { Search, X } from 'lucide-react'; import { useFormContext } from 'react-hook-form'; import { useQueryClient } from '@tanstack/react-query'; import { Constants, EModelEndpoint, QueryKeys } from 'librechat-data-provider'; import { Dialog, DialogPanel, DialogTitle, Description } from '@headlessui/react'; import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query'; import type { TError, AgentToolType } from 'librechat-data-provider'; import type { AgentForm, TPluginStoreDialogProps } from '~/common'; import { usePluginDialogHelpers, useMCPServerManager, useRemoveMCPTool, useLocalize, } from '~/hooks'; import CustomUserVarsSection from '~/components/MCP/CustomUserVarsSection'; import { PluginPagination } from '~/components/Plugins/Store'; import { useAgentPanelContext } from '~/Providers'; import { useMCPToolsQuery } from '~/data-provider'; import MCPToolItem from './MCPToolItem'; function MCPToolSelectDialog({ isOpen, agentId, setIsOpen, mcpServerNames, }: TPluginStoreDialogProps & { agentId: string; mcpServerNames?: string[]; endpoint: EModelEndpoint.agents; }) { const localize = useLocalize(); const queryClient = useQueryClient(); const { initializeServer } = useMCPServerManager(); const { getValues, setValue } = useFormContext(); const { removeTool } = useRemoveMCPTool({ showToast: false }); const { mcpServersMap, startupConfig } = useAgentPanelContext(); const { refetch: refetchMCPTools } = useMCPToolsQuery({ enabled: mcpServersMap.size > 0, }); const [isSavingCustomVars, setIsSavingCustomVars] = useState(false); const [isInitializing, setIsInitializing] = useState(null); const [configuringServer, setConfiguringServer] = useState(null); const { maxPage, setMaxPage, currentPage, setCurrentPage, itemsPerPage, searchChanged, setSearchChanged, searchValue, setSearchValue, gridRef, handleSearch, handleChangePage, error, setError, errorMessage, setErrorMessage, } = usePluginDialogHelpers(); const updateUserPlugins = useUpdateUserPluginsMutation(); const handleInstallError = (error: TError) => { setError(true); const errorMessage = error.response?.data?.message ?? ''; if (errorMessage) { setErrorMessage(errorMessage); } setTimeout(() => { setError(false); setErrorMessage(''); }, 5000); }; const handleDirectAdd = async (serverName: string, authData?: Record) => { try { setIsInitializing(serverName); // First, save auth if provided if (authData && Object.keys(authData).length > 0) { await updateUserPlugins.mutateAsync({ pluginKey: `${Constants.mcp_prefix}${serverName}`, action: 'install', auth: authData, isEntityTool: true, }); // Invalidate auth values query to ensure fresh data await queryClient.invalidateQueries([QueryKeys.mcpAuthValues, serverName]); // Small delay to ensure backend has processed the auth await new Promise((resolve) => setTimeout(resolve, 500)); } // Then initialize server if needed const serverInfo = mcpServersMap.get(serverName); if (!serverInfo?.isConnected) { const result = await initializeServer(serverName); if (result?.success && result.oauthRequired && result.oauthUrl) { setIsInitializing(null); return; } } // Finally, add tools to form await addToolsToForm(serverName); setIsInitializing(null); } catch (error) { console.error('Error adding MCP server:', error); handleInstallError(error as TError); setIsInitializing(null); } }; const addToolsToForm = async (serverName: string) => { const { data: updatedMCPData } = await refetchMCPTools(); const currentTools = getValues('tools') || []; const toolsToAdd: string[] = [`${Constants.mcp_server}${Constants.mcp_delimiter}${serverName}`]; if (updatedMCPData?.servers?.[serverName]) { const serverData = updatedMCPData.servers[serverName]; serverData.tools.forEach((tool) => { toolsToAdd.push(tool.pluginKey); }); } const newTools = toolsToAdd.filter((tool) => !currentTools.includes(tool)); if (newTools.length > 0) { setValue('tools', [...currentTools, ...newTools]); } }; const handleSaveCustomVars = async (serverName: string, authData: Record) => { try { setIsSavingCustomVars(true); // Filter out empty values to avoid overwriting existing values with empty ones const filteredAuthData: Record = {}; Object.entries(authData).forEach(([key, value]) => { if (value && value.trim()) { filteredAuthData[key] = value.trim(); } }); // Always add the tool, but only pass auth data if there are values to save // Empty auth data is fine - the tool can work without credentials await handleDirectAdd( serverName, Object.keys(filteredAuthData).length > 0 ? filteredAuthData : undefined, ); setConfiguringServer(null); } catch (error) { console.error('Error saving custom vars:', error); handleInstallError(error as TError); } finally { setIsSavingCustomVars(false); } }; const handleRevokeCustomVars = (serverName: string) => { setIsSavingCustomVars(true); updateUserPlugins.mutate( { pluginKey: `${Constants.mcp_prefix}${serverName}`, action: 'uninstall', auth: {}, isEntityTool: true, }, { onError: (error: unknown) => { handleInstallError(error as TError); setIsSavingCustomVars(false); }, onSuccess: async () => { setConfiguringServer(null); setIsSavingCustomVars(false); }, }, ); }; const onAddTool = async (serverName: string) => { if (configuringServer === serverName) { setConfiguringServer(null); await handleDirectAdd(serverName); return; } const serverConfig = startupConfig?.mcpServers?.[serverName]; const hasCustomUserVars = serverConfig?.customUserVars && Object.keys(serverConfig.customUserVars).length > 0; if (hasCustomUserVars) { setConfiguringServer(serverName); } else { await handleDirectAdd(serverName); } }; const installedToolsSet = useMemo(() => { return new Set(mcpServerNames); }, [mcpServerNames]); const mcpServers = useMemo(() => { const servers = Array.from(mcpServersMap.values()); return servers.sort((a, b) => a.serverName.localeCompare(b.serverName)); }, [mcpServersMap]); const filteredServers = useMemo(() => { if (!searchValue) { return mcpServers; } return mcpServers.filter((serverInfo) => serverInfo.serverName.toLowerCase().includes(searchValue.toLowerCase()), ); }, [mcpServers, searchValue]); useEffect(() => { setMaxPage(Math.ceil(filteredServers.length / itemsPerPage)); if (searchChanged) { setCurrentPage(1); setSearchChanged(false); } }, [ setMaxPage, itemsPerPage, searchChanged, setCurrentPage, setSearchChanged, filteredServers.length, ]); return ( { setIsOpen(false); setCurrentPage(1); setSearchValue(''); setConfiguringServer(null); setIsInitializing(null); }} className="relative z-[102]" >
{localize('com_nav_tool_dialog_mcp_server_tools')} {localize('com_nav_tool_dialog_description')}
{error && (
{localize('com_nav_plugin_auth_error')} {errorMessage}
)} {configuringServer && (

{localize('com_ui_mcp_configure_server_description', { 0: configuringServer })}

handleSaveCustomVars(configuringServer, authData)} onRevoke={() => handleRevokeCustomVars(configuringServer)} />
)}
setConfiguringServer(null)} >
{filteredServers .slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage) .map((serverInfo) => { const isInstalled = installedToolsSet.has(serverInfo.serverName); const isConfiguring = configuringServer === serverInfo.serverName; const isServerInitializing = isInitializing === serverInfo.serverName; const tool: AgentToolType = { agent_id: agentId, tool_id: serverInfo.serverName, metadata: { ...serverInfo.metadata, description: `${localize('com_ui_tool_collection_prefix')} ${serverInfo.serverName}`, }, }; return ( onAddTool(serverInfo.serverName)} onRemoveTool={() => removeTool(serverInfo.serverName)} /> ); })}
{maxPage > 0 ? ( ) : (
)}
); } export default MCPToolSelectDialog;