diff --git a/api/models/Agent.spec.js b/api/models/Agent.spec.js index 8a14739301..f95db65013 100644 --- a/api/models/Agent.spec.js +++ b/api/models/Agent.spec.js @@ -8,6 +8,7 @@ process.env.CREDS_IV = '0123456789abcdef'; jest.mock('~/server/services/Config', () => ({ getCachedTools: jest.fn(), + getMCPServerTools: jest.fn(), })); const mongoose = require('mongoose'); @@ -30,7 +31,7 @@ const { generateActionMetadataHash, } = require('./Agent'); const permissionService = require('~/server/services/PermissionService'); -const { getCachedTools } = require('~/server/services/Config'); +const { getCachedTools, getMCPServerTools } = require('~/server/services/Config'); const { AclEntry } = require('~/db/models'); /** @@ -1929,6 +1930,16 @@ describe('models/Agent', () => { another_tool: {}, }); + // Mock getMCPServerTools to return tools for each server + getMCPServerTools.mockImplementation(async (server) => { + if (server === 'server1') { + return { tool1_mcp_server1: {} }; + } else if (server === 'server2') { + return { tool2_mcp_server2: {} }; + } + return null; + }); + const mockReq = { user: { id: 'user123' }, body: { @@ -2113,6 +2124,14 @@ describe('models/Agent', () => { getCachedTools.mockResolvedValue(availableTools); + // Mock getMCPServerTools to return all tools for server1 + getMCPServerTools.mockImplementation(async (server) => { + if (server === 'server1') { + return availableTools; // All 100 tools belong to server1 + } + return null; + }); + const mockReq = { user: { id: 'user123' }, body: { @@ -2654,6 +2673,17 @@ describe('models/Agent', () => { tool_mcp_server2: {}, // Different server }); + // Mock getMCPServerTools to return only tools matching the server + getMCPServerTools.mockImplementation(async (server) => { + if (server === 'server1') { + // Only return tool that correctly matches server1 format + return { tool_mcp_server1: {} }; + } else if (server === 'server2') { + return { tool_mcp_server2: {} }; + } + return null; + }); + const mockReq = { user: { id: 'user123' }, body: { diff --git a/api/server/controllers/mcp.js b/api/server/controllers/mcp.js index 17c710dae3..839d9bd17b 100644 --- a/api/server/controllers/mcp.js +++ b/api/server/controllers/mcp.js @@ -4,7 +4,6 @@ */ const { logger } = require('@librechat/data-schemas'); const { Constants } = require('librechat-data-provider'); -const { convertMCPToolToPlugin } = require('@librechat/api'); const { cacheMCPServerTools, getMCPServerTools, @@ -14,7 +13,6 @@ const { getMCPManager } = require('~/config'); /** * Get all MCP tools available to the user - * Returns only MCP tools, not regular LibreChat tools */ const getMCPTools = async (req, res) => { try { @@ -26,77 +24,97 @@ const getMCPTools = async (req, res) => { const appConfig = req.config ?? (await getAppConfig({ role: req.user?.role })); if (!appConfig?.mcpConfig) { - return res.status(200).json([]); + return res.status(200).json({ servers: {} }); } const mcpManager = getMCPManager(); const configuredServers = Object.keys(appConfig.mcpConfig); - const mcpTools = []; + const mcpServers = {}; - // Fetch tools from each configured server + const cachePromises = configuredServers.map((serverName) => + getMCPServerTools(serverName).then((tools) => ({ serverName, tools })), + ); + const cacheResults = await Promise.all(cachePromises); + + const serverToolsMap = new Map(); + for (const { serverName, tools } of cacheResults) { + if (tools) { + serverToolsMap.set(serverName, tools); + continue; + } + + const serverTools = await mcpManager.getServerToolFunctions(userId, serverName); + if (!serverTools) { + logger.debug(`[getMCPTools] No tools found for server ${serverName}`); + continue; + } + serverToolsMap.set(serverName, serverTools); + + if (Object.keys(serverTools).length > 0) { + // Cache asynchronously without blocking + cacheMCPServerTools({ serverName, serverTools }).catch((err) => + logger.error(`[getMCPTools] Failed to cache tools for ${serverName}:`, err), + ); + } + } + + // Process each configured server for (const serverName of configuredServers) { try { - // First check server-specific cache - let serverTools = await getMCPServerTools(serverName); + const serverTools = serverToolsMap.get(serverName); - if (!serverTools) { - // If not cached, fetch from MCP manager - const allTools = await mcpManager.getAllToolFunctions(userId); - serverTools = {}; + // Get server config once + const serverConfig = appConfig.mcpConfig[serverName]; + const rawServerConfig = mcpManager.getRawConfig(serverName); - // Filter tools for this specific server - for (const [toolKey, toolData] of Object.entries(allTools)) { - if (toolKey.endsWith(`${Constants.mcp_delimiter}${serverName}`)) { - serverTools[toolKey] = toolData; - } - } + // Initialize server object with all server-level data + const server = { + name: serverName, + icon: rawServerConfig?.iconPath || '', + authenticated: true, + authConfig: [], + tools: [], + }; - // Cache server tools if found - if (Object.keys(serverTools).length > 0) { - await cacheMCPServerTools({ serverName, serverTools }); + // Set authentication config once for the server + if (serverConfig?.customUserVars) { + const customVarKeys = Object.keys(serverConfig.customUserVars); + if (customVarKeys.length > 0) { + server.authConfig = Object.entries(serverConfig.customUserVars).map(([key, value]) => ({ + authField: key, + label: value.title || key, + description: value.description || '', + })); + server.authenticated = false; } } - // Convert to plugin format - for (const [toolKey, toolData] of Object.entries(serverTools)) { - const plugin = convertMCPToolToPlugin({ - toolKey, - toolData, - mcpManager, - }); - - if (plugin) { - // Add authentication config from server config - const serverConfig = appConfig.mcpConfig[serverName]; - if (serverConfig?.customUserVars) { - const customVarKeys = Object.keys(serverConfig.customUserVars); - if (customVarKeys.length === 0) { - plugin.authConfig = []; - plugin.authenticated = true; - } else { - plugin.authConfig = Object.entries(serverConfig.customUserVars).map( - ([key, value]) => ({ - authField: key, - label: value.title || key, - description: value.description || '', - }), - ); - plugin.authenticated = false; - } - } else { - plugin.authConfig = []; - plugin.authenticated = true; + // Process tools efficiently - no need for convertMCPToolToPlugin + if (serverTools) { + for (const [toolKey, toolData] of Object.entries(serverTools)) { + if (!toolData.function || !toolKey.includes(Constants.mcp_delimiter)) { + continue; } - mcpTools.push(plugin); + const toolName = toolKey.split(Constants.mcp_delimiter)[0]; + server.tools.push({ + name: toolName, + pluginKey: toolKey, + description: toolData.function.description || '', + }); } } + + // Only add server if it has tools or is configured + if (server.tools.length > 0 || serverConfig) { + mcpServers[serverName] = server; + } } catch (error) { logger.error(`[getMCPTools] Error loading tools for server ${serverName}:`, error); } } - res.status(200).json(mcpTools); + res.status(200).json({ servers: mcpServers }); } catch (error) { logger.error('[getMCPTools]', error); res.status(500).json({ message: error.message }); diff --git a/api/server/controllers/mcp.spec.js b/api/server/controllers/mcp.spec.js deleted file mode 100644 index 6dea3ac348..0000000000 --- a/api/server/controllers/mcp.spec.js +++ /dev/null @@ -1,561 +0,0 @@ -const { getMCPTools } = require('./mcp'); -const { getAppConfig, getMCPServerTools } = require('~/server/services/Config'); -const { getMCPManager } = require('~/config'); -const { convertMCPToolToPlugin } = require('@librechat/api'); - -jest.mock('@librechat/data-schemas', () => ({ - logger: { - debug: jest.fn(), - error: jest.fn(), - warn: jest.fn(), - }, -})); - -jest.mock('librechat-data-provider', () => ({ - Constants: { - mcp_delimiter: '~~~', - }, -})); - -jest.mock('@librechat/api', () => ({ - convertMCPToolToPlugin: jest.fn(), -})); - -jest.mock('~/server/services/Config', () => ({ - getAppConfig: jest.fn(), - getMCPServerTools: jest.fn(), - cacheMCPServerTools: jest.fn(), -})); - -jest.mock('~/config', () => ({ - getMCPManager: jest.fn(), -})); - -describe('MCP Controller', () => { - let mockReq, mockRes, mockMCPManager; - - beforeEach(() => { - jest.clearAllMocks(); - - mockReq = { - user: { id: 'test-user-id', role: 'user' }, - config: null, - }; - - mockRes = { - status: jest.fn().mockReturnThis(), - json: jest.fn(), - }; - - mockMCPManager = { - getAllToolFunctions: jest.fn().mockResolvedValue({}), - }; - - getMCPManager.mockReturnValue(mockMCPManager); - getAppConfig.mockResolvedValue({ - mcpConfig: {}, - }); - getMCPServerTools.mockResolvedValue(null); - }); - - describe('getMCPTools', () => { - it('should return 401 when user ID is not found', async () => { - mockReq.user = null; - - await getMCPTools(mockReq, mockRes); - - expect(mockRes.status).toHaveBeenCalledWith(401); - expect(mockRes.json).toHaveBeenCalledWith({ message: 'Unauthorized' }); - const { logger } = require('@librechat/data-schemas'); - expect(logger.warn).toHaveBeenCalledWith('[getMCPTools] User ID not found in request'); - }); - - it('should return empty array when no mcpConfig exists', async () => { - getAppConfig.mockResolvedValue({ - // No mcpConfig - }); - - await getMCPTools(mockReq, mockRes); - - expect(mockRes.status).toHaveBeenCalledWith(200); - expect(mockRes.json).toHaveBeenCalledWith([]); - }); - - it('should use cached server tools when available', async () => { - const cachedTools = { - 'tool1~~~server1': { - type: 'function', - function: { - name: 'tool1', - description: 'Tool 1', - parameters: {}, - }, - }, - }; - - getMCPServerTools.mockResolvedValue(cachedTools); - getAppConfig.mockResolvedValue({ - mcpConfig: { - server1: {}, - }, - }); - - const mockPlugin = { - name: 'Tool 1', - pluginKey: 'tool1~~~server1', - description: 'Tool 1', - }; - convertMCPToolToPlugin.mockReturnValue(mockPlugin); - - await getMCPTools(mockReq, mockRes); - - expect(getMCPServerTools).toHaveBeenCalledWith('server1'); - expect(mockMCPManager.getAllToolFunctions).not.toHaveBeenCalled(); - expect(convertMCPToolToPlugin).toHaveBeenCalledWith({ - toolKey: 'tool1~~~server1', - toolData: cachedTools['tool1~~~server1'], - mcpManager: mockMCPManager, - }); - expect(mockRes.status).toHaveBeenCalledWith(200); - expect(mockRes.json).toHaveBeenCalledWith([ - { - ...mockPlugin, - authConfig: [], - authenticated: true, - }, - ]); - }); - - it('should fetch from MCP manager when cache is empty', async () => { - getMCPServerTools.mockResolvedValue(null); - - const allTools = { - 'tool1~~~server1': { - type: 'function', - function: { - name: 'tool1', - description: 'Tool 1', - parameters: {}, - }, - }, - 'tool2~~~server2': { - type: 'function', - function: { - name: 'tool2', - description: 'Tool 2', - parameters: {}, - }, - }, - }; - - mockMCPManager.getAllToolFunctions.mockResolvedValue(allTools); - - getAppConfig.mockResolvedValue({ - mcpConfig: { - server1: {}, - }, - }); - - const mockPlugin = { - name: 'Tool 1', - pluginKey: 'tool1~~~server1', - description: 'Tool 1', - }; - convertMCPToolToPlugin.mockReturnValue(mockPlugin); - - await getMCPTools(mockReq, mockRes); - - expect(getMCPServerTools).toHaveBeenCalledWith('server1'); - expect(mockMCPManager.getAllToolFunctions).toHaveBeenCalledWith('test-user-id'); - - // Should cache the server tools - const { cacheMCPServerTools } = require('~/server/services/Config'); - expect(cacheMCPServerTools).toHaveBeenCalledWith({ - serverName: 'server1', - serverTools: { - 'tool1~~~server1': allTools['tool1~~~server1'], - }, - }); - - expect(mockRes.status).toHaveBeenCalledWith(200); - expect(mockRes.json).toHaveBeenCalledWith([ - { - ...mockPlugin, - authConfig: [], - authenticated: true, - }, - ]); - }); - - it('should handle custom user variables in server config', async () => { - getMCPServerTools.mockResolvedValue({ - 'tool1~~~server1': { - type: 'function', - function: { - name: 'tool1', - description: 'Tool 1', - parameters: {}, - }, - }, - }); - - getAppConfig.mockResolvedValue({ - mcpConfig: { - server1: { - customUserVars: { - API_KEY: { - title: 'API Key', - description: 'Your API key', - }, - SECRET: { - title: 'Secret Token', - description: 'Your secret token', - }, - }, - }, - }, - }); - - const mockPlugin = { - name: 'Tool 1', - pluginKey: 'tool1~~~server1', - description: 'Tool 1', - }; - convertMCPToolToPlugin.mockReturnValue(mockPlugin); - - await getMCPTools(mockReq, mockRes); - - expect(mockRes.status).toHaveBeenCalledWith(200); - expect(mockRes.json).toHaveBeenCalledWith([ - { - ...mockPlugin, - authConfig: [ - { - authField: 'API_KEY', - label: 'API Key', - description: 'Your API key', - }, - { - authField: 'SECRET', - label: 'Secret Token', - description: 'Your secret token', - }, - ], - authenticated: false, - }, - ]); - }); - - it('should handle empty custom user variables', async () => { - getMCPServerTools.mockResolvedValue({ - 'tool1~~~server1': { - type: 'function', - function: { - name: 'tool1', - description: 'Tool 1', - parameters: {}, - }, - }, - }); - - getAppConfig.mockResolvedValue({ - mcpConfig: { - server1: { - customUserVars: {}, - }, - }, - }); - - const mockPlugin = { - name: 'Tool 1', - pluginKey: 'tool1~~~server1', - description: 'Tool 1', - }; - convertMCPToolToPlugin.mockReturnValue(mockPlugin); - - await getMCPTools(mockReq, mockRes); - - expect(mockRes.status).toHaveBeenCalledWith(200); - expect(mockRes.json).toHaveBeenCalledWith([ - { - ...mockPlugin, - authConfig: [], - authenticated: true, - }, - ]); - }); - - it('should handle multiple servers', async () => { - getMCPServerTools.mockResolvedValue(null); - - const allTools = { - 'tool1~~~server1': { - type: 'function', - function: { - name: 'tool1', - description: 'Tool 1', - parameters: {}, - }, - }, - 'tool2~~~server2': { - type: 'function', - function: { - name: 'tool2', - description: 'Tool 2', - parameters: {}, - }, - }, - }; - - mockMCPManager.getAllToolFunctions.mockResolvedValue(allTools); - - getAppConfig.mockResolvedValue({ - mcpConfig: { - server1: {}, - server2: {}, - }, - }); - - const mockPlugin1 = { - name: 'Tool 1', - pluginKey: 'tool1~~~server1', - description: 'Tool 1', - }; - const mockPlugin2 = { - name: 'Tool 2', - pluginKey: 'tool2~~~server2', - description: 'Tool 2', - }; - - convertMCPToolToPlugin.mockReturnValueOnce(mockPlugin1).mockReturnValueOnce(mockPlugin2); - - await getMCPTools(mockReq, mockRes); - - expect(getMCPServerTools).toHaveBeenCalledWith('server1'); - expect(getMCPServerTools).toHaveBeenCalledWith('server2'); - expect(mockMCPManager.getAllToolFunctions).toHaveBeenCalledTimes(2); - - expect(mockRes.status).toHaveBeenCalledWith(200); - expect(mockRes.json).toHaveBeenCalledWith([ - { - ...mockPlugin1, - authConfig: [], - authenticated: true, - }, - { - ...mockPlugin2, - authConfig: [], - authenticated: true, - }, - ]); - }); - - it('should handle server-specific errors gracefully', async () => { - getMCPServerTools.mockResolvedValue(null); - mockMCPManager.getAllToolFunctions.mockRejectedValue(new Error('Server connection failed')); - - getAppConfig.mockResolvedValue({ - mcpConfig: { - server1: {}, - server2: {}, - }, - }); - - await getMCPTools(mockReq, mockRes); - - const { logger } = require('@librechat/data-schemas'); - expect(logger.error).toHaveBeenCalledWith( - '[getMCPTools] Error loading tools for server server1:', - expect.any(Error), - ); - expect(logger.error).toHaveBeenCalledWith( - '[getMCPTools] Error loading tools for server server2:', - expect.any(Error), - ); - - // Should still return 200 with empty array - expect(mockRes.status).toHaveBeenCalledWith(200); - expect(mockRes.json).toHaveBeenCalledWith([]); - }); - - it('should skip tools when convertMCPToolToPlugin returns null', async () => { - getMCPServerTools.mockResolvedValue({ - 'tool1~~~server1': { - type: 'function', - function: { - name: 'tool1', - description: 'Tool 1', - parameters: {}, - }, - }, - 'tool2~~~server1': { - type: 'function', - function: { - name: 'tool2', - description: 'Tool 2', - parameters: {}, - }, - }, - }); - - getAppConfig.mockResolvedValue({ - mcpConfig: { - server1: {}, - }, - }); - - const mockPlugin = { - name: 'Tool 1', - pluginKey: 'tool1~~~server1', - description: 'Tool 1', - }; - - // First tool returns plugin, second returns null - convertMCPToolToPlugin.mockReturnValueOnce(mockPlugin).mockReturnValueOnce(null); - - await getMCPTools(mockReq, mockRes); - - expect(mockRes.status).toHaveBeenCalledWith(200); - expect(mockRes.json).toHaveBeenCalledWith([ - { - ...mockPlugin, - authConfig: [], - authenticated: true, - }, - ]); - }); - - it('should use req.config when available', async () => { - const reqConfig = { - mcpConfig: { - server1: {}, - }, - }; - mockReq.config = reqConfig; - - getMCPServerTools.mockResolvedValue({ - 'tool1~~~server1': { - type: 'function', - function: { - name: 'tool1', - description: 'Tool 1', - parameters: {}, - }, - }, - }); - - const mockPlugin = { - name: 'Tool 1', - pluginKey: 'tool1~~~server1', - description: 'Tool 1', - }; - convertMCPToolToPlugin.mockReturnValue(mockPlugin); - - await getMCPTools(mockReq, mockRes); - - // Should not call getAppConfig when req.config is available - expect(getAppConfig).not.toHaveBeenCalled(); - - expect(mockRes.status).toHaveBeenCalledWith(200); - expect(mockRes.json).toHaveBeenCalledWith([ - { - ...mockPlugin, - authConfig: [], - authenticated: true, - }, - ]); - }); - - it('should handle general error in getMCPTools', async () => { - const error = new Error('Unexpected error'); - getAppConfig.mockRejectedValue(error); - - await getMCPTools(mockReq, mockRes); - - const { logger } = require('@librechat/data-schemas'); - expect(logger.error).toHaveBeenCalledWith('[getMCPTools]', error); - expect(mockRes.status).toHaveBeenCalledWith(500); - expect(mockRes.json).toHaveBeenCalledWith({ message: 'Unexpected error' }); - }); - - it('should handle custom user variables without title or description', async () => { - getMCPServerTools.mockResolvedValue({ - 'tool1~~~server1': { - type: 'function', - function: { - name: 'tool1', - description: 'Tool 1', - parameters: {}, - }, - }, - }); - - getAppConfig.mockResolvedValue({ - mcpConfig: { - server1: { - customUserVars: { - MY_VAR: { - // No title or description - }, - }, - }, - }, - }); - - const mockPlugin = { - name: 'Tool 1', - pluginKey: 'tool1~~~server1', - description: 'Tool 1', - }; - convertMCPToolToPlugin.mockReturnValue(mockPlugin); - - await getMCPTools(mockReq, mockRes); - - expect(mockRes.status).toHaveBeenCalledWith(200); - expect(mockRes.json).toHaveBeenCalledWith([ - { - ...mockPlugin, - authConfig: [ - { - authField: 'MY_VAR', - label: 'MY_VAR', // Falls back to key - description: '', // Empty string - }, - ], - authenticated: false, - }, - ]); - }); - - it('should not cache when no tools are found for a server', async () => { - getMCPServerTools.mockResolvedValue(null); - - const allTools = { - 'tool1~~~otherserver': { - type: 'function', - function: { - name: 'tool1', - description: 'Tool 1', - parameters: {}, - }, - }, - }; - - mockMCPManager.getAllToolFunctions.mockResolvedValue(allTools); - - getAppConfig.mockResolvedValue({ - mcpConfig: { - server1: {}, - }, - }); - - await getMCPTools(mockReq, mockRes); - - const { cacheMCPServerTools } = require('~/server/services/Config'); - expect(cacheMCPServerTools).not.toHaveBeenCalled(); - - expect(mockRes.status).toHaveBeenCalledWith(200); - expect(mockRes.json).toHaveBeenCalledWith([]); - }); - }); -}); diff --git a/client/src/Providers/AgentPanelContext.tsx b/client/src/Providers/AgentPanelContext.tsx index e97e25403b..891e1a91fc 100644 --- a/client/src/Providers/AgentPanelContext.tsx +++ b/client/src/Providers/AgentPanelContext.tsx @@ -1,5 +1,5 @@ import React, { createContext, useContext, useState, useMemo } from 'react'; -import { Constants, EModelEndpoint } from 'librechat-data-provider'; +import { EModelEndpoint } from 'librechat-data-provider'; import type { MCP, Action, TPlugin } from 'librechat-data-provider'; import type { AgentPanelContextType, MCPServerInfo } from '~/common'; import { @@ -30,6 +30,7 @@ export function AgentPanelProvider({ children }: { children: React.ReactNode }) const [activePanel, setActivePanel] = useState(Panel.builder); const [agent_id, setCurrentAgentId] = useState(undefined); + const { data: startupConfig } = useGetStartupConfig(); const { data: actions } = useGetActionsQuery(EModelEndpoint.agents, { enabled: !!agent_id, }); @@ -38,11 +39,10 @@ export function AgentPanelProvider({ children }: { children: React.ReactNode }) enabled: !!agent_id, }); - const { data: mcpTools } = useMCPToolsQuery({ - enabled: !!agent_id, + const { data: mcpData } = useMCPToolsQuery({ + enabled: !!agent_id && startupConfig?.mcpServers != null, }); - const { data: startupConfig } = useGetStartupConfig(); const { agentsConfig, endpointsConfig } = useGetAgentsConfig(); const mcpServerNames = useMemo( () => Object.keys(startupConfig?.mcpServers ?? {}), @@ -57,33 +57,34 @@ export function AgentPanelProvider({ children }: { children: React.ReactNode }) const configuredServers = new Set(mcpServerNames); const serversMap = new Map(); - if (mcpTools) { - for (const pluginTool of mcpTools) { - if (pluginTool.pluginKey.includes(Constants.mcp_delimiter)) { - const [_toolName, serverName] = pluginTool.pluginKey.split(Constants.mcp_delimiter); + if (mcpData?.servers) { + for (const [serverName, serverData] of Object.entries(mcpData.servers)) { + const metadata = { + name: serverName, + pluginKey: serverName, + description: `${localize('com_ui_tool_collection_prefix')} ${serverName}`, + icon: serverData.icon || '', + authConfig: serverData.authConfig, + authenticated: serverData.authenticated, + } as TPlugin; - if (!serversMap.has(serverName)) { - const metadata = { - name: serverName, - pluginKey: serverName, - description: `${localize('com_ui_tool_collection_prefix')} ${serverName}`, - icon: pluginTool.icon || '', - } as TPlugin; + const tools = serverData.tools.map((tool) => ({ + tool_id: tool.pluginKey, + metadata: { + ...tool, + icon: serverData.icon, + authConfig: serverData.authConfig, + authenticated: serverData.authenticated, + } as TPlugin, + })); - serversMap.set(serverName, { - serverName, - tools: [], - isConfigured: configuredServers.has(serverName), - isConnected: connectionStatus?.[serverName]?.connectionState === 'connected', - metadata, - }); - } - - serversMap.get(serverName)!.tools.push({ - tool_id: pluginTool.pluginKey, - metadata: pluginTool as TPlugin, - }); - } + serversMap.set(serverName, { + serverName, + tools, + isConfigured: configuredServers.has(serverName), + isConnected: connectionStatus?.[serverName]?.connectionState === 'connected', + metadata, + }); } } @@ -109,7 +110,7 @@ export function AgentPanelProvider({ children }: { children: React.ReactNode }) } return serversMap; - }, [mcpTools, localize, mcpServerNames, connectionStatus]); + }, [mcpData, localize, mcpServerNames, connectionStatus]); const value: AgentPanelContextType = { mcp, @@ -120,7 +121,6 @@ export function AgentPanelProvider({ children }: { children: React.ReactNode }) setMcps, agent_id, setAction, - mcpTools, activePanel, regularTools, agentsConfig, diff --git a/client/src/common/types.ts b/client/src/common/types.ts index 211c7a7a7d..647950e546 100644 --- a/client/src/common/types.ts +++ b/client/src/common/types.ts @@ -234,7 +234,6 @@ export type AgentPanelContextType = { setMcps: React.Dispatch>; activePanel?: string; regularTools?: t.TPlugin[]; - mcpTools?: t.TPlugin[]; setActivePanel: React.Dispatch>; setCurrentAgentId: React.Dispatch>; agent_id?: string; diff --git a/client/src/components/SidePanel/Agents/AgentConfig.tsx b/client/src/components/SidePanel/Agents/AgentConfig.tsx index dcb047374c..8e4bfb122e 100644 --- a/client/src/components/SidePanel/Agents/AgentConfig.tsx +++ b/client/src/components/SidePanel/Agents/AgentConfig.tsx @@ -473,13 +473,15 @@ export default function AgentConfig({ createMutation }: Pick - + {startupConfig?.mcpServers != null && ( + + )} ); } diff --git a/client/src/components/Tools/MCPToolSelectDialog.tsx b/client/src/components/Tools/MCPToolSelectDialog.tsx index 62710d032c..95588826d6 100644 --- a/client/src/components/Tools/MCPToolSelectDialog.tsx +++ b/client/src/components/Tools/MCPToolSelectDialog.tsx @@ -8,9 +8,9 @@ import type { TError, AgentToolType } from 'librechat-data-provider'; import type { AgentForm, TPluginStoreDialogProps } from '~/common'; import { useLocalize, usePluginDialogHelpers, useMCPServerManager } from '~/hooks'; import CustomUserVarsSection from '~/components/MCP/CustomUserVarsSection'; -import { useGetStartupConfig, useMCPToolsQuery } from '~/data-provider'; import { PluginPagination } from '~/components/Plugins/Store'; import { useAgentPanelContext } from '~/Providers'; +import { useMCPToolsQuery } from '~/data-provider'; import MCPToolItem from './MCPToolItem'; function MCPToolSelectDialog({ @@ -24,11 +24,12 @@ function MCPToolSelectDialog({ endpoint: EModelEndpoint.agents; }) { const localize = useLocalize(); - const { mcpServersMap } = useAgentPanelContext(); const { initializeServer } = useMCPServerManager(); - const { data: startupConfig } = useGetStartupConfig(); - const { refetch: refetchMCPTools } = useMCPToolsQuery(); const { getValues, setValue } = useFormContext(); + const { mcpServersMap, startupConfig } = useAgentPanelContext(); + const { refetch: refetchMCPTools } = useMCPToolsQuery({ + enabled: mcpServersMap.size > 0, + }); const [isInitializing, setIsInitializing] = useState(null); const [configuringServer, setConfiguringServer] = useState(null); @@ -90,18 +91,17 @@ function MCPToolSelectDialog({ setIsInitializing(null); }, onSuccess: async () => { - const { data: updatedMCPTools } = await refetchMCPTools(); + const { data: updatedMCPData } = await refetchMCPTools(); const currentTools = getValues('tools') || []; const toolsToAdd: string[] = [ `${Constants.mcp_server}${Constants.mcp_delimiter}${serverName}`, ]; - if (updatedMCPTools) { - updatedMCPTools.forEach((tool) => { - if (tool.pluginKey.endsWith(`${Constants.mcp_delimiter}${serverName}`)) { - toolsToAdd.push(tool.pluginKey); - } + if (updatedMCPData?.servers?.[serverName]) { + const serverData = updatedMCPData.servers[serverName]; + serverData.tools.forEach((tool) => { + toolsToAdd.push(tool.pluginKey); }); } diff --git a/client/src/data-provider/mcp.ts b/client/src/data-provider/mcp.ts index cc144f78b4..726f4415e6 100644 --- a/client/src/data-provider/mcp.ts +++ b/client/src/data-provider/mcp.ts @@ -5,17 +5,17 @@ import { useQuery } from '@tanstack/react-query'; import { QueryKeys, dataService } from 'librechat-data-provider'; import type { UseQueryOptions, QueryObserverResult } from '@tanstack/react-query'; -import type { TPlugin } from 'librechat-data-provider'; +import type { MCPServersResponse } from 'librechat-data-provider'; /** * Hook for fetching MCP-specific tools * @param config - React Query configuration - * @returns MCP tools grouped by server + * @returns MCP servers with their tools */ -export const useMCPToolsQuery = ( - config?: UseQueryOptions, +export const useMCPToolsQuery = ( + config?: UseQueryOptions, ): QueryObserverResult => { - return useQuery( + return useQuery( [QueryKeys.mcpTools], () => dataService.getMCPTools(), { diff --git a/client/src/hooks/Config/useAppStartup.ts b/client/src/hooks/Config/useAppStartup.ts index 2d53297606..bdac6d616f 100644 --- a/client/src/hooks/Config/useAppStartup.ts +++ b/client/src/hooks/Config/useAppStartup.ts @@ -7,6 +7,7 @@ import type { TStartupConfig, TPlugin, TUser } from 'librechat-data-provider'; import { mapPlugins, selectPlugins, processPlugins } from '~/utils'; import { cleanupTimestampedStorage } from '~/utils/timestamps'; import useSpeechSettingsInit from './useSpeechSettingsInit'; +import { useMCPToolsQuery } from '~/data-provider'; import store from '~/store'; const pluginStore: TPlugin = { @@ -35,6 +36,10 @@ export default function useAppStartup({ useSpeechSettingsInit(!!user); + useMCPToolsQuery({ + enabled: !!startupConfig?.mcpServers && !!user, + }); + /** Clean up old localStorage entries on startup */ useEffect(() => { cleanupTimestampedStorage(); diff --git a/client/src/hooks/MCP/index.ts b/client/src/hooks/MCP/index.ts index fa2a7d7fb6..0ee7e9494d 100644 --- a/client/src/hooks/MCP/index.ts +++ b/client/src/hooks/MCP/index.ts @@ -1,4 +1,3 @@ -export * from './useGetMCPTools'; export * from './useMCPConnectionStatus'; export * from './useMCPSelect'; export * from './useVisibleTools'; diff --git a/client/src/hooks/MCP/useGetMCPTools.ts b/client/src/hooks/MCP/useGetMCPTools.ts deleted file mode 100644 index 2785675b99..0000000000 --- a/client/src/hooks/MCP/useGetMCPTools.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { useMemo } from 'react'; -import { Constants } from 'librechat-data-provider'; -import type { TPlugin } from 'librechat-data-provider'; -import { useMCPToolsQuery, useGetStartupConfig } from '~/data-provider'; - -/** - * Hook for fetching and filtering MCP tools based on server configuration - * Uses the dedicated MCP tools query instead of filtering from general tools - */ -export function useGetMCPTools() { - const { data: startupConfig } = useGetStartupConfig(); - - // Use dedicated MCP tools query - const { data: rawMcpTools } = useMCPToolsQuery({ - select: (data: TPlugin[]) => { - // Group tools by server for easier management - const mcpToolsMap = new Map(); - data.forEach((tool) => { - const parts = tool.pluginKey.split(Constants.mcp_delimiter); - const serverName = parts[parts.length - 1]; - if (!mcpToolsMap.has(serverName)) { - mcpToolsMap.set(serverName, { - name: serverName, - pluginKey: tool.pluginKey, - authConfig: tool.authConfig, - authenticated: tool.authenticated, - }); - } - }); - return Array.from(mcpToolsMap.values()); - }, - }); - - // Filter out servers that have chatMenu disabled - const mcpToolDetails = useMemo(() => { - if (!rawMcpTools || !startupConfig?.mcpServers) { - return rawMcpTools; - } - return rawMcpTools.filter((tool) => { - const serverConfig = startupConfig?.mcpServers?.[tool.name]; - return serverConfig?.chatMenu !== false; - }); - }, [rawMcpTools, startupConfig?.mcpServers]); - - return { - mcpToolDetails, - }; -} diff --git a/client/src/hooks/MCP/useMCPServerManager.ts b/client/src/hooks/MCP/useMCPServerManager.ts index c301939193..440e3bb14a 100644 --- a/client/src/hooks/MCP/useMCPServerManager.ts +++ b/client/src/hooks/MCP/useMCPServerManager.ts @@ -7,9 +7,9 @@ import { useUpdateUserPluginsMutation, useReinitializeMCPServerMutation, } from 'librechat-data-provider/react-query'; -import type { TUpdateUserPlugins, TPlugin } from 'librechat-data-provider'; +import type { TUpdateUserPlugins, TPlugin, MCPServersResponse } from 'librechat-data-provider'; import type { ConfigFieldDetail } from '~/common'; -import { useLocalize, useMCPSelect, useGetMCPTools, useMCPConnectionStatus } from '~/hooks'; +import { useLocalize, useMCPSelect, useMCPConnectionStatus } from '~/hooks'; import { useGetStartupConfig } from '~/data-provider'; interface ServerState { @@ -24,7 +24,6 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin const localize = useLocalize(); const queryClient = useQueryClient(); const { showToast } = useToastContext(); - const { mcpToolDetails } = useGetMCPTools(); const { data: startupConfig } = useGetStartupConfig(); const { mcpValues, setMCPValues, isPinned, setIsPinned } = useMCPSelect({ conversationId }); @@ -448,7 +447,10 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin const getServerStatusIconProps = useCallback( (serverName: string) => { - const tool = mcpToolDetails?.find((t) => t.name === serverName); + const mcpData = queryClient.getQueryData([ + QueryKeys.mcpTools, + ]); + const serverData = mcpData?.servers?.[serverName]; const serverStatus = connectionStatus?.[serverName]; const serverConfig = startupConfig?.mcpServers?.[serverName]; @@ -458,17 +460,20 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin previousFocusRef.current = document.activeElement as HTMLElement; - const configTool = tool || { + /** Minimal TPlugin object for the config dialog */ + const configTool: TPlugin = { name: serverName, pluginKey: `${Constants.mcp_prefix}${serverName}`, - authConfig: serverConfig?.customUserVars - ? Object.entries(serverConfig.customUserVars).map(([key, config]) => ({ - authField: key, - label: config.title, - description: config.description, - })) - : [], - authenticated: false, + authConfig: + serverData?.authConfig || + (serverConfig?.customUserVars + ? Object.entries(serverConfig.customUserVars).map(([key, config]) => ({ + authField: key, + label: config.title, + description: config.description, + })) + : []), + authenticated: serverData?.authenticated ?? false, }; setSelectedToolForConfig(configTool); setIsConfigModalOpen(true); @@ -486,7 +491,14 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin return { serverName, serverStatus, - tool, + tool: serverData + ? ({ + name: serverName, + pluginKey: `${Constants.mcp_prefix}${serverName}`, + icon: serverData.icon, + authenticated: serverData.authenticated, + } as TPlugin) + : undefined, onConfigClick: handleConfigClick, isInitializing: isInitializing(serverName), canCancel: isCancellable(serverName), @@ -495,8 +507,8 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin }; }, [ + queryClient, isCancellable, - mcpToolDetails, isInitializing, cancelOAuthFlow, connectionStatus, diff --git a/packages/api/src/mcp/MCPManager.ts b/packages/api/src/mcp/MCPManager.ts index f0a86113d0..d25f652b40 100644 --- a/packages/api/src/mcp/MCPManager.ts +++ b/packages/api/src/mcp/MCPManager.ts @@ -13,7 +13,6 @@ import { ConnectionsRepository } from '~/mcp/ConnectionsRepository'; import { formatToolContent } from './parsers'; import { MCPConnection } from './connection'; import { processMCPEnv } from '~/utils/env'; -import { CONSTANTS } from './enum'; /** * Centralized manager for MCP server connections and tool execution. @@ -78,6 +77,28 @@ export class MCPManager extends UserConnectionManager { return allToolFunctions; } + /** Returns all available tool functions from all connections available to user */ + public async getServerToolFunctions( + userId: string, + serverName: string, + ): Promise { + if (this.appConnections?.has(serverName)) { + return this.serversRegistry.getToolFunctions( + serverName, + await this.appConnections.get(serverName), + ); + } + + const userConnections = this.getUserConnections(userId); + if (!userConnections || userConnections.size === 0) { + return null; + } + if (!userConnections.has(serverName)) { + return null; + } + + return this.serversRegistry.getToolFunctions(serverName, userConnections.get(serverName)!); + } /** * Get instructions for MCP servers @@ -121,72 +142,6 @@ ${formattedInstructions} Please follow these instructions when using tools from the respective MCP servers.`; } - private async loadAppManifestTools(): Promise { - const connections = await this.appConnections!.getAll(); - return await this.loadManifestTools(connections); - } - - private async loadUserManifestTools(userId: string): Promise { - const connections = this.getUserConnections(userId); - return await this.loadManifestTools(connections); - } - - public async loadAllManifestTools(userId: string): Promise { - const appTools = await this.loadAppManifestTools(); - const userTools = await this.loadUserManifestTools(userId); - return [...appTools, ...userTools]; - } - - /** Loads tools from all app-level connections into the manifest. */ - private async loadManifestTools( - connections?: Map | null, - ): Promise { - const mcpTools: t.LCManifestTool[] = []; - if (!connections || connections.size === 0) { - return mcpTools; - } - for (const [serverName, connection] of connections.entries()) { - try { - if (!(await connection.isConnected())) { - logger.warn( - `[MCP][${serverName}] Connection not available for ${serverName} manifest tools.`, - ); - continue; - } - - const tools = await connection.fetchTools(); - const serverTools: t.LCManifestTool[] = []; - for (const tool of tools) { - const pluginKey = `${tool.name}${CONSTANTS.mcp_delimiter}${serverName}`; - - const config = this.serversRegistry.parsedConfigs[serverName]; - const manifestTool: t.LCManifestTool = { - name: tool.name, - pluginKey, - description: tool.description ?? '', - icon: connection.iconPath, - authConfig: config?.customUserVars - ? Object.entries(config.customUserVars).map(([key, value]) => ({ - authField: key, - label: value.title || key, - description: value.description || '', - })) - : undefined, - }; - if (config?.chatMenu === false) { - manifestTool.chatMenu = false; - } - mcpTools.push(manifestTool); - serverTools.push(manifestTool); - } - } catch (error) { - logger.error(`[MCP][${serverName}] Error fetching tools for manifest:`, error); - } - } - - return mcpTools; - } - /** * Calls a tool on an MCP server, using either a user-specific connection * (if userId is provided) or an app-level connection. Updates the last activity timestamp diff --git a/packages/api/src/mcp/MCPServersRegistry.ts b/packages/api/src/mcp/MCPServersRegistry.ts index 23850358d0..905a62bef8 100644 --- a/packages/api/src/mcp/MCPServersRegistry.ts +++ b/packages/api/src/mcp/MCPServersRegistry.ts @@ -2,6 +2,7 @@ import pick from 'lodash/pick'; import pickBy from 'lodash/pickBy'; import mapValues from 'lodash/mapValues'; import { logger } from '@librechat/data-schemas'; +import { Constants } from 'librechat-data-provider'; import type { MCPConnection } from '~/mcp/connection'; import type { JsonSchemaType } from '~/types'; import type * as t from '~/mcp/types'; @@ -9,7 +10,6 @@ import { ConnectionsRepository } from '~/mcp/ConnectionsRepository'; import { detectOAuthRequirement } from '~/mcp/oauth'; import { sanitizeUrlForLogging } from '~/mcp/utils'; import { processMCPEnv, isEnabled } from '~/utils'; -import { CONSTANTS } from '~/mcp/enum'; /** * Manages MCP server configurations and metadata discovery. @@ -127,7 +127,7 @@ export class MCPServersRegistry { const toolFunctions: t.LCAvailableTools = {}; tools.forEach((tool) => { - const name = `${tool.name}${CONSTANTS.mcp_delimiter}${serverName}`; + const name = `${tool.name}${Constants.mcp_delimiter}${serverName}`; toolFunctions[name] = { type: 'function', ['function']: { diff --git a/packages/api/src/mcp/enum.ts b/packages/api/src/mcp/enum.ts index 806e5c0054..ade34af437 100644 --- a/packages/api/src/mcp/enum.ts +++ b/packages/api/src/mcp/enum.ts @@ -1,5 +1,4 @@ export enum CONSTANTS { - mcp_delimiter = '_mcp_', /** System user ID for app-level OAuth tokens (all zeros ObjectId) */ SYSTEM_USER_ID = '000000000000000000000000', } diff --git a/packages/api/src/tools/format.spec.ts b/packages/api/src/tools/format.spec.ts index 3a02fd7c62..2119cd724d 100644 --- a/packages/api/src/tools/format.spec.ts +++ b/packages/api/src/tools/format.spec.ts @@ -1,12 +1,6 @@ -import { AuthType, Constants, EToolResources } from 'librechat-data-provider'; -import type { TPlugin, FunctionTool } from 'librechat-data-provider'; -import type { MCPManager } from '~/mcp/MCPManager'; -import { - convertMCPToolsToPlugins, - filterUniquePlugins, - checkPluginAuth, - getToolkitKey, -} from './format'; +import { AuthType, EToolResources } from 'librechat-data-provider'; +import type { TPlugin } from 'librechat-data-provider'; +import { filterUniquePlugins, checkPluginAuth, getToolkitKey } from './format'; describe('format.ts helper functions', () => { describe('filterUniquePlugins', () => { @@ -197,212 +191,6 @@ describe('format.ts helper functions', () => { }); }); - describe('convertMCPToolsToPlugins', () => { - it('should return undefined when functionTools is undefined', () => { - const result = convertMCPToolsToPlugins({ functionTools: undefined }); - expect(result).toBeUndefined(); - }); - - it('should return undefined when functionTools is not an object', () => { - const result = convertMCPToolsToPlugins({ - functionTools: 'not-an-object' as unknown as Record, - }); - expect(result).toBeUndefined(); - }); - - it('should return empty array when functionTools is empty object', () => { - const result = convertMCPToolsToPlugins({ functionTools: {} }); - expect(result).toEqual([]); - }); - - it('should skip entries without function property', () => { - const functionTools: Record = { - tool1: { type: 'function' } as FunctionTool, - tool2: { function: { name: 'tool2', description: 'Tool 2' } } as FunctionTool, - }; - - const result = convertMCPToolsToPlugins({ functionTools }); - expect(result).toHaveLength(0); // tool2 doesn't have mcp_delimiter in key - }); - - it('should skip entries without mcp_delimiter in key', () => { - const functionTools: Record = { - 'regular-tool': { - type: 'function', - function: { name: 'regular-tool', description: 'Regular tool' }, - } as FunctionTool, - }; - - const result = convertMCPToolsToPlugins({ functionTools }); - expect(result).toHaveLength(0); - }); - - it('should convert MCP tools to plugins correctly', () => { - const functionTools: Record = { - [`tool1${Constants.mcp_delimiter}server1`]: { - type: 'function', - function: { name: 'tool1', description: 'Tool 1 description' }, - } as FunctionTool, - }; - - const result = convertMCPToolsToPlugins({ functionTools }); - expect(result).toHaveLength(1); - expect(result![0]).toEqual({ - name: 'tool1', - pluginKey: `tool1${Constants.mcp_delimiter}server1`, - description: 'Tool 1 description', - authenticated: true, - icon: undefined, - authConfig: [], - }); - }); - - it('should handle missing description', () => { - const functionTools: Record = { - [`tool1${Constants.mcp_delimiter}server1`]: { - type: 'function', - function: { name: 'tool1' }, - } as FunctionTool, - }; - - const result = convertMCPToolsToPlugins({ functionTools }); - expect(result).toHaveLength(1); - expect(result![0].description).toBe(''); - }); - - it('should add icon from server config', () => { - const functionTools: Record = { - [`tool1${Constants.mcp_delimiter}server1`]: { - type: 'function', - function: { name: 'tool1', description: 'Tool 1' }, - } as FunctionTool, - }; - - const mockMcpManager = { - getRawConfig: jest.fn().mockReturnValue({ - command: 'test', - args: [], - iconPath: '/path/to/icon.png', - }), - } as unknown as MCPManager; - - 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', () => { - const functionTools: Record = { - [`tool1${Constants.mcp_delimiter}server1`]: { - type: 'function', - function: { name: 'tool1', description: 'Tool 1' }, - } as FunctionTool, - }; - - 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, 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', () => { - const functionTools: Record = { - [`tool1${Constants.mcp_delimiter}server1`]: { - type: 'function', - function: { name: 'tool1', description: 'Tool 1' }, - } as FunctionTool, - }; - - 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, 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', () => { - const functionTools: Record = { - [`tool1${Constants.mcp_delimiter}server1`]: { - type: 'function', - function: { name: 'tool1', description: 'Tool 1' }, - } as FunctionTool, - }; - - const mockMcpManager = { - getRawConfig: jest.fn().mockReturnValue({ - command: 'test', - args: [], - customUserVars: {}, - }), - } as unknown as MCPManager; - - 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'); - }); - }); - describe('getToolkitKey', () => { it('should return undefined when toolName is undefined', () => { const toolkits: TPlugin[] = [ diff --git a/packages/api/src/tools/format.ts b/packages/api/src/tools/format.ts index 1ff1d7f386..2525743ff2 100644 --- a/packages/api/src/tools/format.ts +++ b/packages/api/src/tools/format.ts @@ -1,7 +1,5 @@ -import { AuthType, Constants, EToolResources } from 'librechat-data-provider'; +import { AuthType, EToolResources } from 'librechat-data-provider'; import type { TPlugin } from 'librechat-data-provider'; -import type { MCPManager } from '~/mcp/MCPManager'; -import { LCAvailableTools, LCFunctionTool } from '~/mcp/types'; /** * Filters out duplicate plugins from the list of plugins. @@ -48,90 +46,6 @@ 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, - mcpManager, -}: { - toolKey: string; - toolData: LCFunctionTool; - mcpManager?: MCPManager; -}): 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 = mcpManager?.getRawConfig(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 - * @param customConfig - Custom configuration for MCP servers - * @returns Array of plugin objects - */ -export function convertMCPToolsToPlugins({ - functionTools, - mcpManager, -}: { - functionTools?: LCAvailableTools; - mcpManager?: MCPManager; -}): TPlugin[] | undefined { - if (!functionTools || typeof functionTools !== 'object') { - return; - } - - const plugins: TPlugin[] = []; - for (const [toolKey, toolData] of Object.entries(functionTools)) { - const plugin = convertMCPToolToPlugin({ toolKey, toolData, mcpManager }); - if (plugin) { - plugins.push(plugin); - } - } - - return plugins; -} - /** * @param toolkits * @param toolName diff --git a/packages/data-provider/src/data-service.ts b/packages/data-provider/src/data-service.ts index 9926e18be8..c45ab4e0b4 100644 --- a/packages/data-provider/src/data-service.ts +++ b/packages/data-provider/src/data-service.ts @@ -299,7 +299,7 @@ export const getAvailableTools = ( /* MCP Tools - Decoupled from regular tools */ -export const getMCPTools = (): Promise => { +export const getMCPTools = (): Promise => { return request.get(endpoints.mcp.tools); }; diff --git a/packages/data-provider/src/types/queries.ts b/packages/data-provider/src/types/queries.ts index 0aa78d0545..62b033f6ba 100644 --- a/packages/data-provider/src/types/queries.ts +++ b/packages/data-provider/src/types/queries.ts @@ -101,6 +101,25 @@ export type AllPromptGroupsResponse = t.TPromptGroup[]; export type ConversationTagsResponse = s.TConversationTag[]; +/* MCP Types */ +export type MCPTool = { + name: string; + pluginKey: string; + description: string; +}; + +export type MCPServer = { + name: string; + icon: string; + authenticated: boolean; + authConfig: s.TPluginAuthConfig[]; + tools: MCPTool[]; +}; + +export type MCPServersResponse = { + servers: Record; +}; + export type VerifyToolAuthParams = { toolId: string }; export type VerifyToolAuthResponse = { authenticated: boolean;