refactor: Update convertMCPToolsToPlugins to use mcpManager for server configuration and adjust related tests

This commit is contained in:
Danny Avila 2025-08-17 20:05:48 -04:00
parent b2b2aee945
commit e7af3bdaed
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
4 changed files with 110 additions and 66 deletions

View file

@ -106,7 +106,9 @@ const getAvailableTools = async (req, res) => {
const cache = getLogStores(CacheKeys.CONFIG_STORE); const cache = getLogStores(CacheKeys.CONFIG_STORE);
const cachedToolsArray = await cache.get(CacheKeys.TOOLS); const cachedToolsArray = await cache.get(CacheKeys.TOOLS);
const cachedUserTools = await getCachedTools({ userId }); 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) { if (cachedToolsArray != null && userPlugins != null) {
const dedupedTools = filterUniquePlugins([...userPlugins, ...cachedToolsArray]); const dedupedTools = filterUniquePlugins([...userPlugins, ...cachedToolsArray]);
@ -117,7 +119,6 @@ const getAvailableTools = async (req, res) => {
let pluginManifest = availableTools; let pluginManifest = availableTools;
if (customConfig?.mcpServers != null) { if (customConfig?.mcpServers != null) {
try { try {
const mcpManager = getMCPManager();
const flowsCache = getLogStores(CacheKeys.FLOWS); const flowsCache = getLogStores(CacheKeys.FLOWS);
const flowManager = flowsCache ? getFlowStateManager(flowsCache) : null; const flowManager = flowsCache ? getFlowStateManager(flowsCache) : null;
const serverToolsCallback = createServerToolsCallback(); const serverToolsCallback = createServerToolsCallback();

View file

@ -23,6 +23,7 @@ jest.mock('~/server/services/ToolService', () => ({
jest.mock('~/config', () => ({ jest.mock('~/config', () => ({
getMCPManager: jest.fn(() => ({ getMCPManager: jest.fn(() => ({
loadManifestTools: jest.fn().mockResolvedValue([]), loadManifestTools: jest.fn().mockResolvedValue([]),
getRawConfig: jest.fn(),
})), })),
getFlowStateManager: jest.fn(), getFlowStateManager: jest.fn(),
})); }));
@ -167,7 +168,7 @@ describe('PluginController', () => {
expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({ expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({
functionTools: mockUserTools, functionTools: mockUserTools,
customConfig: null, mcpManager: expect.any(Object),
}); });
}); });
@ -240,9 +241,9 @@ describe('PluginController', () => {
}); });
describe('plugin.icon behavior', () => { describe('plugin.icon behavior', () => {
const callGetAvailableToolsWithMCPServer = async (mcpServers) => { const callGetAvailableToolsWithMCPServer = async (serverConfig) => {
mockCache.get.mockResolvedValue(null); mockCache.get.mockResolvedValue(null);
getCustomConfig.mockResolvedValue({ mcpServers }); getCustomConfig.mockResolvedValue(null);
const functionTools = { const functionTools = {
[`test-tool${Constants.mcp_delimiter}test-server`]: { [`test-tool${Constants.mcp_delimiter}test-server`]: {
@ -254,11 +255,18 @@ describe('PluginController', () => {
name: 'test-tool', name: 'test-tool',
pluginKey: `test-tool${Constants.mcp_delimiter}test-server`, pluginKey: `test-tool${Constants.mcp_delimiter}test-server`,
description: 'A test tool', description: 'A test tool',
icon: mcpServers['test-server']?.iconPath, icon: serverConfig?.iconPath,
authenticated: true, authenticated: true,
authConfig: [], 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); getCachedTools.mockResolvedValueOnce(functionTools);
convertMCPToolsToPlugins.mockReturnValue([mockConvertedPlugin]); convertMCPToolsToPlugins.mockReturnValue([mockConvertedPlugin]);
filterUniquePlugins.mockImplementation((plugins) => plugins); filterUniquePlugins.mockImplementation((plugins) => plugins);
@ -275,20 +283,16 @@ describe('PluginController', () => {
}; };
it('should set plugin.icon when iconPath is defined', async () => { it('should set plugin.icon when iconPath is defined', async () => {
const mcpServers = { const serverConfig = {
'test-server': {
iconPath: '/path/to/icon.png', iconPath: '/path/to/icon.png',
},
}; };
const testTool = await callGetAvailableToolsWithMCPServer(mcpServers); const testTool = await callGetAvailableToolsWithMCPServer(serverConfig);
expect(testTool.icon).toBe('/path/to/icon.png'); expect(testTool.icon).toBe('/path/to/icon.png');
}); });
it('should set plugin.icon to undefined when iconPath is not defined', async () => { it('should set plugin.icon to undefined when iconPath is not defined', async () => {
const mcpServers = { const serverConfig = {};
'test-server': {}, const testTool = await callGetAvailableToolsWithMCPServer(serverConfig);
};
const testTool = await callGetAvailableToolsWithMCPServer(mcpServers);
expect(testTool.icon).toBeUndefined(); expect(testTool.icon).toBeUndefined();
}); });
}); });
@ -318,6 +322,7 @@ describe('PluginController', () => {
// Mock the MCP manager to return tools // Mock the MCP manager to return tools
const mockMCPManager = { const mockMCPManager = {
loadManifestTools: jest.fn().mockResolvedValue(mcpManagerTools), loadManifestTools: jest.fn().mockResolvedValue(mcpManagerTools),
getRawConfig: jest.fn(),
}; };
require('~/config').getMCPManager.mockReturnValue(mockMCPManager); require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
@ -388,7 +393,7 @@ describe('PluginController', () => {
expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({ expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({
functionTools: null, functionTools: null,
customConfig: null, mcpManager: expect.any(Object),
}); });
}); });
@ -408,7 +413,7 @@ describe('PluginController', () => {
expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({ expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({
functionTools: undefined, 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); mockCache.get.mockResolvedValue(null);
getCustomConfig.mockResolvedValue(customConfig); getCustomConfig.mockResolvedValue(customConfig);
getCachedTools.mockResolvedValueOnce(mockUserTools); getCachedTools.mockResolvedValueOnce(mockUserTools);

View file

@ -1,5 +1,6 @@
import { AuthType, Constants, EToolResources } from 'librechat-data-provider'; 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 { import {
convertMCPToolsToPlugins, convertMCPToolsToPlugins,
filterUniquePlugins, filterUniquePlugins,
@ -277,19 +278,18 @@ describe('format.ts helper functions', () => {
} as FunctionTool, } as FunctionTool,
}; };
const customConfig: Partial<TCustomConfig> = { const mockMcpManager = {
mcpServers: { getRawConfig: jest.fn().mockReturnValue({
server1: {
command: 'test', command: 'test',
args: [], args: [],
iconPath: '/path/to/icon.png', 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).toHaveLength(1);
expect(result![0].icon).toBe('/path/to/icon.png'); expect(result![0].icon).toBe('/path/to/icon.png');
expect(mockMcpManager.getRawConfig).toHaveBeenCalledWith('server1');
}); });
it('should handle customUserVars in server config', () => { it('should handle customUserVars in server config', () => {
@ -300,26 +300,25 @@ describe('format.ts helper functions', () => {
} as FunctionTool, } as FunctionTool,
}; };
const customConfig: Partial<TCustomConfig> = { const mockMcpManager = {
mcpServers: { getRawConfig: jest.fn().mockReturnValue({
server1: {
command: 'test', command: 'test',
args: [], args: [],
customUserVars: { customUserVars: {
API_KEY: { title: 'API Key', description: 'Your API key' }, API_KEY: { title: 'API Key', description: 'Your API key' },
SECRET: { title: 'Secret', description: 'Your secret' }, 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).toHaveLength(1);
expect(result![0].authConfig).toHaveLength(2); expect(result![0].authConfig).toHaveLength(2);
expect(result![0].authConfig).toEqual([ expect(result![0].authConfig).toEqual([
{ authField: 'API_KEY', label: 'API Key', description: 'Your API key' }, { authField: 'API_KEY', label: 'API Key', description: 'Your API key' },
{ authField: 'SECRET', label: 'Secret', description: 'Your secret' }, { authField: 'SECRET', label: 'Secret', description: 'Your secret' },
]); ]);
expect(mockMcpManager.getRawConfig).toHaveBeenCalledWith('server1');
}); });
it('should use key as label when title is missing in customUserVars', () => { it('should use key as label when title is missing in customUserVars', () => {
@ -330,23 +329,22 @@ describe('format.ts helper functions', () => {
} as FunctionTool, } as FunctionTool,
}; };
const customConfig: Partial<TCustomConfig> = { const mockMcpManager = {
mcpServers: { getRawConfig: jest.fn().mockReturnValue({
server1: {
command: 'test', command: 'test',
args: [], args: [],
customUserVars: { customUserVars: {
API_KEY: { title: 'API Key', description: 'Your API key' }, 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).toHaveLength(1);
expect(result![0].authConfig).toEqual([ expect(result![0].authConfig).toEqual([
{ authField: 'API_KEY', label: 'API Key', description: 'Your API key' }, { authField: 'API_KEY', label: 'API Key', description: 'Your API key' },
]); ]);
expect(mockMcpManager.getRawConfig).toHaveBeenCalledWith('server1');
}); });
it('should handle empty customUserVars', () => { it('should handle empty customUserVars', () => {
@ -357,19 +355,51 @@ describe('format.ts helper functions', () => {
} as FunctionTool, } as FunctionTool,
}; };
const customConfig: Partial<TCustomConfig> = { const mockMcpManager = {
mcpServers: { getRawConfig: jest.fn().mockReturnValue({
server1: {
command: 'test', command: 'test',
args: [], args: [],
customUserVars: {}, customUserVars: {},
}, }),
}, } as unknown as MCPManager;
};
const result = convertMCPToolsToPlugins({ functionTools, customConfig }); const result = convertMCPToolsToPlugins({ functionTools, mcpManager: mockMcpManager });
expect(result).toHaveLength(1); expect(result).toHaveLength(1);
expect(result![0].authConfig).toEqual([]); expect(result![0].authConfig).toEqual([]);
expect(mockMcpManager.getRawConfig).toHaveBeenCalledWith('server1');
});
it('should handle missing mcpManager', () => {
const functionTools: Record<string, FunctionTool> = {
[`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<string, FunctionTool> = {
[`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');
}); });
}); });

View file

@ -1,5 +1,6 @@
import { AuthType, Constants, EToolResources } from 'librechat-data-provider'; 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. * 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 * Converts MCP function format tools to plugin format
* @param functionTools - Object with function format tools * @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 * @returns Array of plugin objects
*/ */
export function convertMCPToolsToPlugins({ export function convertMCPToolsToPlugins({
functionTools, functionTools,
customConfig, mcpManager,
}: { }: {
functionTools?: Record<string, FunctionTool>; functionTools?: Record<string, FunctionTool>;
customConfig?: Partial<TCustomConfig> | null; mcpManager?: MCPManager;
}): TPlugin[] | undefined { }): TPlugin[] | undefined {
if (!functionTools || typeof functionTools !== 'object') { if (!functionTools || typeof functionTools !== 'object') {
return; return;
@ -73,7 +74,7 @@ export function convertMCPToolsToPlugins({
const parts = toolKey.split(Constants.mcp_delimiter); const parts = toolKey.split(Constants.mcp_delimiter);
const serverName = parts[parts.length - 1]; const serverName = parts[parts.length - 1];
const serverConfig = customConfig?.mcpServers?.[serverName]; const serverConfig = mcpManager?.getRawConfig(serverName);
const plugin: TPlugin = { const plugin: TPlugin = {
/** Tool name without server suffix */ /** Tool name without server suffix */