🧬 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:
Danny Avila 2025-09-21 20:19:51 -04:00
parent 5b1a31ef4d
commit f0599ad36c
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
19 changed files with 235 additions and 1104 deletions

View file

@ -8,6 +8,7 @@ process.env.CREDS_IV = '0123456789abcdef';
jest.mock('~/server/services/Config', () => ({
getCachedTools: jest.fn(),
getMCPServerTools: jest.fn(),
}));
const mongoose = require('mongoose');
@ -30,7 +31,7 @@ const {
generateActionMetadataHash,
} = require('./Agent');
const permissionService = require('~/server/services/PermissionService');
const { getCachedTools } = require('~/server/services/Config');
const { getCachedTools, getMCPServerTools } = require('~/server/services/Config');
const { AclEntry } = require('~/db/models');
/**
@ -1929,6 +1930,16 @@ describe('models/Agent', () => {
another_tool: {},
});
// Mock getMCPServerTools to return tools for each server
getMCPServerTools.mockImplementation(async (server) => {
if (server === 'server1') {
return { tool1_mcp_server1: {} };
} else if (server === 'server2') {
return { tool2_mcp_server2: {} };
}
return null;
});
const mockReq = {
user: { id: 'user123' },
body: {
@ -2113,6 +2124,14 @@ describe('models/Agent', () => {
getCachedTools.mockResolvedValue(availableTools);
// Mock getMCPServerTools to return all tools for server1
getMCPServerTools.mockImplementation(async (server) => {
if (server === 'server1') {
return availableTools; // All 100 tools belong to server1
}
return null;
});
const mockReq = {
user: { id: 'user123' },
body: {
@ -2654,6 +2673,17 @@ describe('models/Agent', () => {
tool_mcp_server2: {}, // Different server
});
// Mock getMCPServerTools to return only tools matching the server
getMCPServerTools.mockImplementation(async (server) => {
if (server === 'server1') {
// Only return tool that correctly matches server1 format
return { tool_mcp_server1: {} };
} else if (server === 'server2') {
return { tool_mcp_server2: {} };
}
return null;
});
const mockReq = {
user: { id: 'user123' },
body: {

View file

@ -4,7 +4,6 @@
*/
const { logger } = require('@librechat/data-schemas');
const { Constants } = require('librechat-data-provider');
const { convertMCPToolToPlugin } = require('@librechat/api');
const {
cacheMCPServerTools,
getMCPServerTools,
@ -14,7 +13,6 @@ const { getMCPManager } = require('~/config');
/**
* Get all MCP tools available to the user
* Returns only MCP tools, not regular LibreChat tools
*/
const getMCPTools = async (req, res) => {
try {
@ -26,77 +24,97 @@ const getMCPTools = async (req, res) => {
const appConfig = req.config ?? (await getAppConfig({ role: req.user?.role }));
if (!appConfig?.mcpConfig) {
return res.status(200).json([]);
return res.status(200).json({ servers: {} });
}
const mcpManager = getMCPManager();
const configuredServers = Object.keys(appConfig.mcpConfig);
const mcpTools = [];
const mcpServers = {};
// Fetch tools from each configured server
const cachePromises = configuredServers.map((serverName) =>
getMCPServerTools(serverName).then((tools) => ({ serverName, tools })),
);
const cacheResults = await Promise.all(cachePromises);
const serverToolsMap = new Map();
for (const { serverName, tools } of cacheResults) {
if (tools) {
serverToolsMap.set(serverName, tools);
continue;
}
const serverTools = await mcpManager.getServerToolFunctions(userId, serverName);
if (!serverTools) {
logger.debug(`[getMCPTools] No tools found for server ${serverName}`);
continue;
}
serverToolsMap.set(serverName, serverTools);
if (Object.keys(serverTools).length > 0) {
// Cache asynchronously without blocking
cacheMCPServerTools({ serverName, serverTools }).catch((err) =>
logger.error(`[getMCPTools] Failed to cache tools for ${serverName}:`, err),
);
}
}
// Process each configured server
for (const serverName of configuredServers) {
try {
// First check server-specific cache
let serverTools = await getMCPServerTools(serverName);
const serverTools = serverToolsMap.get(serverName);
if (!serverTools) {
// If not cached, fetch from MCP manager
const allTools = await mcpManager.getAllToolFunctions(userId);
serverTools = {};
// Get server config once
const serverConfig = appConfig.mcpConfig[serverName];
const rawServerConfig = mcpManager.getRawConfig(serverName);
// Filter tools for this specific server
for (const [toolKey, toolData] of Object.entries(allTools)) {
if (toolKey.endsWith(`${Constants.mcp_delimiter}${serverName}`)) {
serverTools[toolKey] = toolData;
}
}
// Initialize server object with all server-level data
const server = {
name: serverName,
icon: rawServerConfig?.iconPath || '',
authenticated: true,
authConfig: [],
tools: [],
};
// Cache server tools if found
if (Object.keys(serverTools).length > 0) {
await cacheMCPServerTools({ serverName, serverTools });
// Set authentication config once for the server
if (serverConfig?.customUserVars) {
const customVarKeys = Object.keys(serverConfig.customUserVars);
if (customVarKeys.length > 0) {
server.authConfig = Object.entries(serverConfig.customUserVars).map(([key, value]) => ({
authField: key,
label: value.title || key,
description: value.description || '',
}));
server.authenticated = false;
}
}
// Convert to plugin format
for (const [toolKey, toolData] of Object.entries(serverTools)) {
const plugin = convertMCPToolToPlugin({
toolKey,
toolData,
mcpManager,
});
if (plugin) {
// Add authentication config from server config
const serverConfig = appConfig.mcpConfig[serverName];
if (serverConfig?.customUserVars) {
const customVarKeys = Object.keys(serverConfig.customUserVars);
if (customVarKeys.length === 0) {
plugin.authConfig = [];
plugin.authenticated = true;
} else {
plugin.authConfig = Object.entries(serverConfig.customUserVars).map(
([key, value]) => ({
authField: key,
label: value.title || key,
description: value.description || '',
}),
);
plugin.authenticated = false;
}
} else {
plugin.authConfig = [];
plugin.authenticated = true;
// Process tools efficiently - no need for convertMCPToolToPlugin
if (serverTools) {
for (const [toolKey, toolData] of Object.entries(serverTools)) {
if (!toolData.function || !toolKey.includes(Constants.mcp_delimiter)) {
continue;
}
mcpTools.push(plugin);
const toolName = toolKey.split(Constants.mcp_delimiter)[0];
server.tools.push({
name: toolName,
pluginKey: toolKey,
description: toolData.function.description || '',
});
}
}
// Only add server if it has tools or is configured
if (server.tools.length > 0 || serverConfig) {
mcpServers[serverName] = server;
}
} catch (error) {
logger.error(`[getMCPTools] Error loading tools for server ${serverName}:`, error);
}
}
res.status(200).json(mcpTools);
res.status(200).json({ servers: mcpServers });
} catch (error) {
logger.error('[getMCPTools]', error);
res.status(500).json({ message: error.message });

View file

@ -1,561 +0,0 @@
const { getMCPTools } = require('./mcp');
const { getAppConfig, getMCPServerTools } = require('~/server/services/Config');
const { getMCPManager } = require('~/config');
const { convertMCPToolToPlugin } = require('@librechat/api');
jest.mock('@librechat/data-schemas', () => ({
logger: {
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
},
}));
jest.mock('librechat-data-provider', () => ({
Constants: {
mcp_delimiter: '~~~',
},
}));
jest.mock('@librechat/api', () => ({
convertMCPToolToPlugin: jest.fn(),
}));
jest.mock('~/server/services/Config', () => ({
getAppConfig: jest.fn(),
getMCPServerTools: jest.fn(),
cacheMCPServerTools: jest.fn(),
}));
jest.mock('~/config', () => ({
getMCPManager: jest.fn(),
}));
describe('MCP Controller', () => {
let mockReq, mockRes, mockMCPManager;
beforeEach(() => {
jest.clearAllMocks();
mockReq = {
user: { id: 'test-user-id', role: 'user' },
config: null,
};
mockRes = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
};
mockMCPManager = {
getAllToolFunctions: jest.fn().mockResolvedValue({}),
};
getMCPManager.mockReturnValue(mockMCPManager);
getAppConfig.mockResolvedValue({
mcpConfig: {},
});
getMCPServerTools.mockResolvedValue(null);
});
describe('getMCPTools', () => {
it('should return 401 when user ID is not found', async () => {
mockReq.user = null;
await getMCPTools(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(401);
expect(mockRes.json).toHaveBeenCalledWith({ message: 'Unauthorized' });
const { logger } = require('@librechat/data-schemas');
expect(logger.warn).toHaveBeenCalledWith('[getMCPTools] User ID not found in request');
});
it('should return empty array when no mcpConfig exists', async () => {
getAppConfig.mockResolvedValue({
// No mcpConfig
});
await getMCPTools(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith([]);
});
it('should use cached server tools when available', async () => {
const cachedTools = {
'tool1~~~server1': {
type: 'function',
function: {
name: 'tool1',
description: 'Tool 1',
parameters: {},
},
},
};
getMCPServerTools.mockResolvedValue(cachedTools);
getAppConfig.mockResolvedValue({
mcpConfig: {
server1: {},
},
});
const mockPlugin = {
name: 'Tool 1',
pluginKey: 'tool1~~~server1',
description: 'Tool 1',
};
convertMCPToolToPlugin.mockReturnValue(mockPlugin);
await getMCPTools(mockReq, mockRes);
expect(getMCPServerTools).toHaveBeenCalledWith('server1');
expect(mockMCPManager.getAllToolFunctions).not.toHaveBeenCalled();
expect(convertMCPToolToPlugin).toHaveBeenCalledWith({
toolKey: 'tool1~~~server1',
toolData: cachedTools['tool1~~~server1'],
mcpManager: mockMCPManager,
});
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith([
{
...mockPlugin,
authConfig: [],
authenticated: true,
},
]);
});
it('should fetch from MCP manager when cache is empty', async () => {
getMCPServerTools.mockResolvedValue(null);
const allTools = {
'tool1~~~server1': {
type: 'function',
function: {
name: 'tool1',
description: 'Tool 1',
parameters: {},
},
},
'tool2~~~server2': {
type: 'function',
function: {
name: 'tool2',
description: 'Tool 2',
parameters: {},
},
},
};
mockMCPManager.getAllToolFunctions.mockResolvedValue(allTools);
getAppConfig.mockResolvedValue({
mcpConfig: {
server1: {},
},
});
const mockPlugin = {
name: 'Tool 1',
pluginKey: 'tool1~~~server1',
description: 'Tool 1',
};
convertMCPToolToPlugin.mockReturnValue(mockPlugin);
await getMCPTools(mockReq, mockRes);
expect(getMCPServerTools).toHaveBeenCalledWith('server1');
expect(mockMCPManager.getAllToolFunctions).toHaveBeenCalledWith('test-user-id');
// Should cache the server tools
const { cacheMCPServerTools } = require('~/server/services/Config');
expect(cacheMCPServerTools).toHaveBeenCalledWith({
serverName: 'server1',
serverTools: {
'tool1~~~server1': allTools['tool1~~~server1'],
},
});
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith([
{
...mockPlugin,
authConfig: [],
authenticated: true,
},
]);
});
it('should handle custom user variables in server config', async () => {
getMCPServerTools.mockResolvedValue({
'tool1~~~server1': {
type: 'function',
function: {
name: 'tool1',
description: 'Tool 1',
parameters: {},
},
},
});
getAppConfig.mockResolvedValue({
mcpConfig: {
server1: {
customUserVars: {
API_KEY: {
title: 'API Key',
description: 'Your API key',
},
SECRET: {
title: 'Secret Token',
description: 'Your secret token',
},
},
},
},
});
const mockPlugin = {
name: 'Tool 1',
pluginKey: 'tool1~~~server1',
description: 'Tool 1',
};
convertMCPToolToPlugin.mockReturnValue(mockPlugin);
await getMCPTools(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith([
{
...mockPlugin,
authConfig: [
{
authField: 'API_KEY',
label: 'API Key',
description: 'Your API key',
},
{
authField: 'SECRET',
label: 'Secret Token',
description: 'Your secret token',
},
],
authenticated: false,
},
]);
});
it('should handle empty custom user variables', async () => {
getMCPServerTools.mockResolvedValue({
'tool1~~~server1': {
type: 'function',
function: {
name: 'tool1',
description: 'Tool 1',
parameters: {},
},
},
});
getAppConfig.mockResolvedValue({
mcpConfig: {
server1: {
customUserVars: {},
},
},
});
const mockPlugin = {
name: 'Tool 1',
pluginKey: 'tool1~~~server1',
description: 'Tool 1',
};
convertMCPToolToPlugin.mockReturnValue(mockPlugin);
await getMCPTools(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith([
{
...mockPlugin,
authConfig: [],
authenticated: true,
},
]);
});
it('should handle multiple servers', async () => {
getMCPServerTools.mockResolvedValue(null);
const allTools = {
'tool1~~~server1': {
type: 'function',
function: {
name: 'tool1',
description: 'Tool 1',
parameters: {},
},
},
'tool2~~~server2': {
type: 'function',
function: {
name: 'tool2',
description: 'Tool 2',
parameters: {},
},
},
};
mockMCPManager.getAllToolFunctions.mockResolvedValue(allTools);
getAppConfig.mockResolvedValue({
mcpConfig: {
server1: {},
server2: {},
},
});
const mockPlugin1 = {
name: 'Tool 1',
pluginKey: 'tool1~~~server1',
description: 'Tool 1',
};
const mockPlugin2 = {
name: 'Tool 2',
pluginKey: 'tool2~~~server2',
description: 'Tool 2',
};
convertMCPToolToPlugin.mockReturnValueOnce(mockPlugin1).mockReturnValueOnce(mockPlugin2);
await getMCPTools(mockReq, mockRes);
expect(getMCPServerTools).toHaveBeenCalledWith('server1');
expect(getMCPServerTools).toHaveBeenCalledWith('server2');
expect(mockMCPManager.getAllToolFunctions).toHaveBeenCalledTimes(2);
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith([
{
...mockPlugin1,
authConfig: [],
authenticated: true,
},
{
...mockPlugin2,
authConfig: [],
authenticated: true,
},
]);
});
it('should handle server-specific errors gracefully', async () => {
getMCPServerTools.mockResolvedValue(null);
mockMCPManager.getAllToolFunctions.mockRejectedValue(new Error('Server connection failed'));
getAppConfig.mockResolvedValue({
mcpConfig: {
server1: {},
server2: {},
},
});
await getMCPTools(mockReq, mockRes);
const { logger } = require('@librechat/data-schemas');
expect(logger.error).toHaveBeenCalledWith(
'[getMCPTools] Error loading tools for server server1:',
expect.any(Error),
);
expect(logger.error).toHaveBeenCalledWith(
'[getMCPTools] Error loading tools for server server2:',
expect.any(Error),
);
// Should still return 200 with empty array
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith([]);
});
it('should skip tools when convertMCPToolToPlugin returns null', async () => {
getMCPServerTools.mockResolvedValue({
'tool1~~~server1': {
type: 'function',
function: {
name: 'tool1',
description: 'Tool 1',
parameters: {},
},
},
'tool2~~~server1': {
type: 'function',
function: {
name: 'tool2',
description: 'Tool 2',
parameters: {},
},
},
});
getAppConfig.mockResolvedValue({
mcpConfig: {
server1: {},
},
});
const mockPlugin = {
name: 'Tool 1',
pluginKey: 'tool1~~~server1',
description: 'Tool 1',
};
// First tool returns plugin, second returns null
convertMCPToolToPlugin.mockReturnValueOnce(mockPlugin).mockReturnValueOnce(null);
await getMCPTools(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith([
{
...mockPlugin,
authConfig: [],
authenticated: true,
},
]);
});
it('should use req.config when available', async () => {
const reqConfig = {
mcpConfig: {
server1: {},
},
};
mockReq.config = reqConfig;
getMCPServerTools.mockResolvedValue({
'tool1~~~server1': {
type: 'function',
function: {
name: 'tool1',
description: 'Tool 1',
parameters: {},
},
},
});
const mockPlugin = {
name: 'Tool 1',
pluginKey: 'tool1~~~server1',
description: 'Tool 1',
};
convertMCPToolToPlugin.mockReturnValue(mockPlugin);
await getMCPTools(mockReq, mockRes);
// Should not call getAppConfig when req.config is available
expect(getAppConfig).not.toHaveBeenCalled();
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith([
{
...mockPlugin,
authConfig: [],
authenticated: true,
},
]);
});
it('should handle general error in getMCPTools', async () => {
const error = new Error('Unexpected error');
getAppConfig.mockRejectedValue(error);
await getMCPTools(mockReq, mockRes);
const { logger } = require('@librechat/data-schemas');
expect(logger.error).toHaveBeenCalledWith('[getMCPTools]', error);
expect(mockRes.status).toHaveBeenCalledWith(500);
expect(mockRes.json).toHaveBeenCalledWith({ message: 'Unexpected error' });
});
it('should handle custom user variables without title or description', async () => {
getMCPServerTools.mockResolvedValue({
'tool1~~~server1': {
type: 'function',
function: {
name: 'tool1',
description: 'Tool 1',
parameters: {},
},
},
});
getAppConfig.mockResolvedValue({
mcpConfig: {
server1: {
customUserVars: {
MY_VAR: {
// No title or description
},
},
},
},
});
const mockPlugin = {
name: 'Tool 1',
pluginKey: 'tool1~~~server1',
description: 'Tool 1',
};
convertMCPToolToPlugin.mockReturnValue(mockPlugin);
await getMCPTools(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith([
{
...mockPlugin,
authConfig: [
{
authField: 'MY_VAR',
label: 'MY_VAR', // Falls back to key
description: '', // Empty string
},
],
authenticated: false,
},
]);
});
it('should not cache when no tools are found for a server', async () => {
getMCPServerTools.mockResolvedValue(null);
const allTools = {
'tool1~~~otherserver': {
type: 'function',
function: {
name: 'tool1',
description: 'Tool 1',
parameters: {},
},
},
};
mockMCPManager.getAllToolFunctions.mockResolvedValue(allTools);
getAppConfig.mockResolvedValue({
mcpConfig: {
server1: {},
},
});
await getMCPTools(mockReq, mockRes);
const { cacheMCPServerTools } = require('~/server/services/Config');
expect(cacheMCPServerTools).not.toHaveBeenCalled();
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith([]);
});
});
});