const { Router } = require('express'); const { logger } = require('@librechat/data-schemas'); const { CacheKeys, Constants } = require('librechat-data-provider'); const { MCPOAuthHandler, getUserMCPAuthMap } = require('@librechat/api'); const { getMCPManager, getFlowStateManager, getOAuthReconnectionManager } = require('~/config'); const { getMCPSetupData, getServerConnectionStatus } = require('~/server/services/MCP'); const { findToken, updateToken, createToken, deleteTokens } = require('~/models'); const { getUserPluginAuthValue } = require('~/server/services/PluginService'); const { updateMCPServerTools } = require('~/server/services/Config/mcp'); const { reinitMCPServer } = require('~/server/services/Tools/mcp'); const { getMCPTools } = require('~/server/controllers/mcp'); const { requireJwtAuth } = require('~/server/middleware'); const { findPluginAuthsByKeys } = require('~/models'); const { getLogStores } = require('~/cache'); const router = Router(); /** * Get all MCP tools available to the user * Returns only MCP tools, completely decoupled from regular LibreChat tools */ router.get('/tools', requireJwtAuth, async (req, res) => { return getMCPTools(req, res); }); /** * Initiate OAuth flow * This endpoint is called when the user clicks the auth link in the UI */ router.get('/:serverName/oauth/initiate', requireJwtAuth, async (req, res) => { try { const { serverName } = req.params; const { userId, flowId } = req.query; const user = req.user; // Verify the userId matches the authenticated user if (userId !== user.id) { return res.status(403).json({ error: 'User mismatch' }); } logger.debug('[MCP OAuth] Initiate request', { serverName, userId, flowId }); const flowsCache = getLogStores(CacheKeys.FLOWS); const flowManager = getFlowStateManager(flowsCache); /** Flow state to retrieve OAuth config */ const flowState = await flowManager.getFlowState(flowId, 'mcp_oauth'); if (!flowState) { logger.error('[MCP OAuth] Flow state not found', { flowId }); return res.status(404).json({ error: 'Flow not found' }); } const { serverUrl, oauth: oauthConfig } = flowState.metadata || {}; if (!serverUrl || !oauthConfig) { logger.error('[MCP OAuth] Missing server URL or OAuth config in flow state'); return res.status(400).json({ error: 'Invalid flow state' }); } const { authorizationUrl, flowId: oauthFlowId } = await MCPOAuthHandler.initiateOAuthFlow( serverName, serverUrl, userId, oauthConfig, ); logger.debug('[MCP OAuth] OAuth flow initiated', { oauthFlowId, authorizationUrl }); // Redirect user to the authorization URL res.redirect(authorizationUrl); } catch (error) { logger.error('[MCP OAuth] Failed to initiate OAuth', error); res.status(500).json({ error: 'Failed to initiate OAuth' }); } }); /** * OAuth callback handler * This handles the OAuth callback after the user has authorized the application */ router.get('/:serverName/oauth/callback', async (req, res) => { try { const { serverName } = req.params; const { code, state, error: oauthError } = req.query; logger.debug('[MCP OAuth] Callback received', { serverName, code: code ? 'present' : 'missing', state, error: oauthError, }); if (oauthError) { logger.error('[MCP OAuth] OAuth error received', { error: oauthError }); return res.redirect(`/oauth/error?error=${encodeURIComponent(String(oauthError))}`); } if (!code || typeof code !== 'string') { logger.error('[MCP OAuth] Missing or invalid code'); return res.redirect('/oauth/error?error=missing_code'); } if (!state || typeof state !== 'string') { logger.error('[MCP OAuth] Missing or invalid state'); return res.redirect('/oauth/error?error=missing_state'); } const flowId = state; logger.debug('[MCP OAuth] Using flow ID from state', { flowId }); const flowsCache = getLogStores(CacheKeys.FLOWS); const flowManager = getFlowStateManager(flowsCache); logger.debug('[MCP OAuth] Getting flow state for flowId: ' + flowId); const flowState = await MCPOAuthHandler.getFlowState(flowId, flowManager); if (!flowState) { logger.error('[MCP OAuth] Flow state not found for flowId:', flowId); return res.redirect('/oauth/error?error=invalid_state'); } logger.debug('[MCP OAuth] Flow state details', { serverName: flowState.serverName, userId: flowState.userId, hasMetadata: !!flowState.metadata, hasClientInfo: !!flowState.clientInfo, hasCodeVerifier: !!flowState.codeVerifier, }); logger.debug('[MCP OAuth] Completing OAuth flow'); const tokens = await MCPOAuthHandler.completeOAuthFlow(flowId, code, flowManager); logger.info('[MCP OAuth] OAuth flow completed, tokens received in callback route'); try { const mcpManager = getMCPManager(flowState.userId); logger.debug(`[MCP OAuth] Attempting to reconnect ${serverName} with new OAuth tokens`); if (flowState.userId !== 'system') { const user = { id: flowState.userId }; const userConnection = await mcpManager.getUserConnection({ user, serverName, flowManager, tokenMethods: { findToken, updateToken, createToken, deleteTokens, }, }); logger.info( `[MCP OAuth] Successfully reconnected ${serverName} for user ${flowState.userId}`, ); // clear any reconnection attempts const oauthReconnectionManager = getOAuthReconnectionManager(); oauthReconnectionManager.clearReconnection(flowState.userId, serverName); const tools = await userConnection.fetchTools(); await updateMCPServerTools({ serverName, tools, }); } else { logger.debug(`[MCP OAuth] System-level OAuth completed for ${serverName}`); } } catch (error) { logger.warn( `[MCP OAuth] Failed to reconnect ${serverName} after OAuth, but tokens are saved:`, error, ); } /** ID of the flow that the tool/connection is waiting for */ const toolFlowId = flowState.metadata?.toolFlowId; if (toolFlowId) { logger.debug('[MCP OAuth] Completing tool flow', { toolFlowId }); await flowManager.completeFlow(toolFlowId, 'mcp_oauth', tokens); } /** Redirect to success page with flowId and serverName */ const redirectUrl = `/oauth/success?serverName=${encodeURIComponent(serverName)}`; res.redirect(redirectUrl); } catch (error) { logger.error('[MCP OAuth] OAuth callback error', error); res.redirect('/oauth/error?error=callback_failed'); } }); /** * Get OAuth tokens for a completed flow * This is primarily for user-level OAuth flows */ router.get('/oauth/tokens/:flowId', requireJwtAuth, async (req, res) => { try { const { flowId } = req.params; const user = req.user; if (!user?.id) { return res.status(401).json({ error: 'User not authenticated' }); } if (!flowId.startsWith(`${user.id}:`) && !flowId.startsWith('system:')) { return res.status(403).json({ error: 'Access denied' }); } const flowsCache = getLogStores(CacheKeys.FLOWS); const flowManager = getFlowStateManager(flowsCache); const flowState = await flowManager.getFlowState(flowId, 'mcp_oauth'); if (!flowState) { return res.status(404).json({ error: 'Flow not found' }); } if (flowState.status !== 'COMPLETED') { return res.status(400).json({ error: 'Flow not completed' }); } res.json({ tokens: flowState.result }); } catch (error) { logger.error('[MCP OAuth] Failed to get tokens', error); res.status(500).json({ error: 'Failed to get tokens' }); } }); /** * Check OAuth flow status * This endpoint can be used to poll the status of an OAuth flow */ router.get('/oauth/status/:flowId', async (req, res) => { try { const { flowId } = req.params; const flowsCache = getLogStores(CacheKeys.FLOWS); const flowManager = getFlowStateManager(flowsCache); const flowState = await flowManager.getFlowState(flowId, 'mcp_oauth'); if (!flowState) { return res.status(404).json({ error: 'Flow not found' }); } res.json({ status: flowState.status, completed: flowState.status === 'COMPLETED', failed: flowState.status === 'FAILED', error: flowState.error, }); } catch (error) { logger.error('[MCP OAuth] Failed to get flow status', error); res.status(500).json({ error: 'Failed to get flow status' }); } }); /** * Cancel OAuth flow * This endpoint cancels a pending OAuth flow */ router.post('/oauth/cancel/:serverName', requireJwtAuth, async (req, res) => { try { const { serverName } = req.params; const user = req.user; if (!user?.id) { return res.status(401).json({ error: 'User not authenticated' }); } logger.info(`[MCP OAuth Cancel] Cancelling OAuth flow for ${serverName} by user ${user.id}`); const flowsCache = getLogStores(CacheKeys.FLOWS); const flowManager = getFlowStateManager(flowsCache); const flowId = MCPOAuthHandler.generateFlowId(user.id, serverName); const flowState = await flowManager.getFlowState(flowId, 'mcp_oauth'); if (!flowState) { logger.debug(`[MCP OAuth Cancel] No active flow found for ${serverName}`); return res.json({ success: true, message: 'No active OAuth flow to cancel', }); } await flowManager.failFlow(flowId, 'mcp_oauth', 'User cancelled OAuth flow'); logger.info(`[MCP OAuth Cancel] Successfully cancelled OAuth flow for ${serverName}`); res.json({ success: true, message: `OAuth flow for ${serverName} cancelled successfully`, }); } catch (error) { logger.error('[MCP OAuth Cancel] Failed to cancel OAuth flow', error); res.status(500).json({ error: 'Failed to cancel OAuth flow' }); } }); /** * Reinitialize MCP server * This endpoint allows reinitializing a specific MCP server */ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => { try { const { serverName } = req.params; const user = req.user; if (!user?.id) { return res.status(401).json({ error: 'User not authenticated' }); } logger.info(`[MCP Reinitialize] Reinitializing server: ${serverName}`); const mcpManager = getMCPManager(); const serverConfig = mcpManager.getRawConfig(serverName); if (!serverConfig) { return res.status(404).json({ error: `MCP server '${serverName}' not found in configuration`, }); } await mcpManager.disconnectUserConnection(user.id, serverName); logger.info( `[MCP Reinitialize] Disconnected existing user connection for server: ${serverName}`, ); /** @type {Record> | undefined} */ let userMCPAuthMap; if (serverConfig.customUserVars && typeof serverConfig.customUserVars === 'object') { userMCPAuthMap = await getUserMCPAuthMap({ userId: user.id, servers: [serverName], findPluginAuthsByKeys, }); } const result = await reinitMCPServer({ req, serverName, userMCPAuthMap, }); if (!result) { return res.status(500).json({ error: 'Failed to reinitialize MCP server for user' }); } const { success, message, oauthRequired, oauthUrl } = result; res.json({ success, message, oauthUrl, serverName, oauthRequired, }); } catch (error) { logger.error('[MCP Reinitialize] Unexpected error', error); res.status(500).json({ error: 'Internal server error' }); } }); /** * Get connection status for all MCP servers * This endpoint returns all app level and user-scoped connection statuses from MCPManager without disconnecting idle connections */ router.get('/connection/status', requireJwtAuth, async (req, res) => { try { const user = req.user; if (!user?.id) { return res.status(401).json({ error: 'User not authenticated' }); } const { mcpConfig, appConnections, userConnections, oauthServers } = await getMCPSetupData( user.id, ); const connectionStatus = {}; for (const [serverName] of Object.entries(mcpConfig)) { connectionStatus[serverName] = await getServerConnectionStatus( user.id, serverName, appConnections, userConnections, oauthServers, ); } res.json({ success: true, connectionStatus, }); } catch (error) { if (error.message === 'MCP config not found') { return res.status(404).json({ error: error.message }); } logger.error('[MCP Connection Status] Failed to get connection status', error); res.status(500).json({ error: 'Failed to get connection status' }); } }); /** * Get connection status for a single MCP server * This endpoint returns the connection status for a specific server for a given user */ router.get('/connection/status/:serverName', requireJwtAuth, async (req, res) => { try { const user = req.user; const { serverName } = req.params; if (!user?.id) { return res.status(401).json({ error: 'User not authenticated' }); } const { mcpConfig, appConnections, userConnections, oauthServers } = await getMCPSetupData( user.id, ); if (!mcpConfig[serverName]) { return res .status(404) .json({ error: `MCP server '${serverName}' not found in configuration` }); } const serverStatus = await getServerConnectionStatus( user.id, serverName, appConnections, userConnections, oauthServers, ); res.json({ success: true, serverName, connectionStatus: serverStatus.connectionState, requiresOAuth: serverStatus.requiresOAuth, }); } catch (error) { if (error.message === 'MCP config not found') { return res.status(404).json({ error: error.message }); } logger.error( `[MCP Per-Server Status] Failed to get connection status for ${req.params.serverName}`, error, ); res.status(500).json({ error: 'Failed to get connection status' }); } }); /** * Check which authentication values exist for a specific MCP server * This endpoint returns only boolean flags indicating if values are set, not the actual values */ router.get('/:serverName/auth-values', requireJwtAuth, async (req, res) => { try { const { serverName } = req.params; const user = req.user; if (!user?.id) { return res.status(401).json({ error: 'User not authenticated' }); } const mcpManager = getMCPManager(); const serverConfig = mcpManager.getRawConfig(serverName); if (!serverConfig) { return res.status(404).json({ error: `MCP server '${serverName}' not found in configuration`, }); } const pluginKey = `${Constants.mcp_prefix}${serverName}`; const authValueFlags = {}; if (serverConfig.customUserVars && typeof serverConfig.customUserVars === 'object') { for (const varName of Object.keys(serverConfig.customUserVars)) { try { const value = await getUserPluginAuthValue(user.id, varName, false, pluginKey); authValueFlags[varName] = !!(value && value.length > 0); } catch (err) { logger.error( `[MCP Auth Value Flags] Error checking ${varName} for user ${user.id}:`, err, ); authValueFlags[varName] = false; } } } res.json({ success: true, serverName, authValueFlags, }); } catch (error) { logger.error( `[MCP Auth Value Flags] Failed to check auth value flags for ${req.params.serverName}`, error, ); res.status(500).json({ error: 'Failed to check auth value flags' }); } }); module.exports = router;