mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 17:30:16 +01:00
🧩 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:
parent
98b188f26c
commit
ef1b7f0157
36 changed files with 548 additions and 301 deletions
|
|
@ -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' } });
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue