| | import { logger } from '@librechat/data-schemas'; |
| | import type * as t from '~/mcp/types'; |
| | import { MCPManager } from '~/mcp/MCPManager'; |
| | import { mcpServersRegistry } from '~/mcp/registry/MCPServersRegistry'; |
| | import { MCPServersInitializer } from '~/mcp/registry/MCPServersInitializer'; |
| | import { MCPServerInspector } from '~/mcp/registry/MCPServerInspector'; |
| | import { ConnectionsRepository } from '~/mcp/ConnectionsRepository'; |
| | import { MCPConnection } from '../connection'; |
| |
|
| | |
| | jest.mock('@librechat/data-schemas', () => ({ |
| | logger: { |
| | info: jest.fn(), |
| | warn: jest.fn(), |
| | error: jest.fn(), |
| | debug: jest.fn(), |
| | }, |
| | })); |
| |
|
| | jest.mock('~/mcp/registry/MCPServersRegistry', () => ({ |
| | mcpServersRegistry: { |
| | sharedAppServers: { |
| | getAll: jest.fn(), |
| | }, |
| | getServerConfig: jest.fn(), |
| | getAllServerConfigs: jest.fn(), |
| | getOAuthServers: jest.fn(), |
| | }, |
| | })); |
| |
|
| | jest.mock('~/mcp/registry/MCPServersInitializer', () => ({ |
| | MCPServersInitializer: { |
| | initialize: jest.fn(), |
| | }, |
| | })); |
| |
|
| | jest.mock('~/mcp/registry/MCPServerInspector'); |
| | jest.mock('~/mcp/ConnectionsRepository'); |
| |
|
| | const mockLogger = logger as jest.Mocked<typeof logger>; |
| |
|
| | describe('MCPManager', () => { |
| | const userId = 'test-user-123'; |
| | const serverName = 'test_server'; |
| |
|
| | beforeEach(() => { |
| | |
| | (MCPManager as unknown as { instance: null }).instance = null; |
| | jest.clearAllMocks(); |
| |
|
| | |
| | (MCPServersInitializer.initialize as jest.Mock).mockResolvedValue(undefined); |
| | (mcpServersRegistry.sharedAppServers.getAll as jest.Mock).mockResolvedValue({}); |
| | (mcpServersRegistry.getAllServerConfigs as jest.Mock).mockResolvedValue({}); |
| | }); |
| |
|
| | function mockAppConnections( |
| | appConnectionsConfig: Partial<ConnectionsRepository>, |
| | ): jest.MockedClass<typeof ConnectionsRepository> { |
| | const mock = { |
| | has: jest.fn().mockReturnValue(false), |
| | get: jest.fn().mockResolvedValue({} as unknown as MCPConnection), |
| | ...appConnectionsConfig, |
| | }; |
| | return ( |
| | ConnectionsRepository as jest.MockedClass<typeof ConnectionsRepository> |
| | ).mockImplementation(() => mock as unknown as ConnectionsRepository); |
| | } |
| |
|
| | function newMCPServersConfig(serverNameOverride?: string): t.MCPServers { |
| | return { |
| | [serverNameOverride ?? serverName]: { |
| | type: 'stdio', |
| | command: 'test', |
| | args: [], |
| | }, |
| | }; |
| | } |
| |
|
| | describe('getAppToolFunctions', () => { |
| | it('should return empty object when no servers have tool functions', async () => { |
| | (mcpServersRegistry.getAllServerConfigs as jest.Mock).mockResolvedValue({ |
| | server1: { type: 'stdio', command: 'test', args: [] }, |
| | server2: { type: 'stdio', command: 'test2', args: [] }, |
| | }); |
| |
|
| | const manager = await MCPManager.createInstance(newMCPServersConfig()); |
| | const result = await manager.getAppToolFunctions(); |
| |
|
| | expect(result).toEqual({}); |
| | }); |
| |
|
| | it('should collect tool functions from multiple servers', async () => { |
| | const toolFunctions1 = { |
| | tool1_mcp_server1: { |
| | type: 'function' as const, |
| | function: { |
| | name: 'tool1_mcp_server1', |
| | description: 'Tool 1', |
| | parameters: { type: 'object' as const }, |
| | }, |
| | }, |
| | }; |
| |
|
| | const toolFunctions2 = { |
| | tool2_mcp_server2: { |
| | type: 'function' as const, |
| | function: { |
| | name: 'tool2_mcp_server2', |
| | description: 'Tool 2', |
| | parameters: { type: 'object' as const }, |
| | }, |
| | }, |
| | }; |
| |
|
| | (mcpServersRegistry.getAllServerConfigs as jest.Mock).mockResolvedValue({ |
| | server1: { |
| | type: 'stdio', |
| | command: 'test', |
| | args: [], |
| | toolFunctions: toolFunctions1, |
| | }, |
| | server2: { |
| | type: 'stdio', |
| | command: 'test2', |
| | args: [], |
| | toolFunctions: toolFunctions2, |
| | }, |
| | }); |
| |
|
| | const manager = await MCPManager.createInstance(newMCPServersConfig()); |
| | const result = await manager.getAppToolFunctions(); |
| |
|
| | expect(result).toEqual({ |
| | ...toolFunctions1, |
| | ...toolFunctions2, |
| | }); |
| | }); |
| |
|
| | it('should handle servers with null or undefined toolFunctions', async () => { |
| | const toolFunctions1 = { |
| | tool1_mcp_server1: { |
| | type: 'function' as const, |
| | function: { |
| | name: 'tool1_mcp_server1', |
| | description: 'Tool 1', |
| | parameters: { type: 'object' as const }, |
| | }, |
| | }, |
| | }; |
| |
|
| | (mcpServersRegistry.getAllServerConfigs as jest.Mock).mockResolvedValue({ |
| | server1: { |
| | type: 'stdio', |
| | command: 'test', |
| | args: [], |
| | toolFunctions: toolFunctions1, |
| | }, |
| | server2: { |
| | type: 'stdio', |
| | command: 'test2', |
| | args: [], |
| | toolFunctions: null, |
| | }, |
| | server3: { |
| | type: 'stdio', |
| | command: 'test3', |
| | args: [], |
| | }, |
| | }); |
| |
|
| | const manager = await MCPManager.createInstance(newMCPServersConfig()); |
| | const result = await manager.getAppToolFunctions(); |
| |
|
| | expect(result).toEqual(toolFunctions1); |
| | }); |
| | }); |
| |
|
| | describe('formatInstructionsForContext', () => { |
| | it('should return empty string when no servers have instructions', async () => { |
| | (mcpServersRegistry.getAllServerConfigs as jest.Mock).mockResolvedValue({ |
| | server1: { type: 'stdio', command: 'test', args: [] }, |
| | server2: { type: 'stdio', command: 'test2', args: [] }, |
| | }); |
| |
|
| | const manager = await MCPManager.createInstance(newMCPServersConfig()); |
| | const result = await manager.formatInstructionsForContext(); |
| |
|
| | expect(result).toBe(''); |
| | }); |
| |
|
| | it('should format instructions from multiple servers', async () => { |
| | (mcpServersRegistry.getAllServerConfigs as jest.Mock).mockResolvedValue({ |
| | github: { |
| | type: 'sse', |
| | url: 'https://api.github.com', |
| | serverInstructions: 'Use GitHub API with care', |
| | }, |
| | files: { |
| | type: 'stdio', |
| | command: 'node', |
| | args: ['files.js'], |
| | serverInstructions: 'Only read/write files in allowed directories', |
| | }, |
| | }); |
| |
|
| | const manager = await MCPManager.createInstance(newMCPServersConfig()); |
| | const result = await manager.formatInstructionsForContext(); |
| |
|
| | expect(result).toContain('# MCP Server Instructions'); |
| | expect(result).toContain('## github MCP Server Instructions'); |
| | expect(result).toContain('Use GitHub API with care'); |
| | expect(result).toContain('## files MCP Server Instructions'); |
| | expect(result).toContain('Only read/write files in allowed directories'); |
| | }); |
| |
|
| | it('should filter instructions by server names when provided', async () => { |
| | (mcpServersRegistry.getAllServerConfigs as jest.Mock).mockResolvedValue({ |
| | github: { |
| | type: 'sse', |
| | url: 'https://api.github.com', |
| | serverInstructions: 'Use GitHub API with care', |
| | }, |
| | files: { |
| | type: 'stdio', |
| | command: 'node', |
| | args: ['files.js'], |
| | serverInstructions: 'Only read/write files in allowed directories', |
| | }, |
| | database: { |
| | type: 'stdio', |
| | command: 'node', |
| | args: ['db.js'], |
| | serverInstructions: 'Be careful with database operations', |
| | }, |
| | }); |
| |
|
| | const manager = await MCPManager.createInstance(newMCPServersConfig()); |
| | const result = await manager.formatInstructionsForContext(['github', 'database']); |
| |
|
| | expect(result).toContain('## github MCP Server Instructions'); |
| | expect(result).toContain('Use GitHub API with care'); |
| | expect(result).toContain('## database MCP Server Instructions'); |
| | expect(result).toContain('Be careful with database operations'); |
| | expect(result).not.toContain('files'); |
| | expect(result).not.toContain('Only read/write files in allowed directories'); |
| | }); |
| |
|
| | it('should handle servers with null or undefined instructions', async () => { |
| | (mcpServersRegistry.getAllServerConfigs as jest.Mock).mockResolvedValue({ |
| | github: { |
| | type: 'sse', |
| | url: 'https://api.github.com', |
| | serverInstructions: 'Use GitHub API with care', |
| | }, |
| | files: { |
| | type: 'stdio', |
| | command: 'node', |
| | args: ['files.js'], |
| | serverInstructions: null, |
| | }, |
| | database: { |
| | type: 'stdio', |
| | command: 'node', |
| | args: ['db.js'], |
| | }, |
| | }); |
| |
|
| | const manager = await MCPManager.createInstance(newMCPServersConfig()); |
| | const result = await manager.formatInstructionsForContext(); |
| |
|
| | expect(result).toContain('## github MCP Server Instructions'); |
| | expect(result).toContain('Use GitHub API with care'); |
| | expect(result).not.toContain('files'); |
| | expect(result).not.toContain('database'); |
| | }); |
| |
|
| | it('should return empty string when filtered servers have no instructions', async () => { |
| | (mcpServersRegistry.getAllServerConfigs as jest.Mock).mockResolvedValue({ |
| | github: { |
| | type: 'sse', |
| | url: 'https://api.github.com', |
| | serverInstructions: 'Use GitHub API with care', |
| | }, |
| | files: { |
| | type: 'stdio', |
| | command: 'node', |
| | args: ['files.js'], |
| | }, |
| | }); |
| |
|
| | const manager = await MCPManager.createInstance(newMCPServersConfig()); |
| | const result = await manager.formatInstructionsForContext(['files']); |
| |
|
| | expect(result).toBe(''); |
| | }); |
| | }); |
| |
|
| | describe('getServerToolFunctions', () => { |
| | it('should catch and handle errors gracefully', async () => { |
| | (MCPServerInspector.getToolFunctions as jest.Mock) = jest.fn(() => { |
| | throw new Error('Connection failed'); |
| | }); |
| |
|
| | mockAppConnections({ |
| | has: jest.fn().mockReturnValue(true), |
| | }); |
| |
|
| | const manager = await MCPManager.createInstance(newMCPServersConfig()); |
| |
|
| | const result = await manager.getServerToolFunctions(userId, serverName); |
| |
|
| | expect(result).toBeNull(); |
| | expect(mockLogger.warn).toHaveBeenCalledWith( |
| | `[getServerToolFunctions] Error getting tool functions for server ${serverName}`, |
| | expect.any(Error), |
| | ); |
| | }); |
| |
|
| | it('should catch synchronous errors from getUserConnections', async () => { |
| | (MCPServerInspector.getToolFunctions as jest.Mock) = jest.fn().mockResolvedValue({}); |
| |
|
| | mockAppConnections({ |
| | has: jest.fn().mockReturnValue(false), |
| | }); |
| |
|
| | const manager = await MCPManager.createInstance(newMCPServersConfig()); |
| |
|
| | const spy = jest.spyOn(manager, 'getUserConnections').mockImplementation(() => { |
| | throw new Error('Failed to get user connections'); |
| | }); |
| |
|
| | const result = await manager.getServerToolFunctions(userId, serverName); |
| |
|
| | expect(result).toBeNull(); |
| | expect(mockLogger.warn).toHaveBeenCalledWith( |
| | `[getServerToolFunctions] Error getting tool functions for server ${serverName}`, |
| | expect.any(Error), |
| | ); |
| | expect(spy).toHaveBeenCalled(); |
| | }); |
| |
|
| | it('should return tools successfully when no errors occur', async () => { |
| | const expectedTools: t.LCAvailableTools = { |
| | [`test_tool_mcp_${serverName}`]: { |
| | type: 'function', |
| | function: { |
| | name: `test_tool_mcp_${serverName}`, |
| | description: 'Test tool', |
| | parameters: { type: 'object' }, |
| | }, |
| | }, |
| | }; |
| |
|
| | (MCPServerInspector.getToolFunctions as jest.Mock) = jest |
| | .fn() |
| | .mockResolvedValue(expectedTools); |
| |
|
| | mockAppConnections({ |
| | has: jest.fn().mockReturnValue(true), |
| | }); |
| |
|
| | const manager = await MCPManager.createInstance(newMCPServersConfig()); |
| |
|
| | const result = await manager.getServerToolFunctions(userId, serverName); |
| |
|
| | expect(result).toEqual(expectedTools); |
| | expect(mockLogger.warn).not.toHaveBeenCalled(); |
| | }); |
| |
|
| | it('should include specific server name in error messages', async () => { |
| | const specificServerName = 'github_mcp_server'; |
| |
|
| | (MCPServerInspector.getToolFunctions as jest.Mock) = jest.fn(() => { |
| | throw new Error('Server specific error'); |
| | }); |
| |
|
| | mockAppConnections({ |
| | has: jest.fn().mockReturnValue(true), |
| | }); |
| |
|
| | const manager = await MCPManager.createInstance(newMCPServersConfig(specificServerName)); |
| |
|
| | const result = await manager.getServerToolFunctions(userId, specificServerName); |
| |
|
| | expect(result).toBeNull(); |
| | expect(mockLogger.warn).toHaveBeenCalledWith( |
| | `[getServerToolFunctions] Error getting tool functions for server ${specificServerName}`, |
| | expect.any(Error), |
| | ); |
| | }); |
| | }); |
| | }); |
| |
|