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 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();

View file

@ -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);

View file

@ -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<TCustomConfig> = {
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<TCustomConfig> = {
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<TCustomConfig> = {
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<TCustomConfig> = {
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<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 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<string, FunctionTool>;
customConfig?: Partial<TCustomConfig> | 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 */