From 4ec19075512157ef26bf022d12f591d4cc9facb2 Mon Sep 17 00:00:00 2001 From: Shreya Keshive Date: Wed, 4 Feb 2026 10:49:39 -0500 Subject: [PATCH 1/8] init --- packages/cli/src/config/config.test.ts | 193 ++++++++++++++++++++++ packages/cli/src/config/config.ts | 44 ++++- packages/cli/src/config/settings.test.ts | 28 +++- packages/cli/src/config/settings.ts | 5 +- packages/cli/src/config/settingsSchema.ts | 14 ++ 5 files changed, 278 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 2ca11be6689..3d1278653d0 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -17,6 +17,7 @@ import { type ExtensionLoader, debugLogger, ApprovalMode, + type MCPServerConfig, } from '@google/gemini-cli-core'; import { loadCliConfig, parseArguments, type CliArgs } from './config.js'; import { type Settings, createTestMergedSettings } from './settings.js'; @@ -1431,6 +1432,198 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { }); }); +describe('loadCliConfig with admin.mcp.config', () => { + beforeEach(() => { + vi.resetAllMocks(); + vi.mocked(os.homedir).mockReturnValue('/mock/home/user'); + vi.stubEnv('GEMINI_API_KEY', 'test-api-key'); + vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + vi.restoreAllMocks(); + }); + + const localMcpServers: Record = { + serverA: { + command: 'npx', + args: ['-y', '@mcp/server-a'], + env: { KEY: 'VALUE' }, + cwd: '/local/cwd', + trust: false, + }, + serverB: { + command: 'npx', + args: ['-y', '@mcp/server-b'], + trust: false, + }, + }; + + const baseSettings = createTestMergedSettings({ + mcp: { serverCommand: 'npx -y @mcp/default-server' }, + mcpServers: localMcpServers, + }); + + it('should use local configuration if admin allowlist is empty', async () => { + process.argv = ['node', 'script.js']; + const argv = await parseArguments(createTestMergedSettings()); + const settings = createTestMergedSettings({ + admin: { + ...baseSettings.admin, + mcp: { enabled: true, config: {} }, + }, + }); + const config = await loadCliConfig(settings, 'test-session', argv); + expect(config.getMcpServers()).toEqual(localMcpServers); + expect(config.getMcpServerCommand()).toBe('npx -y @mcp/default-server'); + }); + + it('should ignore locally configured servers not present in the allowlist', async () => { + process.argv = ['node', 'script.js']; + const argv = await parseArguments(createTestMergedSettings()); + const adminAllowlist: Record = { + serverA: { + type: 'sse', + url: 'https://admin-server-a.com/sse', + trust: true, + }, + }; + const settings = createTestMergedSettings({ + admin: { + ...baseSettings.admin, + mcp: { enabled: true, config: adminAllowlist }, + }, + }); + const config = await loadCliConfig(settings, 'test-session', argv); + + const mergedServers = config.getMcpServers(); + expect(mergedServers).toHaveProperty('serverA'); + expect(mergedServers).not.toHaveProperty('serverB'); + }); + + it('should clear command, args, env, and cwd for present servers', async () => { + process.argv = ['node', 'script.js']; + const argv = await parseArguments(createTestMergedSettings()); + const adminAllowlist: Record = { + serverA: { + type: 'sse', + url: 'https://admin-server-a.com/sse', + trust: true, + }, + }; + const settings = createTestMergedSettings({ + admin: { + ...baseSettings.admin, + mcp: { enabled: true, config: adminAllowlist }, + }, + }); + const config = await loadCliConfig(settings, 'test-session', argv); + + const serverA = config.getMcpServers()?.['serverA']; + expect(serverA).toEqual({ + ...localMcpServers['serverA'], + type: 'sse', + url: 'https://admin-server-a.com/sse', + trust: true, + command: undefined, + args: undefined, + env: undefined, + cwd: undefined, + }); + }); + + it('should not initialize a server if it is in allowlist but missing locally', async () => { + process.argv = ['node', 'script.js']; + const argv = await parseArguments(createTestMergedSettings()); + const adminAllowlist: Record = { + serverC: { + type: 'sse', + url: 'https://admin-server-c.com/sse', + trust: true, + }, + }; + const settings = createTestMergedSettings({ + admin: { + ...baseSettings.admin, + mcp: { enabled: true, config: adminAllowlist }, + }, + }); + const config = await loadCliConfig(settings, 'test-session', argv); + + const mergedServers = config.getMcpServers(); + expect(mergedServers).not.toHaveProperty('serverC'); + expect(Object.keys(mergedServers || {})).toHaveLength(0); + }); + + it('should merge local fields and prefer admin tool filters', async () => { + process.argv = ['node', 'script.js']; + const argv = await parseArguments(createTestMergedSettings()); + const adminAllowlist: Record = { + serverA: { + type: 'sse', + url: 'https://admin-server-a.com/sse', + trust: true, + includeTools: ['admin_tool'], + }, + }; + const localMcpServersWithTools: Record = { + serverA: { + ...localMcpServers['serverA'], + includeTools: ['local_tool'], + timeout: 1234, + }, + }; + const settings = createTestMergedSettings({ + mcpServers: localMcpServersWithTools, + admin: { + ...baseSettings.admin, + mcp: { enabled: true, config: adminAllowlist }, + }, + }); + const config = await loadCliConfig(settings, 'test-session', argv); + + const serverA = config.getMcpServers()?.['serverA']; + expect(serverA).toMatchObject({ + timeout: 1234, + includeTools: ['admin_tool'], + type: 'sse', + url: 'https://admin-server-a.com/sse', + trust: true, + }); + expect(serverA?.command).toBeUndefined(); + }); + + it('should use local tool filters when admin does not define them', async () => { + process.argv = ['node', 'script.js']; + const argv = await parseArguments(createTestMergedSettings()); + const adminAllowlist: Record = { + serverA: { + type: 'sse', + url: 'https://admin-server-a.com/sse', + trust: true, + }, + }; + const localMcpServersWithTools: Record = { + serverA: { + ...localMcpServers['serverA'], + includeTools: ['local_tool'], + }, + }; + const settings = createTestMergedSettings({ + mcpServers: localMcpServersWithTools, + admin: { + ...baseSettings.admin, + mcp: { enabled: true, config: adminAllowlist }, + }, + }); + const config = await loadCliConfig(settings, 'test-session', argv); + + const serverA = config.getMcpServers()?.['serverA']; + expect(serverA?.includeTools).toEqual(['local_tool']); + }); +}); + describe('loadCliConfig model selection', () => { beforeEach(() => { vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]); diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 7890d7e08d8..c0237cb2432 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -11,7 +11,8 @@ import { mcpCommand } from '../commands/mcp.js'; import { extensionsCommand } from '../commands/extensions.js'; import { skillsCommand } from '../commands/skills.js'; import { hooksCommand } from '../commands/hooks.js'; -import { +import type { + MCPServerConfig, Config, setGeminiMdFilename as setServerGeminiMdFilename, getCurrentGeminiMdFilename, @@ -38,7 +39,7 @@ import { type OutputFormat, coreEvents, GEMINI_MODEL_ALIAS_AUTO, - getAdminErrorMessage, + getAdminErrorMessage } from '@google/gemini-cli-core'; import { type Settings, @@ -682,6 +683,41 @@ export async function loadCliConfig( ? mcpEnablementManager.getEnablementCallbacks() : undefined; + const adminAllowlist = settings.admin?.mcp?.config; + let mcpServerCommand = mcpEnabled ? settings.mcp?.serverCommand : undefined; + let mcpServers = mcpEnabled ? settings.mcpServers : {}; + + if (mcpEnabled && adminAllowlist && Object.keys(adminAllowlist).length > 0) { + const filteredMcpServers: Record = {}; + 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, + command: undefined, + args: undefined, + env: undefined, + cwd: undefined, + }; + + 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; + mcpServerCommand = undefined; + } + return new Config({ sessionId, clientVersion: await getVersion(), @@ -701,8 +737,8 @@ export async function loadCliConfig( excludeTools, toolDiscoveryCommand: settings.tools?.discoveryCommand, toolCallCommand: settings.tools?.callCommand, - mcpServerCommand: mcpEnabled ? settings.mcp?.serverCommand : undefined, - mcpServers: mcpEnabled ? settings.mcpServers : {}, + mcpServerCommand, + mcpServers, mcpEnablementCallbacks, mcpEnabled, extensionsEnabled, diff --git a/packages/cli/src/config/settings.test.ts b/packages/cli/src/config/settings.test.ts index 449a5e0b0bb..e9535db95cd 100644 --- a/packages/cli/src/config/settings.test.ts +++ b/packages/cli/src/config/settings.test.ts @@ -75,7 +75,11 @@ import { SettingScope, LoadedSettings, } from './settings.js'; -import { FatalConfigError, GEMINI_DIR } from '@google/gemini-cli-core'; +import { + FatalConfigError, + GEMINI_DIR, + type MCPServerConfig, +} from '@google/gemini-cli-core'; import { updateSettingsFilePreservingFormat } from '../utils/commentJson.js'; import { getSettingsSchema, @@ -2306,6 +2310,28 @@ describe('Settings Loading and Merging', () => { expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(true); }); + it('should un-nest MCP configuration from remote settings', () => { + const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR); + const mcpServers: Record = { + 'admin-server': { + url: 'http://admin-mcp.com', + type: 'sse', + trust: true, + }, + }; + + loadedSettings.setRemoteAdminSettings({ + mcpSetting: { + mcpEnabled: true, + mcpConfig: { + mcpServers, + }, + }, + }); + + expect(loadedSettings.merged.admin?.mcp?.config).toEqual(mcpServers); + }); + it('should set skills based on unmanagedCapabilitiesEnabled', () => { const loadedSettings = loadSettings(); loadedSettings.setRemoteAdminSettings({ diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index 90cef7c7fe5..0f316ef365b 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -360,7 +360,10 @@ export class LoadedSettings { } admin.secureModeEnabled = !strictModeDisabled; - admin.mcp = { enabled: mcpSetting?.mcpEnabled }; + admin.mcp = { + enabled: mcpSetting?.mcpEnabled, + config: mcpSetting?.mcpConfig?.mcpServers, + }; admin.extensions = { enabled: cliFeatureSetting?.extensionsSetting?.extensionsEnabled, }; diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 63718dad0b8..a1410fea436 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -1867,6 +1867,20 @@ const SETTINGS_SCHEMA = { showInDialog: false, mergeStrategy: MergeStrategy.REPLACE, }, + config: { + type: 'object', + label: 'MCP Config', + category: 'Admin', + requiresRestart: false, + default: {} as Record, + description: 'Admin-configured MCP servers.', + showInDialog: false, + mergeStrategy: MergeStrategy.REPLACE, + additionalProperties: { + type: 'object', + ref: 'MCPServerConfig', + }, + }, }, }, skills: { From 323f71b8fd6ea67abee631fa277769bf840b8189 Mon Sep 17 00:00:00 2001 From: Shreya Keshive Date: Wed, 4 Feb 2026 11:34:35 -0500 Subject: [PATCH 2/8] fix build --- packages/cli/src/config/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index c0237cb2432..18668b2d622 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -39,7 +39,7 @@ import type { type OutputFormat, coreEvents, GEMINI_MODEL_ALIAS_AUTO, - getAdminErrorMessage + getAdminErrorMessage, } from '@google/gemini-cli-core'; import { type Settings, From b2c63de875d33cd9c23da6eae8b8f68f785e0830 Mon Sep 17 00:00:00 2001 From: Shreya Keshive Date: Wed, 4 Feb 2026 11:55:08 -0500 Subject: [PATCH 3/8] fix --- docs/get-started/configuration.md | 4 ++++ packages/cli/src/config/config.test.ts | 2 ++ packages/cli/src/config/config.ts | 2 ++ schemas/settings.schema.json | 10 ++++++++++ 4 files changed, 18 insertions(+) diff --git a/docs/get-started/configuration.md b/docs/get-started/configuration.md index bc6c47a1f28..af89f699784 100644 --- a/docs/get-started/configuration.md +++ b/docs/get-started/configuration.md @@ -995,6 +995,10 @@ their corresponding top-level category object in your `settings.json` file. - **Description:** If false, disallows MCP servers from being used. - **Default:** `true` +- **`admin.mcp.config`** (object): + - **Description:** Admin-configured MCP servers. + - **Default:** `{}` + - **`admin.skills.enabled`** (boolean): - **Description:** If false, disallows agent skills from being used. - **Default:** `true` diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 3d1278653d0..856d1e2870e 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -1530,6 +1530,8 @@ describe('loadCliConfig with admin.mcp.config', () => { args: undefined, env: undefined, cwd: undefined, + httpUrl: undefined, + tcp: undefined, }); }); diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 18668b2d622..0162cfa05ff 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -701,6 +701,8 @@ export async function loadCliConfig( args: undefined, env: undefined, cwd: undefined, + httpUrl: undefined, + tcp: undefined, }; if ( diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index 23aa7e1de00..030a4018ccf 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -1712,6 +1712,16 @@ "markdownDescription": "If false, disallows MCP servers from being used.\n\n- Category: `Admin`\n- Requires restart: `no`\n- Default: `true`", "default": true, "type": "boolean" + }, + "config": { + "title": "MCP Config", + "description": "Admin-configured MCP servers.", + "markdownDescription": "Admin-configured MCP servers.\n\n- Category: `Admin`\n- Requires restart: `no`\n- Default: `{}`", + "default": {}, + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/MCPServerConfig" + } } }, "additionalProperties": false From 2ac5bf9820a70a70d05dd74eebda6bf9253ca4a7 Mon Sep 17 00:00:00 2001 From: Shreya Keshive Date: Wed, 4 Feb 2026 11:58:29 -0500 Subject: [PATCH 4/8] fix --- packages/cli/src/config/config.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 0162cfa05ff..c027e311d97 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -11,9 +11,7 @@ import { mcpCommand } from '../commands/mcp.js'; import { extensionsCommand } from '../commands/extensions.js'; import { skillsCommand } from '../commands/skills.js'; import { hooksCommand } from '../commands/hooks.js'; -import type { - MCPServerConfig, - Config, +import { setGeminiMdFilename as setServerGeminiMdFilename, getCurrentGeminiMdFilename, ApprovalMode, @@ -34,13 +32,17 @@ import type { WEB_FETCH_TOOL_NAME, getVersion, PREVIEW_GEMINI_MODEL_AUTO, - type HookDefinition, - type HookEventName, - type OutputFormat, coreEvents, GEMINI_MODEL_ALIAS_AUTO, getAdminErrorMessage, } from '@google/gemini-cli-core'; +import type { + MCPServerConfig, + Config, + HookDefinition, + HookEventName, + OutputFormat, +} from '@google/gemini-cli-core'; import { type Settings, type MergedSettings, From bd752aae7327cd1c24e592b2f8f27b98c506d304 Mon Sep 17 00:00:00 2001 From: Shreya Keshive Date: Wed, 4 Feb 2026 12:00:51 -0500 Subject: [PATCH 5/8] fix --- packages/cli/src/config/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index c027e311d97..dc92edc218a 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -35,10 +35,10 @@ import { coreEvents, GEMINI_MODEL_ALIAS_AUTO, getAdminErrorMessage, + Config, } from '@google/gemini-cli-core'; import type { MCPServerConfig, - Config, HookDefinition, HookEventName, OutputFormat, From 18d8f6664905b725c7d6f2160242dfc4c1673bdc Mon Sep 17 00:00:00 2001 From: Shreya Keshive Date: Wed, 4 Feb 2026 12:22:30 -0500 Subject: [PATCH 6/8] fix test --- packages/cli/src/config/config.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 856d1e2870e..782a2bb3d68 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -1469,6 +1469,7 @@ describe('loadCliConfig with admin.mcp.config', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments(createTestMergedSettings()); const settings = createTestMergedSettings({ + mcpServers: localMcpServers, admin: { ...baseSettings.admin, mcp: { enabled: true, config: {} }, @@ -1490,6 +1491,7 @@ describe('loadCliConfig with admin.mcp.config', () => { }, }; const settings = createTestMergedSettings({ + mcpServers: localMcpServers, admin: { ...baseSettings.admin, mcp: { enabled: true, config: adminAllowlist }, @@ -1513,6 +1515,7 @@ describe('loadCliConfig with admin.mcp.config', () => { }, }; const settings = createTestMergedSettings({ + mcpServers: localMcpServers, admin: { ...baseSettings.admin, mcp: { enabled: true, config: adminAllowlist }, @@ -1546,6 +1549,7 @@ describe('loadCliConfig with admin.mcp.config', () => { }, }; const settings = createTestMergedSettings({ + mcpServers: localMcpServers, admin: { ...baseSettings.admin, mcp: { enabled: true, config: adminAllowlist }, From 929a6f4aaa985d4b8915bf65ca231dbbd4a3d89a Mon Sep 17 00:00:00 2001 From: Shreya Keshive Date: Wed, 4 Feb 2026 12:24:11 -0500 Subject: [PATCH 7/8] actually fix --- packages/cli/src/config/config.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 782a2bb3d68..544510ab6dc 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -1469,6 +1469,7 @@ describe('loadCliConfig with admin.mcp.config', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments(createTestMergedSettings()); const settings = createTestMergedSettings({ + mcp: baseSettings.mcp, mcpServers: localMcpServers, admin: { ...baseSettings.admin, @@ -1491,6 +1492,7 @@ describe('loadCliConfig with admin.mcp.config', () => { }, }; const settings = createTestMergedSettings({ + mcp: baseSettings.mcp, mcpServers: localMcpServers, admin: { ...baseSettings.admin, From d5aa5d0b99596e24850250dea9a69598fa26afb3 Mon Sep 17 00:00:00 2001 From: Shreya Keshive Date: Wed, 4 Feb 2026 17:21:55 -0500 Subject: [PATCH 8/8] fix --- packages/cli/src/config/config.test.ts | 7 ++++++- packages/cli/src/config/config.ts | 14 ++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 544510ab6dc..10bf886a897 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -1599,7 +1599,12 @@ describe('loadCliConfig with admin.mcp.config', () => { url: 'https://admin-server-a.com/sse', trust: true, }); - expect(serverA?.command).toBeUndefined(); + expect(serverA).not.toHaveProperty('command'); + expect(serverA).not.toHaveProperty('args'); + expect(serverA).not.toHaveProperty('env'); + expect(serverA).not.toHaveProperty('cwd'); + expect(serverA).not.toHaveProperty('httpUrl'); + expect(serverA).not.toHaveProperty('tcp'); }); it('should use local tool filters when admin does not define them', async () => { diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index dc92edc218a..a73ef50a4ac 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -699,14 +699,16 @@ export async function loadCliConfig( url: adminConfig.url, type: adminConfig.type, trust: adminConfig.trust, - command: undefined, - args: undefined, - env: undefined, - cwd: undefined, - httpUrl: undefined, - tcp: undefined, }; + // 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)