Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions packages/cli/src/commands/mcp/list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
return {
...original,
createTransport: vi.fn(),
applyAdminAllowlist: vi.fn((servers) => servers),
MCPServerStatus: {
CONNECTED: 'CONNECTED',
CONNECTING: 'CONNECTING',
Expand Down Expand Up @@ -223,4 +224,43 @@ describe('mcp list command', () => {
),
);
});

it('should filter servers based on admin allowlist passed in settings', async () => {
const settingsWithAllowlist = mergeSettings({}, {}, {}, {}, true);
settingsWithAllowlist.admin = {
secureModeEnabled: false,
extensions: { enabled: true },
skills: { enabled: true },
mcp: {
enabled: true,
config: {
'allowed-server': { url: 'http://allowed' },
},
},
};

settingsWithAllowlist.mcpServers = {
'allowed-server': { command: 'cmd1' },
'forbidden-server': { command: 'cmd2' },
};

mockClient.connect.mockResolvedValue(undefined);
mockClient.ping.mockResolvedValue(undefined);

await listMcpServers(settingsWithAllowlist);

expect(debugLogger.log).toHaveBeenCalledWith(
expect.stringContaining('allowed-server'),
);
expect(debugLogger.log).not.toHaveBeenCalledWith(
expect.stringContaining('forbidden-server'),
);
expect(mockedCreateTransport).toHaveBeenCalledWith(
'allowed-server',
expect.objectContaining({ url: 'http://allowed' }), // Should use admin config
false,
expect.anything(),
);
});
});

55 changes: 41 additions & 14 deletions packages/cli/src/commands/mcp/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@

// File for 'gemini mcp list' command
import type { CommandModule } from 'yargs';
import { loadSettings } from '../../config/settings.js';
import { type MergedSettings, loadSettings } from '../../config/settings.js';
import type { MCPServerConfig } from '@google/gemini-cli-core';
import {
MCPServerStatus,
createTransport,
debugLogger,
applyAdminAllowlist,
getAdminBlockedMcpServersMessage,
} from '@google/gemini-cli-core';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { ExtensionManager } from '../../config/extension-manager.js';
Expand All @@ -24,18 +26,24 @@ const COLOR_YELLOW = '\u001b[33m';
const COLOR_RED = '\u001b[31m';
const RESET_COLOR = '\u001b[0m';

export async function getMcpServersFromConfig(): Promise<
Record<string, MCPServerConfig>
> {
const settings = loadSettings();
export async function getMcpServersFromConfig(
settings?: MergedSettings,
): Promise<{
mcpServers: Record<string, MCPServerConfig>;
blockedServerNames: string[];
}> {
if (!settings) {
settings = loadSettings().merged;
}

const extensionManager = new ExtensionManager({
settings: settings.merged,
settings,
workspaceDir: process.cwd(),
requestConsent: requestConsentNonInteractive,
requestSetting: promptForSetting,
});
const extensions = await extensionManager.loadExtensions();
const mcpServers = { ...settings.merged.mcpServers };
const mcpServers = { ...settings.mcpServers };
for (const extension of extensions) {
Object.entries(extension.mcpServers || {}).forEach(([key, server]) => {
if (mcpServers[key]) {
Expand All @@ -47,7 +55,11 @@ export async function getMcpServersFromConfig(): Promise<
};
});
}
return mcpServers;

const adminAllowlist = settings.admin?.mcp?.config;
const filteredResult = applyAdminAllowlist(mcpServers, adminAllowlist);

return filteredResult;
}

async function testMCPConnection(
Expand Down Expand Up @@ -103,12 +115,23 @@ async function getServerStatus(
return testMCPConnection(serverName, server);
}

export async function listMcpServers(): Promise<void> {
const mcpServers = await getMcpServersFromConfig();
export async function listMcpServers(settings?: MergedSettings): Promise<void> {
const { mcpServers, blockedServerNames } =
await getMcpServersFromConfig(settings);
const serverNames = Object.keys(mcpServers);

if (blockedServerNames.length > 0) {
const message = getAdminBlockedMcpServersMessage(
blockedServerNames,
undefined,
);
debugLogger.log(COLOR_YELLOW + message + RESET_COLOR + '\n');
}

if (serverNames.length === 0) {
debugLogger.log('No MCP servers configured.');
if (blockedServerNames.length === 0) {
debugLogger.log('No MCP servers configured.');
}
return;
}

Expand Down Expand Up @@ -154,11 +177,15 @@ export async function listMcpServers(): Promise<void> {
}
}

export const listCommand: CommandModule = {
interface ListArgs {
settings?: MergedSettings;
}

export const listCommand: CommandModule<object, ListArgs> = {
command: 'list',
describe: 'List all configured MCP servers',
handler: async () => {
await listMcpServers();
handler: async (argv) => {
await listMcpServers(argv.settings);
await exitCli();
},
};
1 change: 1 addition & 0 deletions packages/cli/src/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ vi.mock('@google/gemini-cli-core', async () => {
defaultDecision: ServerConfig.PolicyDecision.ASK_USER,
approvalMode: ServerConfig.ApprovalMode.DEFAULT,
})),
applyAdminAllowlist: vi.fn((servers) => servers),
};
});

Expand Down
42 changes: 11 additions & 31 deletions packages/cli/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ import {
GEMINI_MODEL_ALIAS_AUTO,
getAdminErrorMessage,
Config,
applyAdminAllowlist,
getAdminBlockedMcpServersMessage,
} from '@google/gemini-cli-core';
import type {
MCPServerConfig,
HookDefinition,
HookEventName,
OutputFormat,
Expand Down Expand Up @@ -695,38 +696,17 @@ export async function loadCliConfig(
let mcpServers = mcpEnabled ? settings.mcpServers : {};

if (mcpEnabled && adminAllowlist && Object.keys(adminAllowlist).length > 0) {
const filteredMcpServers: Record<string, MCPServerConfig> = {};
for (const [serverId, localConfig] of Object.entries(mcpServers)) {
const adminConfig = adminAllowlist[serverId];
if (adminConfig) {
const mergedConfig = {
...localConfig,
url: adminConfig.url,
type: adminConfig.type,
trust: adminConfig.trust,
};

// Remove local connection details
delete mergedConfig.command;
delete mergedConfig.args;
delete mergedConfig.env;
delete mergedConfig.cwd;
delete mergedConfig.httpUrl;
delete mergedConfig.tcp;

if (
(adminConfig.includeTools && adminConfig.includeTools.length > 0) ||
(adminConfig.excludeTools && adminConfig.excludeTools.length > 0)
) {
mergedConfig.includeTools = adminConfig.includeTools;
mergedConfig.excludeTools = adminConfig.excludeTools;
}
const result = applyAdminAllowlist(mcpServers, adminAllowlist);
mcpServers = result.mcpServers;
mcpServerCommand = undefined;

filteredMcpServers[serverId] = mergedConfig;
}
if (result.blockedServerNames && result.blockedServerNames.length > 0) {
const message = getAdminBlockedMcpServersMessage(
result.blockedServerNames,
undefined,
);
coreEvents.emitConsoleLog('warn', message);
}
mcpServers = filteredMcpServers;
mcpServerCommand = undefined;
}

return new Config({
Expand Down
35 changes: 29 additions & 6 deletions packages/cli/src/config/extension-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ import {
type HookEventName,
type ResolvedExtensionSetting,
coreEvents,
applyAdminAllowlist,
getAdminBlockedMcpServersMessage,
} from '@google/gemini-cli-core';
import { maybeRequestConsentOrFail } from './extensions/consent.js';
import { resolveEnvVarsInObject } from '../utils/envVarResolver.js';
Expand Down Expand Up @@ -661,12 +663,33 @@ Would you like to attempt to install via "git clone" instead?`,
if (this.settings.admin.mcp.enabled === false) {
config.mcpServers = undefined;
} else {
config.mcpServers = Object.fromEntries(
Object.entries(config.mcpServers).map(([key, value]) => [
key,
filterMcpConfig(value),
]),
);
// Apply admin allowlist if configured
const adminAllowlist = this.settings.admin.mcp.config;
if (adminAllowlist && Object.keys(adminAllowlist).length > 0) {
const result = applyAdminAllowlist(
config.mcpServers,
adminAllowlist,
);
config.mcpServers = result.mcpServers;

if (result.blockedServerNames.length > 0) {
const message = getAdminBlockedMcpServersMessage(
result.blockedServerNames,
undefined,
);
coreEvents.emitConsoleLog('warn', message);
}
}

// Then apply local filtering/sanitization
if (config.mcpServers) {
config.mcpServers = Object.fromEntries(
Object.entries(config.mcpServers).map(([key, value]) => [
key,
filterMcpConfig(value),
]),
);
}
}
}

Expand Down
42 changes: 34 additions & 8 deletions packages/cli/src/config/mcp/mcpServerEnablement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@

import fs from 'node:fs/promises';
import path from 'node:path';
import { Storage, coreEvents } from '@google/gemini-cli-core';
import {
Storage,
coreEvents,
applyAdminAllowlist,
getAdminErrorMessage,
type MCPServerConfig,
} from '@google/gemini-cli-core';

/**
* Stored in JSON file - represents persistent enablement state.
Expand Down Expand Up @@ -90,20 +96,14 @@ export function isInSettingsList(
return { found: false };
}

/**
* Single source of truth for whether a server can be loaded.
* Used by: isAllowedMcpServer(), connectServer(), CLI handlers, slash handlers.
*
* Uses callbacks instead of direct enablementManager reference to keep
* packages/core independent of packages/cli.
*/
export async function canLoadServer(
serverId: string,
config: {
adminMcpEnabled: boolean;
allowedList?: string[];
excludedList?: string[];
enablement?: EnablementCallbacks;
adminAllowlist?: Record<string, MCPServerConfig>;
},
): Promise<ServerLoadResult> {
const normalizedId = normalizeServerId(serverId);
Expand All @@ -118,6 +118,32 @@ export async function canLoadServer(
};
}

// 2. Admin allowlist (mcp.config)
if (config.adminAllowlist && Object.keys(config.adminAllowlist).length > 0) {
// Check if effective config remains after applying allowlist
const dummyConfig: Record<string, MCPServerConfig> = {
[normalizedId]: { url: 'dummy', type: 'sse' }, // Dummy values, just checking key existence/filtering
};
// applyAdminAllowlist filters keys. If key is gone, it's blocked.
// Note: applyAdminAllowlist matches keys exactly (case-sensitive usually, but let's see implementation).
// The implementation (core/src/code_assist/admin/mcpUtils.ts) iterates localMcpServers and looks up in adminAllowlist.
// It uses exact key match.
// Our normalizeServerId uses lower case. Admin allowlist keys might not be normalized?
// Let's assume strict key matching for now as per previous implementation patterns.

// Actually we need to check if the serverId exists in the allowlist.
const filtered = applyAdminAllowlist(dummyConfig, config.adminAllowlist);
if (!filtered.mcpServers[normalizedId]) {
return {
allowed: false,
reason: getAdminErrorMessage(`Server '${serverId}'`, undefined),
blockType: 'allowlist',
};
}
}

// 3. Allowlist check (mcp.allowed)

// 2. Allowlist check
if (config.allowedList && config.allowedList.length > 0) {
const { found, deprecationWarning } = isInSettingsList(
Expand Down
8 changes: 7 additions & 1 deletion packages/cli/src/deferred.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,13 @@ export async function runDeferredCommand(settings: MergedSettings) {
process.exit(ExitCodes.FATAL_CONFIG_ERROR);
}

await deferredCommand.handler(deferredCommand.argv);
// Inject settings into argv
const argvWithSettings = {
...deferredCommand.argv,
settings,
};

await deferredCommand.handler(argvWithSettings);
await runExitCleanup();
process.exit(ExitCodes.SUCCESS);
}
Expand Down
18 changes: 18 additions & 0 deletions packages/core/src/code_assist/admin/admin_controls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,3 +238,21 @@ export function getAdminErrorMessage(
const projectParam = projectId ? `?project=${projectId}` : '';
return `${featureName} is disabled by your administrator. To enable it, please request an update to the settings at: https://goo.gle/manage-gemini-cli${projectParam}`;
}

/**
* Returns a standardized error message for MCP servers blocked by the admin allowlist.
*
* @param blockedServers List of blocked server names
* @param config The application config
* @returns The formatted error message
*/
export function getAdminBlockedMcpServersMessage(
blockedServers: string[],
config: Config | undefined,
): string {
const server = config ? getCodeAssistServer(config) : undefined;
const projectId = server?.projectId;
const projectParam = projectId ? `?project=${projectId}` : '';
const serversList = blockedServers.join(', ');
return `The following MCP servers are not allowlisted by your administrator: ${serversList}. To enable them, please request an update to the settings at: https://goo.gle/manage-gemini-cli${projectParam}`;
}
Loading
Loading