mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-01 16:18:51 +01:00
🧬 refactor: Optimize MCP Tool Queries with Server-Centric Architecture
🧬 refactor: Optimize MCP Tool Queries with Server-Centric Architecture
refactor: optimize mcp tool queries by removing redundancy, making server-centric structure, enabling query only when expected, minimize looping/transforming query data, eliminating unused/compute-heavy methods
ci: MCP Server Tools Mocking in Agent Tests
This commit is contained in:
parent
5b1a31ef4d
commit
f0599ad36c
19 changed files with 235 additions and 1104 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import React, { createContext, useContext, useState, useMemo } from 'react';
|
||||
import { Constants, EModelEndpoint } from 'librechat-data-provider';
|
||||
import { EModelEndpoint } from 'librechat-data-provider';
|
||||
import type { MCP, Action, TPlugin } from 'librechat-data-provider';
|
||||
import type { AgentPanelContextType, MCPServerInfo } from '~/common';
|
||||
import {
|
||||
|
|
@ -30,6 +30,7 @@ export function AgentPanelProvider({ children }: { children: React.ReactNode })
|
|||
const [activePanel, setActivePanel] = useState<Panel>(Panel.builder);
|
||||
const [agent_id, setCurrentAgentId] = useState<string | undefined>(undefined);
|
||||
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const { data: actions } = useGetActionsQuery(EModelEndpoint.agents, {
|
||||
enabled: !!agent_id,
|
||||
});
|
||||
|
|
@ -38,11 +39,10 @@ export function AgentPanelProvider({ children }: { children: React.ReactNode })
|
|||
enabled: !!agent_id,
|
||||
});
|
||||
|
||||
const { data: mcpTools } = useMCPToolsQuery({
|
||||
enabled: !!agent_id,
|
||||
const { data: mcpData } = useMCPToolsQuery({
|
||||
enabled: !!agent_id && startupConfig?.mcpServers != null,
|
||||
});
|
||||
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const { agentsConfig, endpointsConfig } = useGetAgentsConfig();
|
||||
const mcpServerNames = useMemo(
|
||||
() => Object.keys(startupConfig?.mcpServers ?? {}),
|
||||
|
|
@ -57,33 +57,34 @@ export function AgentPanelProvider({ children }: { children: React.ReactNode })
|
|||
const configuredServers = new Set(mcpServerNames);
|
||||
const serversMap = new Map<string, MCPServerInfo>();
|
||||
|
||||
if (mcpTools) {
|
||||
for (const pluginTool of mcpTools) {
|
||||
if (pluginTool.pluginKey.includes(Constants.mcp_delimiter)) {
|
||||
const [_toolName, serverName] = pluginTool.pluginKey.split(Constants.mcp_delimiter);
|
||||
if (mcpData?.servers) {
|
||||
for (const [serverName, serverData] of Object.entries(mcpData.servers)) {
|
||||
const metadata = {
|
||||
name: serverName,
|
||||
pluginKey: serverName,
|
||||
description: `${localize('com_ui_tool_collection_prefix')} ${serverName}`,
|
||||
icon: serverData.icon || '',
|
||||
authConfig: serverData.authConfig,
|
||||
authenticated: serverData.authenticated,
|
||||
} as TPlugin;
|
||||
|
||||
if (!serversMap.has(serverName)) {
|
||||
const metadata = {
|
||||
name: serverName,
|
||||
pluginKey: serverName,
|
||||
description: `${localize('com_ui_tool_collection_prefix')} ${serverName}`,
|
||||
icon: pluginTool.icon || '',
|
||||
} as TPlugin;
|
||||
const tools = serverData.tools.map((tool) => ({
|
||||
tool_id: tool.pluginKey,
|
||||
metadata: {
|
||||
...tool,
|
||||
icon: serverData.icon,
|
||||
authConfig: serverData.authConfig,
|
||||
authenticated: serverData.authenticated,
|
||||
} as TPlugin,
|
||||
}));
|
||||
|
||||
serversMap.set(serverName, {
|
||||
serverName,
|
||||
tools: [],
|
||||
isConfigured: configuredServers.has(serverName),
|
||||
isConnected: connectionStatus?.[serverName]?.connectionState === 'connected',
|
||||
metadata,
|
||||
});
|
||||
}
|
||||
|
||||
serversMap.get(serverName)!.tools.push({
|
||||
tool_id: pluginTool.pluginKey,
|
||||
metadata: pluginTool as TPlugin,
|
||||
});
|
||||
}
|
||||
serversMap.set(serverName, {
|
||||
serverName,
|
||||
tools,
|
||||
isConfigured: configuredServers.has(serverName),
|
||||
isConnected: connectionStatus?.[serverName]?.connectionState === 'connected',
|
||||
metadata,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -109,7 +110,7 @@ export function AgentPanelProvider({ children }: { children: React.ReactNode })
|
|||
}
|
||||
|
||||
return serversMap;
|
||||
}, [mcpTools, localize, mcpServerNames, connectionStatus]);
|
||||
}, [mcpData, localize, mcpServerNames, connectionStatus]);
|
||||
|
||||
const value: AgentPanelContextType = {
|
||||
mcp,
|
||||
|
|
@ -120,7 +121,6 @@ export function AgentPanelProvider({ children }: { children: React.ReactNode })
|
|||
setMcps,
|
||||
agent_id,
|
||||
setAction,
|
||||
mcpTools,
|
||||
activePanel,
|
||||
regularTools,
|
||||
agentsConfig,
|
||||
|
|
|
|||
|
|
@ -234,7 +234,6 @@ export type AgentPanelContextType = {
|
|||
setMcps: React.Dispatch<React.SetStateAction<t.MCP[] | undefined>>;
|
||||
activePanel?: string;
|
||||
regularTools?: t.TPlugin[];
|
||||
mcpTools?: t.TPlugin[];
|
||||
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
|
||||
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
agent_id?: string;
|
||||
|
|
|
|||
|
|
@ -473,13 +473,15 @@ export default function AgentConfig({ createMutation }: Pick<AgentPanelProps, 'c
|
|||
setIsOpen={setShowToolDialog}
|
||||
endpoint={EModelEndpoint.agents}
|
||||
/>
|
||||
<MCPToolSelectDialog
|
||||
agentId={agent_id}
|
||||
isOpen={showMCPToolDialog}
|
||||
mcpServerNames={mcpServerNames}
|
||||
setIsOpen={setShowMCPToolDialog}
|
||||
endpoint={EModelEndpoint.agents}
|
||||
/>
|
||||
{startupConfig?.mcpServers != null && (
|
||||
<MCPToolSelectDialog
|
||||
agentId={agent_id}
|
||||
isOpen={showMCPToolDialog}
|
||||
mcpServerNames={mcpServerNames}
|
||||
setIsOpen={setShowMCPToolDialog}
|
||||
endpoint={EModelEndpoint.agents}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ import type { TError, AgentToolType } from 'librechat-data-provider';
|
|||
import type { AgentForm, TPluginStoreDialogProps } from '~/common';
|
||||
import { useLocalize, usePluginDialogHelpers, useMCPServerManager } from '~/hooks';
|
||||
import CustomUserVarsSection from '~/components/MCP/CustomUserVarsSection';
|
||||
import { useGetStartupConfig, useMCPToolsQuery } from '~/data-provider';
|
||||
import { PluginPagination } from '~/components/Plugins/Store';
|
||||
import { useAgentPanelContext } from '~/Providers';
|
||||
import { useMCPToolsQuery } from '~/data-provider';
|
||||
import MCPToolItem from './MCPToolItem';
|
||||
|
||||
function MCPToolSelectDialog({
|
||||
|
|
@ -24,11 +24,12 @@ function MCPToolSelectDialog({
|
|||
endpoint: EModelEndpoint.agents;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const { mcpServersMap } = useAgentPanelContext();
|
||||
const { initializeServer } = useMCPServerManager();
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const { refetch: refetchMCPTools } = useMCPToolsQuery();
|
||||
const { getValues, setValue } = useFormContext<AgentForm>();
|
||||
const { mcpServersMap, startupConfig } = useAgentPanelContext();
|
||||
const { refetch: refetchMCPTools } = useMCPToolsQuery({
|
||||
enabled: mcpServersMap.size > 0,
|
||||
});
|
||||
|
||||
const [isInitializing, setIsInitializing] = useState<string | null>(null);
|
||||
const [configuringServer, setConfiguringServer] = useState<string | null>(null);
|
||||
|
|
@ -90,18 +91,17 @@ function MCPToolSelectDialog({
|
|||
setIsInitializing(null);
|
||||
},
|
||||
onSuccess: async () => {
|
||||
const { data: updatedMCPTools } = await refetchMCPTools();
|
||||
const { data: updatedMCPData } = await refetchMCPTools();
|
||||
|
||||
const currentTools = getValues('tools') || [];
|
||||
const toolsToAdd: string[] = [
|
||||
`${Constants.mcp_server}${Constants.mcp_delimiter}${serverName}`,
|
||||
];
|
||||
|
||||
if (updatedMCPTools) {
|
||||
updatedMCPTools.forEach((tool) => {
|
||||
if (tool.pluginKey.endsWith(`${Constants.mcp_delimiter}${serverName}`)) {
|
||||
toolsToAdd.push(tool.pluginKey);
|
||||
}
|
||||
if (updatedMCPData?.servers?.[serverName]) {
|
||||
const serverData = updatedMCPData.servers[serverName];
|
||||
serverData.tools.forEach((tool) => {
|
||||
toolsToAdd.push(tool.pluginKey);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,17 +5,17 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { QueryKeys, dataService } from 'librechat-data-provider';
|
||||
import type { UseQueryOptions, QueryObserverResult } from '@tanstack/react-query';
|
||||
import type { TPlugin } from 'librechat-data-provider';
|
||||
import type { MCPServersResponse } from 'librechat-data-provider';
|
||||
|
||||
/**
|
||||
* Hook for fetching MCP-specific tools
|
||||
* @param config - React Query configuration
|
||||
* @returns MCP tools grouped by server
|
||||
* @returns MCP servers with their tools
|
||||
*/
|
||||
export const useMCPToolsQuery = <TData = TPlugin[]>(
|
||||
config?: UseQueryOptions<TPlugin[], unknown, TData>,
|
||||
export const useMCPToolsQuery = <TData = MCPServersResponse>(
|
||||
config?: UseQueryOptions<MCPServersResponse, unknown, TData>,
|
||||
): QueryObserverResult<TData> => {
|
||||
return useQuery<TPlugin[], unknown, TData>(
|
||||
return useQuery<MCPServersResponse, unknown, TData>(
|
||||
[QueryKeys.mcpTools],
|
||||
() => dataService.getMCPTools(),
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type { TStartupConfig, TPlugin, TUser } from 'librechat-data-provider';
|
|||
import { mapPlugins, selectPlugins, processPlugins } from '~/utils';
|
||||
import { cleanupTimestampedStorage } from '~/utils/timestamps';
|
||||
import useSpeechSettingsInit from './useSpeechSettingsInit';
|
||||
import { useMCPToolsQuery } from '~/data-provider';
|
||||
import store from '~/store';
|
||||
|
||||
const pluginStore: TPlugin = {
|
||||
|
|
@ -35,6 +36,10 @@ export default function useAppStartup({
|
|||
|
||||
useSpeechSettingsInit(!!user);
|
||||
|
||||
useMCPToolsQuery({
|
||||
enabled: !!startupConfig?.mcpServers && !!user,
|
||||
});
|
||||
|
||||
/** Clean up old localStorage entries on startup */
|
||||
useEffect(() => {
|
||||
cleanupTimestampedStorage();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
export * from './useGetMCPTools';
|
||||
export * from './useMCPConnectionStatus';
|
||||
export * from './useMCPSelect';
|
||||
export * from './useVisibleTools';
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
import { useMemo } from 'react';
|
||||
import { Constants } from 'librechat-data-provider';
|
||||
import type { TPlugin } from 'librechat-data-provider';
|
||||
import { useMCPToolsQuery, useGetStartupConfig } from '~/data-provider';
|
||||
|
||||
/**
|
||||
* Hook for fetching and filtering MCP tools based on server configuration
|
||||
* Uses the dedicated MCP tools query instead of filtering from general tools
|
||||
*/
|
||||
export function useGetMCPTools() {
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
|
||||
// Use dedicated MCP tools query
|
||||
const { data: rawMcpTools } = useMCPToolsQuery({
|
||||
select: (data: TPlugin[]) => {
|
||||
// Group tools by server for easier management
|
||||
const mcpToolsMap = new Map<string, TPlugin>();
|
||||
data.forEach((tool) => {
|
||||
const parts = tool.pluginKey.split(Constants.mcp_delimiter);
|
||||
const serverName = parts[parts.length - 1];
|
||||
if (!mcpToolsMap.has(serverName)) {
|
||||
mcpToolsMap.set(serverName, {
|
||||
name: serverName,
|
||||
pluginKey: tool.pluginKey,
|
||||
authConfig: tool.authConfig,
|
||||
authenticated: tool.authenticated,
|
||||
});
|
||||
}
|
||||
});
|
||||
return Array.from(mcpToolsMap.values());
|
||||
},
|
||||
});
|
||||
|
||||
// Filter out servers that have chatMenu disabled
|
||||
const mcpToolDetails = useMemo(() => {
|
||||
if (!rawMcpTools || !startupConfig?.mcpServers) {
|
||||
return rawMcpTools;
|
||||
}
|
||||
return rawMcpTools.filter((tool) => {
|
||||
const serverConfig = startupConfig?.mcpServers?.[tool.name];
|
||||
return serverConfig?.chatMenu !== false;
|
||||
});
|
||||
}, [rawMcpTools, startupConfig?.mcpServers]);
|
||||
|
||||
return {
|
||||
mcpToolDetails,
|
||||
};
|
||||
}
|
||||
|
|
@ -7,9 +7,9 @@ import {
|
|||
useUpdateUserPluginsMutation,
|
||||
useReinitializeMCPServerMutation,
|
||||
} from 'librechat-data-provider/react-query';
|
||||
import type { TUpdateUserPlugins, TPlugin } from 'librechat-data-provider';
|
||||
import type { TUpdateUserPlugins, TPlugin, MCPServersResponse } from 'librechat-data-provider';
|
||||
import type { ConfigFieldDetail } from '~/common';
|
||||
import { useLocalize, useMCPSelect, useGetMCPTools, useMCPConnectionStatus } from '~/hooks';
|
||||
import { useLocalize, useMCPSelect, useMCPConnectionStatus } from '~/hooks';
|
||||
import { useGetStartupConfig } from '~/data-provider';
|
||||
|
||||
interface ServerState {
|
||||
|
|
@ -24,7 +24,6 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
|||
const localize = useLocalize();
|
||||
const queryClient = useQueryClient();
|
||||
const { showToast } = useToastContext();
|
||||
const { mcpToolDetails } = useGetMCPTools();
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const { mcpValues, setMCPValues, isPinned, setIsPinned } = useMCPSelect({ conversationId });
|
||||
|
||||
|
|
@ -448,7 +447,10 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
|||
|
||||
const getServerStatusIconProps = useCallback(
|
||||
(serverName: string) => {
|
||||
const tool = mcpToolDetails?.find((t) => t.name === serverName);
|
||||
const mcpData = queryClient.getQueryData<MCPServersResponse | undefined>([
|
||||
QueryKeys.mcpTools,
|
||||
]);
|
||||
const serverData = mcpData?.servers?.[serverName];
|
||||
const serverStatus = connectionStatus?.[serverName];
|
||||
const serverConfig = startupConfig?.mcpServers?.[serverName];
|
||||
|
||||
|
|
@ -458,17 +460,20 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
|||
|
||||
previousFocusRef.current = document.activeElement as HTMLElement;
|
||||
|
||||
const configTool = tool || {
|
||||
/** Minimal TPlugin object for the config dialog */
|
||||
const configTool: TPlugin = {
|
||||
name: serverName,
|
||||
pluginKey: `${Constants.mcp_prefix}${serverName}`,
|
||||
authConfig: serverConfig?.customUserVars
|
||||
? Object.entries(serverConfig.customUserVars).map(([key, config]) => ({
|
||||
authField: key,
|
||||
label: config.title,
|
||||
description: config.description,
|
||||
}))
|
||||
: [],
|
||||
authenticated: false,
|
||||
authConfig:
|
||||
serverData?.authConfig ||
|
||||
(serverConfig?.customUserVars
|
||||
? Object.entries(serverConfig.customUserVars).map(([key, config]) => ({
|
||||
authField: key,
|
||||
label: config.title,
|
||||
description: config.description,
|
||||
}))
|
||||
: []),
|
||||
authenticated: serverData?.authenticated ?? false,
|
||||
};
|
||||
setSelectedToolForConfig(configTool);
|
||||
setIsConfigModalOpen(true);
|
||||
|
|
@ -486,7 +491,14 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
|||
return {
|
||||
serverName,
|
||||
serverStatus,
|
||||
tool,
|
||||
tool: serverData
|
||||
? ({
|
||||
name: serverName,
|
||||
pluginKey: `${Constants.mcp_prefix}${serverName}`,
|
||||
icon: serverData.icon,
|
||||
authenticated: serverData.authenticated,
|
||||
} as TPlugin)
|
||||
: undefined,
|
||||
onConfigClick: handleConfigClick,
|
||||
isInitializing: isInitializing(serverName),
|
||||
canCancel: isCancellable(serverName),
|
||||
|
|
@ -495,8 +507,8 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
|||
};
|
||||
},
|
||||
[
|
||||
queryClient,
|
||||
isCancellable,
|
||||
mcpToolDetails,
|
||||
isInitializing,
|
||||
cancelOAuthFlow,
|
||||
connectionStatus,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue