Skip to content

Commit ee0dc2f

Browse files
dustinhealyjhrozek
authored andcommitted
🏹 feat: Concurrent MCP Initialization Support (danny-avila#8677)
* ✨ feat: Enhance MCP Connection Status Management - Introduced new functions to retrieve and manage connection status for multiple MCP servers, including OAuth flow checks and server-specific status retrieval. - Refactored the MCP connection status endpoints to support both all servers and individual server queries. - Replaced the old server initialization hook with a new `useMCPServerManager` hook for improved state management and handling of multiple OAuth flows. - Updated the MCPPanel component to utilize the new context provider for better state handling and UI updates. - Fixed a number of UI bugs when initializing servers * 🗣️ i18n: Remove unused strings from translation.json * refactor: move helper functions out of the route module into mcp service file * ci: add tests for newly added functions in mcp service file * fix: memoize setMCPValues to avoid render loop
1 parent 500ad3b commit ee0dc2f

File tree

15 files changed

+1079
-529
lines changed

15 files changed

+1079
-529
lines changed

api/server/routes/mcp.js

Lines changed: 64 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const { MCPOAuthHandler } = require('@librechat/api');
44
const { CacheKeys, Constants } = require('librechat-data-provider');
55
const { findToken, updateToken, createToken, deleteTokens } = require('~/models');
66
const { setCachedTools, getCachedTools, loadCustomConfig } = require('~/server/services/Config');
7+
const { getMCPSetupData, getServerConnectionStatus } = require('~/server/services/MCP');
78
const { getUserPluginAuthValue } = require('~/server/services/PluginService');
89
const { getMCPManager, getFlowStateManager } = require('~/config');
910
const { requireJwtAuth } = require('~/server/middleware');
@@ -468,7 +469,7 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => {
468469

469470
/**
470471
* Get connection status for all MCP servers
471-
* This endpoint returns the actual connection status from MCPManager without disconnecting idle connections
472+
* This endpoint returns all app level and user-scoped connection statuses from MCPManager without disconnecting idle connections
472473
*/
473474
router.get('/connection/status', requireJwtAuth, async (req, res) => {
474475
try {
@@ -478,92 +479,83 @@ router.get('/connection/status', requireJwtAuth, async (req, res) => {
478479
return res.status(401).json({ error: 'User not authenticated' });
479480
}
480481

481-
const mcpManager = getMCPManager(user.id);
482+
const { mcpConfig, appConnections, userConnections, oauthServers } = await getMCPSetupData(
483+
user.id,
484+
);
482485
const connectionStatus = {};
483486

484-
const printConfig = false;
485-
const config = await loadCustomConfig(printConfig);
486-
const mcpConfig = config?.mcpServers;
487-
488-
const appConnections = mcpManager.getAllConnections() || new Map();
489-
const userConnections = mcpManager.getUserConnections(user.id) || new Map();
490-
const oauthServers = mcpManager.getOAuthServers() || new Set();
491-
492-
if (!mcpConfig) {
493-
return res.status(404).json({ error: 'MCP config not found' });
487+
for (const [serverName] of Object.entries(mcpConfig)) {
488+
connectionStatus[serverName] = await getServerConnectionStatus(
489+
user.id,
490+
serverName,
491+
appConnections,
492+
userConnections,
493+
oauthServers,
494+
);
494495
}
495496

496-
// Get flow manager to check for active/timed-out OAuth flows
497-
const flowsCache = getLogStores(CacheKeys.FLOWS);
498-
const flowManager = getFlowStateManager(flowsCache);
499-
500-
for (const [serverName] of Object.entries(mcpConfig)) {
501-
const getConnectionState = (serverName) =>
502-
appConnections.get(serverName)?.connectionState ??
503-
userConnections.get(serverName)?.connectionState ??
504-
'disconnected';
497+
res.json({
498+
success: true,
499+
connectionStatus,
500+
});
501+
} catch (error) {
502+
if (error.message === 'MCP config not found') {
503+
return res.status(404).json({ error: error.message });
504+
}
505+
logger.error('[MCP Connection Status] Failed to get connection status', error);
506+
res.status(500).json({ error: 'Failed to get connection status' });
507+
}
508+
});
505509

506-
const baseConnectionState = getConnectionState(serverName);
510+
/**
511+
* Get connection status for a single MCP server
512+
* This endpoint returns the connection status for a specific server for a given user
513+
*/
514+
router.get('/connection/status/:serverName', requireJwtAuth, async (req, res) => {
515+
try {
516+
const user = req.user;
517+
const { serverName } = req.params;
507518

508-
let hasActiveOAuthFlow = false;
509-
let hasFailedOAuthFlow = false;
519+
if (!user?.id) {
520+
return res.status(401).json({ error: 'User not authenticated' });
521+
}
510522

511-
if (baseConnectionState === 'disconnected' && oauthServers.has(serverName)) {
512-
try {
513-
// Check for user-specific OAuth flows
514-
const flowId = MCPOAuthHandler.generateFlowId(user.id, serverName);
515-
const flowState = await flowManager.getFlowState(flowId, 'mcp_oauth');
516-
if (flowState) {
517-
// Check if flow failed or timed out
518-
const flowAge = Date.now() - flowState.createdAt;
519-
const flowTTL = flowState.ttl || 180000; // Default 3 minutes
520-
521-
if (flowState.status === 'FAILED' || flowAge > flowTTL) {
522-
hasFailedOAuthFlow = true;
523-
logger.debug(`[MCP Connection Status] Found failed OAuth flow for ${serverName}`, {
524-
flowId,
525-
status: flowState.status,
526-
flowAge,
527-
flowTTL,
528-
timedOut: flowAge > flowTTL,
529-
});
530-
} else if (flowState.status === 'PENDING') {
531-
hasActiveOAuthFlow = true;
532-
logger.debug(`[MCP Connection Status] Found active OAuth flow for ${serverName}`, {
533-
flowId,
534-
flowAge,
535-
flowTTL,
536-
});
537-
}
538-
}
539-
} catch (error) {
540-
logger.error(
541-
`[MCP Connection Status] Error checking OAuth flows for ${serverName}:`,
542-
error,
543-
);
544-
}
545-
}
523+
if (!serverName) {
524+
return res.status(400).json({ error: 'Server name is required' });
525+
}
546526

547-
// Determine the final connection state
548-
let finalConnectionState = baseConnectionState;
549-
if (hasFailedOAuthFlow) {
550-
finalConnectionState = 'error'; // Report as error if OAuth failed
551-
} else if (hasActiveOAuthFlow && baseConnectionState === 'disconnected') {
552-
finalConnectionState = 'connecting'; // Still waiting for OAuth
553-
}
527+
const { mcpConfig, appConnections, userConnections, oauthServers } = await getMCPSetupData(
528+
user.id,
529+
);
554530

555-
connectionStatus[serverName] = {
556-
requiresOAuth: oauthServers.has(serverName),
557-
connectionState: finalConnectionState,
558-
};
531+
if (!mcpConfig[serverName]) {
532+
return res
533+
.status(404)
534+
.json({ error: `MCP server '${serverName}' not found in configuration` });
559535
}
560536

537+
const serverStatus = await getServerConnectionStatus(
538+
user.id,
539+
serverName,
540+
appConnections,
541+
userConnections,
542+
oauthServers,
543+
);
544+
561545
res.json({
562546
success: true,
563-
connectionStatus,
547+
serverName,
548+
connectionStatus: serverStatus.connectionState,
549+
requiresOAuth: serverStatus.requiresOAuth,
564550
});
565551
} catch (error) {
566-
logger.error('[MCP Connection Status] Failed to get connection status', error);
552+
if (error.message === 'MCP config not found') {
553+
return res.status(404).json({ error: error.message });
554+
}
555+
logger.error(
556+
`[MCP Per-Server Status] Failed to get connection status for ${req.params.serverName}`,
557+
error,
558+
);
567559
res.status(500).json({ error: 'Failed to get connection status' });
568560
}
569561
});

api/server/services/MCP.js

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const {
1212
} = require('@librechat/api');
1313
const { findToken, createToken, updateToken } = require('~/models');
1414
const { getMCPManager, getFlowStateManager } = require('~/config');
15-
const { getCachedTools } = require('./Config');
15+
const { getCachedTools, loadCustomConfig } = require('./Config');
1616
const { getLogStores } = require('~/cache');
1717

1818
/**
@@ -239,6 +239,123 @@ async function createMCPTool({ req, res, toolKey, provider: _provider }) {
239239
return toolInstance;
240240
}
241241

242+
/**
243+
* Get MCP setup data including config, connections, and OAuth servers
244+
* @param {string} userId - The user ID
245+
* @returns {Object} Object containing mcpConfig, appConnections, userConnections, and oauthServers
246+
*/
247+
async function getMCPSetupData(userId) {
248+
const printConfig = false;
249+
const config = await loadCustomConfig(printConfig);
250+
const mcpConfig = config?.mcpServers;
251+
252+
if (!mcpConfig) {
253+
throw new Error('MCP config not found');
254+
}
255+
256+
const mcpManager = getMCPManager(userId);
257+
const appConnections = mcpManager.getAllConnections() || new Map();
258+
const userConnections = mcpManager.getUserConnections(userId) || new Map();
259+
const oauthServers = mcpManager.getOAuthServers() || new Set();
260+
261+
return {
262+
mcpConfig,
263+
appConnections,
264+
userConnections,
265+
oauthServers,
266+
};
267+
}
268+
269+
/**
270+
* Check OAuth flow status for a user and server
271+
* @param {string} userId - The user ID
272+
* @param {string} serverName - The server name
273+
* @returns {Object} Object containing hasActiveFlow and hasFailedFlow flags
274+
*/
275+
async function checkOAuthFlowStatus(userId, serverName) {
276+
const flowsCache = getLogStores(CacheKeys.FLOWS);
277+
const flowManager = getFlowStateManager(flowsCache);
278+
const flowId = MCPOAuthHandler.generateFlowId(userId, serverName);
279+
280+
try {
281+
const flowState = await flowManager.getFlowState(flowId, 'mcp_oauth');
282+
if (!flowState) {
283+
return { hasActiveFlow: false, hasFailedFlow: false };
284+
}
285+
286+
const flowAge = Date.now() - flowState.createdAt;
287+
const flowTTL = flowState.ttl || 180000; // Default 3 minutes
288+
289+
if (flowState.status === 'FAILED' || flowAge > flowTTL) {
290+
logger.debug(`[MCP Connection Status] Found failed OAuth flow for ${serverName}`, {
291+
flowId,
292+
status: flowState.status,
293+
flowAge,
294+
flowTTL,
295+
timedOut: flowAge > flowTTL,
296+
});
297+
return { hasActiveFlow: false, hasFailedFlow: true };
298+
}
299+
300+
if (flowState.status === 'PENDING') {
301+
logger.debug(`[MCP Connection Status] Found active OAuth flow for ${serverName}`, {
302+
flowId,
303+
flowAge,
304+
flowTTL,
305+
});
306+
return { hasActiveFlow: true, hasFailedFlow: false };
307+
}
308+
309+
return { hasActiveFlow: false, hasFailedFlow: false };
310+
} catch (error) {
311+
logger.error(`[MCP Connection Status] Error checking OAuth flows for ${serverName}:`, error);
312+
return { hasActiveFlow: false, hasFailedFlow: false };
313+
}
314+
}
315+
316+
/**
317+
* Get connection status for a specific MCP server
318+
* @param {string} userId - The user ID
319+
* @param {string} serverName - The server name
320+
* @param {Map} appConnections - App-level connections
321+
* @param {Map} userConnections - User-level connections
322+
* @param {Set} oauthServers - Set of OAuth servers
323+
* @returns {Object} Object containing requiresOAuth and connectionState
324+
*/
325+
async function getServerConnectionStatus(
326+
userId,
327+
serverName,
328+
appConnections,
329+
userConnections,
330+
oauthServers,
331+
) {
332+
const getConnectionState = () =>
333+
appConnections.get(serverName)?.connectionState ??
334+
userConnections.get(serverName)?.connectionState ??
335+
'disconnected';
336+
337+
const baseConnectionState = getConnectionState();
338+
let finalConnectionState = baseConnectionState;
339+
340+
if (baseConnectionState === 'disconnected' && oauthServers.has(serverName)) {
341+
const { hasActiveFlow, hasFailedFlow } = await checkOAuthFlowStatus(userId, serverName);
342+
343+
if (hasFailedFlow) {
344+
finalConnectionState = 'error';
345+
} else if (hasActiveFlow) {
346+
finalConnectionState = 'connecting';
347+
}
348+
}
349+
350+
return {
351+
requiresOAuth: oauthServers.has(serverName),
352+
connectionState: finalConnectionState,
353+
};
354+
}
355+
242356
module.exports = {
243357
createMCPTool,
358+
getMCPSetupData,
359+
checkOAuthFlowStatus,
360+
getServerConnectionStatus,
244361
};

0 commit comments

Comments
 (0)