| import React, { useState, useMemo, useCallback } from 'react'; |
| import { ChevronLeft, Trash2 } from 'lucide-react'; |
| import { useQueryClient } from '@tanstack/react-query'; |
| import { Button, useToastContext } from '@librechat/client'; |
| import { Constants, QueryKeys } from 'librechat-data-provider'; |
| import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query'; |
| import type { TUpdateUserPlugins } from 'librechat-data-provider'; |
| import ServerInitializationSection from '~/components/MCP/ServerInitializationSection'; |
| import CustomUserVarsSection from '~/components/MCP/CustomUserVarsSection'; |
| import { MCPPanelProvider, useMCPPanelContext } from '~/Providers'; |
| import { useLocalize, useMCPConnectionStatus } from '~/hooks'; |
| import { useGetStartupConfig } from '~/data-provider'; |
| import MCPPanelSkeleton from './MCPPanelSkeleton'; |
|
|
| function MCPPanelContent() { |
| const localize = useLocalize(); |
| const queryClient = useQueryClient(); |
| const { showToast } = useToastContext(); |
| const { conversationId } = useMCPPanelContext(); |
| const { data: startupConfig, isLoading: startupConfigLoading } = useGetStartupConfig(); |
| const { connectionStatus } = useMCPConnectionStatus({ |
| enabled: !!startupConfig?.mcpServers && Object.keys(startupConfig.mcpServers).length > 0, |
| }); |
|
|
| const [selectedServerNameForEditing, setSelectedServerNameForEditing] = useState<string | null>( |
| null, |
| ); |
|
|
| const updateUserPluginsMutation = useUpdateUserPluginsMutation({ |
| onSuccess: async () => { |
| showToast({ message: localize('com_nav_mcp_vars_updated'), status: 'success' }); |
|
|
| await Promise.all([ |
| queryClient.invalidateQueries([QueryKeys.mcpTools]), |
| queryClient.invalidateQueries([QueryKeys.mcpAuthValues]), |
| queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]), |
| ]); |
| }, |
| onError: (error: unknown) => { |
| console.error('Error updating MCP auth:', error); |
| showToast({ |
| message: localize('com_nav_mcp_vars_update_error'), |
| status: 'error', |
| }); |
| }, |
| }); |
|
|
| const mcpServerDefinitions = useMemo(() => { |
| if (!startupConfig?.mcpServers) { |
| return []; |
| } |
| return Object.entries(startupConfig.mcpServers).map(([serverName, config]) => ({ |
| serverName, |
| iconPath: null, |
| config: { |
| ...config, |
| customUserVars: config.customUserVars ?? {}, |
| }, |
| })); |
| }, [startupConfig?.mcpServers]); |
|
|
| const handleServerClickToEdit = (serverName: string) => { |
| setSelectedServerNameForEditing(serverName); |
| }; |
|
|
| const handleGoBackToList = () => { |
| setSelectedServerNameForEditing(null); |
| }; |
|
|
| const handleConfigSave = useCallback( |
| (targetName: string, authData: Record<string, string>) => { |
| console.log( |
| `[MCP Panel] Saving config for ${targetName}, pluginKey: ${`${Constants.mcp_prefix}${targetName}`}`, |
| ); |
| const payload: TUpdateUserPlugins = { |
| pluginKey: `${Constants.mcp_prefix}${targetName}`, |
| action: 'install', |
| auth: authData, |
| }; |
| updateUserPluginsMutation.mutate(payload); |
| }, |
| [updateUserPluginsMutation], |
| ); |
|
|
| const handleConfigRevoke = useCallback( |
| (targetName: string) => { |
| const payload: TUpdateUserPlugins = { |
| pluginKey: `${Constants.mcp_prefix}${targetName}`, |
| action: 'uninstall', |
| auth: {}, |
| }; |
| updateUserPluginsMutation.mutate(payload); |
| }, |
| [updateUserPluginsMutation], |
| ); |
|
|
| if (startupConfigLoading) { |
| return <MCPPanelSkeleton />; |
| } |
|
|
| if (mcpServerDefinitions.length === 0) { |
| return ( |
| <div className="p-4 text-center text-sm text-gray-500"> |
| {localize('com_sidepanel_mcp_no_servers_with_vars')} |
| </div> |
| ); |
| } |
|
|
| if (selectedServerNameForEditing) { |
| |
| const serverBeingEdited = mcpServerDefinitions.find( |
| (s) => s.serverName === selectedServerNameForEditing, |
| ); |
|
|
| if (!serverBeingEdited) { |
| |
| setSelectedServerNameForEditing(null); |
| return ( |
| <div className="p-4 text-center text-sm text-gray-500"> |
| {localize('com_ui_error')}: {localize('com_ui_mcp_server_not_found')} |
| </div> |
| ); |
| } |
|
|
| const serverStatus = connectionStatus?.[selectedServerNameForEditing]; |
| const isConnected = serverStatus?.connectionState === 'connected'; |
|
|
| return ( |
| <div className="h-auto max-w-full space-y-4 overflow-x-hidden py-2"> |
| <Button |
| variant="outline" |
| onClick={handleGoBackToList} |
| size="sm" |
| aria-label={localize('com_ui_back')} |
| > |
| <ChevronLeft className="mr-1 h-4 w-4" /> |
| {localize('com_ui_back')} |
| </Button> |
| |
| <div className="mb-4"> |
| <CustomUserVarsSection |
| serverName={selectedServerNameForEditing} |
| fields={serverBeingEdited.config.customUserVars} |
| onSave={(authData) => { |
| if (selectedServerNameForEditing) { |
| handleConfigSave(selectedServerNameForEditing, authData); |
| } |
| }} |
| onRevoke={() => { |
| if (selectedServerNameForEditing) { |
| handleConfigRevoke(selectedServerNameForEditing); |
| } |
| }} |
| isSubmitting={updateUserPluginsMutation.isLoading} |
| /> |
| </div> |
| |
| <ServerInitializationSection |
| sidePanel={true} |
| conversationId={conversationId} |
| serverName={selectedServerNameForEditing} |
| requiresOAuth={serverStatus?.requiresOAuth || false} |
| hasCustomUserVars={ |
| serverBeingEdited.config.customUserVars && |
| Object.keys(serverBeingEdited.config.customUserVars).length > 0 |
| } |
| /> |
| {serverStatus?.requiresOAuth && isConnected && ( |
| <Button |
| className="w-full" |
| size="sm" |
| variant="destructive" |
| onClick={() => handleConfigRevoke(selectedServerNameForEditing)} |
| aria-label={localize('com_ui_oauth_revoke')} |
| > |
| <Trash2 className="h-4 w-4" /> |
| {localize('com_ui_oauth_revoke')} |
| </Button> |
| )} |
| </div> |
| ); |
| } else { |
| |
| return ( |
| <div className="h-auto max-w-full overflow-x-hidden py-2"> |
| <div className="space-y-2"> |
| {mcpServerDefinitions.map((server) => { |
| const serverStatus = connectionStatus?.[server.serverName]; |
| const isConnected = serverStatus?.connectionState === 'connected'; |
| |
| return ( |
| <div key={server.serverName} className="flex items-center gap-2"> |
| <Button |
| variant="outline" |
| className="flex-1 justify-start dark:hover:bg-gray-700" |
| onClick={() => handleServerClickToEdit(server.serverName)} |
| aria-label={localize('com_ui_edit') + ' ' + server.serverName} |
| > |
| <div className="flex items-center gap-2"> |
| <span>{server.serverName}</span> |
| {serverStatus && ( |
| <span |
| className={`rounded-xl px-2 py-0.5 text-xs ${ |
| isConnected |
| ? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300' |
| : 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300' |
| }`} |
| > |
| {serverStatus.connectionState} |
| </span> |
| )} |
| </div> |
| </Button> |
| </div> |
| ); |
| })} |
| </div> |
| </div> |
| ); |
| } |
| } |
|
|
| export default function MCPPanel() { |
| return ( |
| <MCPPanelProvider> |
| <MCPPanelContent /> |
| </MCPPanelProvider> |
| ); |
| } |
|
|