@@ -56,6 +56,14 @@ interface McpContextOptions {
5656 performanceCrux : boolean ;
5757}
5858
59+ export interface PageSummary {
60+ id : number ;
61+ url : string ;
62+ selected : boolean ;
63+ isExtension : boolean ;
64+ isolatedContextName ?: string ;
65+ }
66+
5967const DEFAULT_TIMEOUT = 5_000 ;
6068const NAVIGATION_TIMEOUT = 10_000 ;
6169
@@ -88,13 +96,18 @@ export class McpContext implements Context {
8896 #nextIsolatedContextId = 1 ;
8997
9098 #pages: Page [ ] = [ ] ;
99+ #pageSummaries: PageSummary [ ] = [ ] ;
91100 #extensionServiceWorkers: ExtensionServiceWorker [ ] = [ ] ;
92101
93102 #mcpPages = new Map < Page , McpPage > ( ) ;
103+ #pageTargets = new Map < Target , number > ( ) ;
104+ #targetsByPageId = new Map < number , Target > ( ) ;
94105 #selectedPage?: McpPage ;
106+ #selectedPageId?: number ;
95107 #networkCollector: NetworkCollector ;
96108 #consoleCollector: ConsoleCollector ;
97109 #devtoolsUniverseManager: UniverseManager ;
110+ #collectorsInitialized = false ;
98111
99112 #isRunningTrace = false ;
100113 #screenRecorderData: { recorder : ScreenRecorder ; filePath : string } | null =
@@ -143,11 +156,14 @@ export class McpContext implements Context {
143156 }
144157
145158 async #init( ) {
146- const pages = await this . createPagesSnapshot ( ) ;
159+ this . createPageTargetSnapshot ( ) ;
160+ await this . #ensureInitialSelectedPage( ) ;
161+ const pages = this . #selectedPage ? [ this . #selectedPage. pptrPage ] : [ ] ;
147162 await this . createExtensionServiceWorkersSnapshot ( ) ;
148163 await this . #networkCollector. init ( pages ) ;
149164 await this . #consoleCollector. init ( pages ) ;
150165 await this . #devtoolsUniverseManager. init ( pages ) ;
166+ this . #collectorsInitialized = true ;
151167 }
152168
153169 dispose ( ) {
@@ -275,22 +291,24 @@ export class McpContext implements Context {
275291 } else {
276292 page = await this . browser . newPage ( { background} ) ;
277293 }
278- await this . createPagesSnapshot ( ) ;
279- this . selectPage ( this . #getMcpPage( page ) ) ;
280- this . #networkCollector. addPage ( page ) ;
281- this . #consoleCollector. addPage ( page ) ;
282- return this . #getMcpPage( page ) ;
294+ this . createPageTargetSnapshot ( ) ;
295+ const target = page . target ( ) ;
296+ const pageId = this . #getOrCreatePageIdForTarget( target ) ;
297+ const mcpPage = this . #registerPage( page , pageId ) ;
298+ mcpPage . isolatedContextName = isolatedContextName ;
299+ this . selectPage ( mcpPage ) ;
300+ return mcpPage ;
283301 }
284302 async closePage ( pageId : number ) : Promise < void > {
285- if ( this . #pages. length === 1 ) {
303+ this . createPageTargetSnapshot ( ) ;
304+ if ( this . #pageSummaries. length === 1 ) {
286305 throw new Error ( CLOSE_PAGE_ERROR ) ;
287306 }
288- const page = this . getPageById ( pageId ) ;
289- if ( page ) {
290- page . dispose ( ) ;
291- this . #mcpPages. delete ( page . pptrPage ) ;
292- }
307+ const page = await this . ensurePageById ( pageId ) ;
308+ page . dispose ( ) ;
309+ this . #mcpPages. delete ( page . pptrPage ) ;
293310 await page . pptrPage . close ( { runBeforeUnload : false } ) ;
311+ this . createPageTargetSnapshot ( ) ;
294312 }
295313
296314 getNetworkRequestById ( page : McpPage , reqid : number ) : HTTPRequest {
@@ -443,6 +461,33 @@ export class McpContext implements Context {
443461 return page ;
444462 }
445463
464+ async ensurePageById ( pageId : number ) : Promise < McpPage > {
465+ const existingPage = this . #mcpPages
466+ . values ( )
467+ . find ( mcpPage => mcpPage . id === pageId ) ;
468+ if ( existingPage && ! existingPage . pptrPage . isClosed ( ) ) {
469+ return existingPage ;
470+ }
471+
472+ this . createPageTargetSnapshot ( ) ;
473+ const target = this . #targetsByPageId. get ( pageId ) ;
474+ if ( ! target ) {
475+ throw new Error ( 'No page found' ) ;
476+ }
477+
478+ let page = await this . #resolveTargetPage( target ) ;
479+ if ( ! page && target . url ( ) . startsWith ( 'chrome-extension://' ) ) {
480+ page = await this . #resolveTargetPage( target , true ) ;
481+ }
482+ if ( ! page ) {
483+ throw new Error (
484+ `Page ${ pageId } is not responding. Call ${ listPages ( ) . name } to see open pages.` ,
485+ ) ;
486+ }
487+
488+ return this . #registerPage( page , pageId ) ;
489+ }
490+
446491 getPageId ( page : Page ) : number | undefined {
447492 return this . #mcpPages. get ( page ) ?. id ;
448493 }
@@ -465,6 +510,7 @@ export class McpContext implements Context {
465510
466511 selectPage ( newPage : McpPage ) : void {
467512 this . #selectedPage = newPage ;
513+ this . #selectedPageId = newPage . id ;
468514 this . #updateSelectedPageTimeouts( ) ;
469515 }
470516
@@ -533,19 +579,76 @@ export class McpContext implements Context {
533579 return this . #extensionServiceWorkers;
534580 }
535581
582+ createPageTargetSnapshot ( ) : PageSummary [ ] {
583+ const contextToName = this . #getContextToName( ) ;
584+ const targets = this . browser . targets ( ) . filter ( target => {
585+ return (
586+ this . #isPageTarget( target ) &&
587+ ( this . #options. experimentalDevToolsDebugging ||
588+ ! target . url ( ) . startsWith ( 'devtools://' ) )
589+ ) ;
590+ } ) ;
591+ const currentTargets = new Set ( targets ) ;
592+
593+ for ( const target of targets ) {
594+ this . #getOrCreatePageIdForTarget( target ) ;
595+ }
596+
597+ for ( const [ target , id ] of this . #pageTargets) {
598+ if ( ! currentTargets . has ( target ) ) {
599+ this . #pageTargets. delete ( target ) ;
600+ this . #targetsByPageId. delete ( id ) ;
601+ }
602+ }
603+
604+ for ( const [ page , mcpPage ] of this . #mcpPages) {
605+ if ( page . isClosed ( ) ) {
606+ mcpPage . dispose ( ) ;
607+ this . #mcpPages. delete ( page ) ;
608+ }
609+ }
610+
611+ if ( this . #selectedPage && this . #selectedPage. pptrPage . isClosed ( ) ) {
612+ this . #selectedPage = undefined ;
613+ }
614+
615+ if (
616+ this . #selectedPageId === undefined ||
617+ ! targets . some ( target => {
618+ return this . #pageTargets. get ( target ) === this . #selectedPageId;
619+ } )
620+ ) {
621+ this . #selectedPageId = targets [ 0 ]
622+ ? this . #getOrCreatePageIdForTarget( targets [ 0 ] )
623+ : undefined ;
624+ this . #selectedPage = undefined ;
625+ }
626+
627+ this . #pageSummaries = targets . map ( target => {
628+ const id = this . #getOrCreatePageIdForTarget( target ) ;
629+ const isolatedContextName = contextToName . get ( target . browserContext ( ) ) ;
630+ const summary : PageSummary = {
631+ id,
632+ url : target . url ( ) ,
633+ selected : id === this . #selectedPageId,
634+ isExtension : target . url ( ) . startsWith ( 'chrome-extension://' ) ,
635+ } ;
636+ if ( isolatedContextName ) {
637+ summary . isolatedContextName = isolatedContextName ;
638+ }
639+ return summary ;
640+ } ) ;
641+
642+ return this . #pageSummaries;
643+ }
644+
536645 async createPagesSnapshot ( ) : Promise < Page [ ] > {
537646 const { pages : allPages , isolatedContextNames} = await this . #getAllPages( ) ;
538647
539648 for ( const page of allPages ) {
540- let mcpPage = this . #mcpPages. get ( page ) ;
541- if ( ! mcpPage ) {
542- mcpPage = new McpPage ( page , this . #nextPageId++ ) ;
543- this . #mcpPages. set ( page , mcpPage ) ;
544- // We emulate a focused page for all pages to support multi-agent workflows.
545- void page . emulateFocusedPage ( true ) . catch ( error => {
546- this . logger ( 'Error turning on focused page emulation' , error ) ;
547- } ) ;
548- }
649+ const target = page . target ( ) ;
650+ const pageId = this . #getOrCreatePageIdForTarget( target ) ;
651+ const mcpPage = this . #registerPage( page , pageId ) ;
549652 mcpPage . isolatedContextName = isolatedContextNames . get ( page ) ;
550653 }
551654
@@ -573,6 +676,7 @@ export class McpContext implements Context {
573676 this . selectPage ( this . #getMcpPage( this . #pages[ 0 ] ) ) ;
574677 }
575678
679+ this . createPageTargetSnapshot ( ) ;
576680 await this . detectOpenDevToolsWindows ( ) ;
577681
578682 return this . #pages;
@@ -582,7 +686,6 @@ export class McpContext implements Context {
582686 pages : Page [ ] ;
583687 isolatedContextNames : Map < Page , string > ;
584688 } > {
585- const defaultCtx = this . browser . defaultBrowserContext ( ) ;
586689 const pagePromises : Array < Promise < Page | null > > = [ ] ;
587690 for ( const target of this . browser . targets ( ) ) {
588691 if ( ! this . #isPageTarget( target ) ) {
@@ -626,7 +729,22 @@ export class McpContext implements Context {
626729 }
627730 }
628731
629- // Build a reverse lookup from BrowserContext instance → name.
732+ const contextToName = this . #getContextToName( ) ;
733+ // Map each page to its isolated context name (if any).
734+ const isolatedContextNames = new Map < Page , string > ( ) ;
735+ for ( const page of allPages ) {
736+ const ctx = page . browserContext ( ) ;
737+ const name = contextToName . get ( ctx ) ;
738+ if ( name ) {
739+ isolatedContextNames . set ( page , name ) ;
740+ }
741+ }
742+
743+ return { pages : allPages , isolatedContextNames} ;
744+ }
745+
746+ #getContextToName( ) : Map < BrowserContext , string > {
747+ const defaultCtx = this . browser . defaultBrowserContext ( ) ;
630748 const contextToName = new Map < BrowserContext , string > ( ) ;
631749 for ( const [ name , ctx ] of this . #isolatedContexts) {
632750 contextToName . set ( ctx , name ) ;
@@ -642,18 +760,48 @@ export class McpContext implements Context {
642760 contextToName . set ( ctx , name ) ;
643761 }
644762 }
763+ return contextToName ;
764+ }
645765
646- // Map each page to its isolated context name (if any).
647- const isolatedContextNames = new Map < Page , string > ( ) ;
648- for ( const page of allPages ) {
649- const ctx = page . browserContext ( ) ;
650- const name = contextToName . get ( ctx ) ;
651- if ( name ) {
652- isolatedContextNames . set ( page , name ) ;
766+ #getOrCreatePageIdForTarget( target : Target ) : number {
767+ const existingId = this . #pageTargets. get ( target ) ;
768+ if ( existingId ) {
769+ return existingId ;
770+ }
771+ const id = this . #nextPageId++ ;
772+ this . #pageTargets. set ( target , id ) ;
773+ this . #targetsByPageId. set ( id , target ) ;
774+ return id ;
775+ }
776+
777+ #registerPage( page : Page , pageId : number ) : McpPage {
778+ let mcpPage = this . #mcpPages. get ( page ) ;
779+ if ( ! mcpPage ) {
780+ mcpPage = new McpPage ( page , pageId ) ;
781+ this . #mcpPages. set ( page , mcpPage ) ;
782+ // We emulate a focused page for all pages to support multi-agent workflows.
783+ void page . emulateFocusedPage ( true ) . catch ( error => {
784+ this . logger ( 'Error turning on focused page emulation' , error ) ;
785+ } ) ;
786+ if ( this . #collectorsInitialized) {
787+ this . #networkCollector. addPage ( page ) ;
788+ this . #consoleCollector. addPage ( page ) ;
653789 }
654790 }
791+ return mcpPage ;
792+ }
655793
656- return { pages : allPages , isolatedContextNames} ;
794+ async #ensureInitialSelectedPage( ) : Promise < void > {
795+ const pageId = this . #selectedPageId;
796+ if ( pageId === undefined ) {
797+ return ;
798+ }
799+ try {
800+ const page = await this . ensurePageById ( pageId ) ;
801+ this . selectPage ( page ) ;
802+ } catch ( error ) {
803+ this . logger ( `Failed to load initial selected page ${ pageId } ` , error ) ;
804+ }
657805 }
658806
659807 #isPageTarget( target : Target ) : boolean {
@@ -694,7 +842,9 @@ export class McpContext implements Context {
694842
695843 async detectOpenDevToolsWindows ( ) {
696844 this . logger ( 'Detecting open DevTools windows' ) ;
697- const { pages} = await this . #getAllPages( ) ;
845+ const pages = [ ...this . #mcpPages. keys ( ) ] . filter ( page => {
846+ return ! page . isClosed ( ) ;
847+ } ) ;
698848
699849 await Promise . all (
700850 pages . map ( async page => {
@@ -733,6 +883,10 @@ export class McpContext implements Context {
733883 return this . #pages;
734884 }
735885
886+ getPageSummaries ( ) : PageSummary [ ] {
887+ return this . #pageSummaries;
888+ }
889+
736890 getIsolatedContextName ( page : Page ) : string | undefined {
737891 return this . #mcpPages. get ( page ) ?. isolatedContextName ;
738892 }
0 commit comments