import { Constants } from 'librechat-data-provider'; import { ChevronLeft, RefreshCw } from 'lucide-react'; import { useForm, Controller } from 'react-hook-form'; import React, { useState, useCallback, useMemo, useEffect } from 'react'; import { useUpdateUserPluginsMutation, useReinitializeMCPServerMutation, } from 'librechat-data-provider/react-query'; import { useQueryClient } from '@tanstack/react-query'; import { QueryKeys } from 'librechat-data-provider'; import type { TUpdateUserPlugins } from 'librechat-data-provider'; import { Button, Input, Label } from '~/components/ui'; import { useGetStartupConfig } from '~/data-provider'; import MCPPanelSkeleton from './MCPPanelSkeleton'; import { useToastContext } from '~/Providers'; import { useLocalize } from '~/hooks'; interface ServerConfigWithVars { serverName: string; config: { customUserVars: Record; }; } export default function MCPPanel() { const localize = useLocalize(); const { showToast } = useToastContext(); const { data: startupConfig, isLoading: startupConfigLoading } = useGetStartupConfig(); const [selectedServerNameForEditing, setSelectedServerNameForEditing] = useState( null, ); const [rotatingServers, setRotatingServers] = useState>(new Set()); const reinitializeMCPMutation = useReinitializeMCPServerMutation(); const queryClient = useQueryClient(); const mcpServerDefinitions = useMemo(() => { if (!startupConfig?.mcpServers) { return []; } return Object.entries(startupConfig.mcpServers) .filter( ([, serverConfig]) => serverConfig.customUserVars && Object.keys(serverConfig.customUserVars).length > 0, ) .map(([serverName, config]) => ({ serverName, iconPath: null, config: { ...config, customUserVars: config.customUserVars ?? {}, }, })); }, [startupConfig?.mcpServers]); const updateUserPluginsMutation = useUpdateUserPluginsMutation({ onSuccess: async (data, variables) => { showToast({ message: localize('com_nav_mcp_vars_updated'), status: 'success' }); // Refetch tools query to refresh authentication state in the dropdown queryClient.refetchQueries([QueryKeys.tools]); // For 'uninstall' actions (revoke), remove the server from selected values if (variables.action === 'uninstall') { const serverName = variables.pluginKey.replace(Constants.mcp_prefix, ''); // Note: MCPPanel doesn't directly manage selected values, but this ensures // the tools query is refreshed so MCPSelect will pick up the changes } // Only reinitialize for 'install' actions (save), not 'uninstall' actions (revoke) if (variables.action === 'install') { // Extract server name from pluginKey (e.g., "mcp_myServer" -> "myServer") const serverName = variables.pluginKey.replace(Constants.mcp_prefix, ''); // Reinitialize the MCP server to pick up the new authentication values try { await reinitializeMCPMutation.mutateAsync(serverName); console.log( `[MCP Panel] Successfully reinitialized server ${serverName} after auth update`, ); } catch (error) { console.error( `[MCP Panel] Error reinitializing server ${serverName} after auth update:`, error, ); // Don't show error toast to user as the auth update was successful } } // For 'uninstall' actions (revoke), the backend already disconnects the connections // so no additional action is needed here }, onError: (error: unknown) => { console.error('Error updating MCP auth:', error); showToast({ message: localize('com_nav_mcp_vars_update_error'), status: 'error', }); }, }); const handleSaveServerVars = useCallback( (serverName: string, updatedValues: Record) => { const payload: TUpdateUserPlugins = { pluginKey: `${Constants.mcp_prefix}${serverName}`, action: 'install', // 'install' action is used to set/update credentials/variables auth: updatedValues, }; updateUserPluginsMutation.mutate(payload); }, [updateUserPluginsMutation], ); const handleRevokeServerVars = useCallback( (serverName: string) => { const payload: TUpdateUserPlugins = { pluginKey: `${Constants.mcp_prefix}${serverName}`, action: 'uninstall', // 'uninstall' action clears the variables auth: {}, // Empty auth for uninstall }; updateUserPluginsMutation.mutate(payload); }, [updateUserPluginsMutation], ); const handleServerClickToEdit = (serverName: string) => { setSelectedServerNameForEditing(serverName); }; const handleGoBackToList = () => { setSelectedServerNameForEditing(null); }; const handleReinitializeServer = useCallback( async (serverName: string) => { setRotatingServers((prev) => new Set(prev).add(serverName)); try { const response = await reinitializeMCPMutation.mutateAsync(serverName); // Check if OAuth is required if (response.oauthRequired) { if (response.authorizationUrl) { // Show OAuth URL to user showToast({ message: `OAuth required for ${serverName}. Please visit the authorization URL.`, status: 'info', }); // Open OAuth URL in new window/tab window.open(response.authorizationUrl, '_blank', 'noopener,noreferrer'); // Show a more detailed message with the URL setTimeout(() => { showToast({ message: `OAuth URL opened for ${serverName}. Complete authentication and try reinitializing again.`, status: 'info', }); }, 1000); } else { showToast({ message: `OAuth authentication required for ${serverName}. Please configure OAuth credentials.`, status: 'warning', }); } } else if (response.oauthCompleted) { showToast({ message: response.message || `MCP server '${serverName}' reinitialized successfully after OAuth`, status: 'success', }); } else { showToast({ message: response.message || `MCP server '${serverName}' reinitialized successfully`, status: 'success', }); } } catch (error) { console.error('Error reinitializing MCP server:', error); // Check if the error response contains OAuth information if (error?.response?.data?.oauthRequired) { const errorData = error.response.data; if (errorData.authorizationUrl) { showToast({ message: `OAuth required for ${serverName}. Please visit the authorization URL.`, status: 'info', }); // Open OAuth URL in new window/tab window.open(errorData.authorizationUrl, '_blank', 'noopener,noreferrer'); setTimeout(() => { showToast({ message: `OAuth URL opened for ${serverName}. Complete authentication and try reinitializing again.`, status: 'info', }); }, 1000); } else { showToast({ message: errorData.message || `OAuth authentication required for ${serverName}`, status: 'warning', }); } } else { showToast({ message: 'Failed to reinitialize MCP server', status: 'error', }); } } finally { setRotatingServers((prev) => { const next = new Set(prev); next.delete(serverName); return next; }); } }, [showToast, reinitializeMCPMutation], ); if (startupConfigLoading) { return ; } if (mcpServerDefinitions.length === 0) { return (
{localize('com_sidepanel_mcp_no_servers_with_vars')}
); } if (selectedServerNameForEditing) { // Editing View const serverBeingEdited = mcpServerDefinitions.find( (s) => s.serverName === selectedServerNameForEditing, ); if (!serverBeingEdited) { // Fallback to list view if server not found setSelectedServerNameForEditing(null); return (
{localize('com_ui_error')}: {localize('com_ui_mcp_server_not_found')}
); } return (

{localize('com_sidepanel_mcp_variables_for', { '0': serverBeingEdited.serverName })}

); } else { // Server List View return (
{mcpServerDefinitions.map((server) => (
))}
); } } // Inner component for the form - remains the same interface MCPVariableEditorProps { server: ServerConfigWithVars; onSave: (serverName: string, updatedValues: Record) => void; onRevoke: (serverName: string) => void; isSubmitting: boolean; } function MCPVariableEditor({ server, onSave, onRevoke, isSubmitting }: MCPVariableEditorProps) { const localize = useLocalize(); const { control, handleSubmit, reset, formState: { errors, isDirty }, } = useForm>({ defaultValues: {}, // Initialize empty, will be reset by useEffect }); useEffect(() => { // Always initialize with empty strings based on the schema const initialFormValues = Object.keys(server.config.customUserVars).reduce( (acc, key) => { acc[key] = ''; return acc; }, {} as Record, ); reset(initialFormValues); }, [reset, server.config.customUserVars]); const onFormSubmit = (data: Record) => { onSave(server.serverName, data); }; const handleRevokeClick = () => { onRevoke(server.serverName); }; return (
{Object.entries(server.config.customUserVars).map(([key, details]) => (
( )} /> {details.description && (

)} {errors[key] &&

{errors[key]?.message}

}
))}
{Object.keys(server.config.customUserVars).length > 0 && ( )}
); }