diff --git a/api/server/controllers/PluginController.js b/api/server/controllers/PluginController.js index 0329834ad9..451868b75d 100644 --- a/api/server/controllers/PluginController.js +++ b/api/server/controllers/PluginController.js @@ -4,6 +4,7 @@ const { getToolkitKey, checkPluginAuth, filterUniquePlugins, + convertMCPToolToPlugin, convertMCPToolsToPlugins, } = require('@librechat/api'); const { @@ -107,16 +108,20 @@ const getAvailableTools = async (req, res) => { if (customConfig?.mcpServers != null) { try { const mcpManager = getMCPManager(); - const mcpTools = await mcpManager.loadAllManifestTools(userId); - const mcpToolsRecord = mcpTools.reduce((acc, tool) => { - pluginManifest.push(tool); - acc[tool.pluginKey] = tool; - if (!toolDefinitions[tool.pluginKey]) { - toolDefinitions[tool.pluginKey] = tool; + const mcpTools = await mcpManager.getAllToolFunctions(userId); + prelimCachedTools = prelimCachedTools ?? {}; + for (const [toolKey, toolData] of Object.entries(mcpTools)) { + const plugin = convertMCPToolToPlugin({ + toolKey, + toolData, + customConfig, + }); + if (plugin) { + pluginManifest.push(plugin); } - return acc; - }, prelimCachedTools ?? {}); - await mergeUserTools({ userId, cachedUserTools, userTools: mcpToolsRecord }); + prelimCachedTools[toolKey] = toolData; + } + await mergeUserTools({ userId, cachedUserTools, userTools: prelimCachedTools }); } catch (error) { logger.error( '[getAvailableTools] Error loading MCP Tools, servers may still be initializing:', diff --git a/api/server/controllers/PluginController.spec.js b/api/server/controllers/PluginController.spec.js index 07dd02b1d7..69ef99605e 100644 --- a/api/server/controllers/PluginController.spec.js +++ b/api/server/controllers/PluginController.spec.js @@ -38,21 +38,7 @@ jest.mock('~/cache', () => ({ getLogStores: jest.fn(), })); -jest.mock('@librechat/api', () => ({ - getToolkitKey: jest.fn(), - checkPluginAuth: jest.fn(), - filterUniquePlugins: jest.fn(), - convertMCPToolsToPlugins: jest.fn(), -})); - -// Import the actual module with the function we want to test const { getAvailableTools, getAvailablePluginsController } = require('./PluginController'); -const { - filterUniquePlugins, - checkPluginAuth, - convertMCPToolsToPlugins, - getToolkitKey, -} = require('@librechat/api'); const { loadAndFormatTools } = require('~/server/services/ToolService'); describe('PluginController', () => { @@ -60,10 +46,23 @@ describe('PluginController', () => { beforeEach(() => { jest.clearAllMocks(); - mockReq = { user: { id: 'test-user-id' } }; + mockReq = { + user: { id: 'test-user-id' }, + app: { + locals: { + paths: { structuredTools: '/mock/path' }, + filteredTools: null, + includedTools: null, + }, + }, + }; mockRes = { status: jest.fn().mockReturnThis(), json: jest.fn() }; mockCache = { get: jest.fn(), set: jest.fn() }; getLogStores.mockReturnValue(mockCache); + + // Clear availableTools and toolkits arrays before each test + require('~/app/clients/tools').availableTools.length = 0; + require('~/app/clients/tools').toolkits.length = 0; }); describe('getAvailablePluginsController', () => { @@ -72,38 +71,39 @@ describe('PluginController', () => { }); it('should use filterUniquePlugins to remove duplicate plugins', async () => { + // Add plugins with duplicates to availableTools const mockPlugins = [ { name: 'Plugin1', pluginKey: 'key1', description: 'First' }, + { name: 'Plugin1', pluginKey: 'key1', description: 'First duplicate' }, { name: 'Plugin2', pluginKey: 'key2', description: 'Second' }, ]; + require('~/app/clients/tools').availableTools.push(...mockPlugins); + mockCache.get.mockResolvedValue(null); - filterUniquePlugins.mockReturnValue(mockPlugins); - checkPluginAuth.mockReturnValue(true); await getAvailablePluginsController(mockReq, mockRes); - expect(filterUniquePlugins).toHaveBeenCalled(); expect(mockRes.status).toHaveBeenCalledWith(200); - // The response includes authenticated: true for each plugin when checkPluginAuth returns true - expect(mockRes.json).toHaveBeenCalledWith([ - { name: 'Plugin1', pluginKey: 'key1', description: 'First', authenticated: true }, - { name: 'Plugin2', pluginKey: 'key2', description: 'Second', authenticated: true }, - ]); + const responseData = mockRes.json.mock.calls[0][0]; + expect(responseData).toHaveLength(2); + expect(responseData[0].pluginKey).toBe('key1'); + expect(responseData[1].pluginKey).toBe('key2'); }); it('should use checkPluginAuth to verify plugin authentication', async () => { + // checkPluginAuth returns false for plugins without authConfig + // so authenticated property won't be added const mockPlugin = { name: 'Plugin1', pluginKey: 'key1', description: 'First' }; + require('~/app/clients/tools').availableTools.push(mockPlugin); mockCache.get.mockResolvedValue(null); - filterUniquePlugins.mockReturnValue([mockPlugin]); - checkPluginAuth.mockReturnValueOnce(true); await getAvailablePluginsController(mockReq, mockRes); - expect(checkPluginAuth).toHaveBeenCalledWith(mockPlugin); const responseData = mockRes.json.mock.calls[0][0]; - expect(responseData[0].authenticated).toBe(true); + // checkPluginAuth returns false, so authenticated property is not added + expect(responseData[0].authenticated).toBeUndefined(); }); it('should return cached plugins when available', async () => { @@ -115,8 +115,7 @@ describe('PluginController', () => { await getAvailablePluginsController(mockReq, mockRes); - expect(filterUniquePlugins).not.toHaveBeenCalled(); - expect(checkPluginAuth).not.toHaveBeenCalled(); + // When cache is hit, we return immediately without processing expect(mockRes.json).toHaveBeenCalledWith(cachedPlugins); }); @@ -126,10 +125,9 @@ describe('PluginController', () => { { name: 'Plugin2', pluginKey: 'key2', description: 'Second' }, ]; + require('~/app/clients/tools').availableTools.push(...mockPlugins); mockReq.app.locals.includedTools = ['key1']; mockCache.get.mockResolvedValue(null); - filterUniquePlugins.mockReturnValue(mockPlugins); - checkPluginAuth.mockReturnValue(false); await getAvailablePluginsController(mockReq, mockRes); @@ -143,70 +141,102 @@ describe('PluginController', () => { it('should use convertMCPToolsToPlugins for user-specific MCP tools', async () => { const mockUserTools = { [`tool1${Constants.mcp_delimiter}server1`]: { - function: { name: 'tool1', description: 'Tool 1' }, + type: 'function', + function: { + name: `tool1${Constants.mcp_delimiter}server1`, + description: 'Tool 1', + parameters: { type: 'object', properties: {} }, + }, }, }; - const mockConvertedPlugins = [ - { - name: 'tool1', - pluginKey: `tool1${Constants.mcp_delimiter}server1`, - description: 'Tool 1', - }, - ]; mockCache.get.mockResolvedValue(null); getCachedTools.mockResolvedValueOnce(mockUserTools); - convertMCPToolsToPlugins.mockReturnValue(mockConvertedPlugins); - filterUniquePlugins.mockImplementation((plugins) => plugins); getCustomConfig.mockResolvedValue(null); + // Mock second call to return tool definitions + getCachedTools.mockResolvedValueOnce(mockUserTools); + await getAvailableTools(mockReq, mockRes); - expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({ - functionTools: mockUserTools, - customConfig: null, - }); + const responseData = mockRes.json.mock.calls[0][0]; + // convertMCPToolsToPlugins should have converted the tool + expect(responseData.length).toBeGreaterThan(0); + const convertedTool = responseData.find( + (tool) => tool.pluginKey === `tool1${Constants.mcp_delimiter}server1`, + ); + expect(convertedTool).toBeDefined(); + expect(convertedTool.name).toBe('tool1'); }); it('should use filterUniquePlugins to deduplicate combined tools', async () => { - const mockUserPlugins = [ - { name: 'UserTool', pluginKey: 'user-tool', description: 'User tool' }, - ]; - const mockManifestPlugins = [ + const mockUserTools = { + 'user-tool': { + type: 'function', + function: { + name: 'user-tool', + description: 'User tool', + parameters: { type: 'object', properties: {} }, + }, + }, + }; + + const mockCachedPlugins = [ + { name: 'user-tool', pluginKey: 'user-tool', description: 'Duplicate user tool' }, { name: 'ManifestTool', pluginKey: 'manifest-tool', description: 'Manifest tool' }, ]; - mockCache.get.mockResolvedValue(mockManifestPlugins); - getCachedTools.mockResolvedValueOnce({}); - convertMCPToolsToPlugins.mockReturnValue(mockUserPlugins); - filterUniquePlugins.mockReturnValue([...mockUserPlugins, ...mockManifestPlugins]); + mockCache.get.mockResolvedValue(mockCachedPlugins); + getCachedTools.mockResolvedValueOnce(mockUserTools); getCustomConfig.mockResolvedValue(null); + // Mock second call to return tool definitions + getCachedTools.mockResolvedValueOnce(mockUserTools); + await getAvailableTools(mockReq, mockRes); - // Should be called to deduplicate the combined array - expect(filterUniquePlugins).toHaveBeenLastCalledWith([ - ...mockUserPlugins, - ...mockManifestPlugins, - ]); + const responseData = mockRes.json.mock.calls[0][0]; + // Should have deduplicated tools with same pluginKey + const userToolCount = responseData.filter((tool) => tool.pluginKey === 'user-tool').length; + expect(userToolCount).toBe(1); }); it('should use checkPluginAuth to verify authentication status', async () => { - const mockPlugin = { name: 'Tool1', pluginKey: 'tool1', description: 'Tool 1' }; + // Add a plugin to availableTools that will be checked + const mockPlugin = { + name: 'Tool1', + pluginKey: 'tool1', + description: 'Tool 1', + // No authConfig means checkPluginAuth returns false + }; + + require('~/app/clients/tools').availableTools.push(mockPlugin); mockCache.get.mockResolvedValue(null); - getCachedTools.mockResolvedValue({}); - convertMCPToolsToPlugins.mockReturnValue([]); - filterUniquePlugins.mockReturnValue([mockPlugin]); - checkPluginAuth.mockReturnValue(true); + getCachedTools.mockResolvedValue(null); getCustomConfig.mockResolvedValue(null); - // Mock getCachedTools second call to return tool definitions - getCachedTools.mockResolvedValueOnce({}).mockResolvedValueOnce({ tool1: true }); + // Mock loadAndFormatTools to return tool definitions including our tool + loadAndFormatTools.mockReturnValue({ + tool1: { + type: 'function', + function: { + name: 'tool1', + description: 'Tool 1', + parameters: {}, + }, + }, + }); await getAvailableTools(mockReq, mockRes); - expect(checkPluginAuth).toHaveBeenCalledWith(mockPlugin); + expect(mockRes.status).toHaveBeenCalledWith(200); + const responseData = mockRes.json.mock.calls[0][0]; + expect(Array.isArray(responseData)).toBe(true); + const tool = responseData.find((t) => t.pluginKey === 'tool1'); + expect(tool).toBeDefined(); + // checkPluginAuth returns false, so authenticated property is not added + expect(tool.authenticated).toBeUndefined(); }); it('should use getToolkitKey for toolkit validation', async () => { @@ -217,22 +247,38 @@ describe('PluginController', () => { toolkit: true, }; + require('~/app/clients/tools').availableTools.push(mockToolkit); + + // Mock toolkits to have a mapping + require('~/app/clients/tools').toolkits.push({ + name: 'Toolkit1', + pluginKey: 'toolkit1', + tools: ['toolkit1_function'], + }); + mockCache.get.mockResolvedValue(null); - getCachedTools.mockResolvedValue({}); - convertMCPToolsToPlugins.mockReturnValue([]); - filterUniquePlugins.mockReturnValue([mockToolkit]); - checkPluginAuth.mockReturnValue(false); - getToolkitKey.mockReturnValue('toolkit1'); + getCachedTools.mockResolvedValue(null); getCustomConfig.mockResolvedValue(null); - // Mock getCachedTools second call to return tool definitions - getCachedTools.mockResolvedValueOnce({}).mockResolvedValueOnce({ - toolkit1_function: true, + // Mock loadAndFormatTools to return tool definitions + loadAndFormatTools.mockReturnValue({ + toolkit1_function: { + type: 'function', + function: { + name: 'toolkit1_function', + description: 'Toolkit function', + parameters: {}, + }, + }, }); await getAvailableTools(mockReq, mockRes); - expect(getToolkitKey).toHaveBeenCalled(); + expect(mockRes.status).toHaveBeenCalledWith(200); + const responseData = mockRes.json.mock.calls[0][0]; + expect(Array.isArray(responseData)).toBe(true); + const toolkit = responseData.find((t) => t.pluginKey === 'toolkit1'); + expect(toolkit).toBeDefined(); }); }); @@ -243,32 +289,33 @@ describe('PluginController', () => { const functionTools = { [`test-tool${Constants.mcp_delimiter}test-server`]: { - function: { name: 'test-tool', description: 'A test tool' }, + type: 'function', + function: { + name: `test-tool${Constants.mcp_delimiter}test-server`, + description: 'A test tool', + parameters: { type: 'object', properties: {} }, + }, }, }; - const mockConvertedPlugin = { - name: 'test-tool', - pluginKey: `test-tool${Constants.mcp_delimiter}test-server`, - description: 'A test tool', - icon: mcpServers['test-server']?.iconPath, - authenticated: true, - authConfig: [], + // Mock the MCP manager to return tools + const mockMCPManager = { + getAllToolFunctions: jest.fn().mockResolvedValue(functionTools), }; + require('~/config').getMCPManager.mockReturnValue(mockMCPManager); + + getCachedTools.mockResolvedValueOnce({}); + + // Mock loadAndFormatTools to return empty object since these are MCP tools + loadAndFormatTools.mockReturnValue({}); getCachedTools.mockResolvedValueOnce(functionTools); - convertMCPToolsToPlugins.mockReturnValue([mockConvertedPlugin]); - filterUniquePlugins.mockImplementation((plugins) => plugins); - checkPluginAuth.mockReturnValue(true); - getToolkitKey.mockReturnValue(undefined); - - getCachedTools.mockResolvedValueOnce({ - [`test-tool${Constants.mcp_delimiter}test-server`]: true, - }); await getAvailableTools(mockReq, mockRes); const responseData = mockRes.json.mock.calls[0][0]; - return responseData.find((tool) => tool.name === 'test-tool'); + return responseData.find( + (tool) => tool.pluginKey === `test-tool${Constants.mcp_delimiter}test-server`, + ); }; it('should set plugin.icon when iconPath is defined', async () => { @@ -302,19 +349,21 @@ describe('PluginController', () => { }, }; - // We need to test the actual flow where MCP manager tools are included - const mcpManagerTools = [ - { - name: 'tool1', - pluginKey: `tool1${Constants.mcp_delimiter}test-server`, - description: 'Tool 1', - authenticated: true, + // Mock MCP tools returned by getAllToolFunctions + const mcpToolFunctions = { + [`tool1${Constants.mcp_delimiter}test-server`]: { + type: 'function', + function: { + name: `tool1${Constants.mcp_delimiter}test-server`, + description: 'Tool 1', + parameters: {}, + }, }, - ]; + }; // Mock the MCP manager to return tools const mockMCPManager = { - loadAllManifestTools: jest.fn().mockResolvedValue(mcpManagerTools), + getAllToolFunctions: jest.fn().mockResolvedValue(mcpToolFunctions), }; require('~/config').getMCPManager.mockReturnValue(mockMCPManager); @@ -324,19 +373,11 @@ describe('PluginController', () => { // First call returns user tools (empty in this case) getCachedTools.mockResolvedValueOnce({}); - // Mock convertMCPToolsToPlugins to return empty array for user tools - convertMCPToolsToPlugins.mockReturnValue([]); + // Mock loadAndFormatTools to return empty object for MCP tools + loadAndFormatTools.mockReturnValue({}); - // Mock filterUniquePlugins to pass through - filterUniquePlugins.mockImplementation((plugins) => plugins || []); - - // Mock checkPluginAuth - checkPluginAuth.mockReturnValue(true); - - // Second call returns tool definitions - getCachedTools.mockResolvedValueOnce({ - [`tool1${Constants.mcp_delimiter}test-server`]: true, - }); + // Second call returns tool definitions including our MCP tool + getCachedTools.mockResolvedValueOnce(mcpToolFunctions); await getAvailableTools(mockReq, mockRes); @@ -377,23 +418,23 @@ describe('PluginController', () => { it('should handle null cachedTools and cachedUserTools', async () => { mockCache.get.mockResolvedValue(null); getCachedTools.mockResolvedValue(null); - convertMCPToolsToPlugins.mockReturnValue(undefined); - filterUniquePlugins.mockImplementation((plugins) => plugins || []); getCustomConfig.mockResolvedValue(null); + // Mock loadAndFormatTools to return empty object when getCachedTools returns null + loadAndFormatTools.mockReturnValue({}); + await getAvailableTools(mockReq, mockRes); - // When cachedUserTools is null, convertMCPToolsToPlugins is not called - expect(convertMCPToolsToPlugins).not.toHaveBeenCalled(); + // Should handle null values gracefully + expect(mockRes.status).toHaveBeenCalledWith(200); }); it('should handle when getCachedTools returns undefined', async () => { mockCache.get.mockResolvedValue(null); - getCachedTools.mockResolvedValue(undefined); - convertMCPToolsToPlugins.mockReturnValue(undefined); - filterUniquePlugins.mockImplementation((plugins) => plugins || []); getCustomConfig.mockResolvedValue(null); - checkPluginAuth.mockReturnValue(false); + + // Mock loadAndFormatTools to return empty object when getCachedTools returns undefined + loadAndFormatTools.mockReturnValue({}); // Mock getCachedTools to return undefined for both calls getCachedTools.mockReset(); @@ -401,35 +442,40 @@ describe('PluginController', () => { await getAvailableTools(mockReq, mockRes); - // When cachedUserTools is undefined, convertMCPToolsToPlugins is not called - expect(convertMCPToolsToPlugins).not.toHaveBeenCalled(); + // Should handle undefined values gracefully + expect(mockRes.status).toHaveBeenCalledWith(200); }); it('should handle cachedToolsArray and userPlugins 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 = { - 'user-tool': { function: { name: 'user-tool', description: 'User tool' } }, + [`user-tool${Constants.mcp_delimiter}server1`]: { + type: 'function', + function: { + name: `user-tool${Constants.mcp_delimiter}server1`, + description: 'User tool', + parameters: {}, + }, + }, }; - const userPlugins = [{ name: 'UserTool', pluginKey: 'user-tool', description: 'User tool' }]; mockCache.get.mockResolvedValue(cachedTools); getCachedTools.mockResolvedValue(userTools); - convertMCPToolsToPlugins.mockReturnValue(userPlugins); - filterUniquePlugins.mockReturnValue([...userPlugins, ...cachedTools]); + getCustomConfig.mockResolvedValue(null); await getAvailableTools(mockReq, mockRes); expect(mockRes.status).toHaveBeenCalledWith(200); - expect(mockRes.json).toHaveBeenCalledWith([...userPlugins, ...cachedTools]); + const responseData = mockRes.json.mock.calls[0][0]; + // Should have both cached and user tools + expect(responseData.length).toBeGreaterThanOrEqual(2); }); it('should handle empty toolDefinitions object', async () => { mockCache.get.mockResolvedValue(null); getCachedTools.mockResolvedValueOnce({}).mockResolvedValueOnce({}); - convertMCPToolsToPlugins.mockReturnValue([]); - filterUniquePlugins.mockImplementation((plugins) => plugins || []); getCustomConfig.mockResolvedValue(null); - checkPluginAuth.mockReturnValue(true); await getAvailableTools(mockReq, mockRes); @@ -456,18 +502,6 @@ describe('PluginController', () => { getCustomConfig.mockResolvedValue(customConfig); getCachedTools.mockResolvedValueOnce(mockUserTools); - const mockPlugin = { - name: 'tool1', - pluginKey: `tool1${Constants.mcp_delimiter}test-server`, - description: 'Tool 1', - authenticated: true, - authConfig: [], - }; - - convertMCPToolsToPlugins.mockReturnValue([mockPlugin]); - filterUniquePlugins.mockImplementation((plugins) => plugins); - checkPluginAuth.mockReturnValue(true); - getCachedTools.mockResolvedValueOnce({ [`tool1${Constants.mcp_delimiter}test-server`]: true, }); @@ -483,8 +517,6 @@ describe('PluginController', () => { it('should handle req.app.locals with undefined filteredTools and includedTools', async () => { mockReq.app = { locals: {} }; mockCache.get.mockResolvedValue(null); - filterUniquePlugins.mockReturnValue([]); - checkPluginAuth.mockReturnValue(false); await getAvailablePluginsController(mockReq, mockRes); @@ -509,12 +541,11 @@ describe('PluginController', () => { }, }; + // Add the toolkit to availableTools + require('~/app/clients/tools').availableTools.push(mockToolkit); + mockCache.get.mockResolvedValue(null); getCachedTools.mockResolvedValue({}); - convertMCPToolsToPlugins.mockReturnValue([]); - filterUniquePlugins.mockReturnValue([mockToolkit]); - checkPluginAuth.mockReturnValue(false); - getToolkitKey.mockReturnValue(undefined); getCustomConfig.mockResolvedValue(null); // Mock loadAndFormatTools to return an empty object when toolDefinitions is null diff --git a/packages/api/src/mcp/MCPManager.ts b/packages/api/src/mcp/MCPManager.ts index b561e71fd9..4d5848ae7a 100644 --- a/packages/api/src/mcp/MCPManager.ts +++ b/packages/api/src/mcp/MCPManager.ts @@ -58,6 +58,21 @@ export class MCPManager extends UserConnectionManager { public getAppToolFunctions(): t.LCAvailableTools | null { return this.serversRegistry.toolFunctions!; } + /** Returns all available tool functions from all connections available to user */ + public async getAllToolFunctions(userId: string): Promise { + const allToolFunctions: t.LCAvailableTools = this.getAppToolFunctions() ?? {}; + const userConnections = this.getUserConnections(userId); + if (!userConnections || userConnections.size === 0) { + return allToolFunctions; + } + + for (const [serverName, connection] of userConnections.entries()) { + const toolFunctions = await this.serversRegistry.getToolFunctions(serverName, connection); + Object.assign(allToolFunctions, toolFunctions); + } + + return allToolFunctions; + } /** * Get instructions for MCP servers diff --git a/packages/api/src/mcp/MCPServersRegistry.ts b/packages/api/src/mcp/MCPServersRegistry.ts index 2e52f65e3a..cc913e2229 100644 --- a/packages/api/src/mcp/MCPServersRegistry.ts +++ b/packages/api/src/mcp/MCPServersRegistry.ts @@ -118,7 +118,7 @@ export class MCPServersRegistry { } /** Converts server tools to LibreChat-compatible tool functions format */ - private async getToolFunctions( + public async getToolFunctions( serverName: string, conn: MCPConnection, ): Promise { diff --git a/packages/api/src/tools/format.ts b/packages/api/src/tools/format.ts index 13075ab819..79ea4df085 100644 --- a/packages/api/src/tools/format.ts +++ b/packages/api/src/tools/format.ts @@ -46,6 +46,62 @@ export const checkPluginAuth = (plugin?: TPlugin): boolean => { }); }; +/** + * Converts MCP function format tool to plugin format + * @param params + * @param params.toolKey + * @param params.toolData + * @param params.customConfig + * @returns + */ +export function convertMCPToolToPlugin({ + toolKey, + toolData, + customConfig, +}: { + toolKey: string; + toolData: FunctionTool; + customConfig?: Partial | null; +}): TPlugin | undefined { + if (!toolData.function || !toolKey.includes(Constants.mcp_delimiter)) { + return; + } + + const functionData = toolData.function; + const parts = toolKey.split(Constants.mcp_delimiter); + const serverName = parts[parts.length - 1]; + + const serverConfig = customConfig?.mcpServers?.[serverName]; + + const plugin: TPlugin = { + /** Tool name without server suffix */ + name: parts[0], + pluginKey: toolKey, + description: functionData.description || '', + authenticated: true, + icon: serverConfig?.iconPath, + }; + + if (!serverConfig?.customUserVars) { + /** `authConfig` for MCP tools */ + plugin.authConfig = []; + return plugin; + } + + const customVarKeys = Object.keys(serverConfig.customUserVars); + if (customVarKeys.length === 0) { + plugin.authConfig = []; + } else { + plugin.authConfig = Object.entries(serverConfig.customUserVars).map(([key, value]) => ({ + authField: key, + label: value.title || key, + description: value.description || '', + })); + } + + return plugin; +} + /** * Converts MCP function format tools to plugin format * @param functionTools - Object with function format tools @@ -65,44 +121,10 @@ export function convertMCPToolsToPlugins({ const plugins: TPlugin[] = []; for (const [toolKey, toolData] of Object.entries(functionTools)) { - if (!toolData.function || !toolKey.includes(Constants.mcp_delimiter)) { - continue; - } - - const functionData = toolData.function; - const parts = toolKey.split(Constants.mcp_delimiter); - const serverName = parts[parts.length - 1]; - - const serverConfig = customConfig?.mcpServers?.[serverName]; - - const plugin: TPlugin = { - /** Tool name without server suffix */ - name: parts[0], - pluginKey: toolKey, - description: functionData.description || '', - authenticated: true, - icon: serverConfig?.iconPath, - }; - - if (!serverConfig?.customUserVars) { - /** `authConfig` for MCP tools */ - plugin.authConfig = []; + const plugin = convertMCPToolToPlugin({ toolKey, toolData, customConfig }); + if (plugin) { plugins.push(plugin); - continue; } - - const customVarKeys = Object.keys(serverConfig.customUserVars); - if (customVarKeys.length === 0) { - plugin.authConfig = []; - } else { - plugin.authConfig = Object.entries(serverConfig.customUserVars).map(([key, value]) => ({ - authField: key, - label: value.title || key, - description: value.description || '', - })); - } - - plugins.push(plugin); } return plugins;