@@ -24,6 +24,7 @@ import {
2424import { Storage } from '../config/storage.js' ;
2525import * as tomlLoader from './toml-loader.js' ;
2626import { coreEvents } from '../utils/events.js' ;
27+ import { MCPServerConfig } from '../config/config.js' ;
2728
2829vi . unmock ( '../config/storage.js' ) ;
2930
@@ -279,6 +280,145 @@ describe('createPolicyEngineConfig', () => {
279280 expect ( untrustedRule ) . toBeUndefined ( ) ;
280281 } ) ;
281282
283+ it ( 'should NOT automatically allow configured MCP servers in non-interactive mode by default' , async ( ) => {
284+ const config = await createPolicyEngineConfig (
285+ {
286+ mcpServers : {
287+ 'server-1' : new MCPServerConfig ( 'node' , [ ] ) ,
288+ } ,
289+ } ,
290+ ApprovalMode . DEFAULT ,
291+ MOCK_DEFAULT_DIR ,
292+ false , // non-interactive
293+ ) ;
294+
295+ const rule = config . rules ?. find (
296+ ( r ) => r . mcpName === 'server-1' && r . decision === PolicyDecision . ALLOW ,
297+ ) ;
298+ expect ( rule ) . toBeUndefined ( ) ;
299+ } ) ;
300+
301+ it ( 'should automatically allow configured MCP servers in non-interactive mode if opted-in' , async ( ) => {
302+ const config = await createPolicyEngineConfig (
303+ {
304+ mcp : { autoAllowInHeadless : true } ,
305+ mcpServers : {
306+ 'server-1' : new MCPServerConfig ( 'node' , [ ] ) ,
307+ 'server-2' : new MCPServerConfig ( 'python' , [ ] ) ,
308+ } ,
309+ } ,
310+ ApprovalMode . DEFAULT ,
311+ MOCK_DEFAULT_DIR ,
312+ false , // non-interactive
313+ ) ;
314+
315+ const rule1 = config . rules ?. find (
316+ ( r ) => r . mcpName === 'server-1' && r . decision === PolicyDecision . ALLOW ,
317+ ) ;
318+ const rule2 = config . rules ?. find (
319+ ( r ) => r . mcpName === 'server-2' && r . decision === PolicyDecision . ALLOW ,
320+ ) ;
321+
322+ expect ( rule1 ) . toBeDefined ( ) ;
323+ expect ( rule1 ?. source ) . toBe ( 'Settings (Headless MCP Auto-Allow)' ) ;
324+ expect ( rule2 ) . toBeDefined ( ) ;
325+ expect ( rule2 ?. source ) . toBe ( 'Settings (Headless MCP Auto-Allow)' ) ;
326+ } ) ;
327+
328+ it ( 'should NOT automatically allow configured MCP servers in interactive mode even if opted-in' , async ( ) => {
329+ const config = await createPolicyEngineConfig (
330+ {
331+ mcp : { autoAllowInHeadless : true } ,
332+ mcpServers : {
333+ 'server-1' : new MCPServerConfig ( 'node' , [ ] ) ,
334+ } ,
335+ } ,
336+ ApprovalMode . DEFAULT ,
337+ MOCK_DEFAULT_DIR ,
338+ true , // interactive
339+ ) ;
340+
341+ const rule = config . rules ?. find (
342+ ( r ) => r . mcpName === 'server-1' && r . decision === PolicyDecision . ALLOW ,
343+ ) ;
344+ expect ( rule ) . toBeUndefined ( ) ;
345+ } ) ;
346+
347+ it ( 'should NOT duplicate allow rules if an MCP server is already explicitly allowed, wildcard allowed, or trusted' , async ( ) => {
348+ const config = await createPolicyEngineConfig (
349+ {
350+ mcp : {
351+ autoAllowInHeadless : true ,
352+ allowed : [ 'server-1' , '*' ] ,
353+ } ,
354+ mcpServers : {
355+ 'server-1' : new MCPServerConfig ( 'node' , [ ] ) ,
356+ 'server-2' : new MCPServerConfig ( 'node' , [ ] ) ,
357+ 'server-3' : { trust : true } ,
358+ 'server-4' : new MCPServerConfig ( 'node' , [ ] ) ,
359+ } ,
360+ } ,
361+ ApprovalMode . DEFAULT ,
362+ MOCK_DEFAULT_DIR ,
363+ false , // non-interactive
364+ ) ;
365+
366+ // server-1: already in mcp.allowed
367+ const rules1 = config . rules ?. filter (
368+ ( r ) => r . mcpName === 'server-1' && r . decision === PolicyDecision . ALLOW ,
369+ ) ;
370+ expect ( rules1 ) . toHaveLength ( 1 ) ;
371+ expect ( rules1 ?. [ 0 ] . source ) . toBe ( 'Settings (MCP Allowed)' ) ;
372+
373+ // server-2: covered by '*' in mcp.allowed
374+ // Note: the logic adds a rule for '*' which will match server-2 at runtime,
375+ // but the loop in headless auto-allow should skip adding a specific rule for server-2.
376+ const rules2 = config . rules ?. filter (
377+ ( r ) => r . mcpName === 'server-2' && r . decision === PolicyDecision . ALLOW ,
378+ ) ;
379+ expect ( rules2 ) . toHaveLength ( 0 ) ;
380+
381+ // server-3: already trusted
382+ const rules3 = config . rules ?. filter (
383+ ( r ) => r . mcpName === 'server-3' && r . decision === PolicyDecision . ALLOW ,
384+ ) ;
385+ expect ( rules3 ) . toHaveLength ( 1 ) ;
386+ expect ( rules3 ?. [ 0 ] . source ) . toBe ( 'Settings (MCP Trusted)' ) ;
387+
388+ // server-4: NOT explicitly allowed or trusted, but SHOULD NOT be added because '*' exists in mcp.allowed
389+ const rules4 = config . rules ?. filter (
390+ ( r ) => r . mcpName === 'server-4' && r . decision === PolicyDecision . ALLOW ,
391+ ) ;
392+ expect ( rules4 ) . toHaveLength ( 0 ) ;
393+
394+ // Verify the wildcard rule exists
395+ const wildcardRule = config . rules ?. find (
396+ ( r ) => r . mcpName === '*' && r . decision === PolicyDecision . ALLOW ,
397+ ) ;
398+ expect ( wildcardRule ) . toBeDefined ( ) ;
399+ expect ( wildcardRule ?. toolName ) . toBe ( 'mcp_*' ) ;
400+ } ) ;
401+
402+ it ( 'should use correct tool name pattern for wildcard server in headless auto-allow' , async ( ) => {
403+ const config = await createPolicyEngineConfig (
404+ {
405+ mcp : { autoAllowInHeadless : true } ,
406+ mcpServers : {
407+ '*' : new MCPServerConfig ( 'node' , [ ] ) ,
408+ } ,
409+ } ,
410+ ApprovalMode . DEFAULT ,
411+ MOCK_DEFAULT_DIR ,
412+ false , // non-interactive
413+ ) ;
414+
415+ const rule = config . rules ?. find (
416+ ( r ) => r . mcpName === '*' && r . decision === PolicyDecision . ALLOW ,
417+ ) ;
418+ expect ( rule ) . toBeDefined ( ) ;
419+ expect ( rule ?. toolName ) . toBe ( 'mcp_*' ) ;
420+ } ) ;
421+
282422 it ( 'should handle multiple MCP server configurations together' , async ( ) => {
283423 const config = await createPolicyEngineConfig (
284424 {
0 commit comments