| const { logger } = require('@librechat/data-schemas'); |
| const { MCPOAuthHandler } = require('@librechat/api'); |
| const { CacheKeys } = require('librechat-data-provider'); |
| const { |
| createMCPTool, |
| createMCPTools, |
| getMCPSetupData, |
| checkOAuthFlowStatus, |
| getServerConnectionStatus, |
| } = require('./MCP'); |
|
|
| |
| jest.mock('@librechat/data-schemas', () => ({ |
| logger: { |
| debug: jest.fn(), |
| error: jest.fn(), |
| info: jest.fn(), |
| warn: jest.fn(), |
| }, |
| })); |
|
|
| jest.mock('@langchain/core/tools', () => ({ |
| tool: jest.fn((fn, config) => { |
| const toolInstance = { _call: fn, ...config }; |
| return toolInstance; |
| }), |
| })); |
|
|
| jest.mock('@librechat/agents', () => ({ |
| Providers: { |
| VERTEXAI: 'vertexai', |
| GOOGLE: 'google', |
| }, |
| StepTypes: { |
| TOOL_CALLS: 'tool_calls', |
| }, |
| GraphEvents: { |
| ON_RUN_STEP_DELTA: 'on_run_step_delta', |
| ON_RUN_STEP: 'on_run_step', |
| }, |
| Constants: { |
| CONTENT_AND_ARTIFACT: 'content_and_artifact', |
| }, |
| })); |
|
|
| jest.mock('@librechat/api', () => ({ |
| MCPOAuthHandler: { |
| generateFlowId: jest.fn(), |
| }, |
| sendEvent: jest.fn(), |
| normalizeServerName: jest.fn((name) => name), |
| convertWithResolvedRefs: jest.fn((params) => params), |
| mcpServersRegistry: { |
| getOAuthServers: jest.fn(() => Promise.resolve(new Set())), |
| }, |
| })); |
|
|
| jest.mock('librechat-data-provider', () => ({ |
| CacheKeys: { |
| FLOWS: 'flows', |
| }, |
| Constants: { |
| USE_PRELIM_RESPONSE_MESSAGE_ID: 'prelim_response_id', |
| mcp_delimiter: '::', |
| mcp_prefix: 'mcp_', |
| }, |
| ContentTypes: { |
| TEXT: 'text', |
| }, |
| isAssistantsEndpoint: jest.fn(() => false), |
| Time: { |
| TWO_MINUTES: 120000, |
| }, |
| })); |
|
|
| jest.mock('./Config', () => ({ |
| loadCustomConfig: jest.fn(), |
| getAppConfig: jest.fn(), |
| })); |
|
|
| jest.mock('~/config', () => ({ |
| getMCPManager: jest.fn(), |
| getFlowStateManager: jest.fn(), |
| getOAuthReconnectionManager: jest.fn(), |
| })); |
|
|
| jest.mock('~/cache', () => ({ |
| getLogStores: jest.fn(), |
| })); |
|
|
| jest.mock('~/models', () => ({ |
| findToken: jest.fn(), |
| createToken: jest.fn(), |
| updateToken: jest.fn(), |
| })); |
|
|
| jest.mock('./Tools/mcp', () => ({ |
| reinitMCPServer: jest.fn(), |
| })); |
|
|
| describe('tests for the new helper functions used by the MCP connection status endpoints', () => { |
| let mockGetMCPManager; |
| let mockGetFlowStateManager; |
| let mockGetLogStores; |
| let mockGetOAuthReconnectionManager; |
| let mockMcpServersRegistry; |
|
|
| beforeEach(() => { |
| jest.clearAllMocks(); |
|
|
| mockGetMCPManager = require('~/config').getMCPManager; |
| mockGetFlowStateManager = require('~/config').getFlowStateManager; |
| mockGetLogStores = require('~/cache').getLogStores; |
| mockGetOAuthReconnectionManager = require('~/config').getOAuthReconnectionManager; |
| mockMcpServersRegistry = require('@librechat/api').mcpServersRegistry; |
| }); |
|
|
| describe('getMCPSetupData', () => { |
| const mockUserId = 'user-123'; |
| const mockConfig = { |
| mcpServers: { |
| server1: { type: 'stdio' }, |
| server2: { type: 'http' }, |
| }, |
| }; |
| let mockGetAppConfig; |
|
|
| beforeEach(() => { |
| mockGetAppConfig = require('./Config').getAppConfig; |
| mockGetMCPManager.mockReturnValue({ |
| appConnections: { getAll: jest.fn(() => new Map()) }, |
| getUserConnections: jest.fn(() => new Map()), |
| }); |
| mockMcpServersRegistry.getOAuthServers.mockResolvedValue(new Set()); |
| }); |
|
|
| it('should successfully return MCP setup data', async () => { |
| mockGetAppConfig.mockResolvedValue({ mcpConfig: mockConfig.mcpServers }); |
|
|
| const mockAppConnections = new Map([['server1', { status: 'connected' }]]); |
| const mockUserConnections = new Map([['server2', { status: 'disconnected' }]]); |
| const mockOAuthServers = new Set(['server2']); |
|
|
| const mockMCPManager = { |
| appConnections: { getAll: jest.fn(() => mockAppConnections) }, |
| getUserConnections: jest.fn(() => mockUserConnections), |
| }; |
| mockGetMCPManager.mockReturnValue(mockMCPManager); |
| mockMcpServersRegistry.getOAuthServers.mockResolvedValue(mockOAuthServers); |
|
|
| const result = await getMCPSetupData(mockUserId); |
|
|
| expect(mockGetAppConfig).toHaveBeenCalled(); |
| expect(mockGetMCPManager).toHaveBeenCalledWith(mockUserId); |
| expect(mockMCPManager.appConnections.getAll).toHaveBeenCalled(); |
| expect(mockMCPManager.getUserConnections).toHaveBeenCalledWith(mockUserId); |
| expect(mockMcpServersRegistry.getOAuthServers).toHaveBeenCalled(); |
|
|
| expect(result).toEqual({ |
| mcpConfig: mockConfig.mcpServers, |
| appConnections: mockAppConnections, |
| userConnections: mockUserConnections, |
| oauthServers: mockOAuthServers, |
| }); |
| }); |
|
|
| it('should throw error when MCP config not found', async () => { |
| mockGetAppConfig.mockResolvedValue({}); |
| await expect(getMCPSetupData(mockUserId)).rejects.toThrow('MCP config not found'); |
| }); |
|
|
| it('should handle null values from MCP manager gracefully', async () => { |
| mockGetAppConfig.mockResolvedValue({ mcpConfig: mockConfig.mcpServers }); |
|
|
| const mockMCPManager = { |
| appConnections: { getAll: jest.fn(() => null) }, |
| getUserConnections: jest.fn(() => null), |
| }; |
| mockGetMCPManager.mockReturnValue(mockMCPManager); |
| mockMcpServersRegistry.getOAuthServers.mockResolvedValue(new Set()); |
|
|
| const result = await getMCPSetupData(mockUserId); |
|
|
| expect(result).toEqual({ |
| mcpConfig: mockConfig.mcpServers, |
| appConnections: new Map(), |
| userConnections: new Map(), |
| oauthServers: new Set(), |
| }); |
| }); |
| }); |
|
|
| describe('checkOAuthFlowStatus', () => { |
| const mockUserId = 'user-123'; |
| const mockServerName = 'test-server'; |
| const mockFlowId = 'flow-123'; |
|
|
| beforeEach(() => { |
| const mockFlowsCache = {}; |
| const mockFlowManager = { |
| getFlowState: jest.fn(), |
| }; |
|
|
| mockGetLogStores.mockReturnValue(mockFlowsCache); |
| mockGetFlowStateManager.mockReturnValue(mockFlowManager); |
| MCPOAuthHandler.generateFlowId.mockReturnValue(mockFlowId); |
| }); |
|
|
| it('should return false flags when no flow state exists', async () => { |
| const mockFlowManager = { getFlowState: jest.fn(() => null) }; |
| mockGetFlowStateManager.mockReturnValue(mockFlowManager); |
|
|
| const result = await checkOAuthFlowStatus(mockUserId, mockServerName); |
|
|
| expect(mockGetLogStores).toHaveBeenCalledWith(CacheKeys.FLOWS); |
| expect(MCPOAuthHandler.generateFlowId).toHaveBeenCalledWith(mockUserId, mockServerName); |
| expect(mockFlowManager.getFlowState).toHaveBeenCalledWith(mockFlowId, 'mcp_oauth'); |
| expect(result).toEqual({ hasActiveFlow: false, hasFailedFlow: false }); |
| }); |
|
|
| it('should detect failed flow when status is FAILED', async () => { |
| const mockFlowState = { |
| status: 'FAILED', |
| createdAt: Date.now() - 60000, |
| ttl: 180000, |
| }; |
| const mockFlowManager = { getFlowState: jest.fn(() => mockFlowState) }; |
| mockGetFlowStateManager.mockReturnValue(mockFlowManager); |
|
|
| const result = await checkOAuthFlowStatus(mockUserId, mockServerName); |
|
|
| expect(result).toEqual({ hasActiveFlow: false, hasFailedFlow: true }); |
| expect(logger.debug).toHaveBeenCalledWith( |
| expect.stringContaining('Found failed OAuth flow'), |
| expect.objectContaining({ |
| flowId: mockFlowId, |
| status: 'FAILED', |
| }), |
| ); |
| }); |
|
|
| it('should detect failed flow when flow has timed out', async () => { |
| const mockFlowState = { |
| status: 'PENDING', |
| createdAt: Date.now() - 200000, |
| ttl: 180000, |
| }; |
| const mockFlowManager = { getFlowState: jest.fn(() => mockFlowState) }; |
| mockGetFlowStateManager.mockReturnValue(mockFlowManager); |
|
|
| const result = await checkOAuthFlowStatus(mockUserId, mockServerName); |
|
|
| expect(result).toEqual({ hasActiveFlow: false, hasFailedFlow: true }); |
| expect(logger.debug).toHaveBeenCalledWith( |
| expect.stringContaining('Found failed OAuth flow'), |
| expect.objectContaining({ |
| timedOut: true, |
| }), |
| ); |
| }); |
|
|
| it('should detect failed flow when TTL not specified and flow exceeds default TTL', async () => { |
| const mockFlowState = { |
| status: 'PENDING', |
| createdAt: Date.now() - 200000, |
| |
| }; |
| const mockFlowManager = { getFlowState: jest.fn(() => mockFlowState) }; |
| mockGetFlowStateManager.mockReturnValue(mockFlowManager); |
|
|
| const result = await checkOAuthFlowStatus(mockUserId, mockServerName); |
|
|
| expect(result).toEqual({ hasActiveFlow: false, hasFailedFlow: true }); |
| }); |
|
|
| it('should detect active flow when status is PENDING and within TTL', async () => { |
| const mockFlowState = { |
| status: 'PENDING', |
| createdAt: Date.now() - 60000, |
| ttl: 180000, |
| }; |
| const mockFlowManager = { getFlowState: jest.fn(() => mockFlowState) }; |
| mockGetFlowStateManager.mockReturnValue(mockFlowManager); |
|
|
| const result = await checkOAuthFlowStatus(mockUserId, mockServerName); |
|
|
| expect(result).toEqual({ hasActiveFlow: true, hasFailedFlow: false }); |
| expect(logger.debug).toHaveBeenCalledWith( |
| expect.stringContaining('Found active OAuth flow'), |
| expect.objectContaining({ |
| flowId: mockFlowId, |
| }), |
| ); |
| }); |
|
|
| it('should return false flags for other statuses', async () => { |
| const mockFlowState = { |
| status: 'COMPLETED', |
| createdAt: Date.now() - 60000, |
| ttl: 180000, |
| }; |
| const mockFlowManager = { getFlowState: jest.fn(() => mockFlowState) }; |
| mockGetFlowStateManager.mockReturnValue(mockFlowManager); |
|
|
| const result = await checkOAuthFlowStatus(mockUserId, mockServerName); |
|
|
| expect(result).toEqual({ hasActiveFlow: false, hasFailedFlow: false }); |
| }); |
|
|
| it('should handle errors gracefully', async () => { |
| const mockError = new Error('Flow state error'); |
| const mockFlowManager = { |
| getFlowState: jest.fn(() => { |
| throw mockError; |
| }), |
| }; |
| mockGetFlowStateManager.mockReturnValue(mockFlowManager); |
|
|
| const result = await checkOAuthFlowStatus(mockUserId, mockServerName); |
|
|
| expect(result).toEqual({ hasActiveFlow: false, hasFailedFlow: false }); |
| expect(logger.error).toHaveBeenCalledWith( |
| expect.stringContaining('Error checking OAuth flows'), |
| mockError, |
| ); |
| }); |
| }); |
|
|
| describe('getServerConnectionStatus', () => { |
| const mockUserId = 'user-123'; |
| const mockServerName = 'test-server'; |
|
|
| it('should return app connection state when available', async () => { |
| const appConnections = new Map([[mockServerName, { connectionState: 'connected' }]]); |
| const userConnections = new Map(); |
| const oauthServers = new Set(); |
|
|
| const result = await getServerConnectionStatus( |
| mockUserId, |
| mockServerName, |
| appConnections, |
| userConnections, |
| oauthServers, |
| ); |
|
|
| expect(result).toEqual({ |
| requiresOAuth: false, |
| connectionState: 'connected', |
| }); |
| }); |
|
|
| it('should fallback to user connection state when app connection not available', async () => { |
| const appConnections = new Map(); |
| const userConnections = new Map([[mockServerName, { connectionState: 'connecting' }]]); |
| const oauthServers = new Set(); |
|
|
| const result = await getServerConnectionStatus( |
| mockUserId, |
| mockServerName, |
| appConnections, |
| userConnections, |
| oauthServers, |
| ); |
|
|
| expect(result).toEqual({ |
| requiresOAuth: false, |
| connectionState: 'connecting', |
| }); |
| }); |
|
|
| it('should default to disconnected when no connections exist', async () => { |
| const appConnections = new Map(); |
| const userConnections = new Map(); |
| const oauthServers = new Set(); |
|
|
| const result = await getServerConnectionStatus( |
| mockUserId, |
| mockServerName, |
| appConnections, |
| userConnections, |
| oauthServers, |
| ); |
|
|
| expect(result).toEqual({ |
| requiresOAuth: false, |
| connectionState: 'disconnected', |
| }); |
| }); |
|
|
| it('should prioritize app connection over user connection', async () => { |
| const appConnections = new Map([[mockServerName, { connectionState: 'connected' }]]); |
| const userConnections = new Map([[mockServerName, { connectionState: 'disconnected' }]]); |
| const oauthServers = new Set(); |
|
|
| const result = await getServerConnectionStatus( |
| mockUserId, |
| mockServerName, |
| appConnections, |
| userConnections, |
| oauthServers, |
| ); |
|
|
| expect(result).toEqual({ |
| requiresOAuth: false, |
| connectionState: 'connected', |
| }); |
| }); |
|
|
| it('should indicate OAuth requirement when server is in OAuth servers set', async () => { |
| const appConnections = new Map(); |
| const userConnections = new Map(); |
| const oauthServers = new Set([mockServerName]); |
|
|
| |
| const mockOAuthReconnectionManager = { |
| isReconnecting: jest.fn(() => false), |
| }; |
| mockGetOAuthReconnectionManager.mockReturnValue(mockOAuthReconnectionManager); |
|
|
| const result = await getServerConnectionStatus( |
| mockUserId, |
| mockServerName, |
| appConnections, |
| userConnections, |
| oauthServers, |
| ); |
|
|
| expect(result.requiresOAuth).toBe(true); |
| }); |
|
|
| it('should handle OAuth flow status when disconnected and requires OAuth with failed flow', async () => { |
| const appConnections = new Map(); |
| const userConnections = new Map(); |
| const oauthServers = new Set([mockServerName]); |
|
|
| |
| const mockOAuthReconnectionManager = { |
| isReconnecting: jest.fn(() => false), |
| }; |
| mockGetOAuthReconnectionManager.mockReturnValue(mockOAuthReconnectionManager); |
|
|
| |
| const mockFlowManager = { |
| getFlowState: jest.fn(() => ({ |
| status: 'FAILED', |
| createdAt: Date.now() - 60000, |
| ttl: 180000, |
| })), |
| }; |
| mockGetFlowStateManager.mockReturnValue(mockFlowManager); |
| mockGetLogStores.mockReturnValue({}); |
| MCPOAuthHandler.generateFlowId.mockReturnValue('test-flow-id'); |
|
|
| const result = await getServerConnectionStatus( |
| mockUserId, |
| mockServerName, |
| appConnections, |
| userConnections, |
| oauthServers, |
| ); |
|
|
| expect(result).toEqual({ |
| requiresOAuth: true, |
| connectionState: 'error', |
| }); |
| }); |
|
|
| it('should handle OAuth flow status when disconnected and requires OAuth with active flow', async () => { |
| const appConnections = new Map(); |
| const userConnections = new Map(); |
| const oauthServers = new Set([mockServerName]); |
|
|
| |
| const mockOAuthReconnectionManager = { |
| isReconnecting: jest.fn(() => false), |
| }; |
| mockGetOAuthReconnectionManager.mockReturnValue(mockOAuthReconnectionManager); |
|
|
| |
| const mockFlowManager = { |
| getFlowState: jest.fn(() => ({ |
| status: 'PENDING', |
| createdAt: Date.now() - 60000, |
| ttl: 180000, |
| })), |
| }; |
| mockGetFlowStateManager.mockReturnValue(mockFlowManager); |
| mockGetLogStores.mockReturnValue({}); |
| MCPOAuthHandler.generateFlowId.mockReturnValue('test-flow-id'); |
|
|
| const result = await getServerConnectionStatus( |
| mockUserId, |
| mockServerName, |
| appConnections, |
| userConnections, |
| oauthServers, |
| ); |
|
|
| expect(result).toEqual({ |
| requiresOAuth: true, |
| connectionState: 'connecting', |
| }); |
| }); |
|
|
| it('should handle OAuth flow status when disconnected and requires OAuth with no flow', async () => { |
| const appConnections = new Map(); |
| const userConnections = new Map(); |
| const oauthServers = new Set([mockServerName]); |
|
|
| |
| const mockOAuthReconnectionManager = { |
| isReconnecting: jest.fn(() => false), |
| }; |
| mockGetOAuthReconnectionManager.mockReturnValue(mockOAuthReconnectionManager); |
|
|
| |
| const mockFlowManager = { |
| getFlowState: jest.fn(() => null), |
| }; |
| mockGetFlowStateManager.mockReturnValue(mockFlowManager); |
| mockGetLogStores.mockReturnValue({}); |
| MCPOAuthHandler.generateFlowId.mockReturnValue('test-flow-id'); |
|
|
| const result = await getServerConnectionStatus( |
| mockUserId, |
| mockServerName, |
| appConnections, |
| userConnections, |
| oauthServers, |
| ); |
|
|
| expect(result).toEqual({ |
| requiresOAuth: true, |
| connectionState: 'disconnected', |
| }); |
| }); |
|
|
| it('should return connecting state when OAuth server is reconnecting', async () => { |
| const appConnections = new Map(); |
| const userConnections = new Map(); |
| const oauthServers = new Set([mockServerName]); |
|
|
| |
| const mockOAuthReconnectionManager = { |
| isReconnecting: jest.fn(() => true), |
| }; |
| mockGetOAuthReconnectionManager.mockReturnValue(mockOAuthReconnectionManager); |
|
|
| const result = await getServerConnectionStatus( |
| mockUserId, |
| mockServerName, |
| appConnections, |
| userConnections, |
| oauthServers, |
| ); |
|
|
| expect(result).toEqual({ |
| requiresOAuth: true, |
| connectionState: 'connecting', |
| }); |
| expect(mockOAuthReconnectionManager.isReconnecting).toHaveBeenCalledWith( |
| mockUserId, |
| mockServerName, |
| ); |
| }); |
|
|
| it('should not check OAuth flow status when server is connected', async () => { |
| const mockFlowManager = { |
| getFlowState: jest.fn(), |
| }; |
| mockGetFlowStateManager.mockReturnValue(mockFlowManager); |
| mockGetLogStores.mockReturnValue({}); |
|
|
| const appConnections = new Map([[mockServerName, { connectionState: 'connected' }]]); |
| const userConnections = new Map(); |
| const oauthServers = new Set([mockServerName]); |
|
|
| const result = await getServerConnectionStatus( |
| mockUserId, |
| mockServerName, |
| appConnections, |
| userConnections, |
| oauthServers, |
| ); |
|
|
| expect(result).toEqual({ |
| requiresOAuth: true, |
| connectionState: 'connected', |
| }); |
|
|
| |
| expect(mockFlowManager.getFlowState).not.toHaveBeenCalled(); |
| }); |
|
|
| it('should not check OAuth flow status when server does not require OAuth', async () => { |
| const mockFlowManager = { |
| getFlowState: jest.fn(), |
| }; |
| mockGetFlowStateManager.mockReturnValue(mockFlowManager); |
| mockGetLogStores.mockReturnValue({}); |
|
|
| const appConnections = new Map(); |
| const userConnections = new Map(); |
| const oauthServers = new Set(); |
|
|
| const result = await getServerConnectionStatus( |
| mockUserId, |
| mockServerName, |
| appConnections, |
| userConnections, |
| oauthServers, |
| ); |
|
|
| expect(result).toEqual({ |
| requiresOAuth: false, |
| connectionState: 'disconnected', |
| }); |
|
|
| |
| expect(mockFlowManager.getFlowState).not.toHaveBeenCalled(); |
| }); |
| }); |
| }); |
|
|
| describe('User parameter passing tests', () => { |
| let mockReinitMCPServer; |
| let mockGetFlowStateManager; |
| let mockGetLogStores; |
|
|
| beforeEach(() => { |
| jest.clearAllMocks(); |
| mockReinitMCPServer = require('./Tools/mcp').reinitMCPServer; |
| mockGetFlowStateManager = require('~/config').getFlowStateManager; |
| mockGetLogStores = require('~/cache').getLogStores; |
|
|
| |
| mockGetLogStores.mockReturnValue({}); |
| mockGetFlowStateManager.mockReturnValue({ |
| createFlowWithHandler: jest.fn(), |
| failFlow: jest.fn(), |
| }); |
| }); |
|
|
| describe('createMCPTools', () => { |
| it('should pass user parameter to reinitMCPServer when calling reconnectServer internally', async () => { |
| const mockUser = { id: 'test-user-123', name: 'Test User' }; |
| const mockRes = { write: jest.fn(), flush: jest.fn() }; |
| const mockSignal = new AbortController().signal; |
|
|
| mockReinitMCPServer.mockResolvedValue({ |
| tools: [{ name: 'test-tool' }], |
| availableTools: { |
| 'test-tool::test-server': { |
| function: { |
| description: 'Test tool', |
| parameters: { type: 'object', properties: {} }, |
| }, |
| }, |
| }, |
| }); |
|
|
| await createMCPTools({ |
| res: mockRes, |
| user: mockUser, |
| serverName: 'test-server', |
| provider: 'openai', |
| signal: mockSignal, |
| userMCPAuthMap: {}, |
| }); |
|
|
| |
| expect(mockReinitMCPServer).toHaveBeenCalledWith( |
| expect.objectContaining({ |
| user: mockUser, |
| serverName: 'test-server', |
| }), |
| ); |
| expect(mockReinitMCPServer.mock.calls[0][0].user).toBe(mockUser); |
| }); |
|
|
| it('should throw error if user is not provided', async () => { |
| const mockRes = { write: jest.fn(), flush: jest.fn() }; |
|
|
| mockReinitMCPServer.mockResolvedValue({ |
| tools: [], |
| availableTools: {}, |
| }); |
|
|
| |
| await expect( |
| createMCPTools({ |
| res: mockRes, |
| user: undefined, |
| serverName: 'test-server', |
| provider: 'openai', |
| userMCPAuthMap: {}, |
| }), |
| ).rejects.toThrow("Cannot read properties of undefined (reading 'id')"); |
|
|
| |
| expect(mockReinitMCPServer).not.toHaveBeenCalled(); |
| }); |
| }); |
|
|
| describe('createMCPTool', () => { |
| it('should pass user parameter to reinitMCPServer when tool not in cache', async () => { |
| const mockUser = { id: 'test-user-456', email: 'test@example.com' }; |
| const mockRes = { write: jest.fn(), flush: jest.fn() }; |
| const mockSignal = new AbortController().signal; |
|
|
| mockReinitMCPServer.mockResolvedValue({ |
| availableTools: { |
| 'test-tool::test-server': { |
| function: { |
| description: 'Test tool', |
| parameters: { type: 'object', properties: {} }, |
| }, |
| }, |
| }, |
| }); |
|
|
| |
| await createMCPTool({ |
| res: mockRes, |
| user: mockUser, |
| toolKey: 'test-tool::test-server', |
| provider: 'openai', |
| signal: mockSignal, |
| userMCPAuthMap: {}, |
| availableTools: undefined, |
| }); |
|
|
| |
| expect(mockReinitMCPServer).toHaveBeenCalledWith( |
| expect.objectContaining({ |
| user: mockUser, |
| serverName: 'test-server', |
| }), |
| ); |
| expect(mockReinitMCPServer.mock.calls[0][0].user).toBe(mockUser); |
| }); |
|
|
| it('should not call reinitMCPServer when tool is in cache', async () => { |
| const mockUser = { id: 'test-user-789' }; |
| const mockRes = { write: jest.fn(), flush: jest.fn() }; |
|
|
| const availableTools = { |
| 'test-tool::test-server': { |
| function: { |
| description: 'Cached tool', |
| parameters: { type: 'object', properties: {} }, |
| }, |
| }, |
| }; |
|
|
| await createMCPTool({ |
| res: mockRes, |
| user: mockUser, |
| toolKey: 'test-tool::test-server', |
| provider: 'openai', |
| userMCPAuthMap: {}, |
| availableTools: availableTools, |
| }); |
|
|
| |
| expect(mockReinitMCPServer).not.toHaveBeenCalled(); |
| }); |
| }); |
|
|
| describe('reinitMCPServer (via reconnectServer)', () => { |
| it('should always receive user parameter when called from createMCPTools', async () => { |
| const mockUser = { id: 'user-001', role: 'admin' }; |
| const mockRes = { write: jest.fn(), flush: jest.fn() }; |
|
|
| |
| const reinitCalls = []; |
| mockReinitMCPServer.mockImplementation((params) => { |
| reinitCalls.push(params); |
| return Promise.resolve({ |
| tools: [{ name: 'tool1' }, { name: 'tool2' }], |
| availableTools: { |
| 'tool1::server1': { function: { description: 'Tool 1', parameters: {} } }, |
| 'tool2::server1': { function: { description: 'Tool 2', parameters: {} } }, |
| }, |
| }); |
| }); |
|
|
| await createMCPTools({ |
| res: mockRes, |
| user: mockUser, |
| serverName: 'server1', |
| provider: 'anthropic', |
| userMCPAuthMap: {}, |
| }); |
|
|
| |
| expect(reinitCalls.length).toBeGreaterThan(0); |
| reinitCalls.forEach((call) => { |
| expect(call.user).toBe(mockUser); |
| expect(call.user.id).toBe('user-001'); |
| }); |
| }); |
|
|
| it('should always receive user parameter when called from createMCPTool', async () => { |
| const mockUser = { id: 'user-002', permissions: ['read', 'write'] }; |
| const mockRes = { write: jest.fn(), flush: jest.fn() }; |
|
|
| |
| const reinitCalls = []; |
| mockReinitMCPServer.mockImplementation((params) => { |
| reinitCalls.push(params); |
| return Promise.resolve({ |
| availableTools: { |
| 'my-tool::my-server': { |
| function: { description: 'My Tool', parameters: {} }, |
| }, |
| }, |
| }); |
| }); |
|
|
| await createMCPTool({ |
| res: mockRes, |
| user: mockUser, |
| toolKey: 'my-tool::my-server', |
| provider: 'google', |
| userMCPAuthMap: {}, |
| availableTools: undefined, |
| }); |
|
|
| |
| expect(reinitCalls.length).toBe(1); |
| expect(reinitCalls[0].user).toBe(mockUser); |
| expect(reinitCalls[0].user.id).toBe('user-002'); |
| }); |
| }); |
|
|
| describe('User parameter integrity', () => { |
| it('should preserve user object properties through the call chain', async () => { |
| const complexUser = { |
| id: 'complex-user', |
| name: 'John Doe', |
| email: 'john@example.com', |
| metadata: { subscription: 'premium', settings: { theme: 'dark' } }, |
| }; |
| const mockRes = { write: jest.fn(), flush: jest.fn() }; |
|
|
| let capturedUser = null; |
| mockReinitMCPServer.mockImplementation((params) => { |
| capturedUser = params.user; |
| return Promise.resolve({ |
| tools: [{ name: 'test' }], |
| availableTools: { |
| 'test::server': { function: { description: 'Test', parameters: {} } }, |
| }, |
| }); |
| }); |
|
|
| await createMCPTools({ |
| res: mockRes, |
| user: complexUser, |
| serverName: 'server', |
| provider: 'openai', |
| userMCPAuthMap: {}, |
| }); |
|
|
| |
| expect(capturedUser).toEqual(complexUser); |
| expect(capturedUser.id).toBe('complex-user'); |
| expect(capturedUser.metadata.subscription).toBe('premium'); |
| expect(capturedUser.metadata.settings.theme).toBe('dark'); |
| }); |
|
|
| it('should throw error when user is null', async () => { |
| const mockRes = { write: jest.fn(), flush: jest.fn() }; |
|
|
| mockReinitMCPServer.mockResolvedValue({ |
| tools: [], |
| availableTools: {}, |
| }); |
|
|
| await expect( |
| createMCPTools({ |
| res: mockRes, |
| user: null, |
| serverName: 'test-server', |
| provider: 'openai', |
| userMCPAuthMap: {}, |
| }), |
| ).rejects.toThrow("Cannot read properties of null (reading 'id')"); |
|
|
| |
| expect(mockReinitMCPServer).not.toHaveBeenCalled(); |
| }); |
| }); |
| }); |
|
|