mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-19 09:50:15 +01:00
🔧 refactor: Integrate MCPPanel with new MCP components
- **Unified UI Components**: Replace custom MCPVariableEditor with CustomUserVarsSection and ServerInitializationSection for consistent design across all MCP interfaces - **Real-Time Status Indicators**: Add live connection status badges and set/unset authentication pills to match MCPConfigDialog functionality - **Enhanced OAuth Support**: Integrate ServerInitializationSection for proper OAuth flow handling in side panel
This commit is contained in:
parent
b1e346a225
commit
5e2b6e8eb5
6 changed files with 168 additions and 202 deletions
|
|
@ -223,7 +223,6 @@ const getAvailableTools = async (req, res) => {
|
||||||
// Handle MCP servers with customUserVars (user-level auth required)
|
// Handle MCP servers with customUserVars (user-level auth required)
|
||||||
if (serverConfig.customUserVars) {
|
if (serverConfig.customUserVars) {
|
||||||
logger.warn(`[getAvailableTools] Processing user-level MCP server: ${serverName}`);
|
logger.warn(`[getAvailableTools] Processing user-level MCP server: ${serverName}`);
|
||||||
const customVarKeys = Object.keys(serverConfig.customUserVars);
|
|
||||||
|
|
||||||
// Build authConfig for MCP tools
|
// Build authConfig for MCP tools
|
||||||
toolToAdd.authConfig = Object.entries(serverConfig.customUserVars).map(([key, value]) => ({
|
toolToAdd.authConfig = Object.entries(serverConfig.customUserVars).map(([key, value]) => ({
|
||||||
|
|
|
||||||
|
|
@ -542,7 +542,6 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
let userConnection = null;
|
let userConnection = null;
|
||||||
let oauthRequired = false;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
userConnection = await mcpManager.getUserConnection({
|
userConnection = await mcpManager.getUserConnection({
|
||||||
|
|
@ -558,7 +557,6 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => {
|
||||||
},
|
},
|
||||||
oauthStart: (authURL) => {
|
oauthStart: (authURL) => {
|
||||||
// This will be called if OAuth is required
|
// This will be called if OAuth is required
|
||||||
oauthRequired = true;
|
|
||||||
responseSent = true;
|
responseSent = true;
|
||||||
logger.info(`[MCP Reinitialize] OAuth required for ${serverName}, auth URL: ${authURL}`);
|
logger.info(`[MCP Reinitialize] OAuth required for ${serverName}, auth URL: ${authURL}`);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,14 @@ function MCPSelect() {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { showToast } = useToastContext();
|
const { showToast } = useToastContext();
|
||||||
const { mcpSelect, startupConfig } = useBadgeRowContext();
|
const { mcpSelect, startupConfig } = useBadgeRowContext();
|
||||||
const { mcpValues, setMCPValues, mcpToolDetails, isPinned } = mcpSelect;
|
const { mcpValues, setMCPValues, isPinned } = mcpSelect;
|
||||||
|
|
||||||
// Get real connection status from MCPManager
|
// Get real connection status from MCPManager
|
||||||
const { data: statusQuery } = useMCPConnectionStatusQuery();
|
const { data: statusQuery } = useMCPConnectionStatusQuery();
|
||||||
|
const mcpServerStatuses = useMemo(
|
||||||
const mcpServerStatuses = statusQuery?.connectionStatus || {};
|
() => statusQuery?.connectionStatus || {},
|
||||||
|
[statusQuery?.connectionStatus],
|
||||||
|
);
|
||||||
|
|
||||||
console.log('mcpServerStatuses', mcpServerStatuses);
|
console.log('mcpServerStatuses', mcpServerStatuses);
|
||||||
console.log('statusQuery', statusQuery);
|
console.log('statusQuery', statusQuery);
|
||||||
|
|
@ -34,15 +36,10 @@ function MCPSelect() {
|
||||||
const [isConfigModalOpen, setIsConfigModalOpen] = useState(false);
|
const [isConfigModalOpen, setIsConfigModalOpen] = useState(false);
|
||||||
const [selectedToolForConfig, setSelectedToolForConfig] = useState<TPlugin | null>(null);
|
const [selectedToolForConfig, setSelectedToolForConfig] = useState<TPlugin | null>(null);
|
||||||
|
|
||||||
// Fetch auth values for the selected server
|
|
||||||
const { data: authValuesData } = useMCPAuthValuesQuery(selectedToolForConfig?.name || '', {
|
|
||||||
enabled: isConfigModalOpen && !!selectedToolForConfig?.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const updateUserPluginsMutation = useUpdateUserPluginsMutation({
|
const updateUserPluginsMutation = useUpdateUserPluginsMutation({
|
||||||
onSuccess: async (data, variables) => {
|
onSuccess: async () => {
|
||||||
showToast({ message: localize('com_nav_mcp_vars_updated'), status: 'success' });
|
showToast({ message: localize('com_nav_mcp_vars_updated'), status: 'success' });
|
||||||
|
|
||||||
// // For 'uninstall' actions (revoke), remove the server from selected values
|
// // For 'uninstall' actions (revoke), remove the server from selected values
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { Constants } from 'librechat-data-provider';
|
import { Constants } from 'librechat-data-provider';
|
||||||
import { ChevronLeft, RefreshCw } from 'lucide-react';
|
import { ChevronLeft, RefreshCw } from 'lucide-react';
|
||||||
import { useForm, Controller } from 'react-hook-form';
|
import React, { useState, useCallback, useMemo } from 'react';
|
||||||
import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
|
||||||
import {
|
import {
|
||||||
useUpdateUserPluginsMutation,
|
useUpdateUserPluginsMutation,
|
||||||
useReinitializeMCPServerMutation,
|
useReinitializeMCPServerMutation,
|
||||||
|
|
@ -9,11 +8,17 @@ import {
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { QueryKeys } from 'librechat-data-provider';
|
import { QueryKeys } from 'librechat-data-provider';
|
||||||
import type { TUpdateUserPlugins } 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 { useGetStartupConfig } from '~/data-provider';
|
||||||
|
import { useMCPConnectionStatusQuery } from '~/data-provider/Tools/queries';
|
||||||
import MCPPanelSkeleton from './MCPPanelSkeleton';
|
import MCPPanelSkeleton from './MCPPanelSkeleton';
|
||||||
import { useToastContext } from '~/Providers';
|
import { useToastContext } from '~/Providers';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
import {
|
||||||
|
CustomUserVarsSection,
|
||||||
|
ServerInitializationSection,
|
||||||
|
type ConfigFieldDetail,
|
||||||
|
} from '~/components/ui/MCP';
|
||||||
|
|
||||||
interface ServerConfigWithVars {
|
interface ServerConfigWithVars {
|
||||||
serverName: string;
|
serverName: string;
|
||||||
|
|
@ -33,6 +38,13 @@ export default function MCPPanel() {
|
||||||
const reinitializeMCPMutation = useReinitializeMCPServerMutation();
|
const reinitializeMCPMutation = useReinitializeMCPServerMutation();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
// Get real connection status from MCPManager
|
||||||
|
const { data: statusQuery } = useMCPConnectionStatusQuery();
|
||||||
|
const mcpServerStatuses = useMemo(
|
||||||
|
() => statusQuery?.connectionStatus || {},
|
||||||
|
[statusQuery?.connectionStatus],
|
||||||
|
);
|
||||||
|
|
||||||
const mcpServerDefinitions = useMemo(() => {
|
const mcpServerDefinitions = useMemo(() => {
|
||||||
if (!startupConfig?.mcpServers) {
|
if (!startupConfig?.mcpServers) {
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -53,40 +65,16 @@ export default function MCPPanel() {
|
||||||
}, [startupConfig?.mcpServers]);
|
}, [startupConfig?.mcpServers]);
|
||||||
|
|
||||||
const updateUserPluginsMutation = useUpdateUserPluginsMutation({
|
const updateUserPluginsMutation = useUpdateUserPluginsMutation({
|
||||||
onSuccess: async (data, variables) => {
|
onSuccess: async () => {
|
||||||
showToast({ message: localize('com_nav_mcp_vars_updated'), status: 'success' });
|
showToast({ message: localize('com_nav_mcp_vars_updated'), status: 'success' });
|
||||||
|
|
||||||
// Refetch tools query to refresh authentication state in the dropdown
|
// Wait for all queries to refetch before resolving loading state
|
||||||
queryClient.refetchQueries([QueryKeys.tools]);
|
await Promise.all([
|
||||||
|
queryClient.invalidateQueries([QueryKeys.tools]),
|
||||||
// For 'uninstall' actions (revoke), remove the server from selected values
|
queryClient.refetchQueries([QueryKeys.tools]),
|
||||||
if (variables.action === 'uninstall') {
|
queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]),
|
||||||
const serverName = variables.pluginKey.replace(Constants.mcp_prefix, '');
|
queryClient.refetchQueries([QueryKeys.mcpConnectionStatus]),
|
||||||
// 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) => {
|
onError: (error: unknown) => {
|
||||||
console.error('Error updating MCP auth:', error);
|
console.error('Error updating MCP auth:', error);
|
||||||
|
|
@ -137,7 +125,7 @@ export default function MCPPanel() {
|
||||||
|
|
||||||
// Check if OAuth is required
|
// Check if OAuth is required
|
||||||
if (response.oauthRequired) {
|
if (response.oauthRequired) {
|
||||||
if (response.authorizationUrl) {
|
if (response.authURL) {
|
||||||
// Show OAuth URL to user
|
// Show OAuth URL to user
|
||||||
showToast({
|
showToast({
|
||||||
message: `OAuth required for ${serverName}. Please visit the authorization URL.`,
|
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
|
// 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
|
// Show a more detailed message with the URL
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -177,16 +165,16 @@ export default function MCPPanel() {
|
||||||
console.error('Error reinitializing MCP server:', error);
|
console.error('Error reinitializing MCP server:', error);
|
||||||
|
|
||||||
// Check if the error response contains OAuth information
|
// Check if the error response contains OAuth information
|
||||||
if (error?.response?.data?.oauthRequired) {
|
if ((error as any)?.response?.data?.oauthRequired) {
|
||||||
const errorData = error.response.data;
|
const errorData = (error as any).response.data;
|
||||||
if (errorData.authorizationUrl) {
|
if (errorData.authURL) {
|
||||||
showToast({
|
showToast({
|
||||||
message: `OAuth required for ${serverName}. Please visit the authorization URL.`,
|
message: `OAuth required for ${serverName}. Please visit the authorization URL.`,
|
||||||
status: 'info',
|
status: 'info',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Open OAuth URL in new window/tab
|
// Open OAuth URL in new window/tab
|
||||||
window.open(errorData.authorizationUrl, '_blank', 'noopener,noreferrer');
|
window.open(errorData.authURL, '_blank', 'noopener,noreferrer');
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
showToast({
|
showToast({
|
||||||
|
|
@ -217,6 +205,50 @@ export default function MCPPanel() {
|
||||||
[showToast, reinitializeMCPMutation],
|
[showToast, reinitializeMCPMutation],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Create save and revoke handlers with latest state
|
||||||
|
const handleSave = useCallback(
|
||||||
|
(updatedValues: Record<string, string>) => {
|
||||||
|
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<string, ConfigFieldDetail> = {};
|
||||||
|
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<string, string> = {};
|
||||||
|
Object.keys(selectedServer.config.customUserVars).forEach((key) => {
|
||||||
|
values[key] = '';
|
||||||
|
});
|
||||||
|
return values;
|
||||||
|
}, [selectedServer]);
|
||||||
|
|
||||||
if (startupConfigLoading) {
|
if (startupConfigLoading) {
|
||||||
return <MCPPanelSkeleton />;
|
return <MCPPanelSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
@ -229,21 +261,11 @@ export default function MCPPanel() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedServerNameForEditing) {
|
if (selectedServerNameForEditing && selectedServer) {
|
||||||
// Editing View
|
// Editing View - use MCPConfigDialog-style layout but inline
|
||||||
const serverBeingEdited = mcpServerDefinitions.find(
|
const serverStatus = mcpServerStatuses[selectedServerNameForEditing];
|
||||||
(s) => s.serverName === selectedServerNameForEditing,
|
const isConnected = serverStatus?.connected || false;
|
||||||
);
|
const requiresOAuth = serverStatus?.requiresOAuth || false;
|
||||||
|
|
||||||
if (!serverBeingEdited) {
|
|
||||||
// Fallback to list view if server not found
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-auto max-w-full overflow-x-hidden p-3">
|
<div className="h-auto max-w-full overflow-x-hidden p-3">
|
||||||
|
|
@ -255,15 +277,48 @@ export default function MCPPanel() {
|
||||||
<ChevronLeft className="mr-1 h-4 w-4" />
|
<ChevronLeft className="mr-1 h-4 w-4" />
|
||||||
{localize('com_ui_back')}
|
{localize('com_ui_back')}
|
||||||
</Button>
|
</Button>
|
||||||
<h3 className="mb-3 text-lg font-medium">
|
|
||||||
{localize('com_sidepanel_mcp_variables_for', { '0': serverBeingEdited.serverName })}
|
{/* Header with status */}
|
||||||
</h3>
|
<div className="mb-4">
|
||||||
<MCPVariableEditor
|
<div className="mb-2 flex items-center gap-3">
|
||||||
server={serverBeingEdited}
|
<h3 className="text-lg font-medium">
|
||||||
onSave={handleSaveServerVars}
|
{localize('com_sidepanel_mcp_variables_for', { '0': selectedServer.serverName })}
|
||||||
onRevoke={handleRevokeServerVars}
|
</h3>
|
||||||
isSubmitting={updateUserPluginsMutation.isLoading}
|
{isConnected && (
|
||||||
/>
|
<div className="flex items-center gap-2 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700 dark:bg-green-900 dark:text-green-300">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-green-500" />
|
||||||
|
<span>{localize('com_ui_active')}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-text-secondary">
|
||||||
|
{Object.keys(fieldsSchema).length > 0
|
||||||
|
? localize('com_ui_mcp_dialog_desc')
|
||||||
|
: `Manage connection and settings for the ${selectedServer.serverName} MCP server.`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content sections */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Custom User Variables Section */}
|
||||||
|
{Object.keys(fieldsSchema).length > 0 && (
|
||||||
|
<div>
|
||||||
|
<CustomUserVarsSection
|
||||||
|
serverName={selectedServer.serverName}
|
||||||
|
fields={fieldsSchema}
|
||||||
|
onSave={handleSave}
|
||||||
|
onRevoke={handleRevoke}
|
||||||
|
isSubmitting={updateUserPluginsMutation.isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Server Initialization Section */}
|
||||||
|
<ServerInitializationSection
|
||||||
|
serverName={selectedServer.serverName}
|
||||||
|
requiresOAuth={requiresOAuth}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -271,124 +326,46 @@ export default function MCPPanel() {
|
||||||
return (
|
return (
|
||||||
<div className="h-auto max-w-full overflow-x-hidden p-3">
|
<div className="h-auto max-w-full overflow-x-hidden p-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{mcpServerDefinitions.map((server) => (
|
{mcpServerDefinitions.map((server) => {
|
||||||
<div key={server.serverName} className="flex items-center gap-2">
|
const serverStatus = mcpServerStatuses[server.serverName];
|
||||||
<Button
|
const isConnected = serverStatus?.connected || false;
|
||||||
variant="outline"
|
|
||||||
className="flex-1 justify-start dark:hover:bg-gray-700"
|
return (
|
||||||
onClick={() => handleServerClickToEdit(server.serverName)}
|
<div key={server.serverName} className="flex items-center gap-2">
|
||||||
>
|
<Button
|
||||||
{server.serverName}
|
variant="outline"
|
||||||
</Button>
|
className="flex-1 justify-start dark:hover:bg-gray-700"
|
||||||
<Button
|
onClick={() => handleServerClickToEdit(server.serverName)}
|
||||||
variant="ghost"
|
>
|
||||||
size="sm"
|
<div className="flex w-full items-center gap-2">
|
||||||
onClick={() => handleReinitializeServer(server.serverName)}
|
<span>{server.serverName}</span>
|
||||||
className="px-2 py-1"
|
{isConnected && (
|
||||||
title="Reinitialize MCP server"
|
<div className="ml-auto flex items-center gap-1">
|
||||||
disabled={reinitializeMCPMutation.isLoading}
|
<div className="h-1.5 w-1.5 rounded-full bg-green-500" />
|
||||||
>
|
<span className="text-xs text-green-600 dark:text-green-400">
|
||||||
<RefreshCw
|
{localize('com_ui_active')}
|
||||||
className={`h-4 w-4 ${rotatingServers.has(server.serverName) ? 'animate-spin' : ''}`}
|
</span>
|
||||||
/>
|
</div>
|
||||||
</Button>
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleReinitializeServer(server.serverName)}
|
||||||
|
className="px-2 py-1"
|
||||||
|
title="Reinitialize MCP server"
|
||||||
|
disabled={reinitializeMCPMutation.isLoading}
|
||||||
|
>
|
||||||
|
<RefreshCw
|
||||||
|
className={`h-4 w-4 ${rotatingServers.has(server.serverName) ? 'animate-spin' : ''}`}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inner component for the form - remains the same
|
|
||||||
interface MCPVariableEditorProps {
|
|
||||||
server: ServerConfigWithVars;
|
|
||||||
onSave: (serverName: string, updatedValues: Record<string, string>) => 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<Record<string, string>>({
|
|
||||||
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<string, string>,
|
|
||||||
);
|
|
||||||
reset(initialFormValues);
|
|
||||||
}, [reset, server.config.customUserVars]);
|
|
||||||
|
|
||||||
const onFormSubmit = (data: Record<string, string>) => {
|
|
||||||
onSave(server.serverName, data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRevokeClick = () => {
|
|
||||||
onRevoke(server.serverName);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)} className="mb-4 mt-2 space-y-4">
|
|
||||||
{Object.entries(server.config.customUserVars).map(([key, details]) => (
|
|
||||||
<div key={key} className="space-y-2">
|
|
||||||
<Label htmlFor={`${server.serverName}-${key}`} className="text-sm font-medium">
|
|
||||||
{details.title}
|
|
||||||
</Label>
|
|
||||||
<Controller
|
|
||||||
name={key}
|
|
||||||
control={control}
|
|
||||||
defaultValue={''}
|
|
||||||
render={({ field }) => (
|
|
||||||
<Input
|
|
||||||
id={`${server.serverName}-${key}`}
|
|
||||||
type="text"
|
|
||||||
{...field}
|
|
||||||
placeholder={localize('com_sidepanel_mcp_enter_value', { '0': details.title })}
|
|
||||||
className="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white sm:text-sm"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{details.description && (
|
|
||||||
<p
|
|
||||||
className="text-xs text-text-secondary [&_a]:text-blue-500 [&_a]:hover:text-blue-600 dark:[&_a]:text-blue-400 dark:[&_a]:hover:text-blue-300"
|
|
||||||
dangerouslySetInnerHTML={{ __html: details.description }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{errors[key] && <p className="text-xs text-red-500">{errors[key]?.message}</p>}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div className="flex justify-end gap-2 pt-2">
|
|
||||||
{Object.keys(server.config.customUserVars).length > 0 && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
onClick={handleRevokeClick}
|
|
||||||
className="bg-red-600 text-white hover:bg-red-700 dark:hover:bg-red-800"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
{localize('com_ui_revoke')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className="bg-green-500 text-white hover:bg-green-600"
|
|
||||||
disabled={isSubmitting || !isDirty}
|
|
||||||
>
|
|
||||||
{isSubmitting ? localize('com_ui_saving') : localize('com_ui_save')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import React, { useMemo, useCallback } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import { useMCPConnectionStatusQuery } from '~/data-provider/Tools/queries';
|
import { useMCPConnectionStatusQuery } from '~/data-provider/Tools/queries';
|
||||||
import { CustomUserVarsSection, ServerInitializationSection } from './';
|
import { CustomUserVarsSection, ServerInitializationSection } from './';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
|
||||||
import { QueryKeys } from 'librechat-data-provider';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
OGDialog,
|
OGDialog,
|
||||||
|
|
@ -46,10 +44,9 @@ export default function MCPConfigDialog({
|
||||||
serverName,
|
serverName,
|
||||||
}: MCPConfigDialogProps) {
|
}: MCPConfigDialogProps) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
// Get connection status to determine OAuth requirements with aggressive refresh
|
// Get connection status to determine OAuth requirements with aggressive refresh
|
||||||
const { data: statusQuery, refetch: refetchConnectionStatus } = useMCPConnectionStatusQuery({
|
const { data: statusQuery } = useMCPConnectionStatusQuery({
|
||||||
refetchOnMount: true,
|
refetchOnMount: true,
|
||||||
refetchOnWindowFocus: true,
|
refetchOnWindowFocus: true,
|
||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
|
|
|
||||||
|
|
@ -967,8 +967,6 @@
|
||||||
"com_ui_saving": "Saving...",
|
"com_ui_saving": "Saving...",
|
||||||
"com_ui_set": "Set",
|
"com_ui_set": "Set",
|
||||||
"com_ui_unset": "Unset",
|
"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_authorization_url": "Authorization URL",
|
||||||
"com_ui_continue_oauth": "Continue OAuth Flow",
|
"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.",
|
"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_x_selected": "{{0}} selected",
|
||||||
"com_ui_yes": "Yes",
|
"com_ui_yes": "Yes",
|
||||||
"com_ui_zoom": "Zoom",
|
"com_ui_zoom": "Zoom",
|
||||||
"com_user_message": "You"
|
"com_user_message": "You"
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue