@@ -59,6 +59,25 @@ interface McpContextOptions {
5959const DEFAULT_TIMEOUT = 5_000 ;
6060const NAVIGATION_TIMEOUT = 10_000 ;
6161
62+ async function resolveWithTimeout < T > (
63+ promise : Promise < T > ,
64+ timeoutMs : number ,
65+ ) : Promise < T | undefined > {
66+ let timeout : ReturnType < typeof setTimeout > | undefined ;
67+ const timeoutPromise = new Promise < undefined > ( resolve => {
68+ timeout = setTimeout ( ( ) => {
69+ resolve ( undefined ) ;
70+ } , timeoutMs ) ;
71+ } ) ;
72+ try {
73+ return await Promise . race ( [ promise , timeoutPromise ] ) ;
74+ } finally {
75+ if ( timeout !== undefined ) {
76+ clearTimeout ( timeout ) ;
77+ }
78+ }
79+ }
80+
6281export class McpContext implements Context {
6382 browser : Browser ;
6483 logger : Debugger ;
@@ -564,9 +583,20 @@ export class McpContext implements Context {
564583 isolatedContextNames : Map < Page , string > ;
565584 } > {
566585 const defaultCtx = this . browser . defaultBrowserContext ( ) ;
567- const allPages = await this . browser . pages (
568- this . #options. experimentalIncludeAllPages ,
569- ) ;
586+ const pagePromises : Array < Promise < Page | null > > = [ ] ;
587+ for ( const target of this . browser . targets ( ) ) {
588+ if ( ! this . #isPageTarget( target ) ) {
589+ continue ;
590+ }
591+ pagePromises . push ( this . #resolveTargetPage( target ) ) ;
592+ }
593+
594+ const allPages : Page [ ] = [ ] ;
595+ for ( const page of await Promise . all ( pagePromises ) ) {
596+ if ( page && ! allPages . includes ( page ) ) {
597+ allPages . push ( page ) ;
598+ }
599+ }
570600
571601 const allTargets = this . browser . targets ( ) ;
572602 const extensionTargets = allTargets . filter ( target => {
@@ -578,17 +608,15 @@ export class McpContext implements Context {
578608
579609 for ( const target of extensionTargets ) {
580610 // Right now target.page() returns null for popup and side panel pages.
581- let page = await target . page ( ) ;
611+ let page = await this . #resolveTargetPage ( target ) ;
582612 if ( ! page ) {
583613 // We need to cache pages instances for targets because target.asPage()
584614 // returns a new page instance every time.
585615 page = this . #extensionPages. get ( target ) ?? null ;
586616 if ( ! page ) {
587- try {
588- page = await target . asPage ( ) ;
617+ page = await this . #resolveTargetPage ( target , true ) ;
618+ if ( page ) {
589619 this . #extensionPages. set ( target , page ) ;
590- } catch ( e ) {
591- this . logger ( 'Failed to get page for extension target' , e ) ;
592620 }
593621 }
594622 }
@@ -628,6 +656,42 @@ export class McpContext implements Context {
628656 return { pages : allPages , isolatedContextNames} ;
629657 }
630658
659+ #isPageTarget( target : Target ) : boolean {
660+ const type = target . type ( ) ;
661+ if ( type === 'page' ) {
662+ return true ;
663+ }
664+ if ( ! this . #options. experimentalIncludeAllPages ) {
665+ return false ;
666+ }
667+ return type === 'background_page' || type === 'webview' || type === 'other' ;
668+ }
669+
670+ async #resolveTargetPage(
671+ target : Target ,
672+ force = false ,
673+ ) : Promise < Page | null > {
674+ try {
675+ const pagePromise : Promise < Page | null > = force
676+ ? target . asPage ( )
677+ : target . page ( ) ;
678+ const page = await resolveWithTimeout ( pagePromise , DEFAULT_TIMEOUT ) ;
679+ if ( page === undefined ) {
680+ this . logger (
681+ `Timed out getting page for target ${ target . type ( ) } ${ target . url ( ) } ` ,
682+ ) ;
683+ return null ;
684+ }
685+ return page ;
686+ } catch ( error ) {
687+ this . logger (
688+ `Failed to get page for target ${ target . type ( ) } ${ target . url ( ) } ` ,
689+ error ,
690+ ) ;
691+ return null ;
692+ }
693+ }
694+
631695 async detectOpenDevToolsWindows ( ) {
632696 this . logger ( 'Detecting open DevTools windows' ) ;
633697 const { pages} = await this . #getAllPages( ) ;
0 commit comments