| const { Router } = require('express'); |
| const { logger } = require('@librechat/data-schemas'); |
| const { CacheKeys, Constants } = require('librechat-data-provider'); |
| const { |
| createSafeUser, |
| MCPOAuthHandler, |
| MCPTokenStorage, |
| getUserMCPAuthMap, |
| mcpServersRegistry, |
| } = 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(); |
|
|
| |
| |
| |
| |
| router.get('/tools', requireJwtAuth, async (req, res) => { |
| return getMCPTools(req, res); |
| }); |
|
|
| |
| |
| |
| |
| router.get('/:serverName/oauth/initiate', requireJwtAuth, async (req, res) => { |
| try { |
| const { serverName } = req.params; |
| const { userId, flowId } = req.query; |
| const user = req.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); |
|
|
| |
| 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 oauthHeaders = await getOAuthHeaders(serverName, userId); |
| const { authorizationUrl, flowId: oauthFlowId } = await MCPOAuthHandler.initiateOAuthFlow( |
| serverName, |
| serverUrl, |
| userId, |
| oauthHeaders, |
| oauthConfig, |
| ); |
|
|
| logger.debug('[MCP OAuth] OAuth flow initiated', { oauthFlowId, authorizationUrl }); |
|
|
| |
| res.redirect(authorizationUrl); |
| } catch (error) { |
| logger.error('[MCP OAuth] Failed to initiate OAuth', error); |
| res.status(500).json({ error: 'Failed to initiate OAuth' }); |
| } |
| }); |
|
|
| |
| |
| |
| |
| 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, |
| }); |
|
|
| |
| const currentFlowState = await flowManager.getFlowState(flowId, 'mcp_oauth'); |
| if (currentFlowState?.status === 'COMPLETED') { |
| logger.warn('[MCP OAuth] Flow already completed, preventing duplicate token exchange', { |
| flowId, |
| serverName, |
| }); |
| return res.redirect(`/oauth/success?serverName=${encodeURIComponent(serverName)}`); |
| } |
|
|
| logger.debug('[MCP OAuth] Completing OAuth flow'); |
| const oauthHeaders = await getOAuthHeaders(serverName, flowState.userId); |
| const tokens = await MCPOAuthHandler.completeOAuthFlow(flowId, code, flowManager, oauthHeaders); |
| logger.info('[MCP OAuth] OAuth flow completed, tokens received in callback route'); |
|
|
| |
| if (flowState?.userId && tokens) { |
| try { |
| await MCPTokenStorage.storeTokens({ |
| userId: flowState.userId, |
| serverName, |
| tokens, |
| createToken, |
| updateToken, |
| findToken, |
| clientInfo: flowState.clientInfo, |
| metadata: flowState.metadata, |
| }); |
| logger.debug('[MCP OAuth] Stored OAuth tokens prior to reconnection', { |
| serverName, |
| userId: flowState.userId, |
| }); |
| } catch (error) { |
| logger.error('[MCP OAuth] Failed to store OAuth tokens after callback', error); |
| throw error; |
| } |
|
|
| |
| |
| |
| |
| if (typeof flowManager?.deleteFlow === 'function') { |
| try { |
| await flowManager.deleteFlow(flowId, 'mcp_get_tokens'); |
| } catch (error) { |
| logger.warn('[MCP OAuth] Failed to clear cached token flow state', error); |
| } |
| } |
| } |
|
|
| 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}`, |
| ); |
|
|
| |
| const oauthReconnectionManager = getOAuthReconnectionManager(); |
| oauthReconnectionManager.clearReconnection(flowState.userId, serverName); |
|
|
| const tools = await userConnection.fetchTools(); |
| await updateMCPServerTools({ |
| userId: flowState.userId, |
| 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, |
| ); |
| } |
|
|
| |
| const toolFlowId = flowState.metadata?.toolFlowId; |
| if (toolFlowId) { |
| logger.debug('[MCP OAuth] Completing tool flow', { toolFlowId }); |
| await flowManager.completeFlow(toolFlowId, 'mcp_oauth', tokens); |
| } |
|
|
| |
| 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'); |
| } |
| }); |
|
|
| |
| |
| |
| |
| 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' }); |
| } |
| }); |
|
|
| |
| |
| |
| |
| 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' }); |
| } |
| }); |
|
|
| |
| |
| |
| |
| 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' }); |
| } |
| }); |
|
|
| |
| |
| |
| |
| router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => { |
| try { |
| const { serverName } = req.params; |
| const user = createSafeUser(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 = await mcpServersRegistry.getServerConfig(serverName, user.id); |
| 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}`, |
| ); |
|
|
| |
| let userMCPAuthMap; |
| if (serverConfig.customUserVars && typeof serverConfig.customUserVars === 'object') { |
| userMCPAuthMap = await getUserMCPAuthMap({ |
| userId: user.id, |
| servers: [serverName], |
| findPluginAuthsByKeys, |
| }); |
| } |
|
|
| const result = await reinitMCPServer({ |
| user, |
| 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' }); |
| } |
| }); |
|
|
| |
| |
| |
| |
| 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)) { |
| try { |
| connectionStatus[serverName] = await getServerConnectionStatus( |
| user.id, |
| serverName, |
| appConnections, |
| userConnections, |
| oauthServers, |
| ); |
| } catch (error) { |
| const message = `Failed to get status for server "${serverName}"`; |
| logger.error(`[MCP Connection Status] ${message},`, error); |
| connectionStatus[serverName] = { |
| connectionState: 'error', |
| requiresOAuth: oauthServers.has(serverName), |
| error: message, |
| }; |
| } |
| } |
|
|
| 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' }); |
| } |
| }); |
|
|
| |
| |
| |
| |
| 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' }); |
| } |
| }); |
|
|
| |
| |
| |
| |
| 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 serverConfig = await mcpServersRegistry.getServerConfig(serverName, user.id); |
| 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' }); |
| } |
| }); |
|
|
| async function getOAuthHeaders(serverName, userId) { |
| const serverConfig = await mcpServersRegistry.getServerConfig(serverName, userId); |
| return serverConfig?.oauth_headers ?? {}; |
| } |
|
|
| module.exports = router; |
|
|