diff --git a/api/server/controllers/PluginController.js b/api/server/controllers/PluginController.js index 730dcacc49..b3cbc5d9c9 100644 --- a/api/server/controllers/PluginController.js +++ b/api/server/controllers/PluginController.js @@ -106,7 +106,9 @@ const getAvailableTools = async (req, res) => { const cache = getLogStores(CacheKeys.CONFIG_STORE); const cachedToolsArray = await cache.get(CacheKeys.TOOLS); const cachedUserTools = await getCachedTools({ userId }); - const userPlugins = convertMCPToolsToPlugins({ functionTools: cachedUserTools, customConfig }); + + const mcpManager = getMCPManager(); + const userPlugins = convertMCPToolsToPlugins({ functionTools: cachedUserTools, mcpManager }); if (cachedToolsArray != null && userPlugins != null) { const dedupedTools = filterUniquePlugins([...userPlugins, ...cachedToolsArray]); @@ -117,7 +119,6 @@ const getAvailableTools = async (req, res) => { let pluginManifest = availableTools; if (customConfig?.mcpServers != null) { try { - const mcpManager = getMCPManager(); const flowsCache = getLogStores(CacheKeys.FLOWS); const flowManager = flowsCache ? getFlowStateManager(flowsCache) : null; const serverToolsCallback = createServerToolsCallback(); diff --git a/api/server/controllers/PluginController.spec.js b/api/server/controllers/PluginController.spec.js index c0461530d6..a964011c83 100644 --- a/api/server/controllers/PluginController.spec.js +++ b/api/server/controllers/PluginController.spec.js @@ -23,6 +23,7 @@ jest.mock('~/server/services/ToolService', () => ({ jest.mock('~/config', () => ({ getMCPManager: jest.fn(() => ({ loadManifestTools: jest.fn().mockResolvedValue([]), + getRawConfig: jest.fn(), })), getFlowStateManager: jest.fn(), })); @@ -167,7 +168,7 @@ describe('PluginController', () => { expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({ functionTools: mockUserTools, - customConfig: null, + mcpManager: expect.any(Object), }); }); @@ -240,9 +241,9 @@ describe('PluginController', () => { }); describe('plugin.icon behavior', () => { - const callGetAvailableToolsWithMCPServer = async (mcpServers) => { + const callGetAvailableToolsWithMCPServer = async (serverConfig) => { mockCache.get.mockResolvedValue(null); - getCustomConfig.mockResolvedValue({ mcpServers }); + getCustomConfig.mockResolvedValue(null); const functionTools = { [`test-tool${Constants.mcp_delimiter}test-server`]: { @@ -254,11 +255,18 @@ describe('PluginController', () => { name: 'test-tool', pluginKey: `test-tool${Constants.mcp_delimiter}test-server`, description: 'A test tool', - icon: mcpServers['test-server']?.iconPath, + icon: serverConfig?.iconPath, authenticated: true, authConfig: [], }; + // Mock the MCP manager to return the server config + const mockMCPManager = { + loadManifestTools: jest.fn().mockResolvedValue([]), + getRawConfig: jest.fn().mockReturnValue(serverConfig), + }; + require('~/config').getMCPManager.mockReturnValue(mockMCPManager); + getCachedTools.mockResolvedValueOnce(functionTools); convertMCPToolsToPlugins.mockReturnValue([mockConvertedPlugin]); filterUniquePlugins.mockImplementation((plugins) => plugins); @@ -275,20 +283,16 @@ describe('PluginController', () => { }; it('should set plugin.icon when iconPath is defined', async () => { - const mcpServers = { - 'test-server': { - iconPath: '/path/to/icon.png', - }, + const serverConfig = { + iconPath: '/path/to/icon.png', }; - const testTool = await callGetAvailableToolsWithMCPServer(mcpServers); + const testTool = await callGetAvailableToolsWithMCPServer(serverConfig); expect(testTool.icon).toBe('/path/to/icon.png'); }); it('should set plugin.icon to undefined when iconPath is not defined', async () => { - const mcpServers = { - 'test-server': {}, - }; - const testTool = await callGetAvailableToolsWithMCPServer(mcpServers); + const serverConfig = {}; + const testTool = await callGetAvailableToolsWithMCPServer(serverConfig); expect(testTool.icon).toBeUndefined(); }); }); @@ -318,6 +322,7 @@ describe('PluginController', () => { // Mock the MCP manager to return tools const mockMCPManager = { loadManifestTools: jest.fn().mockResolvedValue(mcpManagerTools), + getRawConfig: jest.fn(), }; require('~/config').getMCPManager.mockReturnValue(mockMCPManager); @@ -388,7 +393,7 @@ describe('PluginController', () => { expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({ functionTools: null, - customConfig: null, + mcpManager: expect.any(Object), }); }); @@ -408,7 +413,7 @@ describe('PluginController', () => { expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({ functionTools: undefined, - customConfig: null, + mcpManager: expect.any(Object), }); }); @@ -459,6 +464,13 @@ describe('PluginController', () => { }, }; + // Mock the MCP manager to return server config without customUserVars + const mockMCPManager = { + loadManifestTools: jest.fn().mockResolvedValue([]), + getRawConfig: jest.fn(), + }; + require('~/config').getMCPManager.mockReturnValue(mockMCPManager); + mockCache.get.mockResolvedValue(null); getCustomConfig.mockResolvedValue(customConfig); getCachedTools.mockResolvedValueOnce(mockUserTools); diff --git a/packages/api/src/tools/format.spec.ts b/packages/api/src/tools/format.spec.ts index 3226da41f8..3a02fd7c62 100644 --- a/packages/api/src/tools/format.spec.ts +++ b/packages/api/src/tools/format.spec.ts @@ -1,5 +1,6 @@ import { AuthType, Constants, EToolResources } from 'librechat-data-provider'; -import type { TPlugin, FunctionTool, TCustomConfig } from 'librechat-data-provider'; +import type { TPlugin, FunctionTool } from 'librechat-data-provider'; +import type { MCPManager } from '~/mcp/MCPManager'; import { convertMCPToolsToPlugins, filterUniquePlugins, @@ -277,19 +278,18 @@ describe('format.ts helper functions', () => { } as FunctionTool, }; - const customConfig: Partial = { - mcpServers: { - server1: { - command: 'test', - args: [], - iconPath: '/path/to/icon.png', - }, - }, - }; + const mockMcpManager = { + getRawConfig: jest.fn().mockReturnValue({ + command: 'test', + args: [], + iconPath: '/path/to/icon.png', + }), + } as unknown as MCPManager; - const result = convertMCPToolsToPlugins({ functionTools, customConfig }); + const result = convertMCPToolsToPlugins({ functionTools, mcpManager: mockMcpManager }); expect(result).toHaveLength(1); expect(result![0].icon).toBe('/path/to/icon.png'); + expect(mockMcpManager.getRawConfig).toHaveBeenCalledWith('server1'); }); it('should handle customUserVars in server config', () => { @@ -300,26 +300,25 @@ describe('format.ts helper functions', () => { } as FunctionTool, }; - const customConfig: Partial = { - mcpServers: { - server1: { - command: 'test', - args: [], - customUserVars: { - API_KEY: { title: 'API Key', description: 'Your API key' }, - SECRET: { title: 'Secret', description: 'Your secret' }, - }, + const mockMcpManager = { + getRawConfig: jest.fn().mockReturnValue({ + command: 'test', + args: [], + customUserVars: { + API_KEY: { title: 'API Key', description: 'Your API key' }, + SECRET: { title: 'Secret', description: 'Your secret' }, }, - }, - }; + }), + } as unknown as MCPManager; - const result = convertMCPToolsToPlugins({ functionTools, customConfig }); + const result = convertMCPToolsToPlugins({ functionTools, mcpManager: mockMcpManager }); expect(result).toHaveLength(1); expect(result![0].authConfig).toHaveLength(2); expect(result![0].authConfig).toEqual([ { authField: 'API_KEY', label: 'API Key', description: 'Your API key' }, { authField: 'SECRET', label: 'Secret', description: 'Your secret' }, ]); + expect(mockMcpManager.getRawConfig).toHaveBeenCalledWith('server1'); }); it('should use key as label when title is missing in customUserVars', () => { @@ -330,23 +329,22 @@ describe('format.ts helper functions', () => { } as FunctionTool, }; - const customConfig: Partial = { - mcpServers: { - server1: { - command: 'test', - args: [], - customUserVars: { - API_KEY: { title: 'API Key', description: 'Your API key' }, - }, + const mockMcpManager = { + getRawConfig: jest.fn().mockReturnValue({ + command: 'test', + args: [], + customUserVars: { + API_KEY: { title: 'API Key', description: 'Your API key' }, }, - }, - }; + }), + } as unknown as MCPManager; - const result = convertMCPToolsToPlugins({ functionTools, customConfig }); + const result = convertMCPToolsToPlugins({ functionTools, mcpManager: mockMcpManager }); expect(result).toHaveLength(1); expect(result![0].authConfig).toEqual([ { authField: 'API_KEY', label: 'API Key', description: 'Your API key' }, ]); + expect(mockMcpManager.getRawConfig).toHaveBeenCalledWith('server1'); }); it('should handle empty customUserVars', () => { @@ -357,19 +355,51 @@ describe('format.ts helper functions', () => { } as FunctionTool, }; - const customConfig: Partial = { - mcpServers: { - server1: { - command: 'test', - args: [], - customUserVars: {}, - }, - }, - }; + const mockMcpManager = { + getRawConfig: jest.fn().mockReturnValue({ + command: 'test', + args: [], + customUserVars: {}, + }), + } as unknown as MCPManager; - const result = convertMCPToolsToPlugins({ functionTools, customConfig }); + const result = convertMCPToolsToPlugins({ functionTools, mcpManager: mockMcpManager }); expect(result).toHaveLength(1); expect(result![0].authConfig).toEqual([]); + expect(mockMcpManager.getRawConfig).toHaveBeenCalledWith('server1'); + }); + + it('should handle missing mcpManager', () => { + const functionTools: Record = { + [`tool1${Constants.mcp_delimiter}server1`]: { + type: 'function', + function: { name: 'tool1', description: 'Tool 1' }, + } as FunctionTool, + }; + + const result = convertMCPToolsToPlugins({ functionTools }); + expect(result).toHaveLength(1); + expect(result![0].icon).toBeUndefined(); + expect(result![0].authConfig).toEqual([]); + }); + + it('should handle when getRawConfig returns undefined', () => { + const functionTools: Record = { + [`tool1${Constants.mcp_delimiter}server1`]: { + type: 'function', + function: { name: 'tool1', description: 'Tool 1' }, + } as FunctionTool, + }; + + const mockMcpManager = { + getRawConfig: jest.fn().mockReturnValue(undefined), + } as unknown as MCPManager; + + const result = convertMCPToolsToPlugins({ functionTools, mcpManager: mockMcpManager }); + expect(result).toHaveLength(1); + expect(result![0].icon).toBeUndefined(); + expect(result![0].authConfig).toEqual([]); + expect(mockMcpManager.getRawConfig).toHaveBeenCalledWith('server1'); }); }); diff --git a/packages/api/src/tools/format.ts b/packages/api/src/tools/format.ts index 13075ab819..7d11b2f8b3 100644 --- a/packages/api/src/tools/format.ts +++ b/packages/api/src/tools/format.ts @@ -1,5 +1,6 @@ import { AuthType, Constants, EToolResources } from 'librechat-data-provider'; -import type { TCustomConfig, TPlugin, FunctionTool } from 'librechat-data-provider'; +import type { TPlugin, FunctionTool } from 'librechat-data-provider'; +import type { MCPManager } from '~/mcp/MCPManager'; /** * Filters out duplicate plugins from the list of plugins. @@ -49,15 +50,15 @@ export const checkPluginAuth = (plugin?: TPlugin): boolean => { /** * Converts MCP function format tools to plugin format * @param functionTools - Object with function format tools - * @param customConfig - Custom configuration for MCP servers + * @param mcpManager - MCP manager instance for server configuration access * @returns Array of plugin objects */ export function convertMCPToolsToPlugins({ functionTools, - customConfig, + mcpManager, }: { functionTools?: Record; - customConfig?: Partial | null; + mcpManager?: MCPManager; }): TPlugin[] | undefined { if (!functionTools || typeof functionTools !== 'object') { return; @@ -73,7 +74,7 @@ export function convertMCPToolsToPlugins({ const parts = toolKey.split(Constants.mcp_delimiter); const serverName = parts[parts.length - 1]; - const serverConfig = customConfig?.mcpServers?.[serverName]; + const serverConfig = mcpManager?.getRawConfig(serverName); const plugin: TPlugin = { /** Tool name without server suffix */