diff --git a/api/server/controllers/PluginController.js b/api/server/controllers/PluginController.js index 070d53812d..07ab4de775 100644 --- a/api/server/controllers/PluginController.js +++ b/api/server/controllers/PluginController.js @@ -74,14 +74,23 @@ const getAvailableTools = async (req, res) => { const cachedToolsArray = await cache.get(CacheKeys.TOOLS); const cachedUserTools = await getCachedTools({ userId }); - const mcpManager = getMCPManager(); - const userPlugins = - cachedUserTools != null - ? convertMCPToolsToPlugins({ functionTools: cachedUserTools, mcpManager }) - : undefined; + const appConfig = req.config ?? (await getAppConfig({ role: req.user?.role })); - if (cachedToolsArray != null && userPlugins != null) { - const dedupedTools = filterUniquePlugins([...userPlugins, ...cachedToolsArray]); + /** @type {TPlugin[]} */ + let mcpPlugins; + if (appConfig?.mcpConfig) { + const mcpManager = getMCPManager(); + mcpPlugins = + cachedUserTools != null + ? convertMCPToolsToPlugins({ functionTools: cachedUserTools, mcpManager }) + : undefined; + } + + if ( + cachedToolsArray != null && + (appConfig?.mcpConfig != null ? mcpPlugins != null && mcpPlugins.length > 0 : true) + ) { + const dedupedTools = filterUniquePlugins([...(mcpPlugins ?? []), ...cachedToolsArray]); res.status(200).json(dedupedTools); return; } @@ -93,9 +102,9 @@ const getAvailableTools = async (req, res) => { /** @type {import('@librechat/api').LCManifestTool[]} */ let pluginManifest = availableTools; - const appConfig = req.config ?? (await getAppConfig({ role: req.user?.role })); if (appConfig?.mcpConfig != null) { try { + const mcpManager = getMCPManager(); const mcpTools = await mcpManager.getAllToolFunctions(userId); prelimCachedTools = prelimCachedTools ?? {}; for (const [toolKey, toolData] of Object.entries(mcpTools)) { @@ -175,7 +184,7 @@ const getAvailableTools = async (req, res) => { const finalTools = filterUniquePlugins(toolsOutput); await cache.set(CacheKeys.TOOLS, finalTools); - const dedupedTools = filterUniquePlugins([...(userPlugins ?? []), ...finalTools]); + const dedupedTools = filterUniquePlugins([...(mcpPlugins ?? []), ...finalTools]); res.status(200).json(dedupedTools); } catch (error) { logger.error('[getAvailableTools]', error); diff --git a/api/server/controllers/PluginController.spec.js b/api/server/controllers/PluginController.spec.js index 4ed9cccf0a..5cd2fc786c 100644 --- a/api/server/controllers/PluginController.spec.js +++ b/api/server/controllers/PluginController.spec.js @@ -174,10 +174,19 @@ describe('PluginController', () => { mockCache.get.mockResolvedValue(null); getCachedTools.mockResolvedValueOnce(mockUserTools); mockReq.config = { - mcpConfig: null, + mcpConfig: { + server1: {}, + }, paths: { structuredTools: '/mock/path' }, }; + // Mock MCP manager to return empty tools initially (since getAllToolFunctions is called) + const mockMCPManager = { + getAllToolFunctions: jest.fn().mockResolvedValue({}), + getRawConfig: jest.fn().mockReturnValue({}), + }; + require('~/config').getMCPManager.mockReturnValue(mockMCPManager); + // Mock second call to return tool definitions (includeGlobal: true) getCachedTools.mockResolvedValueOnce(mockUserTools); @@ -505,7 +514,7 @@ describe('PluginController', () => { expect(mockRes.json).toHaveBeenCalledWith([]); }); - it('should handle cachedToolsArray and userPlugins both being defined', async () => { + it('should handle `cachedToolsArray` and `mcpPlugins` both being defined', async () => { const cachedTools = [{ name: 'CachedTool', pluginKey: 'cached-tool', description: 'Cached' }]; // Use MCP delimiter for the user tool so convertMCPToolsToPlugins works const userTools = { @@ -522,10 +531,19 @@ describe('PluginController', () => { mockCache.get.mockResolvedValue(cachedTools); getCachedTools.mockResolvedValueOnce(userTools); mockReq.config = { - mcpConfig: null, + mcpConfig: { + server1: {}, + }, paths: { structuredTools: '/mock/path' }, }; + // Mock MCP manager to return empty tools initially + const mockMCPManager = { + getAllToolFunctions: jest.fn().mockResolvedValue({}), + getRawConfig: jest.fn().mockReturnValue({}), + }; + require('~/config').getMCPManager.mockReturnValue(mockMCPManager); + // The controller expects a second call to getCachedTools getCachedTools.mockResolvedValueOnce({ 'cached-tool': { type: 'function', function: { name: 'cached-tool' } }, diff --git a/api/server/routes/config.js b/api/server/routes/config.js index 67be757f7a..4240a6eb23 100644 --- a/api/server/routes/config.js +++ b/api/server/routes/config.js @@ -125,6 +125,9 @@ router.get('/', async function (req, res) { payload.mcpServers = {}; const getMCPServers = () => { try { + if (appConfig?.mcpConfig == null) { + return; + } const mcpManager = getMCPManager(); if (!mcpManager) { return;