🧩 refactor: Decouple MCP Config from Startup Config (#10689)

* Decouple mcp config from start up config

* Chore: Work on AI Review and Copilot Comments

- setRawConfig is not needed since the private raw config is not needed any more
- !!serversLoading bug fixed
- added unit tests for route /api/mcp/servers
- copilot comments addressed

* chore: remove comments

* chore: rename data-provider dir for MCP

* chore: reorganize mcp specific query hooks

* fix: consolidate imports for MCP server manager

* chore: add dev-staging branch to frontend review workflow triggers

* feat: add GitHub Actions workflow for building and pushing Docker images to GitHub Container Registry and Docker Hub

* fix: update label for tag input in BookmarkForm tests to improve clarity

---------

Co-authored-by: Atef Bellaaj <slalom.bellaaj@external.daimlertruck.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Atef Bellaaj 2025-11-26 21:26:40 +01:00 committed by Danny Avila
parent 98b188f26c
commit ef1b7f0157
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
36 changed files with 548 additions and 301 deletions

View file

@ -223,7 +223,7 @@ describe('BookmarkForm - Bookmark Editing', () => {
/>,
);
const tagInput = screen.getByLabelText('Edit Bookmark');
const tagInput = screen.getByLabelText('Title');
await act(async () => {
fireEvent.change(tagInput, { target: { value: 'Existing Tag' } });
@ -265,7 +265,7 @@ describe('BookmarkForm - Bookmark Editing', () => {
/>,
);
const tagInput = screen.getByLabelText('Edit Bookmark');
const tagInput = screen.getByLabelText('Title');
await act(async () => {
fireEvent.change(tagInput, { target: { value: 'Existing Tag' } });
@ -308,7 +308,7 @@ describe('BookmarkForm - Bookmark Editing', () => {
/>,
);
const tagInput = screen.getByLabelText('Edit Bookmark');
const tagInput = screen.getByLabelText('Title');
await act(async () => {
fireEvent.change(tagInput, { target: { value: 'Brand New Tag' } });
@ -401,7 +401,7 @@ describe('BookmarkForm - Bookmark Editing', () => {
/>,
);
const tagInput = screen.getByLabelText('Edit Bookmark');
const tagInput = screen.getByLabelText('Title');
await act(async () => {
fireEvent.change(tagInput, { target: { value: 'Props Tag' } });
@ -477,7 +477,7 @@ describe('BookmarkForm - Bookmark Editing', () => {
/>,
);
const tagInput = screen.getByLabelText('Edit Bookmark');
const tagInput = screen.getByLabelText('Title');
await act(async () => {
fireEvent.change(tagInput, { target: { value: 'New Tag' } });

View file

@ -12,10 +12,10 @@ function MCPSelectContent() {
mcpValues,
isInitializing,
placeholderText,
configuredServers,
batchToggleServers,
getConfigDialogProps,
getServerStatusIconProps,
availableMCPServers,
} = mcpServerManager;
const renderSelectedValues = useCallback(
@ -78,7 +78,7 @@ function MCPSelectContent() {
return (
<>
<MultiSelect
items={configuredServers}
items={availableMCPServers?.map((s) => s.serverName)}
selectedValues={mcpValues ?? []}
setSelectedValues={batchToggleServers}
renderSelectedValues={renderSelectedValues}
@ -99,9 +99,9 @@ function MCPSelectContent() {
function MCPSelect() {
const { mcpServerManager } = useBadgeRowContext();
const { configuredServers } = mcpServerManager;
const { availableMCPServers } = mcpServerManager;
if (!configuredServers || configuredServers.length === 0) {
if (!availableMCPServers || availableMCPServers.length === 0) {
return null;
}

View file

@ -20,7 +20,7 @@ const MCPSubMenu = React.forwardRef<HTMLDivElement, MCPSubMenuProps>(
setIsPinned,
isInitializing,
placeholderText,
configuredServers,
availableMCPServers,
getConfigDialogProps,
toggleServerSelection,
getServerStatusIconProps,
@ -33,7 +33,7 @@ const MCPSubMenu = React.forwardRef<HTMLDivElement, MCPSubMenuProps>(
});
// Don't render if no MCP servers are configured
if (!configuredServers || configuredServers.length === 0) {
if (!availableMCPServers || availableMCPServers.length === 0) {
return null;
}
@ -85,19 +85,19 @@ const MCPSubMenu = React.forwardRef<HTMLDivElement, MCPSubMenuProps>(
'border border-border-light bg-surface-secondary p-1 shadow-lg',
)}
>
{configuredServers.map((serverName) => {
const statusIconProps = getServerStatusIconProps(serverName);
const isSelected = mcpValues?.includes(serverName) ?? false;
const isServerInitializing = isInitializing(serverName);
{availableMCPServers.map((s) => {
const statusIconProps = getServerStatusIconProps(s.serverName);
const isSelected = mcpValues?.includes(s.serverName) ?? false;
const isServerInitializing = isInitializing(s.serverName);
const statusIcon = statusIconProps && <MCPServerStatusIcon {...statusIconProps} />;
return (
<Ariakit.MenuItem
key={serverName}
key={s.serverName}
onClick={(event) => {
event.preventDefault();
toggleServerSelection(serverName);
toggleServerSelection(s.serverName);
}}
disabled={isServerInitializing}
className={cn(
@ -112,7 +112,7 @@ const MCPSubMenu = React.forwardRef<HTMLDivElement, MCPSubMenuProps>(
>
<div className="flex flex-grow items-center gap-2">
<Ariakit.MenuItemCheck checked={isSelected} />
<span>{serverName}</span>
<span>{s.serverName}</span>
</div>
{statusIcon && <div className="ml-2 flex items-center">{statusIcon}</div>}
</Ariakit.MenuItem>

View file

@ -21,12 +21,17 @@ export default function ServerInitializationSection({
}: ServerInitializationSectionProps) {
const localize = useLocalize();
const { initializeServer, cancelOAuthFlow, isInitializing, isCancellable, getOAuthUrl } =
useMCPServerManager({ conversationId });
const {
initializeServer,
availableMCPServers,
cancelOAuthFlow,
isInitializing,
isCancellable,
getOAuthUrl,
} = useMCPServerManager({ conversationId });
const { data: startupConfig } = useGetStartupConfig();
const { connectionStatus } = useMCPConnectionStatus({
enabled: !!startupConfig?.mcpServers && Object.keys(startupConfig.mcpServers).length > 0,
enabled: !!availableMCPServers && availableMCPServers.length > 0,
});
const serverStatus = connectionStatus?.[serverName];

View file

@ -49,7 +49,7 @@ export default function AgentConfig() {
setAction,
regularTools,
agentsConfig,
startupConfig,
availableMCPServers,
mcpServersMap,
setActivePanel,
endpointsConfig,
@ -305,7 +305,7 @@ export default function AgentConfig() {
</div>
)}
{/* MCP Section */}
{startupConfig?.mcpServers != null && (
{availableMCPServers != null && availableMCPServers.length > 0 && (
<MCPTools
agentId={agent_id}
mcpServerNames={mcpServerNames}
@ -491,7 +491,7 @@ export default function AgentConfig() {
setIsOpen={setShowToolDialog}
endpoint={EModelEndpoint.agents}
/>
{startupConfig?.mcpServers != null && (
{availableMCPServers != null && availableMCPServers.length > 0 && (
<MCPToolSelectDialog
agentId={agent_id}
isOpen={showMCPToolDialog}

View file

@ -1,4 +1,4 @@
import React, { useState, useMemo, useCallback } from 'react';
import React, { useState, useCallback } from 'react';
import { ChevronLeft, Trash2 } from 'lucide-react';
import { useQueryClient } from '@tanstack/react-query';
import { Button, useToastContext } from '@librechat/client';
@ -8,8 +8,7 @@ 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 { useLocalize, useMCPServerManager } from '~/hooks';
import MCPPanelSkeleton from './MCPPanelSkeleton';
function MCPPanelContent() {
@ -17,9 +16,8 @@ function MCPPanelContent() {
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 { availableMCPServers, isLoading, connectionStatus } = useMCPServerManager({
conversationId,
});
const [selectedServerNameForEditing, setSelectedServerNameForEditing] = useState<string | null>(
@ -45,20 +43,6 @@ function MCPPanelContent() {
},
});
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);
};
@ -94,11 +78,11 @@ function MCPPanelContent() {
[updateUserPluginsMutation],
);
if (startupConfigLoading) {
if (isLoading) {
return <MCPPanelSkeleton />;
}
if (mcpServerDefinitions.length === 0) {
if (availableMCPServers.length === 0) {
return (
<div className="p-4 text-center text-sm text-gray-500">
{localize('com_sidepanel_mcp_no_servers_with_vars')}
@ -108,7 +92,7 @@ function MCPPanelContent() {
if (selectedServerNameForEditing) {
// Editing View
const serverBeingEdited = mcpServerDefinitions.find(
const serverBeingEdited = availableMCPServers.find(
(s) => s.serverName === selectedServerNameForEditing,
);
@ -140,7 +124,7 @@ function MCPPanelContent() {
<div className="mb-4">
<CustomUserVarsSection
serverName={selectedServerNameForEditing}
fields={serverBeingEdited.config.customUserVars}
fields={serverBeingEdited.config.customUserVars || {}}
onSave={(authData) => {
if (selectedServerNameForEditing) {
handleConfigSave(selectedServerNameForEditing, authData);
@ -184,7 +168,7 @@ function MCPPanelContent() {
return (
<div className="h-auto max-w-full overflow-x-hidden py-2">
<div className="space-y-2">
{mcpServerDefinitions.map((server) => {
{availableMCPServers.map((server) => {
const serverStatus = connectionStatus?.[server.serverName];
const isConnected = serverStatus?.connectionState === 'connected';

View file

@ -34,7 +34,7 @@ function MCPToolSelectDialog({
const { initializeServer } = useMCPServerManager();
const { getValues, setValue } = useFormContext<AgentForm>();
const { removeTool } = useRemoveMCPTool({ showToast: false });
const { mcpServersMap, startupConfig } = useAgentPanelContext();
const { mcpServersMap, availableMCPServersMap } = useAgentPanelContext();
const { refetch: refetchMCPTools } = useMCPToolsQuery({
enabled: mcpServersMap.size > 0,
});
@ -191,7 +191,7 @@ function MCPToolSelectDialog({
return;
}
const serverConfig = startupConfig?.mcpServers?.[serverName];
const serverConfig = availableMCPServersMap?.[serverName];
const hasCustomUserVars =
serverConfig?.customUserVars && Object.keys(serverConfig.customUserVars).length > 0;
@ -300,7 +300,7 @@ function MCPToolSelectDialog({
<CustomUserVarsSection
serverName={configuringServer}
isSubmitting={isSavingCustomVars}
fields={startupConfig?.mcpServers?.[configuringServer]?.customUserVars || {}}
fields={availableMCPServersMap?.[configuringServer]?.customUserVars || {}}
onSave={(authData) => handleSaveCustomVars(configuringServer, authData)}
onRevoke={() => handleRevokeCustomVars(configuringServer)}
/>