diff --git a/api/server/controllers/PluginController.js b/api/server/controllers/PluginController.js index fdd6d227b6..42054eb03f 100644 --- a/api/server/controllers/PluginController.js +++ b/api/server/controllers/PluginController.js @@ -223,7 +223,6 @@ const getAvailableTools = async (req, res) => { // Handle MCP servers with customUserVars (user-level auth required) if (serverConfig.customUserVars) { logger.warn(`[getAvailableTools] Processing user-level MCP server: ${serverName}`); - const customVarKeys = Object.keys(serverConfig.customUserVars); // Build authConfig for MCP tools toolToAdd.authConfig = Object.entries(serverConfig.customUserVars).map(([key, value]) => ({ diff --git a/api/server/routes/mcp.js b/api/server/routes/mcp.js index 04f8ad6a39..eb9259e5f4 100644 --- a/api/server/routes/mcp.js +++ b/api/server/routes/mcp.js @@ -542,7 +542,6 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => { } let userConnection = null; - let oauthRequired = false; try { userConnection = await mcpManager.getUserConnection({ @@ -558,7 +557,6 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => { }, oauthStart: (authURL) => { // This will be called if OAuth is required - oauthRequired = true; responseSent = true; logger.info(`[MCP Reinitialize] OAuth required for ${serverName}, auth URL: ${authURL}`); diff --git a/client/src/components/Chat/Input/MCPSelect.tsx b/client/src/components/Chat/Input/MCPSelect.tsx index 8f2d967883..de461292b3 100644 --- a/client/src/components/Chat/Input/MCPSelect.tsx +++ b/client/src/components/Chat/Input/MCPSelect.tsx @@ -21,12 +21,14 @@ function MCPSelect() { const localize = useLocalize(); const { showToast } = useToastContext(); const { mcpSelect, startupConfig } = useBadgeRowContext(); - const { mcpValues, setMCPValues, mcpToolDetails, isPinned } = mcpSelect; + const { mcpValues, setMCPValues, isPinned } = mcpSelect; // Get real connection status from MCPManager const { data: statusQuery } = useMCPConnectionStatusQuery(); - - const mcpServerStatuses = statusQuery?.connectionStatus || {}; + const mcpServerStatuses = useMemo( + () => statusQuery?.connectionStatus || {}, + [statusQuery?.connectionStatus], + ); console.log('mcpServerStatuses', mcpServerStatuses); console.log('statusQuery', statusQuery); @@ -34,15 +36,10 @@ function MCPSelect() { const [isConfigModalOpen, setIsConfigModalOpen] = useState(false); const [selectedToolForConfig, setSelectedToolForConfig] = useState(null); - // Fetch auth values for the selected server - const { data: authValuesData } = useMCPAuthValuesQuery(selectedToolForConfig?.name || '', { - enabled: isConfigModalOpen && !!selectedToolForConfig?.name, - }); - const queryClient = useQueryClient(); const updateUserPluginsMutation = useUpdateUserPluginsMutation({ - onSuccess: async (data, variables) => { + onSuccess: async () => { showToast({ message: localize('com_nav_mcp_vars_updated'), status: 'success' }); // // For 'uninstall' actions (revoke), remove the server from selected values diff --git a/client/src/components/SidePanel/MCP/MCPPanel.tsx b/client/src/components/SidePanel/MCP/MCPPanel.tsx index 9630e77727..496c6e24ba 100644 --- a/client/src/components/SidePanel/MCP/MCPPanel.tsx +++ b/client/src/components/SidePanel/MCP/MCPPanel.tsx @@ -1,7 +1,6 @@ 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 React, { useState, useCallback, useMemo } from 'react'; import { useUpdateUserPluginsMutation, useReinitializeMCPServerMutation, @@ -9,11 +8,17 @@ import { 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 { Button } from '~/components/ui'; import { useGetStartupConfig } from '~/data-provider'; +import { useMCPConnectionStatusQuery } from '~/data-provider/Tools/queries'; import MCPPanelSkeleton from './MCPPanelSkeleton'; import { useToastContext } from '~/Providers'; import { useLocalize } from '~/hooks'; +import { + CustomUserVarsSection, + ServerInitializationSection, + type ConfigFieldDetail, +} from '~/components/ui/MCP'; interface ServerConfigWithVars { serverName: string; @@ -33,6 +38,13 @@ export default function MCPPanel() { const reinitializeMCPMutation = useReinitializeMCPServerMutation(); const queryClient = useQueryClient(); + // Get real connection status from MCPManager + const { data: statusQuery } = useMCPConnectionStatusQuery(); + const mcpServerStatuses = useMemo( + () => statusQuery?.connectionStatus || {}, + [statusQuery?.connectionStatus], + ); + const mcpServerDefinitions = useMemo(() => { if (!startupConfig?.mcpServers) { return []; @@ -53,40 +65,16 @@ export default function MCPPanel() { }, [startupConfig?.mcpServers]); const updateUserPluginsMutation = useUpdateUserPluginsMutation({ - onSuccess: async (data, variables) => { + onSuccess: async () => { 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 + // Wait for all queries to refetch before resolving loading state + await Promise.all([ + queryClient.invalidateQueries([QueryKeys.tools]), + queryClient.refetchQueries([QueryKeys.tools]), + queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]), + queryClient.refetchQueries([QueryKeys.mcpConnectionStatus]), + ]); }, onError: (error: unknown) => { console.error('Error updating MCP auth:', error); @@ -137,7 +125,7 @@ export default function MCPPanel() { // Check if OAuth is required if (response.oauthRequired) { - if (response.authorizationUrl) { + if (response.authURL) { // Show OAuth URL to user showToast({ message: `OAuth required for ${serverName}. Please visit the authorization URL.`, @@ -145,7 +133,7 @@ export default function MCPPanel() { }); // Open OAuth URL in new window/tab - window.open(response.authorizationUrl, '_blank', 'noopener,noreferrer'); + window.open(response.authURL, '_blank', 'noopener,noreferrer'); // Show a more detailed message with the URL setTimeout(() => { @@ -177,16 +165,16 @@ export default function MCPPanel() { 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) { + if ((error as any)?.response?.data?.oauthRequired) { + const errorData = (error as any).response.data; + if (errorData.authURL) { 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'); + window.open(errorData.authURL, '_blank', 'noopener,noreferrer'); setTimeout(() => { showToast({ @@ -217,6 +205,50 @@ export default function MCPPanel() { [showToast, reinitializeMCPMutation], ); + // Create save and revoke handlers with latest state + const handleSave = useCallback( + (updatedValues: Record) => { + if (selectedServerNameForEditing) { + handleSaveServerVars(selectedServerNameForEditing, updatedValues); + } + }, + [selectedServerNameForEditing, handleSaveServerVars], + ); + + const handleRevoke = useCallback(() => { + if (selectedServerNameForEditing) { + handleRevokeServerVars(selectedServerNameForEditing); + } + }, [selectedServerNameForEditing, handleRevokeServerVars]); + + // Prepare data for MCPConfigDialog + const selectedServer = useMemo(() => { + if (!selectedServerNameForEditing) return null; + return mcpServerDefinitions.find((s) => s.serverName === selectedServerNameForEditing); + }, [selectedServerNameForEditing, mcpServerDefinitions]); + + const fieldsSchema = useMemo(() => { + if (!selectedServer) return {}; + const schema: Record = {}; + Object.entries(selectedServer.config.customUserVars).forEach(([key, value]) => { + schema[key] = { + title: value.title, + description: value.description, + }; + }); + return schema; + }, [selectedServer]); + + const initialValues = useMemo(() => { + if (!selectedServer) return {}; + // Initialize with empty strings for all fields + const values: Record = {}; + Object.keys(selectedServer.config.customUserVars).forEach((key) => { + values[key] = ''; + }); + return values; + }, [selectedServer]); + if (startupConfigLoading) { return ; } @@ -229,21 +261,11 @@ export default function MCPPanel() { ); } - 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')} -
- ); - } + if (selectedServerNameForEditing && selectedServer) { + // Editing View - use MCPConfigDialog-style layout but inline + const serverStatus = mcpServerStatuses[selectedServerNameForEditing]; + const isConnected = serverStatus?.connected || false; + const requiresOAuth = serverStatus?.requiresOAuth || false; return (
@@ -255,15 +277,48 @@ export default function MCPPanel() { {localize('com_ui_back')} -

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

- + + {/* Header with status */} +
+
+

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

+ {isConnected && ( +
+
+ {localize('com_ui_active')} +
+ )} +
+

+ {Object.keys(fieldsSchema).length > 0 + ? localize('com_ui_mcp_dialog_desc') + : `Manage connection and settings for the ${selectedServer.serverName} MCP server.`} +

+
+ + {/* Content sections */} +
+ {/* Custom User Variables Section */} + {Object.keys(fieldsSchema).length > 0 && ( +
+ +
+ )} + + {/* Server Initialization Section */} + +
); } else { @@ -271,124 +326,46 @@ export default function MCPPanel() { return (
- {mcpServerDefinitions.map((server) => ( -
- - -
- ))} + {mcpServerDefinitions.map((server) => { + const serverStatus = mcpServerStatuses[server.serverName]; + const isConnected = serverStatus?.connected || false; + + return ( +
+ + +
+ ); + })}
); } } - -// 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 && ( - - )} - -
-
- ); -} diff --git a/client/src/components/ui/MCP/MCPConfigDialog.tsx b/client/src/components/ui/MCP/MCPConfigDialog.tsx index 08efb6a85c..68b60b3ea6 100644 --- a/client/src/components/ui/MCP/MCPConfigDialog.tsx +++ b/client/src/components/ui/MCP/MCPConfigDialog.tsx @@ -1,9 +1,7 @@ -import React, { useMemo, useCallback } from 'react'; +import React, { useMemo } from 'react'; import { useLocalize } from '~/hooks'; import { useMCPConnectionStatusQuery } from '~/data-provider/Tools/queries'; import { CustomUserVarsSection, ServerInitializationSection } from './'; -import { useQueryClient } from '@tanstack/react-query'; -import { QueryKeys } from 'librechat-data-provider'; import { OGDialog, @@ -46,10 +44,9 @@ export default function MCPConfigDialog({ serverName, }: MCPConfigDialogProps) { const localize = useLocalize(); - const queryClient = useQueryClient(); // Get connection status to determine OAuth requirements with aggressive refresh - const { data: statusQuery, refetch: refetchConnectionStatus } = useMCPConnectionStatusQuery({ + const { data: statusQuery } = useMCPConnectionStatusQuery({ refetchOnMount: true, refetchOnWindowFocus: true, staleTime: 0, diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 68fadfd19e..23b7a4c23d 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -967,8 +967,6 @@ "com_ui_saving": "Saving...", "com_ui_set": "Set", "com_ui_unset": "Unset", - "com_ui_configuration": "Configuration", - "com_ui_mcp_auth_desc": "Configure authentication credentials for this MCP server.", "com_ui_authorization_url": "Authorization URL", "com_ui_continue_oauth": "Continue OAuth Flow", "com_ui_oauth_flow_desc": "Click the button above to continue the OAuth flow in a new tab.", @@ -1100,5 +1098,5 @@ "com_ui_x_selected": "{{0}} selected", "com_ui_yes": "Yes", "com_ui_zoom": "Zoom", - "com_user_message": "You" -} \ No newline at end of file + "com_user_message": "You" +}