🧬 refactor: Wire Database Methods into MCP Package via Registry Pattern (#10715)

* Refactor: MCPServersRegistry Singleton Pattern with Dependency Injection for DB methods consumption

* refactor: error handling in MCP initialization and improve logging for MCPServersRegistry instance creation.

- Added checks for mongoose instance in ServerConfigsDB constructor and refined error messages for clarity.
- Reorder and use type imports

---------

Co-authored-by: Atef Bellaaj <slalom.bellaaj@external.daimlertruck.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Atef Bellaaj 2025-12-01 00:57:46 +01:00 committed by Danny Avila
parent da473bf43a
commit ad6ba4b6d1
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
24 changed files with 328 additions and 150 deletions

View file

@ -1,7 +1,6 @@
import { TokenMethods } from '@librechat/data-schemas';
import { FlowStateManager, MCPConnection, MCPOAuthTokens, MCPOptions } from '../..';
import { MCPManager } from '../MCPManager';
import { mcpServersRegistry } from '../../mcp/registry/MCPServersRegistry';
import { OAuthReconnectionManager } from './OAuthReconnectionManager';
import { OAuthReconnectionTracker } from './OAuthReconnectionTracker';
@ -14,11 +13,15 @@ jest.mock('@librechat/data-schemas', () => ({
},
}));
const mockRegistryInstance = {
getServerConfig: jest.fn(),
getOAuthServers: jest.fn(),
};
jest.mock('../MCPManager');
jest.mock('../../mcp/registry/MCPServersRegistry', () => ({
mcpServersRegistry: {
getServerConfig: jest.fn(),
getOAuthServers: jest.fn(),
MCPServersRegistry: {
getInstance: () => mockRegistryInstance,
},
}));
@ -61,7 +64,7 @@ describe('OAuthReconnectionManager', () => {
} as unknown as jest.Mocked<MCPManager>;
(MCPManager.getInstance as jest.Mock).mockReturnValue(mockMCPManager);
(mcpServersRegistry.getServerConfig as jest.Mock).mockResolvedValue({});
(mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue({});
});
afterEach(() => {
@ -159,7 +162,7 @@ describe('OAuthReconnectionManager', () => {
it('should reconnect eligible servers', async () => {
const userId = 'user-123';
const oauthServers = new Set(['server1', 'server2', 'server3']);
(mcpServersRegistry.getOAuthServers as jest.Mock).mockResolvedValue(oauthServers);
(mockRegistryInstance.getOAuthServers as jest.Mock).mockResolvedValue(oauthServers);
// server1: has failed reconnection
reconnectionTracker.setFailed(userId, 'server1');
@ -193,7 +196,7 @@ describe('OAuthReconnectionManager', () => {
mockMCPManager.getUserConnection.mockResolvedValue(
mockNewConnection as unknown as MCPConnection,
);
(mcpServersRegistry.getServerConfig as jest.Mock).mockResolvedValue({
(mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue({
initTimeout: 5000,
} as unknown as MCPOptions);
@ -224,7 +227,7 @@ describe('OAuthReconnectionManager', () => {
it('should handle failed reconnection attempts', async () => {
const userId = 'user-123';
const oauthServers = new Set(['server1']);
(mcpServersRegistry.getOAuthServers as jest.Mock).mockResolvedValue(oauthServers);
(mockRegistryInstance.getOAuthServers as jest.Mock).mockResolvedValue(oauthServers);
// server1: has valid token
tokenMethods.findToken.mockResolvedValue({
@ -235,7 +238,7 @@ describe('OAuthReconnectionManager', () => {
// Mock failed connection
mockMCPManager.getUserConnection.mockRejectedValue(new Error('Connection failed'));
(mcpServersRegistry.getServerConfig as jest.Mock).mockResolvedValue(
(mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue(
{} as unknown as MCPOptions,
);
@ -253,7 +256,7 @@ describe('OAuthReconnectionManager', () => {
it('should not reconnect servers with expired tokens', async () => {
const userId = 'user-123';
const oauthServers = new Set(['server1']);
(mcpServersRegistry.getOAuthServers as jest.Mock).mockResolvedValue(oauthServers);
(mockRegistryInstance.getOAuthServers as jest.Mock).mockResolvedValue(oauthServers);
// server1: has expired token
tokenMethods.findToken.mockResolvedValue({
@ -272,7 +275,7 @@ describe('OAuthReconnectionManager', () => {
it('should handle connection that returns but is not connected', async () => {
const userId = 'user-123';
const oauthServers = new Set(['server1']);
(mcpServersRegistry.getOAuthServers as jest.Mock).mockResolvedValue(oauthServers);
(mockRegistryInstance.getOAuthServers as jest.Mock).mockResolvedValue(oauthServers);
tokenMethods.findToken.mockResolvedValue({
userId,
@ -288,7 +291,7 @@ describe('OAuthReconnectionManager', () => {
mockMCPManager.getUserConnection.mockResolvedValue(
mockConnection as unknown as MCPConnection,
);
(mcpServersRegistry.getServerConfig as jest.Mock).mockResolvedValue(
(mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue(
{} as unknown as MCPOptions,
);
@ -372,7 +375,7 @@ describe('OAuthReconnectionManager', () => {
it('should not attempt to reconnect servers that have timed out during reconnection', async () => {
const userId = 'user-123';
const oauthServers = new Set(['server1', 'server2']);
(mcpServersRegistry.getOAuthServers as jest.Mock).mockResolvedValue(oauthServers);
(mockRegistryInstance.getOAuthServers as jest.Mock).mockResolvedValue(oauthServers);
const now = Date.now();
jest.setSystemTime(now);
@ -427,7 +430,7 @@ describe('OAuthReconnectionManager', () => {
const userId = 'user-123';
const serverName = 'server1';
const oauthServers = new Set([serverName]);
(mcpServersRegistry.getOAuthServers as jest.Mock).mockResolvedValue(oauthServers);
(mockRegistryInstance.getOAuthServers as jest.Mock).mockResolvedValue(oauthServers);
const now = Date.now();
jest.setSystemTime(now);
@ -441,7 +444,7 @@ describe('OAuthReconnectionManager', () => {
// First reconnect attempt - will fail
mockMCPManager.getUserConnection.mockRejectedValueOnce(new Error('Connection failed'));
(mcpServersRegistry.getServerConfig as jest.Mock).mockResolvedValue(
(mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue(
{} as unknown as MCPOptions,
);

View file

@ -4,7 +4,7 @@ import type { MCPOAuthTokens } from './types';
import { OAuthReconnectionTracker } from './OAuthReconnectionTracker';
import { FlowStateManager } from '~/flow/manager';
import { MCPManager } from '~/mcp/MCPManager';
import { mcpServersRegistry } from '~/mcp/registry/MCPServersRegistry';
import { MCPServersRegistry } from '~/mcp/registry/MCPServersRegistry';
const DEFAULT_CONNECTION_TIMEOUT_MS = 10_000; // ms
@ -72,7 +72,7 @@ export class OAuthReconnectionManager {
// 1. derive the servers to reconnect
const serversToReconnect = [];
for (const serverName of await mcpServersRegistry.getOAuthServers()) {
for (const serverName of await MCPServersRegistry.getInstance().getOAuthServers()) {
const canReconnect = await this.canReconnect(userId, serverName);
if (canReconnect) {
serversToReconnect.push(serverName);
@ -104,7 +104,7 @@ export class OAuthReconnectionManager {
logger.info(`${logPrefix} Attempting reconnection`);
const config = await mcpServersRegistry.getServerConfig(serverName, userId);
const config = await MCPServersRegistry.getInstance().getServerConfig(serverName, userId);
const cleanupOnFailedReconnect = () => {
this.reconnectionsTracker.setFailed(userId, serverName);