const express = require('express'); const request = require('supertest'); const mongoose = require('mongoose'); const { MongoMemoryServer } = require('mongodb-memory-server'); jest.mock('@librechat/api', () => ({ ...jest.requireActual('@librechat/api'), MCPOAuthHandler: { initiateOAuthFlow: jest.fn(), getFlowState: jest.fn(), completeOAuthFlow: jest.fn(), generateFlowId: jest.fn(), }, getUserMCPAuthMap: jest.fn(), })); jest.mock('@librechat/data-schemas', () => ({ logger: { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn(), }, createModels: jest.fn(() => ({ User: { findOne: jest.fn(), findById: jest.fn(), }, Conversation: { findOne: jest.fn(), findById: jest.fn(), }, })), })); jest.mock('~/models', () => ({ findToken: jest.fn(), updateToken: jest.fn(), createToken: jest.fn(), deleteTokens: jest.fn(), findPluginAuthsByKeys: jest.fn(), })); jest.mock('~/server/services/Config', () => ({ setCachedTools: jest.fn(), getCachedTools: jest.fn(), loadCustomConfig: jest.fn(), })); jest.mock('~/server/services/Config/mcpToolsCache', () => ({ updateMCPUserTools: jest.fn(), })); jest.mock('~/server/services/MCP', () => ({ getMCPSetupData: jest.fn(), getServerConnectionStatus: jest.fn(), })); jest.mock('~/server/services/PluginService', () => ({ getUserPluginAuthValue: jest.fn(), })); jest.mock('~/config', () => ({ getMCPManager: jest.fn(), getFlowStateManager: jest.fn(), })); jest.mock('~/cache', () => ({ getLogStores: jest.fn(), })); jest.mock('~/server/middleware', () => ({ requireJwtAuth: (req, res, next) => next(), })); jest.mock('~/server/services/Tools/mcp', () => ({ reinitMCPServer: jest.fn(), })); describe('MCP Routes', () => { let app; let mongoServer; let mcpRouter; beforeAll(async () => { mongoServer = await MongoMemoryServer.create(); await mongoose.connect(mongoServer.getUri()); require('~/db/models'); mcpRouter = require('../mcp'); app = express(); app.use(express.json()); app.use((req, res, next) => { req.user = { id: 'test-user-id' }; next(); }); app.use('/api/mcp', mcpRouter); }); afterAll(async () => { await mongoose.disconnect(); await mongoServer.stop(); }); beforeEach(() => { jest.clearAllMocks(); }); describe('GET /:serverName/oauth/initiate', () => { const { MCPOAuthHandler } = require('@librechat/api'); const { getLogStores } = require('~/cache'); it('should initiate OAuth flow successfully', async () => { const mockFlowManager = { getFlowState: jest.fn().mockResolvedValue({ metadata: { serverUrl: 'https://test-server.com', oauth: { clientId: 'test-client-id' }, }, }), }; getLogStores.mockReturnValue({}); require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); MCPOAuthHandler.initiateOAuthFlow.mockResolvedValue({ authorizationUrl: 'https://oauth.example.com/auth', flowId: 'test-flow-id', }); const response = await request(app).get('/api/mcp/test-server/oauth/initiate').query({ userId: 'test-user-id', flowId: 'test-flow-id', }); expect(response.status).toBe(302); expect(response.headers.location).toBe('https://oauth.example.com/auth'); expect(MCPOAuthHandler.initiateOAuthFlow).toHaveBeenCalledWith( 'test-server', 'https://test-server.com', 'test-user-id', { clientId: 'test-client-id' }, ); }); it('should return 403 when userId does not match authenticated user', async () => { const response = await request(app).get('/api/mcp/test-server/oauth/initiate').query({ userId: 'different-user-id', flowId: 'test-flow-id', }); expect(response.status).toBe(403); expect(response.body).toEqual({ error: 'User mismatch' }); }); it('should return 404 when flow state is not found', async () => { const mockFlowManager = { getFlowState: jest.fn().mockResolvedValue(null), }; getLogStores.mockReturnValue({}); require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); const response = await request(app).get('/api/mcp/test-server/oauth/initiate').query({ userId: 'test-user-id', flowId: 'non-existent-flow-id', }); expect(response.status).toBe(404); expect(response.body).toEqual({ error: 'Flow not found' }); }); it('should return 400 when flow state has missing OAuth config', async () => { const mockFlowManager = { getFlowState: jest.fn().mockResolvedValue({ metadata: { serverUrl: 'https://test-server.com', }, }), }; getLogStores.mockReturnValue({}); require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); const response = await request(app).get('/api/mcp/test-server/oauth/initiate').query({ userId: 'test-user-id', flowId: 'test-flow-id', }); expect(response.status).toBe(400); expect(response.body).toEqual({ error: 'Invalid flow state' }); }); it('should return 500 when OAuth initiation throws unexpected error', async () => { const mockFlowManager = { getFlowState: jest.fn().mockRejectedValue(new Error('Database error')), }; getLogStores.mockReturnValue({}); require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); const response = await request(app).get('/api/mcp/test-server/oauth/initiate').query({ userId: 'test-user-id', flowId: 'test-flow-id', }); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Failed to initiate OAuth' }); }); it('should return 400 when flow state metadata is null', async () => { const mockFlowManager = { getFlowState: jest.fn().mockResolvedValue({ id: 'test-flow-id', metadata: null, }), }; getLogStores.mockReturnValue({}); require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); const response = await request(app).get('/api/mcp/test-server/oauth/initiate').query({ userId: 'test-user-id', flowId: 'test-flow-id', }); expect(response.status).toBe(400); expect(response.body).toEqual({ error: 'Invalid flow state' }); }); }); describe('GET /:serverName/oauth/callback', () => { const { MCPOAuthHandler } = require('@librechat/api'); const { getLogStores } = require('~/cache'); it('should redirect to error page when OAuth error is received', async () => { const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({ error: 'access_denied', state: 'test-flow-id', }); expect(response.status).toBe(302); expect(response.headers.location).toBe('/oauth/error?error=access_denied'); }); it('should redirect to error page when code is missing', async () => { const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({ state: 'test-flow-id', }); expect(response.status).toBe(302); expect(response.headers.location).toBe('/oauth/error?error=missing_code'); }); it('should redirect to error page when state is missing', async () => { const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({ code: 'test-auth-code', }); expect(response.status).toBe(302); expect(response.headers.location).toBe('/oauth/error?error=missing_state'); }); it('should redirect to error page when flow state is not found', async () => { MCPOAuthHandler.getFlowState.mockResolvedValue(null); const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({ code: 'test-auth-code', state: 'invalid-flow-id', }); expect(response.status).toBe(302); expect(response.headers.location).toBe('/oauth/error?error=invalid_state'); }); it('should handle OAuth callback successfully', async () => { const mockFlowManager = { completeFlow: jest.fn().mockResolvedValue(), }; const mockFlowState = { serverName: 'test-server', userId: 'test-user-id', metadata: { toolFlowId: 'tool-flow-123' }, clientInfo: {}, codeVerifier: 'test-verifier', }; const mockTokens = { access_token: 'test-access-token', refresh_token: 'test-refresh-token', }; MCPOAuthHandler.getFlowState.mockResolvedValue(mockFlowState); MCPOAuthHandler.completeOAuthFlow.mockResolvedValue(mockTokens); getLogStores.mockReturnValue({}); require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); const mockUserConnection = { fetchTools: jest.fn().mockResolvedValue([ { name: 'test-tool', description: 'A test tool', inputSchema: { type: 'object' }, }, ]), }; const mockMcpManager = { getUserConnection: jest.fn().mockResolvedValue(mockUserConnection), }; require('~/config').getMCPManager.mockReturnValue(mockMcpManager); const { getCachedTools, setCachedTools } = require('~/server/services/Config'); const { Constants } = require('librechat-data-provider'); getCachedTools.mockResolvedValue({ [`existing-tool${Constants.mcp_delimiter}test-server`]: { type: 'function' }, [`other-tool${Constants.mcp_delimiter}other-server`]: { type: 'function' }, }); setCachedTools.mockResolvedValue(); const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({ code: 'test-auth-code', state: 'test-flow-id', }); expect(response.status).toBe(302); expect(response.headers.location).toBe('/oauth/success?serverName=test-server'); expect(MCPOAuthHandler.completeOAuthFlow).toHaveBeenCalledWith( 'test-flow-id', 'test-auth-code', mockFlowManager, ); expect(mockFlowManager.completeFlow).toHaveBeenCalledWith( 'tool-flow-123', 'mcp_oauth', mockTokens, ); }); it('should redirect to error page when callback processing fails', async () => { MCPOAuthHandler.getFlowState.mockRejectedValue(new Error('Callback error')); const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({ code: 'test-auth-code', state: 'test-flow-id', }); expect(response.status).toBe(302); expect(response.headers.location).toBe('/oauth/error?error=callback_failed'); }); it('should handle system-level OAuth completion', async () => { const mockFlowManager = { completeFlow: jest.fn().mockResolvedValue(), }; const mockFlowState = { serverName: 'test-server', userId: 'system', metadata: { toolFlowId: 'tool-flow-123' }, clientInfo: {}, codeVerifier: 'test-verifier', }; const mockTokens = { access_token: 'test-access-token', refresh_token: 'test-refresh-token', }; MCPOAuthHandler.getFlowState.mockResolvedValue(mockFlowState); MCPOAuthHandler.completeOAuthFlow.mockResolvedValue(mockTokens); getLogStores.mockReturnValue({}); require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({ code: 'test-auth-code', state: 'test-flow-id', }); expect(response.status).toBe(302); expect(response.headers.location).toBe('/oauth/success?serverName=test-server'); }); it('should handle reconnection failure after OAuth', async () => { const mockFlowManager = { completeFlow: jest.fn().mockResolvedValue(), }; const mockFlowState = { serverName: 'test-server', userId: 'test-user-id', metadata: { toolFlowId: 'tool-flow-123' }, clientInfo: {}, codeVerifier: 'test-verifier', }; const mockTokens = { access_token: 'test-access-token', refresh_token: 'test-refresh-token', }; MCPOAuthHandler.getFlowState.mockResolvedValue(mockFlowState); MCPOAuthHandler.completeOAuthFlow.mockResolvedValue(mockTokens); getLogStores.mockReturnValue({}); require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); const mockMcpManager = { getUserConnection: jest.fn().mockRejectedValue(new Error('Reconnection failed')), }; require('~/config').getMCPManager.mockReturnValue(mockMcpManager); const { getCachedTools, setCachedTools } = require('~/server/services/Config'); getCachedTools.mockResolvedValue({}); setCachedTools.mockResolvedValue(); const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({ code: 'test-auth-code', state: 'test-flow-id', }); expect(response.status).toBe(302); expect(response.headers.location).toBe('/oauth/success?serverName=test-server'); }); }); describe('GET /oauth/tokens/:flowId', () => { const { getLogStores } = require('~/cache'); it('should return tokens for completed flow', async () => { const mockFlowManager = { getFlowState: jest.fn().mockResolvedValue({ status: 'COMPLETED', result: { access_token: 'test-access-token', refresh_token: 'test-refresh-token', }, }), }; getLogStores.mockReturnValue({}); require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); const response = await request(app).get('/api/mcp/oauth/tokens/test-user-id:flow-123'); expect(response.status).toBe(200); expect(response.body).toEqual({ tokens: { access_token: 'test-access-token', refresh_token: 'test-refresh-token', }, }); }); it('should return 401 when user is not authenticated', async () => { const unauthApp = express(); unauthApp.use(express.json()); unauthApp.use((req, res, next) => { req.user = null; next(); }); unauthApp.use('/api/mcp', mcpRouter); const response = await request(unauthApp).get('/api/mcp/oauth/tokens/test-flow-id'); expect(response.status).toBe(401); expect(response.body).toEqual({ error: 'User not authenticated' }); }); it('should return 403 when user tries to access flow they do not own', async () => { const response = await request(app).get('/api/mcp/oauth/tokens/other-user-id:flow-123'); expect(response.status).toBe(403); expect(response.body).toEqual({ error: 'Access denied' }); }); it('should return 404 when flow is not found', async () => { const mockFlowManager = { getFlowState: jest.fn().mockResolvedValue(null), }; getLogStores.mockReturnValue({}); require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); const response = await request(app).get( '/api/mcp/oauth/tokens/test-user-id:non-existent-flow', ); expect(response.status).toBe(404); expect(response.body).toEqual({ error: 'Flow not found' }); }); it('should return 400 when flow is not completed', async () => { const mockFlowManager = { getFlowState: jest.fn().mockResolvedValue({ status: 'PENDING', result: null, }), }; getLogStores.mockReturnValue({}); require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); const response = await request(app).get('/api/mcp/oauth/tokens/test-user-id:pending-flow'); expect(response.status).toBe(400); expect(response.body).toEqual({ error: 'Flow not completed' }); }); it('should return 500 when token retrieval throws an unexpected error', async () => { getLogStores.mockImplementation(() => { throw new Error('Database connection failed'); }); const response = await request(app).get('/api/mcp/oauth/tokens/test-user-id:error-flow'); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Failed to get tokens' }); }); }); describe('GET /oauth/status/:flowId', () => { const { getLogStores } = require('~/cache'); it('should return flow status when flow exists', async () => { const mockFlowManager = { getFlowState: jest.fn().mockResolvedValue({ status: 'PENDING', error: null, }), }; getLogStores.mockReturnValue({}); require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); const response = await request(app).get('/api/mcp/oauth/status/test-flow-id'); expect(response.status).toBe(200); expect(response.body).toEqual({ status: 'PENDING', completed: false, failed: false, error: null, }); }); it('should return 404 when flow is not found', async () => { const mockFlowManager = { getFlowState: jest.fn().mockResolvedValue(null), }; getLogStores.mockReturnValue({}); require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); const response = await request(app).get('/api/mcp/oauth/status/non-existent-flow'); expect(response.status).toBe(404); expect(response.body).toEqual({ error: 'Flow not found' }); }); it('should return 500 when status check fails', async () => { const mockFlowManager = { getFlowState: jest.fn().mockRejectedValue(new Error('Database error')), }; getLogStores.mockReturnValue({}); require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); const response = await request(app).get('/api/mcp/oauth/status/error-flow-id'); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Failed to get flow status' }); }); }); describe('POST /oauth/cancel/:serverName', () => { const { MCPOAuthHandler } = require('@librechat/api'); const { getLogStores } = require('~/cache'); it('should cancel OAuth flow successfully', async () => { const mockFlowManager = { getFlowState: jest.fn().mockResolvedValue({ status: 'PENDING', }), failFlow: jest.fn().mockResolvedValue(), }; getLogStores.mockReturnValue({}); require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); MCPOAuthHandler.generateFlowId.mockReturnValue('test-user-id:test-server'); const response = await request(app).post('/api/mcp/oauth/cancel/test-server'); expect(response.status).toBe(200); expect(response.body).toEqual({ success: true, message: 'OAuth flow for test-server cancelled successfully', }); expect(mockFlowManager.failFlow).toHaveBeenCalledWith( 'test-user-id:test-server', 'mcp_oauth', 'User cancelled OAuth flow', ); }); it('should return success message when no active flow exists', async () => { const mockFlowManager = { getFlowState: jest.fn().mockResolvedValue(null), }; getLogStores.mockReturnValue({}); require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); MCPOAuthHandler.generateFlowId.mockReturnValue('test-user-id:test-server'); const response = await request(app).post('/api/mcp/oauth/cancel/test-server'); expect(response.status).toBe(200); expect(response.body).toEqual({ success: true, message: 'No active OAuth flow to cancel', }); }); it('should return 500 when cancellation fails', async () => { const mockFlowManager = { getFlowState: jest.fn().mockResolvedValue({ status: 'PENDING' }), failFlow: jest.fn().mockRejectedValue(new Error('Database error')), }; getLogStores.mockReturnValue({}); require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); MCPOAuthHandler.generateFlowId.mockReturnValue('test-user-id:test-server'); const response = await request(app).post('/api/mcp/oauth/cancel/test-server'); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Failed to cancel OAuth flow' }); }); it('should return 401 when user is not authenticated', async () => { const unauthApp = express(); unauthApp.use(express.json()); unauthApp.use((req, res, next) => { req.user = null; next(); }); unauthApp.use('/api/mcp', mcpRouter); const response = await request(unauthApp).post('/api/mcp/oauth/cancel/test-server'); expect(response.status).toBe(401); expect(response.body).toEqual({ error: 'User not authenticated' }); }); }); describe('POST /:serverName/reinitialize', () => { it('should return 404 when server is not found in configuration', async () => { const mockMcpManager = { getRawConfig: jest.fn().mockReturnValue(null), disconnectUserConnection: jest.fn().mockResolvedValue(), }; require('~/config').getMCPManager.mockReturnValue(mockMcpManager); require('~/config').getFlowStateManager.mockReturnValue({}); require('~/cache').getLogStores.mockReturnValue({}); const response = await request(app).post('/api/mcp/non-existent-server/reinitialize'); expect(response.status).toBe(404); expect(response.body).toEqual({ error: "MCP server 'non-existent-server' not found in configuration", }); }); it('should handle OAuth requirement during reinitialize', async () => { const mockMcpManager = { getRawConfig: jest.fn().mockReturnValue({ customUserVars: {}, }), disconnectUserConnection: jest.fn().mockResolvedValue(), mcpConfigs: {}, getUserConnection: jest.fn().mockImplementation(async ({ oauthStart }) => { if (oauthStart) { await oauthStart('https://oauth.example.com/auth'); } throw new Error('OAuth flow initiated - return early'); }), }; require('~/config').getMCPManager.mockReturnValue(mockMcpManager); require('~/config').getFlowStateManager.mockReturnValue({}); require('~/cache').getLogStores.mockReturnValue({}); require('~/server/services/Tools/mcp').reinitMCPServer.mockResolvedValue({ success: true, message: "MCP server 'oauth-server' ready for OAuth authentication", serverName: 'oauth-server', oauthRequired: true, oauthUrl: 'https://oauth.example.com/auth', }); const response = await request(app).post('/api/mcp/oauth-server/reinitialize'); expect(response.status).toBe(200); expect(response.body).toEqual({ success: true, message: "MCP server 'oauth-server' ready for OAuth authentication", serverName: 'oauth-server', oauthRequired: true, oauthUrl: 'https://oauth.example.com/auth', }); }); it('should return 500 when reinitialize fails with non-OAuth error', async () => { const mockMcpManager = { getRawConfig: jest.fn().mockReturnValue({}), disconnectUserConnection: jest.fn().mockResolvedValue(), mcpConfigs: {}, getUserConnection: jest.fn().mockRejectedValue(new Error('Connection failed')), }; require('~/config').getMCPManager.mockReturnValue(mockMcpManager); require('~/config').getFlowStateManager.mockReturnValue({}); require('~/cache').getLogStores.mockReturnValue({}); require('~/server/services/Tools/mcp').reinitMCPServer.mockResolvedValue(null); const response = await request(app).post('/api/mcp/error-server/reinitialize'); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Failed to reinitialize MCP server for user', }); }); it('should return 500 when unexpected error occurs', async () => { const mockMcpManager = { getRawConfig: jest.fn().mockImplementation(() => { throw new Error('Config loading failed'); }), }; require('~/config').getMCPManager.mockReturnValue(mockMcpManager); const response = await request(app).post('/api/mcp/test-server/reinitialize'); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Internal server error' }); }); it('should return 401 when user is not authenticated', async () => { const unauthApp = express(); unauthApp.use(express.json()); unauthApp.use((req, res, next) => { req.user = null; next(); }); unauthApp.use('/api/mcp', mcpRouter); const response = await request(unauthApp).post('/api/mcp/test-server/reinitialize'); expect(response.status).toBe(401); expect(response.body).toEqual({ error: 'User not authenticated' }); }); it('should successfully reinitialize server and cache tools', async () => { const mockUserConnection = { fetchTools: jest.fn().mockResolvedValue([ { name: 'tool1', description: 'Test tool 1', inputSchema: { type: 'object' } }, { name: 'tool2', description: 'Test tool 2', inputSchema: { type: 'object' } }, ]), }; const mockMcpManager = { getRawConfig: jest.fn().mockReturnValue({ endpoint: 'http://test-server.com' }), disconnectUserConnection: jest.fn().mockResolvedValue(), getUserConnection: jest.fn().mockResolvedValue(mockUserConnection), }; require('~/config').getMCPManager.mockReturnValue(mockMcpManager); require('~/config').getFlowStateManager.mockReturnValue({}); require('~/cache').getLogStores.mockReturnValue({}); const { getCachedTools, setCachedTools } = require('~/server/services/Config'); const { updateMCPUserTools } = require('~/server/services/Config/mcpToolsCache'); getCachedTools.mockResolvedValue({}); setCachedTools.mockResolvedValue(); updateMCPUserTools.mockResolvedValue(); require('~/server/services/Tools/mcp').reinitMCPServer.mockResolvedValue({ success: true, message: "MCP server 'test-server' reinitialized successfully", serverName: 'test-server', oauthRequired: false, oauthUrl: null, }); const response = await request(app).post('/api/mcp/test-server/reinitialize'); expect(response.status).toBe(200); expect(response.body).toEqual({ success: true, message: "MCP server 'test-server' reinitialized successfully", serverName: 'test-server', oauthRequired: false, oauthUrl: null, }); expect(mockMcpManager.disconnectUserConnection).toHaveBeenCalledWith( 'test-user-id', 'test-server', ); }); it('should handle server with custom user variables', async () => { const mockUserConnection = { fetchTools: jest.fn().mockResolvedValue([]), }; const mockMcpManager = { getRawConfig: jest.fn().mockReturnValue({ endpoint: 'http://test-server.com', customUserVars: { API_KEY: 'some-env-var', }, }), disconnectUserConnection: jest.fn().mockResolvedValue(), getUserConnection: jest.fn().mockResolvedValue(mockUserConnection), }; require('~/config').getMCPManager.mockReturnValue(mockMcpManager); require('~/config').getFlowStateManager.mockReturnValue({}); require('~/cache').getLogStores.mockReturnValue({}); require('@librechat/api').getUserMCPAuthMap.mockResolvedValue({ 'mcp:test-server': { API_KEY: 'api-key-value', }, }); require('~/models').findPluginAuthsByKeys.mockResolvedValue([ { key: 'API_KEY', value: 'api-key-value' }, ]); const { getCachedTools, setCachedTools } = require('~/server/services/Config'); const { updateMCPUserTools } = require('~/server/services/Config/mcpToolsCache'); getCachedTools.mockResolvedValue({}); setCachedTools.mockResolvedValue(); updateMCPUserTools.mockResolvedValue(); require('~/server/services/Tools/mcp').reinitMCPServer.mockResolvedValue({ success: true, message: "MCP server 'test-server' reinitialized successfully", serverName: 'test-server', oauthRequired: false, oauthUrl: null, }); const response = await request(app).post('/api/mcp/test-server/reinitialize'); expect(response.status).toBe(200); expect(response.body.success).toBe(true); expect(require('@librechat/api').getUserMCPAuthMap).toHaveBeenCalledWith({ userId: 'test-user-id', servers: ['test-server'], findPluginAuthsByKeys: require('~/models').findPluginAuthsByKeys, }); }); }); describe('GET /connection/status', () => { const { getMCPSetupData, getServerConnectionStatus } = require('~/server/services/MCP'); it('should return connection status for all servers', async () => { const mockMcpConfig = { server1: { endpoint: 'http://server1.com' }, server2: { endpoint: 'http://server2.com' }, }; getMCPSetupData.mockResolvedValue({ mcpConfig: mockMcpConfig, appConnections: {}, userConnections: {}, oauthServers: [], }); getServerConnectionStatus .mockResolvedValueOnce({ connectionState: 'connected', requiresOAuth: false, }) .mockResolvedValueOnce({ connectionState: 'disconnected', requiresOAuth: true, }); const response = await request(app).get('/api/mcp/connection/status'); expect(response.status).toBe(200); expect(response.body).toEqual({ success: true, connectionStatus: { server1: { connectionState: 'connected', requiresOAuth: false, }, server2: { connectionState: 'disconnected', requiresOAuth: true, }, }, }); expect(getMCPSetupData).toHaveBeenCalledWith('test-user-id'); expect(getServerConnectionStatus).toHaveBeenCalledTimes(2); }); it('should return 404 when MCP config is not found', async () => { getMCPSetupData.mockRejectedValue(new Error('MCP config not found')); const response = await request(app).get('/api/mcp/connection/status'); expect(response.status).toBe(404); expect(response.body).toEqual({ error: 'MCP config not found' }); }); it('should return 500 when connection status check fails', async () => { getMCPSetupData.mockRejectedValue(new Error('Database error')); const response = await request(app).get('/api/mcp/connection/status'); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Failed to get connection status' }); }); it('should return 401 when user is not authenticated', async () => { const unauthApp = express(); unauthApp.use(express.json()); unauthApp.use((req, res, next) => { req.user = null; next(); }); unauthApp.use('/api/mcp', mcpRouter); const response = await request(unauthApp).get('/api/mcp/connection/status'); expect(response.status).toBe(401); expect(response.body).toEqual({ error: 'User not authenticated' }); }); }); describe('GET /connection/status/:serverName', () => { const { getMCPSetupData, getServerConnectionStatus } = require('~/server/services/MCP'); it('should return connection status for OAuth-required server', async () => { const mockMcpConfig = { 'oauth-server': { endpoint: 'http://oauth-server.com' }, }; getMCPSetupData.mockResolvedValue({ mcpConfig: mockMcpConfig, appConnections: {}, userConnections: {}, oauthServers: [], }); getServerConnectionStatus.mockResolvedValue({ connectionState: 'requires_auth', requiresOAuth: true, }); const response = await request(app).get('/api/mcp/connection/status/oauth-server'); expect(response.status).toBe(200); expect(response.body).toEqual({ success: true, serverName: 'oauth-server', connectionStatus: 'requires_auth', requiresOAuth: true, }); }); it('should return 404 when server is not found in configuration', async () => { getMCPSetupData.mockResolvedValue({ mcpConfig: { 'other-server': { endpoint: 'http://other-server.com' }, }, appConnections: {}, userConnections: {}, oauthServers: [], }); const response = await request(app).get('/api/mcp/connection/status/non-existent-server'); expect(response.status).toBe(404); expect(response.body).toEqual({ error: "MCP server 'non-existent-server' not found in configuration", }); }); it('should return 404 when MCP config is not found', async () => { getMCPSetupData.mockRejectedValue(new Error('MCP config not found')); const response = await request(app).get('/api/mcp/connection/status/test-server'); expect(response.status).toBe(404); expect(response.body).toEqual({ error: 'MCP config not found' }); }); it('should return 500 when connection status check fails', async () => { getMCPSetupData.mockRejectedValue(new Error('Database connection failed')); const response = await request(app).get('/api/mcp/connection/status/test-server'); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Failed to get connection status' }); }); it('should return 401 when user is not authenticated', async () => { const unauthApp = express(); unauthApp.use(express.json()); unauthApp.use((req, res, next) => { req.user = null; next(); }); unauthApp.use('/api/mcp', mcpRouter); const response = await request(unauthApp).get('/api/mcp/connection/status/test-server'); expect(response.status).toBe(401); expect(response.body).toEqual({ error: 'User not authenticated' }); }); }); describe('GET /:serverName/auth-values', () => { const { getUserPluginAuthValue } = require('~/server/services/PluginService'); it('should return auth value flags for server', async () => { const mockMcpManager = { getRawConfig: jest.fn().mockReturnValue({ customUserVars: { API_KEY: 'some-env-var', SECRET_TOKEN: 'another-env-var', }, }), }; require('~/config').getMCPManager.mockReturnValue(mockMcpManager); getUserPluginAuthValue.mockResolvedValueOnce('some-api-key-value').mockResolvedValueOnce(''); const response = await request(app).get('/api/mcp/test-server/auth-values'); expect(response.status).toBe(200); expect(response.body).toEqual({ success: true, serverName: 'test-server', authValueFlags: { API_KEY: true, SECRET_TOKEN: false, }, }); expect(getUserPluginAuthValue).toHaveBeenCalledTimes(2); }); it('should return 404 when server is not found in configuration', async () => { const mockMcpManager = { getRawConfig: jest.fn().mockReturnValue(null), }; require('~/config').getMCPManager.mockReturnValue(mockMcpManager); const response = await request(app).get('/api/mcp/non-existent-server/auth-values'); expect(response.status).toBe(404); expect(response.body).toEqual({ error: "MCP server 'non-existent-server' not found in configuration", }); }); it('should handle errors when checking auth values', async () => { const mockMcpManager = { getRawConfig: jest.fn().mockReturnValue({ customUserVars: { API_KEY: 'some-env-var', }, }), }; require('~/config').getMCPManager.mockReturnValue(mockMcpManager); getUserPluginAuthValue.mockRejectedValue(new Error('Database error')); const response = await request(app).get('/api/mcp/test-server/auth-values'); expect(response.status).toBe(200); expect(response.body).toEqual({ success: true, serverName: 'test-server', authValueFlags: { API_KEY: false, }, }); }); it('should return 500 when auth values check throws unexpected error', async () => { const mockMcpManager = { getRawConfig: jest.fn().mockImplementation(() => { throw new Error('Config loading failed'); }), }; require('~/config').getMCPManager.mockReturnValue(mockMcpManager); const response = await request(app).get('/api/mcp/test-server/auth-values'); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Failed to check auth value flags' }); }); it('should handle customUserVars that is not an object', async () => { const mockMcpManager = { getRawConfig: jest.fn().mockReturnValue({ customUserVars: 'not-an-object', }), }; require('~/config').getMCPManager.mockReturnValue(mockMcpManager); const response = await request(app).get('/api/mcp/test-server/auth-values'); expect(response.status).toBe(200); expect(response.body).toEqual({ success: true, serverName: 'test-server', authValueFlags: {}, }); }); it('should return 401 when user is not authenticated in auth-values endpoint', async () => { const appWithoutAuth = express(); appWithoutAuth.use(express.json()); appWithoutAuth.use('/api/mcp', mcpRouter); const response = await request(appWithoutAuth).get('/api/mcp/test-server/auth-values'); expect(response.status).toBe(401); expect(response.body).toEqual({ error: 'User not authenticated' }); }); }); describe('GET /:serverName/oauth/callback - Edge Cases', () => { it('should handle OAuth callback without toolFlowId (falsy toolFlowId)', async () => { const { MCPOAuthHandler } = require('@librechat/api'); MCPOAuthHandler.getFlowState = jest.fn().mockResolvedValue({ id: 'test-flow-id', userId: 'test-user-id', metadata: { serverUrl: 'https://example.com', oauth: {}, // No toolFlowId property }, clientInfo: {}, codeVerifier: 'test-verifier', }); const mockFlowManager = { completeFlow: jest.fn(), }; require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); const mockMcpManager = { getUserConnection: jest.fn().mockResolvedValue({ fetchTools: jest.fn().mockResolvedValue([]), }), }; require('~/config').getMCPManager.mockReturnValue(mockMcpManager); const response = await request(app) .get('/api/mcp/test-server/oauth/callback?code=test-code&state=test-flow-id') .expect(302); expect(mockFlowManager.completeFlow).not.toHaveBeenCalled(); expect(response.headers.location).toContain('/oauth/success'); }); it('should handle null cached tools in OAuth callback (triggers || {} fallback)', async () => { const { getCachedTools } = require('~/server/services/Config'); getCachedTools.mockResolvedValue(null); const mockFlowManager = { getFlowState: jest.fn().mockResolvedValue({ id: 'test-flow-id', userId: 'test-user-id', metadata: { serverUrl: 'https://example.com', oauth: {} }, clientInfo: {}, codeVerifier: 'test-verifier', }), completeFlow: jest.fn(), }; require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); const mockMcpManager = { getUserConnection: jest.fn().mockResolvedValue({ fetchTools: jest .fn() .mockResolvedValue([{ name: 'test-tool', description: 'Test tool' }]), }), }; require('~/config').getMCPManager.mockReturnValue(mockMcpManager); const response = await request(app) .get('/api/mcp/test-server/oauth/callback?code=test-code&state=test-flow-id') .expect(302); expect(response.headers.location).toContain('/oauth/success'); }); }); });