mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-11 08:43:48 +00:00
* fix: prevent MCP server name collisions * chore: address MCP registry review nits * fix: reserve MCP config names from request context * chore: format MCP registry changes * chore: address MCP collision review findings
175 lines
5.8 KiB
JavaScript
175 lines
5.8 KiB
JavaScript
const mockRegistry = {
|
|
ensureConfigServers: jest.fn(),
|
|
getAllServerConfigs: jest.fn(),
|
|
};
|
|
|
|
jest.mock('~/config', () => ({
|
|
getMCPServersRegistry: jest.fn(() => mockRegistry),
|
|
getMCPManager: jest.fn(),
|
|
getFlowStateManager: jest.fn(),
|
|
getOAuthReconnectionManager: jest.fn(),
|
|
}));
|
|
|
|
jest.mock('@librechat/data-schemas', () => ({
|
|
getTenantId: jest.fn(() => 'tenant-1'),
|
|
logger: { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() },
|
|
}));
|
|
|
|
jest.mock('~/server/services/Config', () => ({
|
|
getAppConfig: jest.fn(),
|
|
setCachedTools: jest.fn(),
|
|
getCachedTools: jest.fn(),
|
|
getMCPServerTools: jest.fn(),
|
|
loadCustomConfig: jest.fn(),
|
|
}));
|
|
|
|
jest.mock('@librechat/api', () => ({
|
|
sendEvent: jest.fn(),
|
|
MCPOAuthHandler: jest.fn(),
|
|
isMCPDomainAllowed: jest.fn(),
|
|
normalizeServerName: jest.fn((name) => name),
|
|
normalizeJsonSchema: jest.fn((schema) => schema),
|
|
GenerationJobManager: jest.fn(),
|
|
resolveJsonSchemaRefs: jest.fn((schema) => schema),
|
|
buildOAuthToolCallName: jest.fn((name) => name),
|
|
}));
|
|
|
|
jest.mock('~/cache', () => ({ getLogStores: jest.fn() }));
|
|
jest.mock('~/models', () => ({
|
|
findToken: jest.fn(),
|
|
createToken: jest.fn(),
|
|
updateToken: jest.fn(),
|
|
}));
|
|
jest.mock('~/server/services/GraphTokenService', () => ({
|
|
getGraphApiToken: jest.fn(),
|
|
}));
|
|
jest.mock('~/server/services/Tools/mcp', () => ({
|
|
reinitMCPServer: jest.fn(),
|
|
}));
|
|
|
|
const { getAppConfig } = require('~/server/services/Config');
|
|
const { resolveConfigServers, resolveMcpConfigNames, resolveAllMcpConfigs } = require('../MCP');
|
|
|
|
describe('resolveConfigServers', () => {
|
|
beforeEach(() => jest.clearAllMocks());
|
|
|
|
it('resolves config servers for the current request context', async () => {
|
|
getAppConfig.mockResolvedValue({ mcpConfig: { srv: { url: 'http://a' } } });
|
|
mockRegistry.ensureConfigServers.mockResolvedValue({ srv: { name: 'srv' } });
|
|
|
|
const result = await resolveConfigServers({ user: { id: 'u1', role: 'admin' } });
|
|
|
|
expect(result).toEqual({ srv: { name: 'srv' } });
|
|
expect(getAppConfig).toHaveBeenCalledWith(
|
|
expect.objectContaining({ role: 'admin', userId: 'u1' }),
|
|
);
|
|
expect(mockRegistry.ensureConfigServers).toHaveBeenCalledWith({ srv: { url: 'http://a' } });
|
|
});
|
|
|
|
it('returns {} when ensureConfigServers throws', async () => {
|
|
getAppConfig.mockResolvedValue({ mcpConfig: { srv: {} } });
|
|
mockRegistry.ensureConfigServers.mockRejectedValue(new Error('inspect failed'));
|
|
|
|
const result = await resolveConfigServers({ user: { id: 'u1' } });
|
|
|
|
expect(result).toEqual({});
|
|
});
|
|
|
|
it('returns {} when getAppConfig throws', async () => {
|
|
getAppConfig.mockRejectedValue(new Error('db timeout'));
|
|
|
|
const result = await resolveConfigServers({ user: { id: 'u1' } });
|
|
|
|
expect(result).toEqual({});
|
|
});
|
|
|
|
it('passes empty mcpConfig when appConfig has none', async () => {
|
|
getAppConfig.mockResolvedValue({});
|
|
mockRegistry.ensureConfigServers.mockResolvedValue({});
|
|
|
|
await resolveConfigServers({ user: { id: 'u1' } });
|
|
|
|
expect(mockRegistry.ensureConfigServers).toHaveBeenCalledWith({});
|
|
});
|
|
});
|
|
|
|
describe('resolveMcpConfigNames', () => {
|
|
beforeEach(() => jest.clearAllMocks());
|
|
|
|
it('resolves current request config server names', async () => {
|
|
getAppConfig.mockResolvedValue({ mcpConfig: { cfg_srv: {}, yaml_srv: {} } });
|
|
|
|
const result = await resolveMcpConfigNames({ user: { id: 'u1', role: 'admin' } });
|
|
|
|
expect(result).toEqual(['cfg_srv', 'yaml_srv']);
|
|
expect(getAppConfig).toHaveBeenCalledWith(
|
|
expect.objectContaining({ role: 'admin', userId: 'u1' }),
|
|
);
|
|
});
|
|
|
|
it('returns [] when mcpConfig is absent', async () => {
|
|
getAppConfig.mockResolvedValue({});
|
|
|
|
const result = await resolveMcpConfigNames({ user: { id: 'u1' } });
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('propagates getAppConfig failures for write-path callers', async () => {
|
|
getAppConfig.mockRejectedValue(new Error('db timeout'));
|
|
|
|
await expect(resolveMcpConfigNames({ user: { id: 'u1' } })).rejects.toThrow('db timeout');
|
|
});
|
|
});
|
|
|
|
describe('resolveAllMcpConfigs', () => {
|
|
beforeEach(() => jest.clearAllMocks());
|
|
|
|
it('merges config servers with base servers', async () => {
|
|
getAppConfig.mockResolvedValue({ mcpConfig: { cfg_srv: {} } });
|
|
mockRegistry.ensureConfigServers.mockResolvedValue({ cfg_srv: { name: 'cfg_srv' } });
|
|
mockRegistry.getAllServerConfigs.mockResolvedValue({
|
|
cfg_srv: { name: 'cfg_srv' },
|
|
yaml_srv: { name: 'yaml_srv' },
|
|
});
|
|
|
|
const result = await resolveAllMcpConfigs('u1', { id: 'u1', role: 'user' });
|
|
|
|
expect(result).toEqual({
|
|
cfg_srv: { name: 'cfg_srv' },
|
|
yaml_srv: { name: 'yaml_srv' },
|
|
});
|
|
expect(mockRegistry.getAllServerConfigs).toHaveBeenCalledWith(
|
|
'u1',
|
|
{
|
|
cfg_srv: { name: 'cfg_srv' },
|
|
},
|
|
'user',
|
|
);
|
|
});
|
|
|
|
it('continues with empty configServers when ensureConfigServers fails', async () => {
|
|
getAppConfig.mockResolvedValue({ mcpConfig: { srv: {} } });
|
|
mockRegistry.ensureConfigServers.mockRejectedValue(new Error('inspect failed'));
|
|
mockRegistry.getAllServerConfigs.mockResolvedValue({ yaml_srv: { name: 'yaml_srv' } });
|
|
|
|
const result = await resolveAllMcpConfigs('u1', { id: 'u1' });
|
|
|
|
expect(result).toEqual({ yaml_srv: { name: 'yaml_srv' } });
|
|
expect(mockRegistry.getAllServerConfigs).toHaveBeenCalledWith('u1', {});
|
|
});
|
|
|
|
it('propagates getAllServerConfigs failures', async () => {
|
|
getAppConfig.mockResolvedValue({ mcpConfig: {} });
|
|
mockRegistry.ensureConfigServers.mockResolvedValue({});
|
|
mockRegistry.getAllServerConfigs.mockRejectedValue(new Error('redis down'));
|
|
|
|
await expect(resolveAllMcpConfigs('u1', { id: 'u1' })).rejects.toThrow('redis down');
|
|
});
|
|
|
|
it('propagates getAppConfig failures', async () => {
|
|
getAppConfig.mockRejectedValue(new Error('mongo down'));
|
|
|
|
await expect(resolveAllMcpConfigs('u1', { id: 'u1' })).rejects.toThrow('mongo down');
|
|
});
|
|
});
|