diff --git a/client/src/hooks/Config/__tests__/useAppStartup.spec.tsx b/client/src/hooks/Config/__tests__/useAppStartup.spec.tsx
new file mode 100644
index 0000000000..eef2795a76
--- /dev/null
+++ b/client/src/hooks/Config/__tests__/useAppStartup.spec.tsx
@@ -0,0 +1,123 @@
+import React from 'react';
+import { RecoilRoot } from 'recoil';
+import { renderHook } from '@testing-library/react';
+import { PermissionTypes, Permissions } from 'librechat-data-provider';
+import type { TUser } from 'librechat-data-provider';
+
+const mockUseHasAccess = jest.fn();
+const mockUseMCPServersQuery = jest.fn();
+const mockUseMCPToolsQuery = jest.fn();
+
+jest.mock('~/hooks', () => ({
+ useHasAccess: (args: unknown) => mockUseHasAccess(args),
+}));
+
+jest.mock('~/data-provider', () => ({
+ useMCPServersQuery: (config: unknown) => mockUseMCPServersQuery(config),
+ useMCPToolsQuery: (config: unknown) => mockUseMCPToolsQuery(config),
+}));
+
+jest.mock('../useSpeechSettingsInit', () => ({
+ __esModule: true,
+ default: jest.fn(),
+}));
+
+jest.mock('~/utils/timestamps', () => ({
+ cleanupTimestampedStorage: jest.fn(),
+}));
+
+jest.mock('react-gtm-module', () => ({
+ __esModule: true,
+ default: { initialize: jest.fn() },
+}));
+
+import useAppStartup from '../useAppStartup';
+
+const mockUser = {
+ id: 'user-123',
+ username: 'testuser',
+ email: 'test@example.com',
+ name: 'Test User',
+ avatar: '',
+ role: 'USER',
+ provider: 'local',
+ emailVerified: true,
+ createdAt: '2023-01-01T00:00:00.000Z',
+ updatedAt: '2023-01-01T00:00:00.000Z',
+} as TUser;
+
+const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
+ {children}
+);
+
+describe('useAppStartup — MCP permission gating', () => {
+ beforeEach(() => {
+ mockUseMCPServersQuery.mockReturnValue({ data: undefined, isLoading: false });
+ mockUseMCPToolsQuery.mockReturnValue({ data: undefined, isLoading: false });
+ });
+
+ it('checks the MCP_SERVERS.USE permission via useHasAccess', () => {
+ mockUseHasAccess.mockReturnValue(false);
+
+ renderHook(() => useAppStartup({ startupConfig: undefined, user: mockUser }), { wrapper });
+
+ expect(mockUseHasAccess).toHaveBeenCalledWith({
+ permissionType: PermissionTypes.MCP_SERVERS,
+ permission: Permissions.USE,
+ });
+ });
+
+ it('suppresses all MCP queries when user lacks MCP_SERVERS.USE', () => {
+ mockUseHasAccess.mockReturnValue(false);
+
+ renderHook(() => useAppStartup({ startupConfig: undefined, user: mockUser }), { wrapper });
+
+ expect(mockUseMCPServersQuery).toHaveBeenCalledWith({ enabled: false });
+ expect(mockUseMCPToolsQuery).toHaveBeenCalledWith({ enabled: false });
+ });
+
+ it('enables servers query and tools query when permission granted, servers loaded, and user present', () => {
+ mockUseHasAccess.mockReturnValue(true);
+ mockUseMCPServersQuery.mockReturnValue({
+ data: { 'test-server': { url: 'http://test' } },
+ isLoading: false,
+ });
+
+ renderHook(() => useAppStartup({ startupConfig: undefined, user: mockUser }), { wrapper });
+
+ expect(mockUseMCPServersQuery).toHaveBeenCalledWith({ enabled: true });
+ expect(mockUseMCPToolsQuery).toHaveBeenCalledWith({ enabled: true });
+ });
+
+ it('suppresses tools query when permission granted but user prop is undefined', () => {
+ mockUseHasAccess.mockReturnValue(true);
+ mockUseMCPServersQuery.mockReturnValue({
+ data: { 'test-server': { url: 'http://test' } },
+ isLoading: false,
+ });
+
+ renderHook(() => useAppStartup({ startupConfig: undefined, user: undefined }), { wrapper });
+
+ expect(mockUseMCPServersQuery).toHaveBeenCalledWith({ enabled: true });
+ expect(mockUseMCPToolsQuery).toHaveBeenCalledWith({ enabled: false });
+ });
+
+ it('suppresses tools query when permission granted but no servers loaded', () => {
+ mockUseHasAccess.mockReturnValue(true);
+ mockUseMCPServersQuery.mockReturnValue({ data: {}, isLoading: false });
+
+ renderHook(() => useAppStartup({ startupConfig: undefined, user: mockUser }), { wrapper });
+
+ expect(mockUseMCPServersQuery).toHaveBeenCalledWith({ enabled: true });
+ expect(mockUseMCPToolsQuery).toHaveBeenCalledWith({ enabled: false });
+ });
+
+ it('suppresses tools query while servers are still loading', () => {
+ mockUseHasAccess.mockReturnValue(true);
+ mockUseMCPServersQuery.mockReturnValue({ data: undefined, isLoading: true });
+
+ renderHook(() => useAppStartup({ startupConfig: undefined, user: mockUser }), { wrapper });
+
+ expect(mockUseMCPToolsQuery).toHaveBeenCalledWith({ enabled: false });
+ });
+});
diff --git a/client/src/hooks/Config/useAppStartup.ts b/client/src/hooks/Config/useAppStartup.ts
index 52b4325eea..f40b283ee2 100644
--- a/client/src/hooks/Config/useAppStartup.ts
+++ b/client/src/hooks/Config/useAppStartup.ts
@@ -1,11 +1,12 @@
import { useEffect } from 'react';
import { useRecoilState } from 'recoil';
import TagManager from 'react-gtm-module';
-import { LocalStorageKeys } from 'librechat-data-provider';
+import { LocalStorageKeys, PermissionTypes, Permissions } from 'librechat-data-provider';
import type { TStartupConfig, TUser } from 'librechat-data-provider';
+import { useMCPToolsQuery, useMCPServersQuery } from '~/data-provider';
import { cleanupTimestampedStorage } from '~/utils/timestamps';
import useSpeechSettingsInit from './useSpeechSettingsInit';
-import { useMCPToolsQuery, useMCPServersQuery } from '~/data-provider';
+import { useHasAccess } from '~/hooks';
import store from '~/store';
export default function useAppStartup({
@@ -16,12 +17,23 @@ export default function useAppStartup({
user?: TUser;
}) {
const [defaultPreset, setDefaultPreset] = useRecoilState(store.defaultPreset);
+ const canUseMcp = useHasAccess({
+ permissionType: PermissionTypes.MCP_SERVERS,
+ permission: Permissions.USE,
+ });
useSpeechSettingsInit(!!user);
- const { data: loadedServers, isLoading: serversLoading } = useMCPServersQuery();
+ const { data: loadedServers, isLoading: serversLoading } = useMCPServersQuery({
+ enabled: canUseMcp,
+ });
useMCPToolsQuery({
- enabled: !serversLoading && !!loadedServers && Object.keys(loadedServers).length > 0 && !!user,
+ enabled:
+ canUseMcp &&
+ !serversLoading &&
+ !!loadedServers &&
+ Object.keys(loadedServers).length > 0 &&
+ !!user,
});
/** Clean up old localStorage entries on startup */
diff --git a/client/src/hooks/MCP/useMCPServerManager.ts b/client/src/hooks/MCP/useMCPServerManager.ts
index af65ba4507..4ba1ff6278 100644
--- a/client/src/hooks/MCP/useMCPServerManager.ts
+++ b/client/src/hooks/MCP/useMCPServerManager.ts
@@ -2,7 +2,14 @@ import { useCallback, useState, useMemo, useRef, useEffect } from 'react';
import { useAtom } from 'jotai';
import { useToastContext } from '@librechat/client';
import { useQueryClient } from '@tanstack/react-query';
-import { Constants, QueryKeys, MCPOptions, ResourceType } from 'librechat-data-provider';
+import {
+ Constants,
+ QueryKeys,
+ MCPOptions,
+ Permissions,
+ ResourceType,
+ PermissionTypes,
+} from 'librechat-data-provider';
import {
useCancelMCPOAuthMutation,
useUpdateUserPluginsMutation,
@@ -11,7 +18,7 @@ import {
} from 'librechat-data-provider/react-query';
import type { TUpdateUserPlugins, TPlugin, MCPServersResponse } from 'librechat-data-provider';
import type { ConfigFieldDetail } from '~/common';
-import { useLocalize, useMCPSelect, useMCPConnectionStatus } from '~/hooks';
+import { useLocalize, useHasAccess, useMCPSelect, useMCPConnectionStatus } from '~/hooks';
import { useGetStartupConfig, useMCPServersQuery } from '~/data-provider';
import { mcpServerInitStatesAtom, getServerInitState } from '~/store/mcp';
import type { MCPServerInitState } from '~/store/mcp';
@@ -35,12 +42,19 @@ export function useMCPServerManager({
const localize = useLocalize();
const queryClient = useQueryClient();
const { showToast } = useToastContext();
- const { data: startupConfig } = useGetStartupConfig(); // Keep for UI config only
+ /** Retained for `interface.mcpServers.placeholder` used by `placeholderText` below */
+ const { data: startupConfig } = useGetStartupConfig();
+ const canUseMcp = useHasAccess({
+ permissionType: PermissionTypes.MCP_SERVERS,
+ permission: Permissions.USE,
+ });
- const { data: loadedServers, isLoading } = useMCPServersQuery();
+ const { data: loadedServers, isLoading } = useMCPServersQuery({ enabled: canUseMcp });
// Fetch effective permissions for all MCP servers
- const { data: permissionsMap } = useGetAllEffectivePermissionsQuery(ResourceType.MCPSERVER);
+ const { data: permissionsMap } = useGetAllEffectivePermissionsQuery(ResourceType.MCPSERVER, {
+ enabled: canUseMcp,
+ });
const [isConfigModalOpen, setIsConfigModalOpen] = useState(false);
const [selectedToolForConfig, setSelectedToolForConfig] = useState(null);