🧩 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

@ -1,7 +1,7 @@
import { useCallback, useState, useMemo, useRef, useEffect } from 'react';
import { useToastContext } from '@librechat/client';
import { useQueryClient } from '@tanstack/react-query';
import { Constants, QueryKeys } from 'librechat-data-provider';
import { Constants, QueryKeys, MCPOptions } from 'librechat-data-provider';
import {
useCancelMCPOAuthMutation,
useUpdateUserPluginsMutation,
@ -10,7 +10,15 @@ import {
import type { TUpdateUserPlugins, TPlugin, MCPServersResponse } from 'librechat-data-provider';
import type { ConfigFieldDetail } from '~/common';
import { useLocalize, useMCPSelect, useMCPConnectionStatus } from '~/hooks';
import { useGetStartupConfig } from '~/data-provider';
import { useGetStartupConfig, useMCPServersQuery } from '~/data-provider';
export interface MCPServerDefinition {
serverName: string;
config: MCPOptions;
mcp_id?: string;
_id?: string; // MongoDB ObjectId for database servers (used for permissions)
effectivePermissions: number; // Permission bits (VIEW=1, EDIT=2, DELETE=4, SHARE=8)
}
interface ServerState {
isInitializing: boolean;
@ -24,12 +32,40 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
const localize = useLocalize();
const queryClient = useQueryClient();
const { showToast } = useToastContext();
const { data: startupConfig } = useGetStartupConfig();
const { mcpValues, setMCPValues, isPinned, setIsPinned } = useMCPSelect({ conversationId });
const { data: startupConfig } = useGetStartupConfig(); // Keep for UI config only
const { data: loadedServers, isLoading } = useMCPServersQuery();
const [isConfigModalOpen, setIsConfigModalOpen] = useState(false);
const [selectedToolForConfig, setSelectedToolForConfig] = useState<TPlugin | null>(null);
const previousFocusRef = useRef<HTMLElement | null>(null);
const configuredServers = useMemo(() => {
if (!loadedServers) return [];
return Object.keys(loadedServers).filter((name) => loadedServers[name]?.chatMenu !== false);
}, [loadedServers]);
const availableMCPServers: MCPServerDefinition[] = useMemo<MCPServerDefinition[]>(() => {
const definitions: MCPServerDefinition[] = [];
if (loadedServers) {
for (const [serverName, metadata] of Object.entries(loadedServers)) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { _id, mcp_id, effectivePermissions, author, updatedAt, createdAt, ...config } =
metadata;
definitions.push({
serverName,
mcp_id,
effectivePermissions: effectivePermissions || 1,
config,
});
}
}
return definitions;
}, [loadedServers]);
const { mcpValues, setMCPValues, isPinned, setIsPinned } = useMCPSelect({
conversationId,
servers: availableMCPServers,
});
const mcpValuesRef = useRef(mcpValues);
// fixes the issue where OAuth flows would deselect all the servers except the one that is being authenticated on success
@ -37,13 +73,6 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
mcpValuesRef.current = mcpValues;
}, [mcpValues]);
const configuredServers = useMemo(() => {
if (!startupConfig?.mcpServers) return [];
return Object.entries(startupConfig.mcpServers)
.filter(([, config]) => config.chatMenu !== false)
.map(([serverName]) => serverName);
}, [startupConfig?.mcpServers]);
const reinitializeMutation = useReinitializeMCPServerMutation();
const cancelOAuthMutation = useCancelMCPOAuthMutation();
@ -52,6 +81,7 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
showToast({ message: localize('com_nav_mcp_vars_updated'), status: 'success' });
await Promise.all([
queryClient.invalidateQueries([QueryKeys.mcpServers]),
queryClient.invalidateQueries([QueryKeys.mcpTools]),
queryClient.invalidateQueries([QueryKeys.mcpAuthValues]),
queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]),
@ -81,7 +111,7 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
});
const { connectionStatus } = useMCPConnectionStatus({
enabled: !!startupConfig?.mcpServers && Object.keys(startupConfig.mcpServers).length > 0,
enabled: !isLoading && configuredServers.length > 0,
});
const updateServerState = useCallback((serverName: string, updates: Partial<ServerState>) => {
@ -289,7 +319,12 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
startServerPolling(serverName);
} else {
await queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]);
await Promise.all([
queryClient.invalidateQueries([QueryKeys.mcpServers]),
queryClient.invalidateQueries([QueryKeys.mcpTools]),
queryClient.invalidateQueries([QueryKeys.mcpAuthValues]),
queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]),
]);
showToast({
message: localize('com_ui_mcp_initialized_success', { 0: serverName }),
@ -494,7 +529,7 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
]);
const serverData = mcpData?.servers?.[serverName];
const serverStatus = connectionStatus?.[serverName];
const serverConfig = startupConfig?.mcpServers?.[serverName];
const serverConfig = loadedServers?.[serverName];
const handleConfigClick = (e: React.MouseEvent) => {
e.stopPropagation();
@ -548,14 +583,7 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
hasCustomUserVars,
};
},
[
queryClient,
isCancellable,
isInitializing,
cancelOAuthFlow,
connectionStatus,
startupConfig?.mcpServers,
],
[queryClient, isCancellable, isInitializing, cancelOAuthFlow, connectionStatus, loadedServers],
);
const getConfigDialogProps = useCallback(() => {
@ -600,7 +628,10 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
]);
return {
configuredServers,
availableMCPServers,
availableMCPServersMap: loadedServers,
isLoading,
connectionStatus,
initializeServer,
cancelOAuthFlow,
isInitializing,