mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-18 16:35:31 +01:00
⏳ refactor: MCP OAuth Polling with Gradual Backoff and Timeout Handling (#9752)
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Has been cancelled
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Has been cancelled
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
* refactor: Implement gradual backoff polling for oauth connection status with timeout handling * refactor: Enhance OAuth polling with gradual backoff and timeout handling; update reconnection tracking * refactor: reconnection timeout behavior in OAuthReconnectionManager and OAuthReconnectionTracker - Implement tests to verify reconnection timeout handling, including tracking of reconnection states and cleanup of timed-out entries. - Enhance existing methods in OAuthReconnectionManager and OAuthReconnectionTracker to support timeout checks and cleanup logic. - Ensure proper handling of multiple servers with different timeout periods and edge cases for active states. * chore: remove comment * refactor: Enforce strict 3-minute OAuth timeout with updated polling intervals and improved timeout handling * refactor: Remove unused polling logic and prevent duplicate polling for servers in MCP server manager * refactor: Update localization key for no memories message in MemoryViewer * refactor: Improve MCP tool initialization by handling server failures - Introduced a mechanism to track failed MCP servers, preventing retries for unavailable servers. - Added logging for failed tool creation attempts to enhance debugging and monitoring. * refactor: Update reconnection timeout to enforce a strict 3-minute limit * ci: Update reconnection timeout tests to reflect a strict 3-minute limit * ci: Update reconnection timeout tests to enforce a strict 3-minute limit * chore: Remove unused MCP connection timeout message
This commit is contained in:
parent
f0599ad36c
commit
96870e0da0
9 changed files with 560 additions and 41 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import { ChevronLeft, Trash2 } from 'lucide-react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Button, useToastContext } from '@librechat/client';
|
||||
|
|
@ -12,8 +12,6 @@ import { useLocalize, useMCPConnectionStatus } from '~/hooks';
|
|||
import { useGetStartupConfig } from '~/data-provider';
|
||||
import MCPPanelSkeleton from './MCPPanelSkeleton';
|
||||
|
||||
const POLL_FOR_CONNECTION_STATUS_INTERVAL = 2_000; // ms
|
||||
|
||||
function MCPPanelContent() {
|
||||
const localize = useLocalize();
|
||||
const queryClient = useQueryClient();
|
||||
|
|
@ -28,29 +26,6 @@ function MCPPanelContent() {
|
|||
null,
|
||||
);
|
||||
|
||||
// Check if any connections are in 'connecting' state
|
||||
const hasConnectingServers = useMemo(() => {
|
||||
if (!connectionStatus) {
|
||||
return false;
|
||||
}
|
||||
return Object.values(connectionStatus).some(
|
||||
(status) => status?.connectionState === 'connecting',
|
||||
);
|
||||
}, [connectionStatus]);
|
||||
|
||||
// Set up polling when servers are connecting
|
||||
useEffect(() => {
|
||||
if (!hasConnectingServers) {
|
||||
return;
|
||||
}
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]);
|
||||
}, POLL_FOR_CONNECTION_STATUS_INTERVAL);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, [hasConnectingServers, queryClient]);
|
||||
|
||||
const updateUserPluginsMutation = useUpdateUserPluginsMutation({
|
||||
onSuccess: async () => {
|
||||
showToast({ message: localize('com_nav_mcp_vars_updated'), status: 'success' });
|
||||
|
|
|
|||
|
|
@ -362,7 +362,7 @@ export default function MemoryViewer() {
|
|||
colSpan={hasUpdateAccess ? 2 : 1}
|
||||
className="h-24 text-center text-sm text-text-secondary"
|
||||
>
|
||||
{localize('com_ui_no_data')}
|
||||
{localize('com_ui_no_memories')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
|||
(serverName: string) => {
|
||||
const state = serverStates[serverName];
|
||||
if (state?.pollInterval) {
|
||||
clearInterval(state.pollInterval);
|
||||
clearTimeout(state.pollInterval);
|
||||
}
|
||||
updateServerState(serverName, {
|
||||
isInitializing: false,
|
||||
|
|
@ -144,8 +144,53 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
|||
|
||||
const startServerPolling = useCallback(
|
||||
(serverName: string) => {
|
||||
const pollInterval = setInterval(async () => {
|
||||
// Prevent duplicate polling for the same server
|
||||
const existingState = serverStates[serverName];
|
||||
if (existingState?.pollInterval) {
|
||||
console.debug(`[MCP Manager] Polling already active for ${serverName}, skipping duplicate`);
|
||||
return;
|
||||
}
|
||||
|
||||
let pollAttempts = 0;
|
||||
let timeoutId: NodeJS.Timeout | null = null;
|
||||
|
||||
/** OAuth typically completes in 5 seconds to 3 minutes
|
||||
* We enforce a strict 3-minute timeout with gradual backoff
|
||||
*/
|
||||
const getPollInterval = (attempt: number): number => {
|
||||
if (attempt < 12) return 5000; // First minute: every 5s (12 polls)
|
||||
if (attempt < 22) return 6000; // Second minute: every 6s (10 polls)
|
||||
return 7500; // Final minute: every 7.5s (8 polls)
|
||||
};
|
||||
|
||||
const maxAttempts = 30; // Exactly 3 minutes (180 seconds) total
|
||||
const OAUTH_TIMEOUT_MS = 180000; // 3 minutes in milliseconds
|
||||
|
||||
const pollOnce = async () => {
|
||||
try {
|
||||
pollAttempts++;
|
||||
const state = serverStates[serverName];
|
||||
|
||||
/** Stop polling after 3 minutes or max attempts */
|
||||
const elapsedTime = state?.oauthStartTime
|
||||
? Date.now() - state.oauthStartTime
|
||||
: pollAttempts * 5000; // Rough estimate if no start time
|
||||
|
||||
if (pollAttempts > maxAttempts || elapsedTime > OAUTH_TIMEOUT_MS) {
|
||||
console.warn(
|
||||
`[MCP Manager] OAuth timeout for ${serverName} after ${(elapsedTime / 1000).toFixed(0)}s (attempt ${pollAttempts})`,
|
||||
);
|
||||
showToast({
|
||||
message: localize('com_ui_mcp_oauth_timeout', { 0: serverName }),
|
||||
status: 'error',
|
||||
});
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
cleanupServerState(serverName);
|
||||
return;
|
||||
}
|
||||
|
||||
await queryClient.refetchQueries([QueryKeys.mcpConnectionStatus]);
|
||||
|
||||
const freshConnectionData = queryClient.getQueryData([
|
||||
|
|
@ -153,11 +198,12 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
|||
]) as any;
|
||||
const freshConnectionStatus = freshConnectionData?.connectionStatus || {};
|
||||
|
||||
const state = serverStates[serverName];
|
||||
const serverStatus = freshConnectionStatus[serverName];
|
||||
|
||||
if (serverStatus?.connectionState === 'connected') {
|
||||
clearInterval(pollInterval);
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
showToast({
|
||||
message: localize('com_ui_mcp_authenticated_success', { 0: serverName }),
|
||||
|
|
@ -179,12 +225,15 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
|||
return;
|
||||
}
|
||||
|
||||
if (state?.oauthStartTime && Date.now() - state.oauthStartTime > 180000) {
|
||||
// Check for OAuth timeout (should align with maxAttempts)
|
||||
if (state?.oauthStartTime && Date.now() - state.oauthStartTime > OAUTH_TIMEOUT_MS) {
|
||||
showToast({
|
||||
message: localize('com_ui_mcp_oauth_timeout', { 0: serverName }),
|
||||
status: 'error',
|
||||
});
|
||||
clearInterval(pollInterval);
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
cleanupServerState(serverName);
|
||||
return;
|
||||
}
|
||||
|
|
@ -194,19 +243,38 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
|||
message: localize('com_ui_mcp_init_failed'),
|
||||
status: 'error',
|
||||
});
|
||||
clearInterval(pollInterval);
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
cleanupServerState(serverName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Schedule next poll with smart intervals based on OAuth timing
|
||||
const nextInterval = getPollInterval(pollAttempts);
|
||||
|
||||
// Log progress periodically
|
||||
if (pollAttempts % 5 === 0 || pollAttempts <= 2) {
|
||||
console.debug(
|
||||
`[MCP Manager] Polling ${serverName} attempt ${pollAttempts}/${maxAttempts}, next in ${nextInterval / 1000}s`,
|
||||
);
|
||||
}
|
||||
|
||||
timeoutId = setTimeout(pollOnce, nextInterval);
|
||||
updateServerState(serverName, { pollInterval: timeoutId });
|
||||
} catch (error) {
|
||||
console.error(`[MCP Manager] Error polling server ${serverName}:`, error);
|
||||
clearInterval(pollInterval);
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
cleanupServerState(serverName);
|
||||
return;
|
||||
}
|
||||
}, 3500);
|
||||
};
|
||||
|
||||
updateServerState(serverName, { pollInterval });
|
||||
// Start the first poll
|
||||
timeoutId = setTimeout(pollOnce, getPollInterval(0));
|
||||
updateServerState(serverName, { pollInterval: timeoutId });
|
||||
},
|
||||
[
|
||||
queryClient,
|
||||
|
|
|
|||
|
|
@ -1027,8 +1027,8 @@
|
|||
"com_ui_no_categories": "No categories available",
|
||||
"com_ui_no_category": "No category",
|
||||
"com_ui_no_changes": "No changes were made",
|
||||
"com_ui_no_data": "something needs to go here. was empty",
|
||||
"com_ui_no_individual_access": "No individual users or groups have access to this agent",
|
||||
"com_ui_no_memories": "No memories. Create them manually or prompt the AI to remember something",
|
||||
"com_ui_no_personalization_available": "No personalization options are currently available",
|
||||
"com_ui_no_read_access": "You don't have permission to view memories",
|
||||
"com_ui_no_results_found": "No results found",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue