From 58f1aa6ceb53df94cc5bba77dc787950be340cb9 Mon Sep 17 00:00:00 2001 From: christine betts Date: Tue, 15 Jul 2025 20:45:24 +0000 Subject: [PATCH] Add support for allowed/excluded MCP server names in settings (#4135) Co-authored-by: Scott Densmore --- docs/cli/configuration.md | 12 +++++ packages/cli/src/config/config.test.ts | 60 ++++++++++++++++++++++++ packages/cli/src/config/config.ts | 20 ++++++++ packages/cli/src/config/settings.test.ts | 3 ++ packages/cli/src/config/settings.ts | 2 + 5 files changed, 97 insertions(+) diff --git a/docs/cli/configuration.md b/docs/cli/configuration.md index e6a9ee7258..8ac4fac916 100644 --- a/docs/cli/configuration.md +++ b/docs/cli/configuration.md @@ -81,6 +81,18 @@ In addition to a project settings file, a project's `.gemini` directory can cont `excludeTools` for `run_shell_command` are based on simple string matching and can be easily bypassed. This feature is **not a security mechanism** and should not be relied upon to safely execute untrusted code. It is recommended to use `coreTools` to explicitly select commands that can be executed. +- **`allowMCPServers`** (array of strings): + - **Description:** Allows you to specify a list of MCP server names that should be made available to the model. This can be used to restrict the set of MCP servers to connect to. Note that this will be ignored if `--allowed-mcp-server-names` is set. + - **Default:** All MCP servers are available for use by the Gemini model. + - **Example:** `"allowMCPServers": ["myPythonServer"]`. + - **Security Note:** This uses simple string matching on MCP server names, which can be modified. If you're a system administrator looking to prevent users from bypassing this, consider configuring the `mcpServers` at the system settings level such that the user will not be able to configure any MCP servers of their own. This should not be used as an airtight security mechanism. + +- **`excludeMCPServers`** (array of strings): + - **Description:** Allows you to specify a list of MCP server names that should be excluded from the model. A server listed in both `excludeMCPServers` and `allowMCPServers` is excluded. Note that this will be ignored if `--allowed-mcp-server-names` is set. + - **Default**: No MCP servers excluded. + - **Example:** `"excludeMCPServers": ["myNodeServer"]`. + - **Security Note:** This uses simple string matching on MCP server names, which can be modified. If you're a system administrator looking to prevent users from bypassing this, consider configuring the `mcpServers` at the system settings level such that the user will not be able to configure any MCP servers of their own. This should not be used as an airtight security mechanism. + - **`autoAccept`** (boolean): - **Description:** Controls whether the CLI automatically accepts and executes tool calls that are considered safe (e.g., read-only operations) without explicit user confirmation. If set to `true`, the CLI will bypass the confirmation prompt for tools deemed safe. - **Default:** `false` diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 5043fd5965..4042bf932d 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -725,6 +725,66 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { const config = await loadCliConfig(baseSettings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual({}); }); + + it('should read allowMCPServers from settings', async () => { + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); + const settings: Settings = { + ...baseSettings, + allowMCPServers: ['server1', 'server2'], + }; + const config = await loadCliConfig(settings, [], 'test-session', argv); + expect(config.getMcpServers()).toEqual({ + server1: { url: 'http://localhost:8080' }, + server2: { url: 'http://localhost:8081' }, + }); + }); + + it('should read excludeMCPServers from settings', async () => { + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); + const settings: Settings = { + ...baseSettings, + excludeMCPServers: ['server1', 'server2'], + }; + const config = await loadCliConfig(settings, [], 'test-session', argv); + expect(config.getMcpServers()).toEqual({ + server3: { url: 'http://localhost:8082' }, + }); + }); + + it('should override allowMCPServers with excludeMCPServers if overlapping ', async () => { + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); + const settings: Settings = { + ...baseSettings, + excludeMCPServers: ['server1'], + allowMCPServers: ['server1', 'server2'], + }; + const config = await loadCliConfig(settings, [], 'test-session', argv); + expect(config.getMcpServers()).toEqual({ + server2: { url: 'http://localhost:8081' }, + }); + }); + + it('should prioritize mcp server flag if set ', async () => { + process.argv = [ + 'node', + 'script.js', + '--allowed-mcp-server-names', + 'server1', + ]; + const argv = await parseArguments(); + const settings: Settings = { + ...baseSettings, + excludeMCPServers: ['server1'], + allowMCPServers: ['server2'], + }; + const config = await loadCliConfig(settings, [], 'test-session', argv); + expect(config.getMcpServers()).toEqual({ + server1: { url: 'http://localhost:8080' }, + }); + }); }); describe('loadCliConfig extensions', () => { diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index d116bc6775..bf76fa4c25 100644 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -274,6 +274,26 @@ export async function loadCliConfig( let mcpServers = mergeMcpServers(settings, activeExtensions); const excludeTools = mergeExcludeTools(settings, activeExtensions); + if (!argv.allowedMcpServerNames) { + if (settings.allowMCPServers) { + const allowedNames = new Set(settings.allowMCPServers.filter(Boolean)); + if (allowedNames.size > 0) { + mcpServers = Object.fromEntries( + Object.entries(mcpServers).filter(([key]) => allowedNames.has(key)), + ); + } + } + + if (settings.excludeMCPServers) { + const excludedNames = new Set(settings.excludeMCPServers.filter(Boolean)); + if (excludedNames.size > 0) { + mcpServers = Object.fromEntries( + Object.entries(mcpServers).filter(([key]) => !excludedNames.has(key)), + ); + } + } + } + if (argv.allowedMcpServerNames) { const allowedNames = new Set(argv.allowedMcpServerNames.filter(Boolean)); if (allowedNames.size > 0) { diff --git a/packages/cli/src/config/settings.test.ts b/packages/cli/src/config/settings.test.ts index 44de24fe1d..698ba74502 100644 --- a/packages/cli/src/config/settings.test.ts +++ b/packages/cli/src/config/settings.test.ts @@ -223,6 +223,7 @@ describe('Settings Loading and Merging', () => { const systemSettingsContent = { theme: 'system-theme', sandbox: false, + allowMCPServers: ['server1', 'server2'], telemetry: { enabled: false }, }; const userSettingsContent = { @@ -234,6 +235,7 @@ describe('Settings Loading and Merging', () => { sandbox: false, coreTools: ['tool1'], contextFileName: 'WORKSPACE_CONTEXT.md', + allowMCPServers: ['server1', 'server2', 'server3'], }; (fs.readFileSync as Mock).mockImplementation( @@ -259,6 +261,7 @@ describe('Settings Loading and Merging', () => { telemetry: { enabled: false }, coreTools: ['tool1'], contextFileName: 'WORKSPACE_CONTEXT.md', + allowMCPServers: ['server1', 'server2'], }); }); diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index f0258db33d..604e89dc33 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -64,6 +64,8 @@ export interface Settings { toolCallCommand?: string; mcpServerCommand?: string; mcpServers?: Record; + allowMCPServers?: string[]; + excludeMCPServers?: string[]; showMemoryUsage?: boolean; contextFileName?: string | string[]; accessibility?: AccessibilitySettings;