Skip to content

Commit 8615315

Browse files
authored
feat(core): add support for admin-forced MCP server installations (#23163)
1 parent c9a3369 commit 8615315

File tree

13 files changed

+609
-11
lines changed

13 files changed

+609
-11
lines changed

docs/admin/enterprise-controls.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,67 @@ organization.
106106
ensures users maintain final control over which permitted servers are actually
107107
active in their environment.
108108

109+
#### Required MCP Servers (preview)
110+
111+
**Default**: empty
112+
113+
Allows administrators to define MCP servers that are **always injected** into
114+
the user's environment. Unlike the allowlist (which filters user-configured
115+
servers), required servers are automatically added regardless of the user's
116+
local configuration.
117+
118+
**Required Servers Format:**
119+
120+
```json
121+
{
122+
"requiredMcpServers": {
123+
"corp-compliance-tool": {
124+
"url": "https://mcp.corp/compliance",
125+
"type": "http",
126+
"trust": true,
127+
"description": "Corporate compliance tool"
128+
},
129+
"internal-registry": {
130+
"url": "https://registry.corp/mcp",
131+
"type": "sse",
132+
"authProviderType": "google_credentials",
133+
"oauth": {
134+
"scopes": ["https://www.googleapis.com/auth/scope"]
135+
}
136+
}
137+
}
138+
}
139+
```
140+
141+
**Supported Fields:**
142+
143+
- `url`: (Required) The full URL of the MCP server endpoint.
144+
- `type`: (Required) The connection type (`sse` or `http`).
145+
- `trust`: (Optional) If set to `true`, tool execution will not require user
146+
approval. Defaults to `true` for required servers.
147+
- `description`: (Optional) Human-readable description of the server.
148+
- `authProviderType`: (Optional) Authentication provider (`dynamic_discovery`,
149+
`google_credentials`, or `service_account_impersonation`).
150+
- `oauth`: (Optional) OAuth configuration including `scopes`, `clientId`, and
151+
`clientSecret`.
152+
- `targetAudience`: (Optional) OAuth target audience for service-to-service
153+
auth.
154+
- `targetServiceAccount`: (Optional) Service account email to impersonate.
155+
- `headers`: (Optional) Additional HTTP headers to send with requests.
156+
- `includeTools` / `excludeTools`: (Optional) Tool filtering lists.
157+
- `timeout`: (Optional) Timeout in milliseconds for MCP requests.
158+
159+
**Client Enforcement Logic:**
160+
161+
- Required servers are injected **after** allowlist filtering, so they are
162+
always available even if the allowlist is active.
163+
- If a required server has the **same name** as a locally configured server, the
164+
admin configuration **completely overrides** the local one.
165+
- Required servers only support remote transports (`sse`, `http`). Local
166+
execution fields (`command`, `args`, `env`, `cwd`) are not supported.
167+
- Required servers can coexist with allowlisted servers — both features work
168+
independently.
169+
109170
### Unmanaged Capabilities
110171

111172
**Enabled/Disabled** | Default: disabled

docs/reference/configuration.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1728,7 +1728,11 @@ their corresponding top-level category object in your `settings.json` file.
17281728
- **Default:** `true`
17291729

17301730
- **`admin.mcp.config`** (object):
1731-
- **Description:** Admin-configured MCP servers.
1731+
- **Description:** Admin-configured MCP servers (allowlist).
1732+
- **Default:** `{}`
1733+
1734+
- **`admin.mcp.requiredConfig`** (object):
1735+
- **Description:** Admin-required MCP servers that are always injected.
17321736
- **Default:** `{}`
17331737

17341738
- **`admin.skills.enabled`** (boolean):

packages/cli/src/commands/mcp/list.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ describe('mcp list command', () => {
264264
config: {
265265
'allowed-server': { url: 'http://allowed' },
266266
},
267+
requiredConfig: {},
267268
},
268269
};
269270

packages/cli/src/config/config.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
Config,
3737
resolveToRealPath,
3838
applyAdminAllowlist,
39+
applyRequiredServers,
3940
getAdminBlockedMcpServersMessage,
4041
type HookDefinition,
4142
type HookEventName,
@@ -750,6 +751,25 @@ export async function loadCliConfig(
750751
}
751752
}
752753

754+
// Apply admin-required MCP servers (injected regardless of allowlist)
755+
if (mcpEnabled) {
756+
const requiredMcpConfig = settings.admin?.mcp?.requiredConfig;
757+
if (requiredMcpConfig && Object.keys(requiredMcpConfig).length > 0) {
758+
const requiredResult = applyRequiredServers(
759+
mcpServers ?? {},
760+
requiredMcpConfig,
761+
);
762+
mcpServers = requiredResult.mcpServers;
763+
764+
if (requiredResult.requiredServerNames.length > 0) {
765+
coreEvents.emitConsoleLog(
766+
'info',
767+
`Admin-required MCP servers injected: ${requiredResult.requiredServerNames.join(', ')}`,
768+
);
769+
}
770+
}
771+
}
772+
753773
const isAcpMode = !!argv.acp || !!argv.experimentalAcp;
754774
let clientName: string | undefined = undefined;
755775
if (isAcpMode) {

packages/cli/src/config/settings.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2751,6 +2751,28 @@ describe('Settings Loading and Merging', () => {
27512751
expect(loadedSettings.merged.admin?.mcp?.config).toEqual(mcpServers);
27522752
});
27532753

2754+
it('should map requiredMcpConfig from remote settings', () => {
2755+
const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
2756+
const requiredMcpConfig = {
2757+
'corp-tool': {
2758+
url: 'https://mcp.corp/tool',
2759+
type: 'http' as const,
2760+
trust: true,
2761+
},
2762+
};
2763+
2764+
loadedSettings.setRemoteAdminSettings({
2765+
mcpSetting: {
2766+
mcpEnabled: true,
2767+
requiredMcpConfig,
2768+
},
2769+
});
2770+
2771+
expect(loadedSettings.merged.admin?.mcp?.requiredConfig).toEqual(
2772+
requiredMcpConfig,
2773+
);
2774+
});
2775+
27542776
it('should set skills based on unmanagedCapabilitiesEnabled', () => {
27552777
const loadedSettings = loadSettings();
27562778
loadedSettings.setRemoteAdminSettings({

packages/cli/src/config/settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,7 @@ export class LoadedSettings {
480480
admin.mcp = {
481481
enabled: mcpSetting?.mcpEnabled,
482482
config: mcpSetting?.mcpConfig?.mcpServers,
483+
requiredConfig: mcpSetting?.requiredMcpConfig,
483484
};
484485
admin.extensions = {
485486
enabled: cliFeatureSetting?.extensionsSetting?.extensionsEnabled,

packages/cli/src/config/settingsSchema.ts

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
import {
1313
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
1414
DEFAULT_MODEL_CONFIGS,
15+
AuthProviderType,
1516
type MCPServerConfig,
17+
type RequiredMcpServerConfig,
1618
type BugCommandSettings,
1719
type TelemetrySettings,
1820
type AuthType,
@@ -2435,14 +2437,28 @@ const SETTINGS_SCHEMA = {
24352437
category: 'Admin',
24362438
requiresRestart: false,
24372439
default: {} as Record<string, MCPServerConfig>,
2438-
description: 'Admin-configured MCP servers.',
2440+
description: 'Admin-configured MCP servers (allowlist).',
24392441
showInDialog: false,
24402442
mergeStrategy: MergeStrategy.REPLACE,
24412443
additionalProperties: {
24422444
type: 'object',
24432445
ref: 'MCPServerConfig',
24442446
},
24452447
},
2448+
requiredConfig: {
2449+
type: 'object',
2450+
label: 'Required MCP Config',
2451+
category: 'Admin',
2452+
requiresRestart: false,
2453+
default: {} as Record<string, RequiredMcpServerConfig>,
2454+
description: 'Admin-required MCP servers that are always injected.',
2455+
showInDialog: false,
2456+
mergeStrategy: MergeStrategy.REPLACE,
2457+
additionalProperties: {
2458+
type: 'object',
2459+
ref: 'RequiredMcpServerConfig',
2460+
},
2461+
},
24462462
},
24472463
},
24482464
skills: {
@@ -2567,11 +2583,72 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record<
25672583
type: 'string',
25682584
description:
25692585
'Authentication provider used for acquiring credentials (for example `dynamic_discovery`).',
2570-
enum: [
2571-
'dynamic_discovery',
2572-
'google_credentials',
2573-
'service_account_impersonation',
2574-
],
2586+
enum: Object.values(AuthProviderType),
2587+
},
2588+
targetAudience: {
2589+
type: 'string',
2590+
description:
2591+
'OAuth target audience (CLIENT_ID.apps.googleusercontent.com).',
2592+
},
2593+
targetServiceAccount: {
2594+
type: 'string',
2595+
description:
2596+
'Service account email to impersonate (name@project.iam.gserviceaccount.com).',
2597+
},
2598+
},
2599+
},
2600+
RequiredMcpServerConfig: {
2601+
type: 'object',
2602+
description:
2603+
'Admin-required MCP server configuration (remote transports only).',
2604+
additionalProperties: false,
2605+
properties: {
2606+
url: {
2607+
type: 'string',
2608+
description: 'URL for the required MCP server.',
2609+
},
2610+
type: {
2611+
type: 'string',
2612+
description: 'Transport type for the required server.',
2613+
enum: ['sse', 'http'],
2614+
},
2615+
headers: {
2616+
type: 'object',
2617+
description: 'Additional HTTP headers sent to the server.',
2618+
additionalProperties: { type: 'string' },
2619+
},
2620+
timeout: {
2621+
type: 'number',
2622+
description: 'Timeout in milliseconds for MCP requests.',
2623+
},
2624+
trust: {
2625+
type: 'boolean',
2626+
description:
2627+
'Marks the server as trusted. Defaults to true for admin-required servers.',
2628+
},
2629+
description: {
2630+
type: 'string',
2631+
description: 'Human-readable description of the server.',
2632+
},
2633+
includeTools: {
2634+
type: 'array',
2635+
description: 'Subset of tools enabled for this server.',
2636+
items: { type: 'string' },
2637+
},
2638+
excludeTools: {
2639+
type: 'array',
2640+
description: 'Tools disabled for this server.',
2641+
items: { type: 'string' },
2642+
},
2643+
oauth: {
2644+
type: 'object',
2645+
description: 'OAuth configuration for authenticating with the server.',
2646+
additionalProperties: true,
2647+
},
2648+
authProviderType: {
2649+
type: 'string',
2650+
description: 'Authentication provider used for acquiring credentials.',
2651+
enum: Object.values(AuthProviderType),
25752652
},
25762653
targetAudience: {
25772654
type: 'string',

packages/core/src/code_assist/admin/admin_controls.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,89 @@ describe('Admin Controls', () => {
224224
const result = sanitizeAdminSettings(input);
225225
expect(result.strictModeDisabled).toBe(true);
226226
});
227+
228+
it('should parse requiredMcpServers from mcpConfigJson', () => {
229+
const mcpConfig = {
230+
mcpServers: {
231+
'allowed-server': {
232+
url: 'http://allowed.com',
233+
type: 'sse' as const,
234+
},
235+
},
236+
requiredMcpServers: {
237+
'corp-tool': {
238+
url: 'https://mcp.corp/tool',
239+
type: 'http' as const,
240+
trust: true,
241+
description: 'Corp compliance tool',
242+
},
243+
},
244+
};
245+
246+
const input: FetchAdminControlsResponse = {
247+
mcpSetting: {
248+
mcpEnabled: true,
249+
mcpConfigJson: JSON.stringify(mcpConfig),
250+
},
251+
};
252+
253+
const result = sanitizeAdminSettings(input);
254+
expect(result.mcpSetting?.mcpConfig?.mcpServers).toEqual(
255+
mcpConfig.mcpServers,
256+
);
257+
expect(result.mcpSetting?.requiredMcpConfig).toEqual(
258+
mcpConfig.requiredMcpServers,
259+
);
260+
});
261+
262+
it('should sort requiredMcpServers tool lists for stable comparison', () => {
263+
const mcpConfig = {
264+
requiredMcpServers: {
265+
'corp-tool': {
266+
url: 'https://mcp.corp/tool',
267+
type: 'http' as const,
268+
includeTools: ['toolC', 'toolA', 'toolB'],
269+
excludeTools: ['toolZ', 'toolX'],
270+
},
271+
},
272+
};
273+
274+
const input: FetchAdminControlsResponse = {
275+
mcpSetting: {
276+
mcpEnabled: true,
277+
mcpConfigJson: JSON.stringify(mcpConfig),
278+
},
279+
};
280+
281+
const result = sanitizeAdminSettings(input);
282+
const corpTool = result.mcpSetting?.requiredMcpConfig?.['corp-tool'];
283+
expect(corpTool?.includeTools).toEqual(['toolA', 'toolB', 'toolC']);
284+
expect(corpTool?.excludeTools).toEqual(['toolX', 'toolZ']);
285+
});
286+
287+
it('should handle mcpConfigJson with only requiredMcpServers and no mcpServers', () => {
288+
const mcpConfig = {
289+
requiredMcpServers: {
290+
'required-only': {
291+
url: 'https://required.corp/tool',
292+
type: 'http' as const,
293+
},
294+
},
295+
};
296+
297+
const input: FetchAdminControlsResponse = {
298+
mcpSetting: {
299+
mcpEnabled: true,
300+
mcpConfigJson: JSON.stringify(mcpConfig),
301+
},
302+
};
303+
304+
const result = sanitizeAdminSettings(input);
305+
expect(result.mcpSetting?.mcpConfig?.mcpServers).toBeUndefined();
306+
expect(result.mcpSetting?.requiredMcpConfig).toEqual(
307+
mcpConfig.requiredMcpServers,
308+
);
309+
});
227310
});
228311

229312
describe('isDeepStrictEqual verification', () => {

packages/core/src/code_assist/admin/admin_controls.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ export function sanitizeAdminSettings(
4848
}
4949
}
5050
}
51+
if (mcpConfig.requiredMcpServers) {
52+
for (const server of Object.values(mcpConfig.requiredMcpServers)) {
53+
if (server.includeTools) {
54+
server.includeTools.sort();
55+
}
56+
if (server.excludeTools) {
57+
server.excludeTools.sort();
58+
}
59+
}
60+
}
5161
}
5262
} catch (_e) {
5363
// Ignore parsing errors
@@ -77,6 +87,7 @@ export function sanitizeAdminSettings(
7787
mcpSetting: {
7888
mcpEnabled: sanitized.mcpSetting?.mcpEnabled ?? false,
7989
mcpConfig: mcpConfig ?? {},
90+
requiredMcpConfig: mcpConfig?.requiredMcpServers,
8091
},
8192
};
8293
}

0 commit comments

Comments
 (0)