mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
🧬 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:
parent
da473bf43a
commit
ad6ba4b6d1
24 changed files with 328 additions and 150 deletions
|
|
@ -10,8 +10,8 @@ const {
|
|||
createSafeUser,
|
||||
mcpToolPattern,
|
||||
loadWebSearchAuth,
|
||||
mcpServersRegistry,
|
||||
} = require('@librechat/api');
|
||||
const { getMCPServersRegistry } = require('~/config');
|
||||
const {
|
||||
Tools,
|
||||
Constants,
|
||||
|
|
@ -348,7 +348,10 @@ Anchor pattern: \\ue202turn{N}{type}{index} where N=turn number, type=search|new
|
|||
/** Placeholder used for UI purposes */
|
||||
continue;
|
||||
}
|
||||
if (serverName && (await mcpServersRegistry.getServerConfig(serverName, user)) == undefined) {
|
||||
if (
|
||||
serverName &&
|
||||
(await getMCPServersRegistry().getServerConfig(serverName, user)) == undefined
|
||||
) {
|
||||
logger.warn(
|
||||
`MCP server "${serverName}" for "${toolName}" tool is not configured${agent?.id != null && agent.id ? ` but attached to "${agent.id}"` : ''}`,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
const { EventSource } = require('eventsource');
|
||||
const { Time } = require('librechat-data-provider');
|
||||
const { MCPManager, FlowStateManager, OAuthReconnectionManager } = require('@librechat/api');
|
||||
const {
|
||||
MCPManager,
|
||||
FlowStateManager,
|
||||
MCPServersRegistry,
|
||||
OAuthReconnectionManager,
|
||||
} = require('@librechat/api');
|
||||
const logger = require('./winston');
|
||||
|
||||
global.EventSource = EventSource;
|
||||
|
|
@ -23,6 +28,8 @@ function getFlowStateManager(flowsCache) {
|
|||
|
||||
module.exports = {
|
||||
logger,
|
||||
createMCPServersRegistry: MCPServersRegistry.createInstance,
|
||||
getMCPServersRegistry: MCPServersRegistry.getInstance,
|
||||
createMCPManager: MCPManager.createInstance,
|
||||
getMCPManager: MCPManager.getInstance,
|
||||
getFlowStateManager,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ const { Tools, CacheKeys, Constants, FileSources } = require('librechat-data-pro
|
|||
const {
|
||||
MCPOAuthHandler,
|
||||
MCPTokenStorage,
|
||||
mcpServersRegistry,
|
||||
normalizeHttpError,
|
||||
extractWebSearchEnvVars,
|
||||
} = require('@librechat/api');
|
||||
|
|
@ -34,9 +33,9 @@ const {
|
|||
const { updateUserPluginAuth, deleteUserPluginAuth } = require('~/server/services/PluginService');
|
||||
const { updateUserPluginsService, deleteUserKey } = require('~/server/services/UserService');
|
||||
const { verifyEmail, resendVerificationEmail } = require('~/server/services/AuthService');
|
||||
const { getMCPManager, getFlowStateManager, getMCPServersRegistry } = require('~/config');
|
||||
const { needsRefresh, getNewS3URL } = require('~/server/services/Files/S3/crud');
|
||||
const { processDeleteRequest } = require('~/server/services/Files/process');
|
||||
const { getMCPManager, getFlowStateManager } = require('~/config');
|
||||
const { getAppConfig } = require('~/server/services/Config');
|
||||
const { deleteToolCalls } = require('~/models/ToolCall');
|
||||
const { deleteUserPrompts } = require('~/models/Prompt');
|
||||
|
|
@ -321,9 +320,9 @@ const maybeUninstallOAuthMCP = async (userId, pluginKey, appConfig) => {
|
|||
|
||||
const serverName = pluginKey.replace(Constants.mcp_prefix, '');
|
||||
const serverConfig =
|
||||
(await mcpServersRegistry.getServerConfig(serverName, userId)) ??
|
||||
(await getMCPServersRegistry().getServerConfig(serverName, userId)) ??
|
||||
appConfig?.mcpServers?.[serverName];
|
||||
const oauthServers = await mcpServersRegistry.getOAuthServers();
|
||||
const oauthServers = await getMCPServersRegistry().getOAuthServers();
|
||||
if (!oauthServers.has(serverName)) {
|
||||
// this server does not use OAuth, so nothing to do here as well
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@
|
|||
const { logger } = require('@librechat/data-schemas');
|
||||
const { Constants } = require('librechat-data-provider');
|
||||
const { cacheMCPServerTools, getMCPServerTools } = require('~/server/services/Config');
|
||||
const { getMCPManager } = require('~/config');
|
||||
const { mcpServersRegistry } = require('@librechat/api');
|
||||
const { getMCPManager, getMCPServersRegistry } = require('~/config');
|
||||
|
||||
/**
|
||||
* Get all MCP tools available to the user
|
||||
|
|
@ -19,7 +18,7 @@ const getMCPTools = async (req, res) => {
|
|||
return res.status(401).json({ message: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const mcpConfig = await mcpServersRegistry.getAllServerConfigs(userId);
|
||||
const mcpConfig = await getMCPServersRegistry().getAllServerConfigs(userId);
|
||||
const configuredServers = mcpConfig ? Object.keys(mcpConfig) : [];
|
||||
|
||||
if (!mcpConfig || Object.keys(mcpConfig).length == 0) {
|
||||
|
|
@ -69,7 +68,7 @@ const getMCPTools = async (req, res) => {
|
|||
|
||||
// Get server config once
|
||||
const serverConfig = mcpConfig[serverName];
|
||||
const rawServerConfig = await mcpServersRegistry.getServerConfig(serverName, userId);
|
||||
const rawServerConfig = await getMCPServersRegistry().getServerConfig(serverName, userId);
|
||||
|
||||
// Initialize server object with all server-level data
|
||||
const server = {
|
||||
|
|
@ -137,7 +136,7 @@ const getMCPServersList = async (req, res) => {
|
|||
// TODO - Ensure DB servers loaded into registry (configs only)
|
||||
|
||||
// 2. Get all server configs from registry (YAML + DB)
|
||||
const serverConfigs = await mcpServersRegistry.getAllServerConfigs(userId);
|
||||
const serverConfigs = await getMCPServersRegistry().getAllServerConfigs(userId);
|
||||
|
||||
return res.json(serverConfigs);
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,12 @@ const request = require('supertest');
|
|||
const mongoose = require('mongoose');
|
||||
const { MongoMemoryServer } = require('mongodb-memory-server');
|
||||
|
||||
const mockRegistryInstance = {
|
||||
getServerConfig: jest.fn(),
|
||||
getOAuthServers: jest.fn(),
|
||||
getAllServerConfigs: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock('@librechat/api', () => ({
|
||||
...jest.requireActual('@librechat/api'),
|
||||
MCPOAuthHandler: {
|
||||
|
|
@ -13,12 +19,13 @@ jest.mock('@librechat/api', () => ({
|
|||
},
|
||||
MCPTokenStorage: {
|
||||
storeTokens: jest.fn(),
|
||||
getClientInfoAndMetadata: jest.fn(),
|
||||
getTokens: jest.fn(),
|
||||
deleteUserTokens: jest.fn(),
|
||||
},
|
||||
getUserMCPAuthMap: jest.fn(),
|
||||
mcpServersRegistry: {
|
||||
getServerConfig: jest.fn(),
|
||||
getOAuthServers: jest.fn(),
|
||||
getAllServerConfigs: jest.fn(),
|
||||
MCPServersRegistry: {
|
||||
getInstance: () => mockRegistryInstance,
|
||||
},
|
||||
}));
|
||||
|
||||
|
|
@ -39,6 +46,9 @@ jest.mock('@librechat/data-schemas', () => ({
|
|||
findById: jest.fn(),
|
||||
},
|
||||
})),
|
||||
createMethods: jest.fn(() => ({
|
||||
findUser: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('~/models', () => ({
|
||||
|
|
@ -72,6 +82,8 @@ jest.mock('~/server/services/PluginService', () => ({
|
|||
jest.mock('~/config', () => ({
|
||||
getMCPManager: jest.fn(),
|
||||
getFlowStateManager: jest.fn(),
|
||||
getOAuthReconnectionManager: jest.fn(),
|
||||
getMCPServersRegistry: jest.fn(() => mockRegistryInstance),
|
||||
}));
|
||||
|
||||
jest.mock('~/cache', () => ({
|
||||
|
|
@ -120,7 +132,7 @@ describe('MCP Routes', () => {
|
|||
});
|
||||
|
||||
describe('GET /:serverName/oauth/initiate', () => {
|
||||
const { MCPOAuthHandler, mcpServersRegistry } = require('@librechat/api');
|
||||
const { MCPOAuthHandler } = require('@librechat/api');
|
||||
const { getLogStores } = require('~/cache');
|
||||
|
||||
it('should initiate OAuth flow successfully', async () => {
|
||||
|
|
@ -135,7 +147,7 @@ describe('MCP Routes', () => {
|
|||
|
||||
getLogStores.mockReturnValue({});
|
||||
require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager);
|
||||
mcpServersRegistry.getServerConfig.mockResolvedValue({});
|
||||
mockRegistryInstance.getServerConfig.mockResolvedValue({});
|
||||
|
||||
MCPOAuthHandler.initiateOAuthFlow.mockResolvedValue({
|
||||
authorizationUrl: 'https://oauth.example.com/auth',
|
||||
|
|
@ -289,7 +301,7 @@ describe('MCP Routes', () => {
|
|||
});
|
||||
|
||||
it('should handle OAuth callback successfully', async () => {
|
||||
const { mcpServersRegistry } = require('@librechat/api');
|
||||
// mockRegistryInstance is defined at the top of the file
|
||||
const mockFlowManager = {
|
||||
getFlowState: jest.fn().mockResolvedValue({ status: 'PENDING' }),
|
||||
completeFlow: jest.fn().mockResolvedValue(),
|
||||
|
|
@ -310,7 +322,7 @@ describe('MCP Routes', () => {
|
|||
MCPOAuthHandler.getFlowState.mockResolvedValue(mockFlowState);
|
||||
MCPOAuthHandler.completeOAuthFlow.mockResolvedValue(mockTokens);
|
||||
MCPTokenStorage.storeTokens.mockResolvedValue();
|
||||
mcpServersRegistry.getServerConfig.mockResolvedValue({});
|
||||
mockRegistryInstance.getServerConfig.mockResolvedValue({});
|
||||
getLogStores.mockReturnValue({});
|
||||
require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager);
|
||||
|
||||
|
|
@ -382,7 +394,7 @@ describe('MCP Routes', () => {
|
|||
});
|
||||
|
||||
it('should handle system-level OAuth completion', async () => {
|
||||
const { mcpServersRegistry } = require('@librechat/api');
|
||||
// mockRegistryInstance is defined at the top of the file
|
||||
const mockFlowManager = {
|
||||
getFlowState: jest.fn().mockResolvedValue({ status: 'PENDING' }),
|
||||
completeFlow: jest.fn().mockResolvedValue(),
|
||||
|
|
@ -403,7 +415,7 @@ describe('MCP Routes', () => {
|
|||
MCPOAuthHandler.getFlowState.mockResolvedValue(mockFlowState);
|
||||
MCPOAuthHandler.completeOAuthFlow.mockResolvedValue(mockTokens);
|
||||
MCPTokenStorage.storeTokens.mockResolvedValue();
|
||||
mcpServersRegistry.getServerConfig.mockResolvedValue({});
|
||||
mockRegistryInstance.getServerConfig.mockResolvedValue({});
|
||||
getLogStores.mockReturnValue({});
|
||||
require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager);
|
||||
|
||||
|
|
@ -418,7 +430,7 @@ describe('MCP Routes', () => {
|
|||
});
|
||||
|
||||
it('should handle reconnection failure after OAuth', async () => {
|
||||
const { mcpServersRegistry } = require('@librechat/api');
|
||||
// mockRegistryInstance is defined at the top of the file
|
||||
const mockFlowManager = {
|
||||
getFlowState: jest.fn().mockResolvedValue({ status: 'PENDING' }),
|
||||
completeFlow: jest.fn().mockResolvedValue(),
|
||||
|
|
@ -439,7 +451,7 @@ describe('MCP Routes', () => {
|
|||
MCPOAuthHandler.getFlowState.mockResolvedValue(mockFlowState);
|
||||
MCPOAuthHandler.completeOAuthFlow.mockResolvedValue(mockTokens);
|
||||
MCPTokenStorage.storeTokens.mockResolvedValue();
|
||||
mcpServersRegistry.getServerConfig.mockResolvedValue({});
|
||||
mockRegistryInstance.getServerConfig.mockResolvedValue({});
|
||||
getLogStores.mockReturnValue({});
|
||||
require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager);
|
||||
|
||||
|
|
@ -464,7 +476,7 @@ describe('MCP Routes', () => {
|
|||
});
|
||||
|
||||
it('should redirect to error page if token storage fails', async () => {
|
||||
const { mcpServersRegistry } = require('@librechat/api');
|
||||
// mockRegistryInstance is defined at the top of the file
|
||||
const mockFlowManager = {
|
||||
completeFlow: jest.fn().mockResolvedValue(),
|
||||
deleteFlow: jest.fn().mockResolvedValue(true),
|
||||
|
|
@ -484,7 +496,7 @@ describe('MCP Routes', () => {
|
|||
MCPOAuthHandler.getFlowState.mockResolvedValue(mockFlowState);
|
||||
MCPOAuthHandler.completeOAuthFlow.mockResolvedValue(mockTokens);
|
||||
MCPTokenStorage.storeTokens.mockRejectedValue(new Error('store failed'));
|
||||
mcpServersRegistry.getServerConfig.mockResolvedValue({});
|
||||
mockRegistryInstance.getServerConfig.mockResolvedValue({});
|
||||
getLogStores.mockReturnValue({});
|
||||
require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager);
|
||||
|
||||
|
|
@ -504,7 +516,7 @@ describe('MCP Routes', () => {
|
|||
});
|
||||
|
||||
it('should use original flow state credentials when storing tokens', async () => {
|
||||
const { mcpServersRegistry } = require('@librechat/api');
|
||||
// mockRegistryInstance is defined at the top of the file
|
||||
const mockFlowManager = {
|
||||
getFlowState: jest.fn(),
|
||||
completeFlow: jest.fn().mockResolvedValue(),
|
||||
|
|
@ -536,7 +548,7 @@ describe('MCP Routes', () => {
|
|||
MCPOAuthHandler.getFlowState.mockResolvedValue(flowState);
|
||||
MCPOAuthHandler.completeOAuthFlow.mockResolvedValue(mockTokens);
|
||||
MCPTokenStorage.storeTokens.mockResolvedValue();
|
||||
mcpServersRegistry.getServerConfig.mockResolvedValue({});
|
||||
mockRegistryInstance.getServerConfig.mockResolvedValue({});
|
||||
getLogStores.mockReturnValue({});
|
||||
require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager);
|
||||
|
||||
|
|
@ -837,14 +849,14 @@ describe('MCP Routes', () => {
|
|||
});
|
||||
|
||||
describe('POST /:serverName/reinitialize', () => {
|
||||
const { mcpServersRegistry } = require('@librechat/api');
|
||||
// mockRegistryInstance is defined at the top of the file
|
||||
|
||||
it('should return 404 when server is not found in configuration', async () => {
|
||||
const mockMcpManager = {
|
||||
disconnectUserConnection: jest.fn().mockResolvedValue(),
|
||||
};
|
||||
|
||||
mcpServersRegistry.getServerConfig.mockResolvedValue(null);
|
||||
mockRegistryInstance.getServerConfig.mockResolvedValue(null);
|
||||
require('~/config').getMCPManager.mockReturnValue(mockMcpManager);
|
||||
require('~/config').getFlowStateManager.mockReturnValue({});
|
||||
require('~/cache').getLogStores.mockReturnValue({});
|
||||
|
|
@ -869,7 +881,7 @@ describe('MCP Routes', () => {
|
|||
}),
|
||||
};
|
||||
|
||||
mcpServersRegistry.getServerConfig.mockResolvedValue({
|
||||
mockRegistryInstance.getServerConfig.mockResolvedValue({
|
||||
customUserVars: {},
|
||||
});
|
||||
require('~/config').getMCPManager.mockReturnValue(mockMcpManager);
|
||||
|
|
@ -902,7 +914,7 @@ describe('MCP Routes', () => {
|
|||
getUserConnection: jest.fn().mockRejectedValue(new Error('Connection failed')),
|
||||
};
|
||||
|
||||
mcpServersRegistry.getServerConfig.mockResolvedValue({});
|
||||
mockRegistryInstance.getServerConfig.mockResolvedValue({});
|
||||
require('~/config').getMCPManager.mockReturnValue(mockMcpManager);
|
||||
require('~/config').getFlowStateManager.mockReturnValue({});
|
||||
require('~/cache').getLogStores.mockReturnValue({});
|
||||
|
|
@ -921,7 +933,7 @@ describe('MCP Routes', () => {
|
|||
disconnectUserConnection: jest.fn(),
|
||||
};
|
||||
|
||||
mcpServersRegistry.getServerConfig.mockImplementation(() => {
|
||||
mockRegistryInstance.getServerConfig.mockImplementation(() => {
|
||||
throw new Error('Config loading failed');
|
||||
});
|
||||
require('~/config').getMCPManager.mockReturnValue(mockMcpManager);
|
||||
|
|
@ -960,7 +972,9 @@ describe('MCP Routes', () => {
|
|||
getUserConnection: jest.fn().mockResolvedValue(mockUserConnection),
|
||||
};
|
||||
|
||||
mcpServersRegistry.getServerConfig.mockResolvedValue({ endpoint: 'http://test-server.com' });
|
||||
mockRegistryInstance.getServerConfig.mockResolvedValue({
|
||||
endpoint: 'http://test-server.com',
|
||||
});
|
||||
require('~/config').getMCPManager.mockReturnValue(mockMcpManager);
|
||||
require('~/config').getFlowStateManager.mockReturnValue({});
|
||||
require('~/cache').getLogStores.mockReturnValue({});
|
||||
|
|
@ -1005,7 +1019,7 @@ describe('MCP Routes', () => {
|
|||
getUserConnection: jest.fn().mockResolvedValue(mockUserConnection),
|
||||
};
|
||||
|
||||
mcpServersRegistry.getServerConfig.mockResolvedValue({
|
||||
mockRegistryInstance.getServerConfig.mockResolvedValue({
|
||||
endpoint: 'http://test-server.com',
|
||||
customUserVars: {
|
||||
API_KEY: 'some-env-var',
|
||||
|
|
@ -1215,12 +1229,12 @@ describe('MCP Routes', () => {
|
|||
|
||||
describe('GET /:serverName/auth-values', () => {
|
||||
const { getUserPluginAuthValue } = require('~/server/services/PluginService');
|
||||
const { mcpServersRegistry } = require('@librechat/api');
|
||||
// mockRegistryInstance is defined at the top of the file
|
||||
|
||||
it('should return auth value flags for server', async () => {
|
||||
const mockMcpManager = {};
|
||||
|
||||
mcpServersRegistry.getServerConfig.mockResolvedValue({
|
||||
mockRegistryInstance.getServerConfig.mockResolvedValue({
|
||||
customUserVars: {
|
||||
API_KEY: 'some-env-var',
|
||||
SECRET_TOKEN: 'another-env-var',
|
||||
|
|
@ -1247,7 +1261,7 @@ describe('MCP Routes', () => {
|
|||
it('should return 404 when server is not found in configuration', async () => {
|
||||
const mockMcpManager = {};
|
||||
|
||||
mcpServersRegistry.getServerConfig.mockResolvedValue(null);
|
||||
mockRegistryInstance.getServerConfig.mockResolvedValue(null);
|
||||
require('~/config').getMCPManager.mockReturnValue(mockMcpManager);
|
||||
|
||||
const response = await request(app).get('/api/mcp/non-existent-server/auth-values');
|
||||
|
|
@ -1261,7 +1275,7 @@ describe('MCP Routes', () => {
|
|||
it('should handle errors when checking auth values', async () => {
|
||||
const mockMcpManager = {};
|
||||
|
||||
mcpServersRegistry.getServerConfig.mockResolvedValue({
|
||||
mockRegistryInstance.getServerConfig.mockResolvedValue({
|
||||
customUserVars: {
|
||||
API_KEY: 'some-env-var',
|
||||
},
|
||||
|
|
@ -1284,7 +1298,7 @@ describe('MCP Routes', () => {
|
|||
it('should return 500 when auth values check throws unexpected error', async () => {
|
||||
const mockMcpManager = {};
|
||||
|
||||
mcpServersRegistry.getServerConfig.mockImplementation(() => {
|
||||
mockRegistryInstance.getServerConfig.mockImplementation(() => {
|
||||
throw new Error('Config loading failed');
|
||||
});
|
||||
require('~/config').getMCPManager.mockReturnValue(mockMcpManager);
|
||||
|
|
@ -1298,7 +1312,7 @@ describe('MCP Routes', () => {
|
|||
it('should handle customUserVars that is not an object', async () => {
|
||||
const mockMcpManager = {};
|
||||
|
||||
mcpServersRegistry.getServerConfig.mockResolvedValue({
|
||||
mockRegistryInstance.getServerConfig.mockResolvedValue({
|
||||
customUserVars: 'not-an-object',
|
||||
});
|
||||
require('~/config').getMCPManager.mockReturnValue(mockMcpManager);
|
||||
|
|
@ -1327,7 +1341,7 @@ describe('MCP Routes', () => {
|
|||
|
||||
describe('GET /:serverName/oauth/callback - Edge Cases', () => {
|
||||
it('should handle OAuth callback without toolFlowId (falsy toolFlowId)', async () => {
|
||||
const { MCPOAuthHandler, MCPTokenStorage, mcpServersRegistry } = require('@librechat/api');
|
||||
const { MCPOAuthHandler, MCPTokenStorage } = require('@librechat/api');
|
||||
const mockTokens = {
|
||||
access_token: 'edge-access-token',
|
||||
refresh_token: 'edge-refresh-token',
|
||||
|
|
@ -1345,7 +1359,7 @@ describe('MCP Routes', () => {
|
|||
});
|
||||
MCPOAuthHandler.completeOAuthFlow = jest.fn().mockResolvedValue(mockTokens);
|
||||
MCPTokenStorage.storeTokens.mockResolvedValue();
|
||||
mcpServersRegistry.getServerConfig.mockResolvedValue({});
|
||||
mockRegistryInstance.getServerConfig.mockResolvedValue({});
|
||||
|
||||
const mockFlowManager = {
|
||||
getFlowState: jest.fn().mockResolvedValue({ status: 'PENDING' }),
|
||||
|
|
@ -1372,7 +1386,7 @@ describe('MCP Routes', () => {
|
|||
it('should handle null cached tools in OAuth callback (triggers || {} fallback)', async () => {
|
||||
const { getCachedTools } = require('~/server/services/Config');
|
||||
getCachedTools.mockResolvedValue(null);
|
||||
const { MCPOAuthHandler, MCPTokenStorage, mcpServersRegistry } = require('@librechat/api');
|
||||
const { MCPOAuthHandler, MCPTokenStorage } = require('@librechat/api');
|
||||
const mockTokens = {
|
||||
access_token: 'edge-access-token',
|
||||
refresh_token: 'edge-refresh-token',
|
||||
|
|
@ -1398,7 +1412,7 @@ describe('MCP Routes', () => {
|
|||
});
|
||||
MCPOAuthHandler.completeOAuthFlow.mockResolvedValue(mockTokens);
|
||||
MCPTokenStorage.storeTokens.mockResolvedValue();
|
||||
mcpServersRegistry.getServerConfig.mockResolvedValue({});
|
||||
mockRegistryInstance.getServerConfig.mockResolvedValue({});
|
||||
|
||||
const mockMcpManager = {
|
||||
getUserConnection: jest.fn().mockResolvedValue({
|
||||
|
|
@ -1418,7 +1432,7 @@ describe('MCP Routes', () => {
|
|||
});
|
||||
|
||||
describe('GET /servers', () => {
|
||||
const { mcpServersRegistry } = require('@librechat/api');
|
||||
// mockRegistryInstance is defined at the top of the file
|
||||
|
||||
it('should return all server configs for authenticated user', async () => {
|
||||
const mockServerConfigs = {
|
||||
|
|
@ -1432,17 +1446,17 @@ describe('MCP Routes', () => {
|
|||
},
|
||||
};
|
||||
|
||||
mcpServersRegistry.getAllServerConfigs.mockResolvedValue(mockServerConfigs);
|
||||
mockRegistryInstance.getAllServerConfigs.mockResolvedValue(mockServerConfigs);
|
||||
|
||||
const response = await request(app).get('/api/mcp/servers');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual(mockServerConfigs);
|
||||
expect(mcpServersRegistry.getAllServerConfigs).toHaveBeenCalledWith('test-user-id');
|
||||
expect(mockRegistryInstance.getAllServerConfigs).toHaveBeenCalledWith('test-user-id');
|
||||
});
|
||||
|
||||
it('should return empty object when no servers are configured', async () => {
|
||||
mcpServersRegistry.getAllServerConfigs.mockResolvedValue({});
|
||||
mockRegistryInstance.getAllServerConfigs.mockResolvedValue({});
|
||||
|
||||
const response = await request(app).get('/api/mcp/servers');
|
||||
|
||||
|
|
@ -1466,7 +1480,7 @@ describe('MCP Routes', () => {
|
|||
});
|
||||
|
||||
it('should return 500 when server config retrieval fails', async () => {
|
||||
mcpServersRegistry.getAllServerConfigs.mockRejectedValue(new Error('Database error'));
|
||||
mockRegistryInstance.getAllServerConfigs.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
const response = await request(app).get('/api/mcp/servers');
|
||||
|
||||
|
|
|
|||
|
|
@ -6,9 +6,13 @@ const {
|
|||
MCPOAuthHandler,
|
||||
MCPTokenStorage,
|
||||
getUserMCPAuthMap,
|
||||
mcpServersRegistry,
|
||||
} = require('@librechat/api');
|
||||
const { getMCPManager, getFlowStateManager, getOAuthReconnectionManager } = require('~/config');
|
||||
const {
|
||||
getMCPManager,
|
||||
getFlowStateManager,
|
||||
getOAuthReconnectionManager,
|
||||
getMCPServersRegistry,
|
||||
} = require('~/config');
|
||||
const { getMCPSetupData, getServerConnectionStatus } = require('~/server/services/MCP');
|
||||
const { findToken, updateToken, createToken, deleteTokens } = require('~/models');
|
||||
const { getUserPluginAuthValue } = require('~/server/services/PluginService');
|
||||
|
|
@ -365,7 +369,7 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => {
|
|||
logger.info(`[MCP Reinitialize] Reinitializing server: ${serverName}`);
|
||||
|
||||
const mcpManager = getMCPManager();
|
||||
const serverConfig = await mcpServersRegistry.getServerConfig(serverName, user.id);
|
||||
const serverConfig = await getMCPServersRegistry().getServerConfig(serverName, user.id);
|
||||
if (!serverConfig) {
|
||||
return res.status(404).json({
|
||||
error: `MCP server '${serverName}' not found in configuration`,
|
||||
|
|
@ -526,7 +530,7 @@ router.get('/:serverName/auth-values', requireJwtAuth, async (req, res) => {
|
|||
return res.status(401).json({ error: 'User not authenticated' });
|
||||
}
|
||||
|
||||
const serverConfig = await mcpServersRegistry.getServerConfig(serverName, user.id);
|
||||
const serverConfig = await getMCPServersRegistry().getServerConfig(serverName, user.id);
|
||||
if (!serverConfig) {
|
||||
return res.status(404).json({
|
||||
error: `MCP server '${serverName}' not found in configuration`,
|
||||
|
|
@ -566,7 +570,7 @@ router.get('/:serverName/auth-values', requireJwtAuth, async (req, res) => {
|
|||
});
|
||||
|
||||
async function getOAuthHeaders(serverName, userId) {
|
||||
const serverConfig = await mcpServersRegistry.getServerConfig(serverName, userId);
|
||||
const serverConfig = await getMCPServersRegistry().getServerConfig(serverName, userId);
|
||||
return serverConfig?.oauth_headers ?? {};
|
||||
}
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -20,12 +20,15 @@ const {
|
|||
ContentTypes,
|
||||
isAssistantsEndpoint,
|
||||
} = require('librechat-data-provider');
|
||||
const { getMCPManager, getFlowStateManager, getOAuthReconnectionManager } = require('~/config');
|
||||
const {
|
||||
getMCPManager,
|
||||
getFlowStateManager,
|
||||
getOAuthReconnectionManager,
|
||||
getMCPServersRegistry,
|
||||
} = require('~/config');
|
||||
const { findToken, createToken, updateToken } = require('~/models');
|
||||
const { reinitMCPServer } = require('./Tools/mcp');
|
||||
const { getAppConfig } = require('./Config');
|
||||
const { getLogStores } = require('~/cache');
|
||||
const { mcpServersRegistry } = require('@librechat/api');
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
|
|
@ -435,7 +438,7 @@ function createToolInstance({ res, toolName, serverName, toolDefinition, provide
|
|||
* @returns {Object} Object containing mcpConfig, appConnections, userConnections, and oauthServers
|
||||
*/
|
||||
async function getMCPSetupData(userId) {
|
||||
const mcpConfig = await mcpServersRegistry.getAllServerConfigs(userId);
|
||||
const mcpConfig = await getMCPServersRegistry().getAllServerConfigs(userId);
|
||||
|
||||
if (!mcpConfig) {
|
||||
throw new Error('MCP config not found');
|
||||
|
|
@ -450,7 +453,7 @@ async function getMCPSetupData(userId) {
|
|||
logger.error(`[MCP][User: ${userId}] Error getting app connections:`, error);
|
||||
}
|
||||
const userConnections = mcpManager.getUserConnections(userId) || new Map();
|
||||
const oauthServers = await mcpServersRegistry.getOAuthServers(userId);
|
||||
const oauthServers = await getMCPServersRegistry().getOAuthServers(userId);
|
||||
|
||||
return {
|
||||
mcpConfig,
|
||||
|
|
|
|||
|
|
@ -43,6 +43,11 @@ jest.mock('@librechat/agents', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
const mockRegistryInstance = {
|
||||
getOAuthServers: jest.fn(() => Promise.resolve(new Set())),
|
||||
getAllServerConfigs: jest.fn(() => Promise.resolve({})),
|
||||
};
|
||||
|
||||
jest.mock('@librechat/api', () => ({
|
||||
MCPOAuthHandler: {
|
||||
generateFlowId: jest.fn(),
|
||||
|
|
@ -50,9 +55,8 @@ jest.mock('@librechat/api', () => ({
|
|||
sendEvent: jest.fn(),
|
||||
normalizeServerName: jest.fn((name) => name),
|
||||
convertWithResolvedRefs: jest.fn((params) => params),
|
||||
mcpServersRegistry: {
|
||||
getOAuthServers: jest.fn(() => Promise.resolve(new Set())),
|
||||
getAllServerConfigs: jest.fn(() => Promise.resolve({})),
|
||||
MCPServersRegistry: {
|
||||
getInstance: () => mockRegistryInstance,
|
||||
},
|
||||
}));
|
||||
|
||||
|
|
@ -83,6 +87,7 @@ jest.mock('~/config', () => ({
|
|||
getMCPManager: jest.fn(),
|
||||
getFlowStateManager: jest.fn(),
|
||||
getOAuthReconnectionManager: jest.fn(),
|
||||
getMCPServersRegistry: jest.fn(() => mockRegistryInstance),
|
||||
}));
|
||||
|
||||
jest.mock('~/cache', () => ({
|
||||
|
|
@ -104,7 +109,6 @@ describe('tests for the new helper functions used by the MCP connection status e
|
|||
let mockGetFlowStateManager;
|
||||
let mockGetLogStores;
|
||||
let mockGetOAuthReconnectionManager;
|
||||
let mockMcpServersRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
|
@ -113,7 +117,6 @@ describe('tests for the new helper functions used by the MCP connection status e
|
|||
mockGetFlowStateManager = require('~/config').getFlowStateManager;
|
||||
mockGetLogStores = require('~/cache').getLogStores;
|
||||
mockGetOAuthReconnectionManager = require('~/config').getOAuthReconnectionManager;
|
||||
mockMcpServersRegistry = require('@librechat/api').mcpServersRegistry;
|
||||
});
|
||||
|
||||
describe('getMCPSetupData', () => {
|
||||
|
|
@ -128,12 +131,12 @@ describe('tests for the new helper functions used by the MCP connection status e
|
|||
appConnections: { getAll: jest.fn(() => new Map()) },
|
||||
getUserConnections: jest.fn(() => new Map()),
|
||||
});
|
||||
mockMcpServersRegistry.getOAuthServers.mockResolvedValue(new Set());
|
||||
mockMcpServersRegistry.getAllServerConfigs.mockResolvedValue(mockConfig);
|
||||
mockRegistryInstance.getOAuthServers.mockResolvedValue(new Set());
|
||||
mockRegistryInstance.getAllServerConfigs.mockResolvedValue(mockConfig);
|
||||
});
|
||||
|
||||
it('should successfully return MCP setup data', async () => {
|
||||
mockMcpServersRegistry.getAllServerConfigs.mockResolvedValue(mockConfig);
|
||||
mockRegistryInstance.getAllServerConfigs.mockResolvedValue(mockConfig);
|
||||
|
||||
const mockAppConnections = new Map([['server1', { status: 'connected' }]]);
|
||||
const mockUserConnections = new Map([['server2', { status: 'disconnected' }]]);
|
||||
|
|
@ -144,15 +147,15 @@ describe('tests for the new helper functions used by the MCP connection status e
|
|||
getUserConnections: jest.fn(() => mockUserConnections),
|
||||
};
|
||||
mockGetMCPManager.mockReturnValue(mockMCPManager);
|
||||
mockMcpServersRegistry.getOAuthServers.mockResolvedValue(mockOAuthServers);
|
||||
mockRegistryInstance.getOAuthServers.mockResolvedValue(mockOAuthServers);
|
||||
|
||||
const result = await getMCPSetupData(mockUserId);
|
||||
|
||||
expect(mockMcpServersRegistry.getAllServerConfigs).toHaveBeenCalledWith(mockUserId);
|
||||
expect(mockRegistryInstance.getAllServerConfigs).toHaveBeenCalledWith(mockUserId);
|
||||
expect(mockGetMCPManager).toHaveBeenCalledWith(mockUserId);
|
||||
expect(mockMCPManager.appConnections.getAll).toHaveBeenCalled();
|
||||
expect(mockMCPManager.getUserConnections).toHaveBeenCalledWith(mockUserId);
|
||||
expect(mockMcpServersRegistry.getOAuthServers).toHaveBeenCalledWith(mockUserId);
|
||||
expect(mockRegistryInstance.getOAuthServers).toHaveBeenCalledWith(mockUserId);
|
||||
|
||||
expect(result).toEqual({
|
||||
mcpConfig: mockConfig,
|
||||
|
|
@ -163,19 +166,19 @@ describe('tests for the new helper functions used by the MCP connection status e
|
|||
});
|
||||
|
||||
it('should throw error when MCP config not found', async () => {
|
||||
mockMcpServersRegistry.getAllServerConfigs.mockResolvedValue(null);
|
||||
mockRegistryInstance.getAllServerConfigs.mockResolvedValue(null);
|
||||
await expect(getMCPSetupData(mockUserId)).rejects.toThrow('MCP config not found');
|
||||
});
|
||||
|
||||
it('should handle null values from MCP manager gracefully', async () => {
|
||||
mockMcpServersRegistry.getAllServerConfigs.mockResolvedValue(mockConfig);
|
||||
mockRegistryInstance.getAllServerConfigs.mockResolvedValue(mockConfig);
|
||||
|
||||
const mockMCPManager = {
|
||||
appConnections: { getAll: jest.fn(() => Promise.resolve(null)) },
|
||||
getUserConnections: jest.fn(() => null),
|
||||
};
|
||||
mockGetMCPManager.mockReturnValue(mockMCPManager);
|
||||
mockMcpServersRegistry.getOAuthServers.mockResolvedValue(new Set());
|
||||
mockRegistryInstance.getOAuthServers.mockResolvedValue(new Set());
|
||||
|
||||
const result = await getMCPSetupData(mockUserId);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
const mongoose = require('mongoose');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { mergeAppTools, getAppConfig } = require('./Config');
|
||||
const { createMCPManager } = require('~/config');
|
||||
const { createMCPServersRegistry, createMCPManager } = require('~/config');
|
||||
|
||||
/**
|
||||
* Initialize MCP servers
|
||||
|
|
@ -12,6 +13,14 @@ async function initializeMCPs() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Initialize MCPServersRegistry first (required for MCPManager)
|
||||
try {
|
||||
createMCPServersRegistry(mongoose);
|
||||
} catch (error) {
|
||||
logger.error('[MCP] Failed to initialize MCPServersRegistry:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const mcpManager = await createMCPManager(mcpServers);
|
||||
|
||||
try {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue