@@ -144,7 +144,7 @@ export const mcpOptions = {
144144 describe :
145145 "Restricts browser's network access by blocking specified URL patterns (uses https://urlpattern.spec.whatwg.org/). Silently detaches from targets with blocked URLs upon connection, and blocks runtime requests (including navigations and subresources). Accepts an array of patterns. A pattern that uses a regexp group in any component (for example `(127\\.\\d+\\.\\d+\\.\\d+)` in the hostname) is rejected, because it is not enforced on redirects or subresources; use an exact value or a `*`/`:name` wildcard instead." ,
146146 coerce : ( arg : string [ ] | undefined ) => {
147- if ( arg === undefined ) {
147+ if ( arg === undefined || arg . length === 0 ) {
148148 return undefined ;
149149 }
150150 const pattern = findUnenforceablePattern ( arg ) ;
@@ -165,6 +165,11 @@ export const mcpOptions = {
165165 if ( arg === undefined ) {
166166 return undefined ;
167167 }
168+ if ( arg . length === 0 ) {
169+ throw new Error (
170+ 'Invalid --allowedUrlPattern: at least one pattern is required.' ,
171+ ) ;
172+ }
168173 const pattern = findUnenforceablePattern ( arg ) ;
169174 if ( pattern ) {
170175 throw new Error (
@@ -319,7 +324,9 @@ export const mcpOptions = {
319324 } ,
320325} satisfies Record < string , YargsOptions > ;
321326
322- export type ParsedArguments = ReturnType < typeof parseArguments > ;
327+ export type ParsedArguments = ReturnType <
328+ ReturnType < typeof parser > [ 'parseSync' ]
329+ > ;
323330
324331export function getMcpOptionsForViaCli ( ) : typeof mcpOptions {
325332 if ( ! ( 'default' in mcpOptions . headless ) ) {
@@ -466,140 +473,165 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
466473 return typeof value === 'object' && value !== null && ! Array . isArray ( value ) ;
467474}
468475
476+ function isParsedArguments ( value : unknown ) : value is ParsedArguments {
477+ return isPlainObject ( value ) && Array . isArray ( value [ '_' ] ) ;
478+ }
479+
469480function getErrorMessage ( err : unknown ) : string {
470481 return err instanceof Error ? err . message : String ( err ) ;
471482}
472483
473- /**
474- * Exported only for testing to not trigger process exit.
475- */
476- export function parser (
484+ function buildCliParser < T extends Record < string , YargsOptions > > (
477485 version : string ,
478- argv = process . argv ,
479- env = process . env ,
486+ argv : string [ ] ,
487+ options : T ,
480488) {
481- const isViaCli = argv . includes ( '--viaCli' ) || argv . includes ( '--via-cli' ) ;
482- const options = isViaCli ? getMcpOptionsForViaCli ( ) : mcpOptions ;
483-
484- const yargsInstance = yargs ( hideBin ( argv ) )
489+ const yargsInstance = yargs ( hideBin ( argv ) ) ;
490+ return yargsInstance
485491 . scriptName ( 'npx chrome-devtools-mcp@latest' )
486492 . parserConfiguration ( {
487493 'strip-aliased' : true ,
488494 'strip-dashed' : true ,
489495 } )
490496 . options ( options )
491497 . showHelpOnFail ( false , 'Specify --help for available options' )
492- . check ( args => {
493- const activeArgs = new Set < string > ( ) ;
494-
495- for ( const [ key , val ] of Object . entries ( args ) ) {
496- if ( val !== undefined && val !== false ) {
497- activeArgs . add ( key ) ;
498- }
499- }
500-
501- for ( const group of CONFLICTING_ARGS ) {
502- // Find all active arguments within this conflict group
503- const activeInGroup = group . filter ( arg => activeArgs . has ( arg ) ) ;
504-
505- if ( activeInGroup . length > 1 ) {
506- const [ arg1 , arg2 ] = activeInGroup ;
507- throw new Error (
508- `Arguments ${ arg1 } and ${ arg2 } are mutually exclusive` ,
509- ) ;
510- }
511- }
512-
513- return true ;
514- } )
515- . middleware ( args => {
516- args . channel = args . channel ?? 'stable' ;
517- if ( isViaCli ) {
518- if ( args . filesystemRoot === DEFAULT_FILESYSTEM_ROOT ) {
519- const cliFilesystemArgs : {
520- allowUnrestrictedPaths ?: boolean ;
521- filesystemRoot ?: unknown ;
522- } = args ;
523- cliFilesystemArgs . allowUnrestrictedPaths = true ;
524- cliFilesystemArgs . filesystemRoot = undefined ;
525- }
526- // Defaults that cannot be set in options without affecting yargs conflict resolution.
527- if (
528- args . isolated === undefined &&
529- args . userDataDir === undefined &&
530- ! args . autoConnect &&
531- ! args . browserUrl &&
532- ! args . wsEndpoint
533- ) {
534- args . isolated = true ;
535- }
536- }
537- args . isolated = args . isolated ?? false ;
538- if (
539- args . experimentalToonFormat &&
540- args . experimentalDataFormat === 'default'
541- ) {
542- args . experimentalDataFormat = 'toon' ;
543- }
544- if ( env [ 'CI' ] || env [ 'CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS' ] ) {
545- console . error (
546- "turning off usage statistics. process.env['CI'] || process.env['CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS'] is set." ,
547- ) ;
548- args . usageStatistics = false ;
549- }
550-
551- const cliOptionsAllowedArgs = [
552- ...Object . keys ( options ) ,
553- // Yargs populated with positional args
554- '_' ,
555- '$0' ,
556- ] ;
557-
558- const unknownArgs = Object . keys ( args ) . filter (
559- arg => ! cliOptionsAllowedArgs . includes ( arg ) ,
560- ) ;
561-
562- if ( unknownArgs . length > 0 ) {
563- console . error (
564- `Unknown arguments: ${ unknownArgs . map ( arg => `--${ arg } ` ) } ` ,
565- ) ;
566- }
567- } )
568- . example ( CLI_EXAMPLES ) ;
569-
570- return yargsInstance
571- . config ( 'config' , 'Path to JSON configuration file' , configPath => {
572- try {
573- const parsed : unknown = JSON . parse ( readFileSync ( configPath , 'utf-8' ) ) ;
574- if ( ! isPlainObject ( parsed ) ) {
575- throw new Error ( 'Config must be a JSON object' ) ;
576- }
577-
578- yargs ( )
579- . parserConfiguration ( {
580- 'strip-aliased' : true ,
581- 'camel-case-expansion' : false ,
582- } )
583- . options ( options )
584- . config ( parsed )
585- . strict ( )
586- . fail ( false )
587- . exitProcess ( false )
588- . parseSync ( [ ] ) ;
589- return parsed ;
590- } catch ( err ) {
591- throw new Error ( `Invalid JSON config file: ${ getErrorMessage ( err ) } ` ) ;
592- }
593- } )
498+ . example ( CLI_EXAMPLES )
594499 . wrap ( Math . min ( 120 , yargsInstance . terminalWidth ( ) ) )
595500 . help ( )
596501 . version ( version ) ;
597502}
598503
504+ export function parser ( version : string , argv = process . argv ) {
505+ // Used to derive the ParsedArguments type.
506+ return buildCliParser ( version , argv , mcpOptions ) ;
507+ }
508+
599509export function parseArguments (
600510 version : string ,
601511 argv = process . argv ,
602512 env = process . env ,
603513) {
604- return parser ( version , argv , env ) . parseSync ( ) ;
514+ // Step 1 & 2: Independent Parsing (Bypassing yargs Defaults)
515+ const optionsWithoutDefaults : Record < string , YargsOptions > = { } ;
516+ for ( const [ key , option ] of Object . entries ( mcpOptions ) ) {
517+ const copy : YargsOptions = { ...option } ;
518+ if ( copy . default !== undefined ) {
519+ copy . defaultDescription ??= JSON . stringify ( copy . default ) ;
520+ delete copy . default ;
521+ }
522+ optionsWithoutDefaults [ key ] = copy ;
523+ }
524+
525+ // `--help` and `--version` print and exit the process; parse errors throw.
526+ const yargsInstance = buildCliParser (
527+ version ,
528+ argv ,
529+ optionsWithoutDefaults ,
530+ ) . fail ( false ) ;
531+
532+ const rawCli : Record < string , unknown > = yargsInstance . parseSync ( ) ;
533+
534+ // Config file parsing
535+ let parsedConfigFile : Record < string , unknown > = { } ;
536+ if ( typeof rawCli . config === 'string' ) {
537+ try {
538+ const fileContent : unknown = JSON . parse (
539+ readFileSync ( rawCli . config , 'utf-8' ) ,
540+ ) ;
541+ if ( ! isPlainObject ( fileContent ) ) {
542+ throw new Error ( 'Config must be a JSON object' ) ;
543+ }
544+
545+ // Run the config through yargs to reject unknown keys and apply
546+ // coercions, but without defaults.
547+ parsedConfigFile = yargs ( [ ] )
548+ . parserConfiguration ( {
549+ 'strip-aliased' : true ,
550+ 'camel-case-expansion' : false ,
551+ } )
552+ . options ( optionsWithoutDefaults )
553+ . config ( fileContent )
554+ . strict ( )
555+ . fail ( false )
556+ . exitProcess ( false )
557+ . parseSync ( [ ] ) ;
558+ } catch ( err ) {
559+ throw new Error ( `Invalid JSON config file: ${ getErrorMessage ( err ) } ` ) ;
560+ }
561+ }
562+
563+ // Step 3: Cascade Merge of Explicit Inputs
564+ const explicitConfig = { ...parsedConfigFile , ...rawCli } ;
565+
566+ // Step 4: Dynamic Default Resolution. `viaCli` is an explicit input like any
567+ // other; it selects which defaults apply.
568+ const isViaCli = explicitConfig . viaCli === true ;
569+ const baseOptions = isViaCli ? getMcpOptionsForViaCli ( ) : mcpOptions ;
570+ const resolvedConfig = { ...explicitConfig } ;
571+
572+ for ( const [ key , option ] of Object . entries ( baseOptions ) ) {
573+ if ( resolvedConfig [ key ] === undefined && 'default' in option ) {
574+ resolvedConfig [ key ] = option . default ;
575+ }
576+ }
577+
578+ if ( isViaCli ) {
579+ if ( resolvedConfig . filesystemRoot === DEFAULT_FILESYSTEM_ROOT ) {
580+ resolvedConfig . allowUnrestrictedPaths = true ;
581+ resolvedConfig . filesystemRoot = undefined ;
582+ }
583+ if (
584+ explicitConfig . isolated === undefined &&
585+ resolvedConfig . userDataDir === undefined &&
586+ ! resolvedConfig . autoConnect &&
587+ ! resolvedConfig . browserUrl &&
588+ ! resolvedConfig . wsEndpoint
589+ ) {
590+ resolvedConfig . isolated = true ;
591+ }
592+ }
593+
594+ if (
595+ resolvedConfig . experimentalToonFormat &&
596+ resolvedConfig . experimentalDataFormat === 'default'
597+ ) {
598+ resolvedConfig . experimentalDataFormat = 'toon' ;
599+ }
600+ if ( env [ 'CI' ] || env [ 'CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS' ] ) {
601+ console . error (
602+ "turning off usage statistics. process.env['CI'] || process.env['CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS'] is set." ,
603+ ) ;
604+ resolvedConfig . usageStatistics = false ;
605+ }
606+
607+ const cliOptionsAllowedArgs = [ ...Object . keys ( baseOptions ) , '_' , '$0' ] ;
608+
609+ const unknownArgs = Object . keys ( rawCli ) . filter (
610+ arg => ! cliOptionsAllowedArgs . includes ( arg ) ,
611+ ) ;
612+
613+ if ( unknownArgs . length > 0 ) {
614+ console . error ( `Unknown arguments: ${ unknownArgs . map ( arg => `--${ arg } ` ) } ` ) ;
615+ }
616+
617+ // Step 5: Validation. Conflicts are only checked against explicit user input
618+ // so that dynamic defaults never conflict.
619+ const activeArgs = new Set < string > ( ) ;
620+ for ( const [ key , val ] of Object . entries ( explicitConfig ) ) {
621+ if ( val !== undefined && val !== false ) {
622+ activeArgs . add ( key ) ;
623+ }
624+ }
625+ for ( const group of CONFLICTING_ARGS ) {
626+ const activeInGroup = group . filter ( arg => activeArgs . has ( arg ) ) ;
627+ if ( activeInGroup . length > 1 ) {
628+ const [ arg1 , arg2 ] = activeInGroup ;
629+ throw new Error ( `Arguments ${ arg1 } and ${ arg2 } are mutually exclusive` ) ;
630+ }
631+ }
632+
633+ if ( ! isParsedArguments ( resolvedConfig ) ) {
634+ throw new Error ( 'Failed to resolve configuration' ) ;
635+ }
636+ return resolvedConfig ;
605637}
0 commit comments