const mockGetMCPManager = jest.fn(); const mockInvalidateCachedTools = jest.fn(); jest.mock('~/config', () => ({ getMCPManager: (...args) => mockGetMCPManager(...args), getFlowStateManager: jest.fn(), getMCPServersRegistry: jest.fn(), })); jest.mock('~/server/services/Config/getCachedTools', () => ({ invalidateCachedTools: (...args) => mockInvalidateCachedTools(...args), })); jest.mock('~/server/services/Config', () => ({ getAppConfig: jest.fn(), getMCPServerTools: jest.fn(), })); const mongoose = require('mongoose'); const { mcpServerSchema } = require('@librechat/data-schemas'); const { MongoMemoryServer } = require('mongodb-memory-server'); const { ResourceType, AccessRoleIds, PrincipalType, PermissionBits, } = require('librechat-data-provider'); const permissionService = require('~/server/services/PermissionService'); const { deleteUserMcpServers } = require('~/server/controllers/UserController'); const { AclEntry, AccessRole } = require('~/db/models'); let MCPServer; describe('deleteUserMcpServers', () => { let mongoServer; beforeAll(async () => { mongoServer = await MongoMemoryServer.create(); const mongoUri = mongoServer.getUri(); MCPServer = mongoose.models.MCPServer || mongoose.model('MCPServer', mcpServerSchema); await mongoose.connect(mongoUri); await AccessRole.create({ accessRoleId: AccessRoleIds.MCPSERVER_OWNER, name: 'MCP Server Owner', resourceType: ResourceType.MCPSERVER, permBits: PermissionBits.VIEW | PermissionBits.EDIT | PermissionBits.DELETE | PermissionBits.SHARE, }); await AccessRole.create({ accessRoleId: AccessRoleIds.MCPSERVER_VIEWER, name: 'MCP Server Viewer', resourceType: ResourceType.MCPSERVER, permBits: PermissionBits.VIEW, }); }, 20000); afterAll(async () => { await mongoose.disconnect(); await mongoServer.stop(); }); beforeEach(async () => { await MCPServer.deleteMany({}); await AclEntry.deleteMany({}); jest.clearAllMocks(); }); test('should delete solely-owned MCP servers and their ACL entries', async () => { const userId = new mongoose.Types.ObjectId(); const server = await MCPServer.create({ serverName: 'sole-owned-server', config: { title: 'Test Server' }, author: userId, }); await permissionService.grantPermission({ principalType: PrincipalType.USER, principalId: userId, resourceType: ResourceType.MCPSERVER, resourceId: server._id, accessRoleId: AccessRoleIds.MCPSERVER_OWNER, grantedBy: userId, }); mockGetMCPManager.mockReturnValue({ disconnectUserConnection: jest.fn().mockResolvedValue(undefined), }); await deleteUserMcpServers(userId.toString()); expect(await MCPServer.findById(server._id)).toBeNull(); const aclEntries = await AclEntry.find({ resourceType: ResourceType.MCPSERVER, resourceId: server._id, }); expect(aclEntries).toHaveLength(0); }); test('should disconnect MCP sessions and invalidate tool cache before deletion', async () => { const userId = new mongoose.Types.ObjectId(); const mockDisconnect = jest.fn().mockResolvedValue(undefined); const server = await MCPServer.create({ serverName: 'session-server', config: { title: 'Session Server' }, author: userId, }); await permissionService.grantPermission({ principalType: PrincipalType.USER, principalId: userId, resourceType: ResourceType.MCPSERVER, resourceId: server._id, accessRoleId: AccessRoleIds.MCPSERVER_OWNER, grantedBy: userId, }); mockGetMCPManager.mockReturnValue({ disconnectUserConnection: mockDisconnect }); await deleteUserMcpServers(userId.toString()); expect(mockDisconnect).toHaveBeenCalledWith(userId.toString(), 'session-server'); expect(mockInvalidateCachedTools).toHaveBeenCalledWith({ userId: userId.toString(), serverName: 'session-server', }); }); test('should preserve multi-owned MCP servers', async () => { const deletingUserId = new mongoose.Types.ObjectId(); const otherOwnerId = new mongoose.Types.ObjectId(); const soleServer = await MCPServer.create({ serverName: 'sole-server', config: { title: 'Sole Server' }, author: deletingUserId, }); const multiServer = await MCPServer.create({ serverName: 'multi-server', config: { title: 'Multi Server' }, author: deletingUserId, }); await permissionService.grantPermission({ principalType: PrincipalType.USER, principalId: deletingUserId, resourceType: ResourceType.MCPSERVER, resourceId: soleServer._id, accessRoleId: AccessRoleIds.MCPSERVER_OWNER, grantedBy: deletingUserId, }); await permissionService.grantPermission({ principalType: PrincipalType.USER, principalId: deletingUserId, resourceType: ResourceType.MCPSERVER, resourceId: multiServer._id, accessRoleId: AccessRoleIds.MCPSERVER_OWNER, grantedBy: deletingUserId, }); await permissionService.grantPermission({ principalType: PrincipalType.USER, principalId: otherOwnerId, resourceType: ResourceType.MCPSERVER, resourceId: multiServer._id, accessRoleId: AccessRoleIds.MCPSERVER_OWNER, grantedBy: otherOwnerId, }); mockGetMCPManager.mockReturnValue({ disconnectUserConnection: jest.fn().mockResolvedValue(undefined), }); await deleteUserMcpServers(deletingUserId.toString()); expect(await MCPServer.findById(soleServer._id)).toBeNull(); expect(await MCPServer.findById(multiServer._id)).not.toBeNull(); const soleAcl = await AclEntry.find({ resourceType: ResourceType.MCPSERVER, resourceId: soleServer._id, }); expect(soleAcl).toHaveLength(0); const multiAclOther = await AclEntry.find({ resourceType: ResourceType.MCPSERVER, resourceId: multiServer._id, principalId: otherOwnerId, }); expect(multiAclOther).toHaveLength(1); expect(multiAclOther[0].permBits & PermissionBits.DELETE).toBeTruthy(); const multiAclDeleting = await AclEntry.find({ resourceType: ResourceType.MCPSERVER, resourceId: multiServer._id, principalId: deletingUserId, }); expect(multiAclDeleting).toHaveLength(1); }); test('should be a no-op when user has no owned MCP servers', async () => { const userId = new mongoose.Types.ObjectId(); const otherUserId = new mongoose.Types.ObjectId(); const server = await MCPServer.create({ serverName: 'other-server', config: { title: 'Other Server' }, author: otherUserId, }); await permissionService.grantPermission({ principalType: PrincipalType.USER, principalId: otherUserId, resourceType: ResourceType.MCPSERVER, resourceId: server._id, accessRoleId: AccessRoleIds.MCPSERVER_OWNER, grantedBy: otherUserId, }); await deleteUserMcpServers(userId.toString()); expect(await MCPServer.findById(server._id)).not.toBeNull(); expect(mockGetMCPManager).not.toHaveBeenCalled(); }); test('should handle gracefully when MCPServer model is not registered', async () => { const originalModel = mongoose.models.MCPServer; delete mongoose.models.MCPServer; try { const userId = new mongoose.Types.ObjectId(); await expect(deleteUserMcpServers(userId.toString())).resolves.toBeUndefined(); } finally { mongoose.models.MCPServer = originalModel; } }); test('should handle gracefully when MCPManager is not available', async () => { const userId = new mongoose.Types.ObjectId(); const server = await MCPServer.create({ serverName: 'no-manager-server', config: { title: 'No Manager Server' }, author: userId, }); await permissionService.grantPermission({ principalType: PrincipalType.USER, principalId: userId, resourceType: ResourceType.MCPSERVER, resourceId: server._id, accessRoleId: AccessRoleIds.MCPSERVER_OWNER, grantedBy: userId, }); mockGetMCPManager.mockReturnValue(null); await deleteUserMcpServers(userId.toString()); expect(await MCPServer.findById(server._id)).toBeNull(); }); test('should delete legacy MCP servers that have author but no ACL entries', async () => { const legacyUserId = new mongoose.Types.ObjectId(); const legacyServer = await MCPServer.create({ serverName: 'legacy-server', config: { title: 'Legacy Server' }, author: legacyUserId, }); mockGetMCPManager.mockReturnValue({ disconnectUserConnection: jest.fn().mockResolvedValue(undefined), }); await deleteUserMcpServers(legacyUserId.toString()); expect(await MCPServer.findById(legacyServer._id)).toBeNull(); }); test('should delete both ACL-owned and legacy servers in one call', async () => { const userId = new mongoose.Types.ObjectId(); const aclServer = await MCPServer.create({ serverName: 'acl-server', config: { title: 'ACL Server' }, author: userId, }); await permissionService.grantPermission({ principalType: PrincipalType.USER, principalId: userId, resourceType: ResourceType.MCPSERVER, resourceId: aclServer._id, accessRoleId: AccessRoleIds.MCPSERVER_OWNER, grantedBy: userId, }); const legacyServer = await MCPServer.create({ serverName: 'legacy-mixed-server', config: { title: 'Legacy Mixed' }, author: userId, }); mockGetMCPManager.mockReturnValue({ disconnectUserConnection: jest.fn().mockResolvedValue(undefined), }); await deleteUserMcpServers(userId.toString()); expect(await MCPServer.findById(aclServer._id)).toBeNull(); expect(await MCPServer.findById(legacyServer._id)).toBeNull(); }); });