fix: allow configured MCP servers in non-interactive mode (#27215)

This commit is contained in:
Coco Sheng
2026-05-19 10:44:09 -04:00
committed by GitHub
parent 5650fa90d7
commit 1a024f30a3
3 changed files with 173 additions and 0 deletions
+140
View File
@@ -24,6 +24,7 @@ import {
import { Storage } from '../config/storage.js';
import * as tomlLoader from './toml-loader.js';
import { coreEvents } from '../utils/events.js';
import { MCPServerConfig } from '../config/config.js';
vi.unmock('../config/storage.js');
@@ -279,6 +280,145 @@ describe('createPolicyEngineConfig', () => {
expect(untrustedRule).toBeUndefined();
});
it('should NOT automatically allow configured MCP servers in non-interactive mode by default', async () => {
const config = await createPolicyEngineConfig(
{
mcpServers: {
'server-1': new MCPServerConfig('node', []),
},
},
ApprovalMode.DEFAULT,
MOCK_DEFAULT_DIR,
false, // non-interactive
);
const rule = config.rules?.find(
(r) => r.mcpName === 'server-1' && r.decision === PolicyDecision.ALLOW,
);
expect(rule).toBeUndefined();
});
it('should automatically allow configured MCP servers in non-interactive mode if opted-in', async () => {
const config = await createPolicyEngineConfig(
{
mcp: { autoAllowInHeadless: true },
mcpServers: {
'server-1': new MCPServerConfig('node', []),
'server-2': new MCPServerConfig('python', []),
},
},
ApprovalMode.DEFAULT,
MOCK_DEFAULT_DIR,
false, // non-interactive
);
const rule1 = config.rules?.find(
(r) => r.mcpName === 'server-1' && r.decision === PolicyDecision.ALLOW,
);
const rule2 = config.rules?.find(
(r) => r.mcpName === 'server-2' && r.decision === PolicyDecision.ALLOW,
);
expect(rule1).toBeDefined();
expect(rule1?.source).toBe('Settings (Headless MCP Auto-Allow)');
expect(rule2).toBeDefined();
expect(rule2?.source).toBe('Settings (Headless MCP Auto-Allow)');
});
it('should NOT automatically allow configured MCP servers in interactive mode even if opted-in', async () => {
const config = await createPolicyEngineConfig(
{
mcp: { autoAllowInHeadless: true },
mcpServers: {
'server-1': new MCPServerConfig('node', []),
},
},
ApprovalMode.DEFAULT,
MOCK_DEFAULT_DIR,
true, // interactive
);
const rule = config.rules?.find(
(r) => r.mcpName === 'server-1' && r.decision === PolicyDecision.ALLOW,
);
expect(rule).toBeUndefined();
});
it('should NOT duplicate allow rules if an MCP server is already explicitly allowed, wildcard allowed, or trusted', async () => {
const config = await createPolicyEngineConfig(
{
mcp: {
autoAllowInHeadless: true,
allowed: ['server-1', '*'],
},
mcpServers: {
'server-1': new MCPServerConfig('node', []),
'server-2': new MCPServerConfig('node', []),
'server-3': { trust: true },
'server-4': new MCPServerConfig('node', []),
},
},
ApprovalMode.DEFAULT,
MOCK_DEFAULT_DIR,
false, // non-interactive
);
// server-1: already in mcp.allowed
const rules1 = config.rules?.filter(
(r) => r.mcpName === 'server-1' && r.decision === PolicyDecision.ALLOW,
);
expect(rules1).toHaveLength(1);
expect(rules1?.[0].source).toBe('Settings (MCP Allowed)');
// server-2: covered by '*' in mcp.allowed
// Note: the logic adds a rule for '*' which will match server-2 at runtime,
// but the loop in headless auto-allow should skip adding a specific rule for server-2.
const rules2 = config.rules?.filter(
(r) => r.mcpName === 'server-2' && r.decision === PolicyDecision.ALLOW,
);
expect(rules2).toHaveLength(0);
// server-3: already trusted
const rules3 = config.rules?.filter(
(r) => r.mcpName === 'server-3' && r.decision === PolicyDecision.ALLOW,
);
expect(rules3).toHaveLength(1);
expect(rules3?.[0].source).toBe('Settings (MCP Trusted)');
// server-4: NOT explicitly allowed or trusted, but SHOULD NOT be added because '*' exists in mcp.allowed
const rules4 = config.rules?.filter(
(r) => r.mcpName === 'server-4' && r.decision === PolicyDecision.ALLOW,
);
expect(rules4).toHaveLength(0);
// Verify the wildcard rule exists
const wildcardRule = config.rules?.find(
(r) => r.mcpName === '*' && r.decision === PolicyDecision.ALLOW,
);
expect(wildcardRule).toBeDefined();
expect(wildcardRule?.toolName).toBe('mcp_*');
});
it('should use correct tool name pattern for wildcard server in headless auto-allow', async () => {
const config = await createPolicyEngineConfig(
{
mcp: { autoAllowInHeadless: true },
mcpServers: {
'*': new MCPServerConfig('node', []),
},
},
ApprovalMode.DEFAULT,
MOCK_DEFAULT_DIR,
false, // non-interactive
);
const rule = config.rules?.find(
(r) => r.mcpName === '*' && r.decision === PolicyDecision.ALLOW,
);
expect(rule).toBeDefined();
expect(rule?.toolName).toBe('mcp_*');
});
it('should handle multiple MCP server configurations together', async () => {
const config = await createPolicyEngineConfig(
{
+32
View File
@@ -600,6 +600,38 @@ export async function createPolicyEngineConfig(
}
}
// In non-interactive mode, automatically allow all configured MCP servers if opted-in.
// This ensures that tools provided by these servers are available without
// requiring explicit entries in settings.mcp.allowed.
if (
!interactive &&
settings.mcp?.autoAllowInHeadless &&
settings.mcpServers
) {
for (const serverName of Object.keys(settings.mcpServers)) {
// Avoid duplicates if already explicitly allowed, allowed via wildcard, or trusted.
if (
settings.mcp?.allowed?.includes(serverName) ||
settings.mcp?.allowed?.includes('*') ||
settings.mcpServers[serverName].trust
) {
continue;
}
rules.push({
toolName:
serverName === '*'
? `${MCP_TOOL_PREFIX}*`
: `${MCP_TOOL_PREFIX}${serverName}_*`,
mcpName: serverName,
decision: PolicyDecision.ALLOW,
priority: ALLOWED_MCP_SERVER_PRIORITY,
source: 'Settings (Headless MCP Auto-Allow)',
modes: nonPlanModes,
});
}
}
return {
rules,
checkers,
+1
View File
@@ -333,6 +333,7 @@ export interface PolicySettings {
mcp?: {
excluded?: string[];
allowed?: string[];
autoAllowInHeadless?: boolean;
};
tools?: {
core?: string[];