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 e6a9ee72..8ac4fac9 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 5043fd59..4042bf93 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 d116bc67..bf76fa4c 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 44de24fe..698ba745 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 f0258db3..604e89dc 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;