From 8615315711a8edfe06dee2aafdac3d3f1c6c3558 Mon Sep 17 00:00:00 2001 From: Gaurav <39389231+gsquared94@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:32:43 -0700 Subject: [PATCH] feat(core): add support for admin-forced MCP server installations (#23163) --- docs/admin/enterprise-controls.md | 61 ++++++++ docs/reference/configuration.md | 6 +- packages/cli/src/commands/mcp/list.test.ts | 1 + packages/cli/src/config/config.ts | 20 +++ packages/cli/src/config/settings.test.ts | 22 +++ packages/cli/src/config/settings.ts | 1 + packages/cli/src/config/settingsSchema.ts | 89 ++++++++++- .../code_assist/admin/admin_controls.test.ts | 83 ++++++++++ .../src/code_assist/admin/admin_controls.ts | 11 ++ .../src/code_assist/admin/mcpUtils.test.ts | 148 +++++++++++++++++- .../core/src/code_assist/admin/mcpUtils.ts | 58 ++++++- packages/core/src/code_assist/types.ts | 35 +++++ schemas/settings.schema.json | 85 +++++++++- 13 files changed, 609 insertions(+), 11 deletions(-) diff --git a/docs/admin/enterprise-controls.md b/docs/admin/enterprise-controls.md index 8c9ba60a13..5792a6c5bc 100644 --- a/docs/admin/enterprise-controls.md +++ b/docs/admin/enterprise-controls.md @@ -106,6 +106,67 @@ organization. ensures users maintain final control over which permitted servers are actually active in their environment. +#### Required MCP Servers (preview) + +**Default**: empty + +Allows administrators to define MCP servers that are **always injected** into +the user's environment. Unlike the allowlist (which filters user-configured +servers), required servers are automatically added regardless of the user's +local configuration. + +**Required Servers Format:** + +```json +{ + "requiredMcpServers": { + "corp-compliance-tool": { + "url": "https://mcp.corp/compliance", + "type": "http", + "trust": true, + "description": "Corporate compliance tool" + }, + "internal-registry": { + "url": "https://registry.corp/mcp", + "type": "sse", + "authProviderType": "google_credentials", + "oauth": { + "scopes": ["https://www.googleapis.com/auth/scope"] + } + } + } +} +``` + +**Supported Fields:** + +- `url`: (Required) The full URL of the MCP server endpoint. +- `type`: (Required) The connection type (`sse` or `http`). +- `trust`: (Optional) If set to `true`, tool execution will not require user + approval. Defaults to `true` for required servers. +- `description`: (Optional) Human-readable description of the server. +- `authProviderType`: (Optional) Authentication provider (`dynamic_discovery`, + `google_credentials`, or `service_account_impersonation`). +- `oauth`: (Optional) OAuth configuration including `scopes`, `clientId`, and + `clientSecret`. +- `targetAudience`: (Optional) OAuth target audience for service-to-service + auth. +- `targetServiceAccount`: (Optional) Service account email to impersonate. +- `headers`: (Optional) Additional HTTP headers to send with requests. +- `includeTools` / `excludeTools`: (Optional) Tool filtering lists. +- `timeout`: (Optional) Timeout in milliseconds for MCP requests. + +**Client Enforcement Logic:** + +- Required servers are injected **after** allowlist filtering, so they are + always available even if the allowlist is active. +- If a required server has the **same name** as a locally configured server, the + admin configuration **completely overrides** the local one. +- Required servers only support remote transports (`sse`, `http`). Local + execution fields (`command`, `args`, `env`, `cwd`) are not supported. +- Required servers can coexist with allowlisted servers — both features work + independently. + ### Unmanaged Capabilities **Enabled/Disabled** | Default: disabled diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 81a05bf51c..d3b08d565a 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -1728,7 +1728,11 @@ their corresponding top-level category object in your `settings.json` file. - **Default:** `true` - **`admin.mcp.config`** (object): - - **Description:** Admin-configured MCP servers. + - **Description:** Admin-configured MCP servers (allowlist). + - **Default:** `{}` + +- **`admin.mcp.requiredConfig`** (object): + - **Description:** Admin-required MCP servers that are always injected. - **Default:** `{}` - **`admin.skills.enabled`** (boolean): diff --git a/packages/cli/src/commands/mcp/list.test.ts b/packages/cli/src/commands/mcp/list.test.ts index 54534961dd..578894845e 100644 --- a/packages/cli/src/commands/mcp/list.test.ts +++ b/packages/cli/src/commands/mcp/list.test.ts @@ -264,6 +264,7 @@ describe('mcp list command', () => { config: { 'allowed-server': { url: 'http://allowed' }, }, + requiredConfig: {}, }, }; diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 3c74fd05bd..d5e4851e97 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -36,6 +36,7 @@ import { Config, resolveToRealPath, applyAdminAllowlist, + applyRequiredServers, getAdminBlockedMcpServersMessage, type HookDefinition, type HookEventName, @@ -750,6 +751,25 @@ export async function loadCliConfig( } } + // Apply admin-required MCP servers (injected regardless of allowlist) + if (mcpEnabled) { + const requiredMcpConfig = settings.admin?.mcp?.requiredConfig; + if (requiredMcpConfig && Object.keys(requiredMcpConfig).length > 0) { + const requiredResult = applyRequiredServers( + mcpServers ?? {}, + requiredMcpConfig, + ); + mcpServers = requiredResult.mcpServers; + + if (requiredResult.requiredServerNames.length > 0) { + coreEvents.emitConsoleLog( + 'info', + `Admin-required MCP servers injected: ${requiredResult.requiredServerNames.join(', ')}`, + ); + } + } + } + const isAcpMode = !!argv.acp || !!argv.experimentalAcp; let clientName: string | undefined = undefined; if (isAcpMode) { diff --git a/packages/cli/src/config/settings.test.ts b/packages/cli/src/config/settings.test.ts index 06129a4760..a58b9889a2 100644 --- a/packages/cli/src/config/settings.test.ts +++ b/packages/cli/src/config/settings.test.ts @@ -2751,6 +2751,28 @@ describe('Settings Loading and Merging', () => { expect(loadedSettings.merged.admin?.mcp?.config).toEqual(mcpServers); }); + it('should map requiredMcpConfig from remote settings', () => { + const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR); + const requiredMcpConfig = { + 'corp-tool': { + url: 'https://mcp.corp/tool', + type: 'http' as const, + trust: true, + }, + }; + + loadedSettings.setRemoteAdminSettings({ + mcpSetting: { + mcpEnabled: true, + requiredMcpConfig, + }, + }); + + expect(loadedSettings.merged.admin?.mcp?.requiredConfig).toEqual( + requiredMcpConfig, + ); + }); + 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 711ff93271..beecd6a017 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -480,6 +480,7 @@ export class LoadedSettings { admin.mcp = { enabled: mcpSetting?.mcpEnabled, config: mcpSetting?.mcpConfig?.mcpServers, + requiredConfig: mcpSetting?.requiredMcpConfig, }; admin.extensions = { enabled: cliFeatureSetting?.extensionsSetting?.extensionsEnabled, diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index de8fe65c46..f1711f3b92 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -12,7 +12,9 @@ import { DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD, DEFAULT_MODEL_CONFIGS, + AuthProviderType, type MCPServerConfig, + type RequiredMcpServerConfig, type BugCommandSettings, type TelemetrySettings, type AuthType, @@ -2435,7 +2437,7 @@ const SETTINGS_SCHEMA = { category: 'Admin', requiresRestart: false, default: {} as Record, - description: 'Admin-configured MCP servers.', + description: 'Admin-configured MCP servers (allowlist).', showInDialog: false, mergeStrategy: MergeStrategy.REPLACE, additionalProperties: { @@ -2443,6 +2445,20 @@ const SETTINGS_SCHEMA = { ref: 'MCPServerConfig', }, }, + requiredConfig: { + type: 'object', + label: 'Required MCP Config', + category: 'Admin', + requiresRestart: false, + default: {} as Record, + description: 'Admin-required MCP servers that are always injected.', + showInDialog: false, + mergeStrategy: MergeStrategy.REPLACE, + additionalProperties: { + type: 'object', + ref: 'RequiredMcpServerConfig', + }, + }, }, }, skills: { @@ -2567,11 +2583,72 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record< type: 'string', description: 'Authentication provider used for acquiring credentials (for example `dynamic_discovery`).', - enum: [ - 'dynamic_discovery', - 'google_credentials', - 'service_account_impersonation', - ], + enum: Object.values(AuthProviderType), + }, + targetAudience: { + type: 'string', + description: + 'OAuth target audience (CLIENT_ID.apps.googleusercontent.com).', + }, + targetServiceAccount: { + type: 'string', + description: + 'Service account email to impersonate (name@project.iam.gserviceaccount.com).', + }, + }, + }, + RequiredMcpServerConfig: { + type: 'object', + description: + 'Admin-required MCP server configuration (remote transports only).', + additionalProperties: false, + properties: { + url: { + type: 'string', + description: 'URL for the required MCP server.', + }, + type: { + type: 'string', + description: 'Transport type for the required server.', + enum: ['sse', 'http'], + }, + headers: { + type: 'object', + description: 'Additional HTTP headers sent to the server.', + additionalProperties: { type: 'string' }, + }, + timeout: { + type: 'number', + description: 'Timeout in milliseconds for MCP requests.', + }, + trust: { + type: 'boolean', + description: + 'Marks the server as trusted. Defaults to true for admin-required servers.', + }, + description: { + type: 'string', + description: 'Human-readable description of the server.', + }, + includeTools: { + type: 'array', + description: 'Subset of tools enabled for this server.', + items: { type: 'string' }, + }, + excludeTools: { + type: 'array', + description: 'Tools disabled for this server.', + items: { type: 'string' }, + }, + oauth: { + type: 'object', + description: 'OAuth configuration for authenticating with the server.', + additionalProperties: true, + }, + authProviderType: { + type: 'string', + description: 'Authentication provider used for acquiring credentials.', + enum: Object.values(AuthProviderType), }, targetAudience: { type: 'string', diff --git a/packages/core/src/code_assist/admin/admin_controls.test.ts b/packages/core/src/code_assist/admin/admin_controls.test.ts index d676a59a92..afd80ad758 100644 --- a/packages/core/src/code_assist/admin/admin_controls.test.ts +++ b/packages/core/src/code_assist/admin/admin_controls.test.ts @@ -224,6 +224,89 @@ describe('Admin Controls', () => { const result = sanitizeAdminSettings(input); expect(result.strictModeDisabled).toBe(true); }); + + it('should parse requiredMcpServers from mcpConfigJson', () => { + const mcpConfig = { + mcpServers: { + 'allowed-server': { + url: 'http://allowed.com', + type: 'sse' as const, + }, + }, + requiredMcpServers: { + 'corp-tool': { + url: 'https://mcp.corp/tool', + type: 'http' as const, + trust: true, + description: 'Corp compliance tool', + }, + }, + }; + + const input: FetchAdminControlsResponse = { + mcpSetting: { + mcpEnabled: true, + mcpConfigJson: JSON.stringify(mcpConfig), + }, + }; + + const result = sanitizeAdminSettings(input); + expect(result.mcpSetting?.mcpConfig?.mcpServers).toEqual( + mcpConfig.mcpServers, + ); + expect(result.mcpSetting?.requiredMcpConfig).toEqual( + mcpConfig.requiredMcpServers, + ); + }); + + it('should sort requiredMcpServers tool lists for stable comparison', () => { + const mcpConfig = { + requiredMcpServers: { + 'corp-tool': { + url: 'https://mcp.corp/tool', + type: 'http' as const, + includeTools: ['toolC', 'toolA', 'toolB'], + excludeTools: ['toolZ', 'toolX'], + }, + }, + }; + + const input: FetchAdminControlsResponse = { + mcpSetting: { + mcpEnabled: true, + mcpConfigJson: JSON.stringify(mcpConfig), + }, + }; + + const result = sanitizeAdminSettings(input); + const corpTool = result.mcpSetting?.requiredMcpConfig?.['corp-tool']; + expect(corpTool?.includeTools).toEqual(['toolA', 'toolB', 'toolC']); + expect(corpTool?.excludeTools).toEqual(['toolX', 'toolZ']); + }); + + it('should handle mcpConfigJson with only requiredMcpServers and no mcpServers', () => { + const mcpConfig = { + requiredMcpServers: { + 'required-only': { + url: 'https://required.corp/tool', + type: 'http' as const, + }, + }, + }; + + const input: FetchAdminControlsResponse = { + mcpSetting: { + mcpEnabled: true, + mcpConfigJson: JSON.stringify(mcpConfig), + }, + }; + + const result = sanitizeAdminSettings(input); + expect(result.mcpSetting?.mcpConfig?.mcpServers).toBeUndefined(); + expect(result.mcpSetting?.requiredMcpConfig).toEqual( + mcpConfig.requiredMcpServers, + ); + }); }); describe('isDeepStrictEqual verification', () => { diff --git a/packages/core/src/code_assist/admin/admin_controls.ts b/packages/core/src/code_assist/admin/admin_controls.ts index d18fcf3d66..4812ce013e 100644 --- a/packages/core/src/code_assist/admin/admin_controls.ts +++ b/packages/core/src/code_assist/admin/admin_controls.ts @@ -48,6 +48,16 @@ export function sanitizeAdminSettings( } } } + if (mcpConfig.requiredMcpServers) { + for (const server of Object.values(mcpConfig.requiredMcpServers)) { + if (server.includeTools) { + server.includeTools.sort(); + } + if (server.excludeTools) { + server.excludeTools.sort(); + } + } + } } } catch (_e) { // Ignore parsing errors @@ -77,6 +87,7 @@ export function sanitizeAdminSettings( mcpSetting: { mcpEnabled: sanitized.mcpSetting?.mcpEnabled ?? false, mcpConfig: mcpConfig ?? {}, + requiredMcpConfig: mcpConfig?.requiredMcpServers, }, }; } diff --git a/packages/core/src/code_assist/admin/mcpUtils.test.ts b/packages/core/src/code_assist/admin/mcpUtils.test.ts index 313e654d7d..fadfa59331 100644 --- a/packages/core/src/code_assist/admin/mcpUtils.test.ts +++ b/packages/core/src/code_assist/admin/mcpUtils.test.ts @@ -5,8 +5,10 @@ */ import { describe, it, expect } from 'vitest'; -import { applyAdminAllowlist } from './mcpUtils.js'; +import { applyAdminAllowlist, applyRequiredServers } from './mcpUtils.js'; import type { MCPServerConfig } from '../../config/config.js'; +import { AuthProviderType } from '../../config/config.js'; +import type { RequiredMcpServerConfig } from '../types.js'; describe('applyAdminAllowlist', () => { it('should return original servers if no allowlist provided', () => { @@ -111,3 +113,147 @@ describe('applyAdminAllowlist', () => { expect(result.mcpServers['server1']?.includeTools).toEqual(['local-tool']); }); }); + +describe('applyRequiredServers', () => { + it('should return original servers if no required servers provided', () => { + const mcpServers: Record = { + server1: { command: 'cmd1' }, + }; + const result = applyRequiredServers(mcpServers, undefined); + expect(result.mcpServers).toEqual(mcpServers); + expect(result.requiredServerNames).toEqual([]); + }); + + it('should return original servers if required servers is empty', () => { + const mcpServers: Record = { + server1: { command: 'cmd1' }, + }; + const result = applyRequiredServers(mcpServers, {}); + expect(result.mcpServers).toEqual(mcpServers); + expect(result.requiredServerNames).toEqual([]); + }); + + it('should inject required servers when no local config exists', () => { + const mcpServers: Record = { + 'local-server': { command: 'cmd1' }, + }; + const required: Record = { + 'corp-tool': { + url: 'https://mcp.corp.internal/tool', + type: 'http', + description: 'Corp compliance tool', + }, + }; + + const result = applyRequiredServers(mcpServers, required); + expect(Object.keys(result.mcpServers)).toContain('local-server'); + expect(Object.keys(result.mcpServers)).toContain('corp-tool'); + expect(result.requiredServerNames).toEqual(['corp-tool']); + + const corpTool = result.mcpServers['corp-tool']; + expect(corpTool).toBeDefined(); + expect(corpTool?.url).toBe('https://mcp.corp.internal/tool'); + expect(corpTool?.type).toBe('http'); + expect(corpTool?.description).toBe('Corp compliance tool'); + // trust defaults to true for admin-forced servers + expect(corpTool?.trust).toBe(true); + // stdio fields should not be set + expect(corpTool?.command).toBeUndefined(); + expect(corpTool?.args).toBeUndefined(); + }); + + it('should override local server with same name', () => { + const mcpServers: Record = { + 'shared-server': { + command: 'local-cmd', + args: ['local-arg'], + description: 'Local version', + }, + }; + const required: Record = { + 'shared-server': { + url: 'https://admin.corp/shared', + type: 'sse', + trust: false, + description: 'Admin-mandated version', + }, + }; + + const result = applyRequiredServers(mcpServers, required); + const server = result.mcpServers['shared-server']; + + // Admin config should completely override local + expect(server?.url).toBe('https://admin.corp/shared'); + expect(server?.type).toBe('sse'); + expect(server?.trust).toBe(false); + expect(server?.description).toBe('Admin-mandated version'); + // Local fields should NOT be preserved + expect(server?.command).toBeUndefined(); + expect(server?.args).toBeUndefined(); + }); + + it('should preserve auth configuration', () => { + const required: Record = { + 'auth-server': { + url: 'https://auth.corp/tool', + type: 'http', + authProviderType: AuthProviderType.GOOGLE_CREDENTIALS, + oauth: { + scopes: ['https://www.googleapis.com/auth/scope1'], + }, + targetAudience: 'client-id.apps.googleusercontent.com', + headers: { 'X-Custom': 'value' }, + }, + }; + + const result = applyRequiredServers({}, required); + const server = result.mcpServers['auth-server']; + + expect(server?.authProviderType).toBe(AuthProviderType.GOOGLE_CREDENTIALS); + expect(server?.oauth).toEqual({ + scopes: ['https://www.googleapis.com/auth/scope1'], + }); + expect(server?.targetAudience).toBe('client-id.apps.googleusercontent.com'); + expect(server?.headers).toEqual({ 'X-Custom': 'value' }); + }); + + it('should preserve tool filtering', () => { + const required: Record = { + 'filtered-server': { + url: 'https://corp/tool', + type: 'http', + includeTools: ['toolA', 'toolB'], + excludeTools: ['toolC'], + }, + }; + + const result = applyRequiredServers({}, required); + const server = result.mcpServers['filtered-server']; + + expect(server?.includeTools).toEqual(['toolA', 'toolB']); + expect(server?.excludeTools).toEqual(['toolC']); + }); + + it('should coexist with allowlisted servers', () => { + // Simulate post-allowlist filtering + const afterAllowlist: Record = { + 'allowed-server': { + url: 'http://allowed', + type: 'sse', + trust: true, + }, + }; + const required: Record = { + 'required-server': { + url: 'https://required.corp/tool', + type: 'http', + }, + }; + + const result = applyRequiredServers(afterAllowlist, required); + expect(Object.keys(result.mcpServers)).toHaveLength(2); + expect(result.mcpServers['allowed-server']).toBeDefined(); + expect(result.mcpServers['required-server']).toBeDefined(); + expect(result.requiredServerNames).toEqual(['required-server']); + }); +}); diff --git a/packages/core/src/code_assist/admin/mcpUtils.ts b/packages/core/src/code_assist/admin/mcpUtils.ts index 12c5845d5b..768a40847e 100644 --- a/packages/core/src/code_assist/admin/mcpUtils.ts +++ b/packages/core/src/code_assist/admin/mcpUtils.ts @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type { MCPServerConfig } from '../../config/config.js'; +import { MCPServerConfig } from '../../config/config.js'; +import type { RequiredMcpServerConfig } from '../types.js'; /** * Applies the admin allowlist to the local MCP servers. @@ -65,3 +66,58 @@ export function applyAdminAllowlist( } return { mcpServers: filteredMcpServers, blockedServerNames }; } + +/** + * Applies admin-required MCP servers by injecting them into the MCP server + * list. Required servers always take precedence over locally configured servers + * with the same name and cannot be disabled by the user. + * + * @param mcpServers The current MCP servers (after allowlist filtering). + * @param requiredServers The admin-required MCP server configurations. + * @returns The MCP servers with required servers injected, and the list of + * required server names for informational purposes. + */ +export function applyRequiredServers( + mcpServers: Record, + requiredServers: Record | undefined, +): { + mcpServers: Record; + requiredServerNames: string[]; +} { + if (!requiredServers || Object.keys(requiredServers).length === 0) { + return { mcpServers, requiredServerNames: [] }; + } + + const result: Record = { ...mcpServers }; + const requiredServerNames: string[] = []; + + for (const [serverId, requiredConfig] of Object.entries(requiredServers)) { + requiredServerNames.push(serverId); + + // Convert RequiredMcpServerConfig to MCPServerConfig. + // Required servers completely override any local config with the same name. + result[serverId] = new MCPServerConfig( + undefined, // command (stdio not supported for required servers) + undefined, // args + undefined, // env + undefined, // cwd + requiredConfig.url, // url + undefined, // httpUrl (use url + type instead) + requiredConfig.headers, // headers + undefined, // tcp + requiredConfig.type, // type + requiredConfig.timeout, // timeout + requiredConfig.trust ?? true, // trust defaults to true for admin-forced + requiredConfig.description, // description + requiredConfig.includeTools, // includeTools + requiredConfig.excludeTools, // excludeTools + undefined, // extension + requiredConfig.oauth, // oauth + requiredConfig.authProviderType, // authProviderType + requiredConfig.targetAudience, // targetAudience + requiredConfig.targetServiceAccount, // targetServiceAccount + ); + } + + return { mcpServers: result, requiredServerNames }; +} diff --git a/packages/core/src/code_assist/types.ts b/packages/core/src/code_assist/types.ts index d238d1a75e..d2aa4c3c1d 100644 --- a/packages/core/src/code_assist/types.ts +++ b/packages/core/src/code_assist/types.ts @@ -5,6 +5,7 @@ */ import { z } from 'zod'; +import { AuthProviderType } from '../config/config.js'; export interface ClientMetadata { ideType?: ClientMetadataIdeType; @@ -359,8 +360,41 @@ const McpServerConfigSchema = z.object({ excludeTools: z.array(z.string()).optional(), }); +const RequiredMcpServerOAuthSchema = z.object({ + scopes: z.array(z.string()).optional(), + clientId: z.string().optional(), + clientSecret: z.string().optional(), +}); + +export const RequiredMcpServerConfigSchema = z.object({ + // Connection (required for forced servers) + url: z.string(), + type: z.enum(['sse', 'http']), + + // Auth + authProviderType: z.nativeEnum(AuthProviderType).optional(), + oauth: RequiredMcpServerOAuthSchema.optional(), + targetAudience: z.string().optional(), + targetServiceAccount: z.string().optional(), + headers: z.record(z.string()).optional(), + + // Common + trust: z.boolean().optional(), + timeout: z.number().optional(), + description: z.string().optional(), + + // Tool filtering + includeTools: z.array(z.string()).optional(), + excludeTools: z.array(z.string()).optional(), +}); + +export type RequiredMcpServerConfig = z.infer< + typeof RequiredMcpServerConfigSchema +>; + export const McpConfigDefinitionSchema = z.object({ mcpServers: z.record(McpServerConfigSchema).optional(), + requiredMcpServers: z.record(RequiredMcpServerConfigSchema).optional(), }); export type McpConfigDefinition = z.infer; @@ -377,6 +411,7 @@ export const AdminControlsSettingsSchema = z.object({ .object({ mcpEnabled: z.boolean().optional(), mcpConfig: McpConfigDefinitionSchema.optional(), + requiredMcpConfig: z.record(RequiredMcpServerConfigSchema).optional(), }) .optional(), cliFeatureSetting: CliFeatureSettingSchema.optional(), diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index 17409313ce..9c790c6268 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -3040,13 +3040,23 @@ }, "config": { "title": "MCP Config", - "description": "Admin-configured MCP servers.", - "markdownDescription": "Admin-configured MCP servers.\n\n- Category: `Admin`\n- Requires restart: `no`\n- Default: `{}`", + "description": "Admin-configured MCP servers (allowlist).", + "markdownDescription": "Admin-configured MCP servers (allowlist).\n\n- Category: `Admin`\n- Requires restart: `no`\n- Default: `{}`", "default": {}, "type": "object", "additionalProperties": { "$ref": "#/$defs/MCPServerConfig" } + }, + "requiredConfig": { + "title": "Required MCP Config", + "description": "Admin-required MCP servers that are always injected.", + "markdownDescription": "Admin-required MCP servers that are always injected.\n\n- Category: `Admin`\n- Requires restart: `no`\n- Default: `{}`", + "default": {}, + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/RequiredMcpServerConfig" + } } }, "additionalProperties": false @@ -3181,6 +3191,77 @@ } } }, + "RequiredMcpServerConfig": { + "type": "object", + "description": "Admin-required MCP server configuration (remote transports only).", + "additionalProperties": false, + "properties": { + "url": { + "type": "string", + "description": "URL for the required MCP server." + }, + "type": { + "type": "string", + "description": "Transport type for the required server.", + "enum": ["sse", "http"] + }, + "headers": { + "type": "object", + "description": "Additional HTTP headers sent to the server.", + "additionalProperties": { + "type": "string" + } + }, + "timeout": { + "type": "number", + "description": "Timeout in milliseconds for MCP requests." + }, + "trust": { + "type": "boolean", + "description": "Marks the server as trusted. Defaults to true for admin-required servers." + }, + "description": { + "type": "string", + "description": "Human-readable description of the server." + }, + "includeTools": { + "type": "array", + "description": "Subset of tools enabled for this server.", + "items": { + "type": "string" + } + }, + "excludeTools": { + "type": "array", + "description": "Tools disabled for this server.", + "items": { + "type": "string" + } + }, + "oauth": { + "type": "object", + "description": "OAuth configuration for authenticating with the server.", + "additionalProperties": true + }, + "authProviderType": { + "type": "string", + "description": "Authentication provider used for acquiring credentials.", + "enum": [ + "dynamic_discovery", + "google_credentials", + "service_account_impersonation" + ] + }, + "targetAudience": { + "type": "string", + "description": "OAuth target audience (CLIENT_ID.apps.googleusercontent.com)." + }, + "targetServiceAccount": { + "type": "string", + "description": "Service account email to impersonate (name@project.iam.gserviceaccount.com)." + } + } + }, "TelemetrySettings": { "type": "object", "description": "Telemetry configuration for Gemini CLI.",