mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-10 19:37:17 -07:00
feat(admin): apply MCP allowlist to extensions & gemini mcp list command (#18442)
This commit is contained in:
@@ -32,6 +32,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
return {
|
||||
...original,
|
||||
createTransport: vi.fn(),
|
||||
|
||||
MCPServerStatus: {
|
||||
CONNECTED: 'CONNECTED',
|
||||
CONNECTING: 'CONNECTING',
|
||||
@@ -223,4 +224,46 @@ 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' },
|
||||
};
|
||||
|
||||
mockedLoadSettings.mockReturnValue({
|
||||
merged: settingsWithAllowlist,
|
||||
});
|
||||
|
||||
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(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
@@ -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]) {
|
||||
@@ -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(
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1511,7 +1511,7 @@ describe('loadCliConfig with admin.mcp.config', () => {
|
||||
});
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
|
||||
const mergedServers = config.getMcpServers();
|
||||
const mergedServers = config.getMcpServers() ?? {};
|
||||
expect(mergedServers).toHaveProperty('serverA');
|
||||
expect(mergedServers).not.toHaveProperty('serverB');
|
||||
});
|
||||
@@ -1569,9 +1569,9 @@ describe('loadCliConfig with admin.mcp.config', () => {
|
||||
});
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
|
||||
const mergedServers = config.getMcpServers();
|
||||
const mergedServers = config.getMcpServers() ?? {};
|
||||
expect(mergedServers).not.toHaveProperty('serverC');
|
||||
expect(Object.keys(mergedServers || {})).toHaveLength(0);
|
||||
expect(Object.keys(mergedServers)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should merge local fields and prefer admin tool filters', async () => {
|
||||
@@ -1601,7 +1601,7 @@ describe('loadCliConfig with admin.mcp.config', () => {
|
||||
});
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
|
||||
const serverA = config.getMcpServers()?.['serverA'];
|
||||
const serverA = (config.getMcpServers() ?? {})['serverA'];
|
||||
expect(serverA).toMatchObject({
|
||||
timeout: 1234,
|
||||
includeTools: ['admin_tool'],
|
||||
|
||||
@@ -36,9 +36,10 @@ import {
|
||||
GEMINI_MODEL_ALIAS_AUTO,
|
||||
getAdminErrorMessage,
|
||||
Config,
|
||||
applyAdminAllowlist,
|
||||
getAdminBlockedMcpServersMessage,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type {
|
||||
MCPServerConfig,
|
||||
HookDefinition,
|
||||
HookEventName,
|
||||
OutputFormat,
|
||||
@@ -692,38 +693,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;
|
||||
}
|
||||
|
||||
filteredMcpServers[serverId] = mergedConfig;
|
||||
}
|
||||
}
|
||||
mcpServers = filteredMcpServers;
|
||||
const result = applyAdminAllowlist(mcpServers, adminAllowlist);
|
||||
mcpServers = result.mcpServers;
|
||||
mcpServerCommand = undefined;
|
||||
|
||||
if (result.blockedServerNames && result.blockedServerNames.length > 0) {
|
||||
const message = getAdminBlockedMcpServersMessage(
|
||||
result.blockedServerNames,
|
||||
undefined,
|
||||
);
|
||||
coreEvents.emitConsoleLog('warn', message);
|
||||
}
|
||||
}
|
||||
|
||||
return new Config({
|
||||
|
||||
@@ -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';
|
||||
@@ -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),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -167,7 +167,15 @@ describe('deferred', () => {
|
||||
|
||||
// Now manually run it to verify it captured correctly
|
||||
await runDeferredCommand(createMockSettings().merged);
|
||||
expect(originalHandler).toHaveBeenCalledWith(argv);
|
||||
expect(originalHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
settings: expect.objectContaining({
|
||||
admin: expect.objectContaining({
|
||||
extensions: expect.objectContaining({ enabled: true }),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(mockExit).toHaveBeenCalledWith(ExitCodes.SUCCESS);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user