import { useCallback, useState, useMemo, useRef, useEffect } from 'react'; import { useToastContext } from '@librechat/client'; import { useQueryClient } from '@tanstack/react-query'; import { Constants, QueryKeys } from 'librechat-data-provider'; import { useUpdateUserPluginsMutation, useReinitializeMCPServerMutation, useCancelMCPOAuthMutation, } from 'librechat-data-provider/react-query'; import { useMCPConnectionStatusQuery } from '~/data-provider/Tools/queries'; import type { TUpdateUserPlugins, TPlugin } from 'librechat-data-provider'; import type { ConfigFieldDetail } from '~/components/MCP/MCPConfigDialog'; import { useLocalize } from '~/hooks'; import { useBadgeRowContext } from '~/Providers'; interface ServerState { isInitializing: boolean; oauthUrl: string | null; oauthStartTime: number | null; isCancellable: boolean; pollInterval: NodeJS.Timeout | null; } export function useMCPServerManager() { const localize = useLocalize(); const { showToast } = useToastContext(); const { mcpSelect, startupConfig } = useBadgeRowContext(); const { mcpValues, setMCPValues, mcpToolDetails, isPinned, setIsPinned } = mcpSelect; const queryClient = useQueryClient(); const [isConfigModalOpen, setIsConfigModalOpen] = useState(false); const [selectedToolForConfig, setSelectedToolForConfig] = useState(null); const previousFocusRef = useRef(null); const mcpValuesRef = useRef(mcpValues); // fixes the issue where OAuth flows would deselect all the servers except the one that is being authenticated on success useEffect(() => { mcpValuesRef.current = mcpValues; }, [mcpValues]); const configuredServers = useMemo(() => { if (!startupConfig?.mcpServers) return []; return Object.entries(startupConfig.mcpServers) .filter(([, config]) => config.chatMenu !== false) .map(([serverName]) => serverName); }, [startupConfig?.mcpServers]); const reinitializeMutation = useReinitializeMCPServerMutation(); const cancelOAuthMutation = useCancelMCPOAuthMutation(); const updateUserPluginsMutation = useUpdateUserPluginsMutation({ onSuccess: async () => { showToast({ message: localize('com_nav_mcp_vars_updated'), status: 'success' }); await Promise.all([ queryClient.refetchQueries([QueryKeys.tools]), queryClient.refetchQueries([QueryKeys.mcpAuthValues]), queryClient.refetchQueries([QueryKeys.mcpConnectionStatus]), ]); }, onError: (error: unknown) => { console.error('Error updating MCP auth:', error); showToast({ message: localize('com_nav_mcp_vars_update_error'), status: 'error', }); }, }); const [serverStates, setServerStates] = useState>(() => { const initialStates: Record = {}; configuredServers.forEach((serverName) => { initialStates[serverName] = { isInitializing: false, oauthUrl: null, oauthStartTime: null, isCancellable: false, pollInterval: null, }; }); return initialStates; }); const { data: connectionStatusData } = useMCPConnectionStatusQuery(); const connectionStatus = useMemo( () => connectionStatusData?.connectionStatus || {}, [connectionStatusData?.connectionStatus], ); useEffect(() => { if (!mcpValues?.length) return; const connectedSelected = mcpValues.filter( (serverName) => connectionStatus[serverName]?.connectionState === 'connected', ); if (connectedSelected.length !== mcpValues.length) { setMCPValues(connectedSelected); } }, [connectionStatus, mcpValues, setMCPValues]); const updateServerState = useCallback((serverName: string, updates: Partial) => { setServerStates((prev) => { const newStates = { ...prev }; const currentState = newStates[serverName] || { isInitializing: false, oauthUrl: null, oauthStartTime: null, isCancellable: false, pollInterval: null, }; newStates[serverName] = { ...currentState, ...updates }; return newStates; }); }, []); const cleanupServerState = useCallback( (serverName: string) => { const state = serverStates[serverName]; if (state?.pollInterval) { clearInterval(state.pollInterval); } updateServerState(serverName, { isInitializing: false, oauthUrl: null, oauthStartTime: null, isCancellable: false, pollInterval: null, }); }, [serverStates, updateServerState], ); const startServerPolling = useCallback( (serverName: string) => { const pollInterval = setInterval(async () => { try { await queryClient.refetchQueries([QueryKeys.mcpConnectionStatus]); const freshConnectionData = queryClient.getQueryData([ QueryKeys.mcpConnectionStatus, ]) as any; const freshConnectionStatus = freshConnectionData?.connectionStatus || {}; const state = serverStates[serverName]; const serverStatus = freshConnectionStatus[serverName]; if (serverStatus?.connectionState === 'connected') { clearInterval(pollInterval); showToast({ message: localize('com_ui_mcp_authenticated_success', { 0: serverName }), status: 'success', }); const currentValues = mcpValuesRef.current ?? []; if (!currentValues.includes(serverName)) { setMCPValues([...currentValues, serverName]); } // This delay is to ensure UI has updated with new connection status before cleanup // Otherwise servers will show as disconnected for a second after OAuth flow completes setTimeout(() => { cleanupServerState(serverName); }, 1000); return; } if (state?.oauthStartTime && Date.now() - state.oauthStartTime > 180000) { showToast({ message: localize('com_ui_mcp_oauth_timeout', { 0: serverName }), status: 'error', }); cleanupServerState(serverName); return; } if (serverStatus?.connectionState === 'error') { showToast({ message: localize('com_ui_mcp_init_failed'), status: 'error', }); cleanupServerState(serverName); } } catch (error) { console.error(`[MCP Manager] Error polling server ${serverName}:`, error); } }, 3500); updateServerState(serverName, { pollInterval }); }, [ queryClient, serverStates, showToast, localize, setMCPValues, cleanupServerState, updateServerState, ], ); const initializeServer = useCallback( async (serverName: string) => { updateServerState(serverName, { isInitializing: true }); try { const response = await reinitializeMutation.mutateAsync(serverName); if (response.success) { if (response.oauthRequired && response.oauthUrl) { updateServerState(serverName, { oauthUrl: response.oauthUrl, oauthStartTime: Date.now(), isCancellable: true, isInitializing: true, }); window.open(response.oauthUrl, '_blank', 'noopener,noreferrer'); startServerPolling(serverName); } else { await queryClient.refetchQueries([QueryKeys.mcpConnectionStatus]); showToast({ message: localize('com_ui_mcp_initialized_success', { 0: serverName }), status: 'success', }); const currentValues = mcpValues ?? []; if (!currentValues.includes(serverName)) { setMCPValues([...currentValues, serverName]); } cleanupServerState(serverName); } } else { showToast({ message: localize('com_ui_mcp_init_failed', { 0: serverName }), status: 'error', }); cleanupServerState(serverName); } } catch (error) { console.error(`[MCP Manager] Failed to initialize ${serverName}:`, error); showToast({ message: localize('com_ui_mcp_init_failed', { 0: serverName }), status: 'error', }); cleanupServerState(serverName); } }, [ updateServerState, reinitializeMutation, startServerPolling, queryClient, showToast, localize, mcpValues, cleanupServerState, setMCPValues, ], ); const cancelOAuthFlow = useCallback( (serverName: string) => { queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]); cleanupServerState(serverName); cancelOAuthMutation.mutate(serverName); showToast({ message: localize('com_ui_mcp_oauth_cancelled', { 0: serverName }), status: 'warning', }); }, [queryClient, cleanupServerState, showToast, localize, cancelOAuthMutation], ); const isInitializing = useCallback( (serverName: string) => { return serverStates[serverName]?.isInitializing || false; }, [serverStates], ); const isCancellable = useCallback( (serverName: string) => { return serverStates[serverName]?.isCancellable || false; }, [serverStates], ); const getOAuthUrl = useCallback( (serverName: string) => { return serverStates[serverName]?.oauthUrl || null; }, [serverStates], ); const placeholderText = useMemo( () => startupConfig?.interface?.mcpServers?.placeholder || localize('com_ui_mcp_servers'), [startupConfig?.interface?.mcpServers?.placeholder, localize], ); const batchToggleServers = useCallback( (serverNames: string[]) => { const connectedServers: string[] = []; const disconnectedServers: string[] = []; serverNames.forEach((serverName) => { const serverStatus = connectionStatus[serverName]; if (serverStatus?.connectionState === 'connected') { connectedServers.push(serverName); } else { disconnectedServers.push(serverName); } }); setMCPValues(connectedServers); disconnectedServers.forEach((serverName) => { initializeServer(serverName); }); }, [connectionStatus, setMCPValues, initializeServer], ); const toggleServerSelection = useCallback( (serverName: string) => { const currentValues = mcpValues ?? []; const isCurrentlySelected = currentValues.includes(serverName); if (isCurrentlySelected) { const filteredValues = currentValues.filter((name) => name !== serverName); setMCPValues(filteredValues); } else { const serverStatus = connectionStatus[serverName]; if (serverStatus?.connectionState === 'connected') { setMCPValues([...currentValues, serverName]); } else { initializeServer(serverName); } } }, [mcpValues, setMCPValues, connectionStatus, initializeServer], ); const handleConfigSave = useCallback( (targetName: string, authData: Record) => { if (selectedToolForConfig && selectedToolForConfig.name === targetName) { const payload: TUpdateUserPlugins = { pluginKey: `${Constants.mcp_prefix}${targetName}`, action: 'install', auth: authData, }; updateUserPluginsMutation.mutate(payload); } }, [selectedToolForConfig, updateUserPluginsMutation], ); const handleConfigRevoke = useCallback( (targetName: string) => { if (selectedToolForConfig && selectedToolForConfig.name === targetName) { const payload: TUpdateUserPlugins = { pluginKey: `${Constants.mcp_prefix}${targetName}`, action: 'uninstall', auth: {}, }; updateUserPluginsMutation.mutate(payload); const currentValues = mcpValues ?? []; const filteredValues = currentValues.filter((name) => name !== targetName); setMCPValues(filteredValues); } }, [selectedToolForConfig, updateUserPluginsMutation, mcpValues, setMCPValues], ); const handleSave = useCallback( (authData: Record) => { if (selectedToolForConfig) { handleConfigSave(selectedToolForConfig.name, authData); } }, [selectedToolForConfig, handleConfigSave], ); const handleRevoke = useCallback(() => { if (selectedToolForConfig) { handleConfigRevoke(selectedToolForConfig.name); } }, [selectedToolForConfig, handleConfigRevoke]); const handleDialogOpenChange = useCallback((open: boolean) => { setIsConfigModalOpen(open); if (!open && previousFocusRef.current) { setTimeout(() => { if (previousFocusRef.current && typeof previousFocusRef.current.focus === 'function') { previousFocusRef.current.focus(); } previousFocusRef.current = null; }, 0); } }, []); const getServerStatusIconProps = useCallback( (serverName: string) => { const tool = mcpToolDetails?.find((t) => t.name === serverName); const serverStatus = connectionStatus[serverName]; const serverConfig = startupConfig?.mcpServers?.[serverName]; const handleConfigClick = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); previousFocusRef.current = document.activeElement as HTMLElement; const configTool = tool || { name: serverName, pluginKey: `${Constants.mcp_prefix}${serverName}`, authConfig: serverConfig?.customUserVars ? Object.entries(serverConfig.customUserVars).map(([key, config]) => ({ authField: key, label: config.title, description: config.description, })) : [], authenticated: false, }; setSelectedToolForConfig(configTool); setIsConfigModalOpen(true); }; const handleCancelClick = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); cancelOAuthFlow(serverName); }; const hasCustomUserVars = serverConfig?.customUserVars && Object.keys(serverConfig.customUserVars).length > 0; return { serverName, serverStatus, tool, onConfigClick: handleConfigClick, isInitializing: isInitializing(serverName), canCancel: isCancellable(serverName), onCancel: handleCancelClick, hasCustomUserVars, }; }, [ mcpToolDetails, connectionStatus, startupConfig?.mcpServers, isInitializing, isCancellable, cancelOAuthFlow, ], ); const getConfigDialogProps = useCallback(() => { if (!selectedToolForConfig) return null; const fieldsSchema: Record = {}; if (selectedToolForConfig?.authConfig) { selectedToolForConfig.authConfig.forEach((field) => { fieldsSchema[field.authField] = { title: field.label || field.authField, description: field.description, }; }); } const initialValues: Record = {}; if (selectedToolForConfig?.authConfig) { selectedToolForConfig.authConfig.forEach((field) => { initialValues[field.authField] = ''; }); } return { serverName: selectedToolForConfig.name, serverStatus: connectionStatus[selectedToolForConfig.name], isOpen: isConfigModalOpen, onOpenChange: handleDialogOpenChange, fieldsSchema, initialValues, onSave: handleSave, onRevoke: handleRevoke, isSubmitting: updateUserPluginsMutation.isLoading, }; }, [ selectedToolForConfig, connectionStatus, isConfigModalOpen, handleDialogOpenChange, handleSave, handleRevoke, updateUserPluginsMutation.isLoading, ]); return { configuredServers, connectionStatus, initializeServer, cancelOAuthFlow, isInitializing, isCancellable, getOAuthUrl, mcpValues, setMCPValues, mcpToolDetails, isPinned, setIsPinned, placeholderText, batchToggleServers, toggleServerSelection, localize, isConfigModalOpen, handleDialogOpenChange, selectedToolForConfig, setSelectedToolForConfig, handleSave, handleRevoke, getServerStatusIconProps, getConfigDialogProps, }; }