2025-06-19 18:27:55 -04:00
|
|
|
import { Constants } from 'librechat-data-provider';
|
2025-07-11 19:44:19 -07:00
|
|
|
import { ChevronLeft, RefreshCw } from 'lucide-react';
|
2025-07-21 07:47:33 -07:00
|
|
|
import React, { useState, useCallback, useMemo } from 'react';
|
2025-07-11 19:44:19 -07:00
|
|
|
import {
|
|
|
|
|
useUpdateUserPluginsMutation,
|
|
|
|
|
useReinitializeMCPServerMutation,
|
|
|
|
|
} from 'librechat-data-provider/react-query';
|
2025-07-21 01:29:33 -07:00
|
|
|
import { useQueryClient } from '@tanstack/react-query';
|
|
|
|
|
import { QueryKeys } from 'librechat-data-provider';
|
2025-06-19 18:27:55 -04:00
|
|
|
import type { TUpdateUserPlugins } from 'librechat-data-provider';
|
2025-07-21 07:47:33 -07:00
|
|
|
import { Button } from '~/components/ui';
|
2025-06-19 18:27:55 -04:00
|
|
|
import { useGetStartupConfig } from '~/data-provider';
|
2025-07-21 07:47:33 -07:00
|
|
|
import { useMCPConnectionStatusQuery } from '~/data-provider/Tools/queries';
|
2025-06-19 18:27:55 -04:00
|
|
|
import MCPPanelSkeleton from './MCPPanelSkeleton';
|
|
|
|
|
import { useToastContext } from '~/Providers';
|
|
|
|
|
import { useLocalize } from '~/hooks';
|
2025-07-21 07:47:33 -07:00
|
|
|
import {
|
|
|
|
|
CustomUserVarsSection,
|
|
|
|
|
ServerInitializationSection,
|
|
|
|
|
type ConfigFieldDetail,
|
|
|
|
|
} from '~/components/ui/MCP';
|
2025-06-19 18:27:55 -04:00
|
|
|
|
|
|
|
|
interface ServerConfigWithVars {
|
|
|
|
|
serverName: string;
|
|
|
|
|
config: {
|
|
|
|
|
customUserVars: Record<string, { title: string; description: string }>;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function MCPPanel() {
|
|
|
|
|
const localize = useLocalize();
|
|
|
|
|
const { showToast } = useToastContext();
|
|
|
|
|
const { data: startupConfig, isLoading: startupConfigLoading } = useGetStartupConfig();
|
|
|
|
|
const [selectedServerNameForEditing, setSelectedServerNameForEditing] = useState<string | null>(
|
|
|
|
|
null,
|
|
|
|
|
);
|
2025-07-11 19:44:19 -07:00
|
|
|
const [rotatingServers, setRotatingServers] = useState<Set<string>>(new Set());
|
|
|
|
|
const reinitializeMCPMutation = useReinitializeMCPServerMutation();
|
2025-07-21 01:29:33 -07:00
|
|
|
const queryClient = useQueryClient();
|
2025-06-19 18:27:55 -04:00
|
|
|
|
2025-07-21 07:47:33 -07:00
|
|
|
// Get real connection status from MCPManager
|
|
|
|
|
const { data: statusQuery } = useMCPConnectionStatusQuery();
|
|
|
|
|
const mcpServerStatuses = useMemo(
|
|
|
|
|
() => statusQuery?.connectionStatus || {},
|
|
|
|
|
[statusQuery?.connectionStatus],
|
|
|
|
|
);
|
|
|
|
|
|
2025-06-19 18:27:55 -04:00
|
|
|
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({
|
2025-07-21 07:47:33 -07:00
|
|
|
onSuccess: async () => {
|
2025-06-19 18:27:55 -04:00
|
|
|
showToast({ message: localize('com_nav_mcp_vars_updated'), status: 'success' });
|
2025-07-21 01:29:33 -07:00
|
|
|
|
2025-07-21 07:47:33 -07:00
|
|
|
// 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]),
|
|
|
|
|
]);
|
2025-06-19 18:27:55 -04:00
|
|
|
},
|
2025-07-21 01:29:33 -07:00
|
|
|
onError: (error: unknown) => {
|
|
|
|
|
console.error('Error updating MCP auth:', error);
|
2025-06-19 18:27:55 -04:00
|
|
|
showToast({
|
|
|
|
|
message: localize('com_nav_mcp_vars_update_error'),
|
|
|
|
|
status: 'error',
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const handleSaveServerVars = useCallback(
|
|
|
|
|
(serverName: string, updatedValues: Record<string, string>) => {
|
|
|
|
|
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);
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-11 19:44:19 -07:00
|
|
|
const handleReinitializeServer = useCallback(
|
|
|
|
|
async (serverName: string) => {
|
|
|
|
|
setRotatingServers((prev) => new Set(prev).add(serverName));
|
|
|
|
|
try {
|
2025-07-21 01:29:33 -07:00
|
|
|
const response = await reinitializeMCPMutation.mutateAsync(serverName);
|
|
|
|
|
|
|
|
|
|
// Check if OAuth is required
|
|
|
|
|
if (response.oauthRequired) {
|
2025-07-21 07:47:33 -07:00
|
|
|
if (response.authURL) {
|
2025-07-21 01:29:33 -07:00
|
|
|
// 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
|
2025-07-21 07:47:33 -07:00
|
|
|
window.open(response.authURL, '_blank', 'noopener,noreferrer');
|
2025-07-21 01:29:33 -07:00
|
|
|
|
|
|
|
|
// 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',
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-07-11 19:44:19 -07:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error reinitializing MCP server:', error);
|
2025-07-21 01:29:33 -07:00
|
|
|
|
|
|
|
|
// Check if the error response contains OAuth information
|
2025-07-21 07:47:33 -07:00
|
|
|
if ((error as any)?.response?.data?.oauthRequired) {
|
|
|
|
|
const errorData = (error as any).response.data;
|
|
|
|
|
if (errorData.authURL) {
|
2025-07-21 01:29:33 -07:00
|
|
|
showToast({
|
|
|
|
|
message: `OAuth required for ${serverName}. Please visit the authorization URL.`,
|
|
|
|
|
status: 'info',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Open OAuth URL in new window/tab
|
2025-07-21 07:47:33 -07:00
|
|
|
window.open(errorData.authURL, '_blank', 'noopener,noreferrer');
|
2025-07-21 01:29:33 -07:00
|
|
|
|
|
|
|
|
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',
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-07-11 19:44:19 -07:00
|
|
|
} finally {
|
|
|
|
|
setRotatingServers((prev) => {
|
|
|
|
|
const next = new Set(prev);
|
|
|
|
|
next.delete(serverName);
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[showToast, reinitializeMCPMutation],
|
|
|
|
|
);
|
|
|
|
|
|
2025-07-21 07:47:33 -07:00
|
|
|
// 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]);
|
|
|
|
|
|
2025-06-19 18:27:55 -04:00
|
|
|
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>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-21 07:47:33 -07:00
|
|
|
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;
|
2025-06-19 18:27:55 -04:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="h-auto max-w-full overflow-x-hidden p-3">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={handleGoBackToList}
|
|
|
|
|
className="mb-3 flex items-center px-3 py-2 text-sm"
|
|
|
|
|
>
|
|
|
|
|
<ChevronLeft className="mr-1 h-4 w-4" />
|
|
|
|
|
{localize('com_ui_back')}
|
|
|
|
|
</Button>
|
2025-07-21 07:47:33 -07:00
|
|
|
|
|
|
|
|
{/* Header with status */}
|
|
|
|
|
<div className="mb-4">
|
|
|
|
|
<div className="mb-2 flex items-center gap-3">
|
|
|
|
|
<h3 className="text-lg font-medium">
|
|
|
|
|
{localize('com_sidepanel_mcp_variables_for', { '0': selectedServer.serverName })}
|
|
|
|
|
</h3>
|
|
|
|
|
{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>
|
2025-06-19 18:27:55 -04:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
// Server List View
|
|
|
|
|
return (
|
|
|
|
|
<div className="h-auto max-w-full overflow-x-hidden p-3">
|
|
|
|
|
<div className="space-y-2">
|
2025-07-21 07:47:33 -07:00
|
|
|
{mcpServerDefinitions.map((server) => {
|
|
|
|
|
const serverStatus = mcpServerStatuses[server.serverName];
|
|
|
|
|
const isConnected = serverStatus?.connected || false;
|
|
|
|
|
|
|
|
|
|
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)}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex w-full items-center gap-2">
|
|
|
|
|
<span>{server.serverName}</span>
|
|
|
|
|
{isConnected && (
|
|
|
|
|
<div className="ml-auto flex items-center gap-1">
|
|
|
|
|
<div className="h-1.5 w-1.5 rounded-full bg-green-500" />
|
|
|
|
|
<span className="text-xs text-green-600 dark:text-green-400">
|
|
|
|
|
{localize('com_ui_active')}
|
|
|
|
|
</span>
|
|
|
|
|
</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>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2025-06-19 18:27:55 -04:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|