@@ -15,11 +15,16 @@ import {
1515 urlsEqual ,
1616} from './DevtoolsUtils.js' ;
1717import type { ListenerMap , UncaughtError } from './PageCollector.js' ;
18- import { NetworkCollector , ConsoleCollector } from './PageCollector.js' ;
18+ import {
19+ asTargetEmitter ,
20+ NetworkCollector ,
21+ ConsoleCollector ,
22+ } from './PageCollector.js' ;
1923import { Locator } from './third_party/index.js' ;
2024import type { DevTools } from './third_party/index.js' ;
2125import type {
2226 Browser ,
27+ BrowserContext ,
2328 ConsoleMessage ,
2429 Debugger ,
2530 Dialog ,
@@ -64,7 +69,7 @@ export interface TextSnapshot {
6469 verbose : boolean ;
6570}
6671
67- interface McpContextOptions {
72+ export interface McpContextOptions {
6873 // Whether the DevTools windows are exposed as pages for debugging of DevTools.
6974 experimentalDevToolsDebugging : boolean ;
7075 // Whether all page-like targets are exposed as pages.
@@ -109,11 +114,17 @@ export class McpContext implements Context {
109114 browser : Browser ;
110115 logger : Debugger ;
111116
112- // The most recent page state.
117+ // Maps LLM-provided browserContext name → Puppeteer BrowserContext.
118+ #browserContexts = new Map < string , BrowserContext > ( ) ;
119+ // Reverse lookup: Page → browserContext name (for snapshot labeling).
120+ // WeakMap so closed pages are garbage-collected automatically.
121+ #pageToBrowserContextName = new WeakMap < Page , string > ( ) ;
122+ // Auto-generated name counter for when no name is provided.
123+ #nextBrowserContextId = 1 ;
124+
113125 #pages: Page [ ] = [ ] ;
114126 #pageToDevToolsPage = new Map < Page , Page > ( ) ;
115127 #selectedPage?: Page ;
116- // The most recent snapshot.
117128 #textSnapshot: TextSnapshot | null = null ;
118129 #networkCollector: NetworkCollector ;
119130 #consoleCollector: ConsoleCollector ;
@@ -151,9 +162,11 @@ export class McpContext implements Context {
151162 this . #locatorClass = locatorClass ;
152163 this . #options = options ;
153164
154- this . #networkCollector = new NetworkCollector ( this . browser ) ;
165+ const targetEmitter = asTargetEmitter ( this . browser ) ;
155166
156- this . #consoleCollector = new ConsoleCollector ( this . browser , collect => {
167+ this . #networkCollector = new NetworkCollector ( targetEmitter ) ;
168+
169+ this . #consoleCollector = new ConsoleCollector ( targetEmitter , collect => {
157170 return {
158171 console : event => {
159172 collect ( event ) ;
@@ -166,7 +179,7 @@ export class McpContext implements Context {
166179 } ,
167180 } as ListenerMap ;
168181 } ) ;
169- this . #devtoolsUniverseManager = new UniverseManager ( this . browser ) ;
182+ this . #devtoolsUniverseManager = new UniverseManager ( targetEmitter ) ;
170183 }
171184
172185 async #init( ) {
@@ -180,6 +193,12 @@ export class McpContext implements Context {
180193 this . #networkCollector. dispose ( ) ;
181194 this . #consoleCollector. dispose ( ) ;
182195 this . #devtoolsUniverseManager. dispose ( ) ;
196+ for ( const ctx of this . #browserContexts. values ( ) ) {
197+ if ( ! ctx . closed ) {
198+ void ctx . close ( ) . catch ( ( ) => { } ) ;
199+ }
200+ }
201+ this . #browserContexts. clear ( ) ;
183202 }
184203
185204 static async from (
@@ -262,8 +281,42 @@ export class McpContext implements Context {
262281 return this . #consoleCollector. getById ( this . getSelectedPage ( ) , id ) ;
263282 }
264283
265- async newPage ( background ?: boolean ) : Promise < Page > {
266- const page = await this . browser . newPage ( { background} ) ;
284+ async newPage (
285+ background ?: boolean ,
286+ browserContextName ?: string ,
287+ ) : Promise < Page > {
288+ let page : Page ;
289+ if ( browserContextName !== undefined ) {
290+ const isFirstBrowserContext = this . #browserContexts. size === 0 ;
291+ let ctx = this . #browserContexts. get ( browserContextName ) ;
292+ if ( ! ctx ) {
293+ ctx = await this . browser . createBrowserContext ( ) ;
294+ this . #browserContexts. set ( browserContextName , ctx ) ;
295+ }
296+ page = await ctx . newPage ( ) ;
297+ this . #pageToBrowserContextName. set ( page , browserContextName ) ;
298+
299+ // On the first browser context creation, close any leftover
300+ // about:blank pages from the default context. Chrome always opens
301+ // an initial about:blank tab that is no longer needed once named
302+ // browser contexts are in use. We only do this once to avoid
303+ // closing pages the LLM may have explicitly opened in the default
304+ // context later.
305+ if ( isFirstBrowserContext ) {
306+ const defaultPages = await this . browser . defaultBrowserContext ( ) . pages ( ) ;
307+ for ( const dp of defaultPages ) {
308+ if ( dp . url ( ) === 'about:blank' ) {
309+ try {
310+ await dp . close ( ) ;
311+ } catch {
312+ // Page may already be closed.
313+ }
314+ }
315+ }
316+ }
317+ } else {
318+ page = await this . browser . newPage ( { background} ) ;
319+ }
267320 await this . createPagesSnapshot ( ) ;
268321 this . selectPage ( page ) ;
269322 this . #networkCollector. addPage ( page ) ;
@@ -275,7 +328,20 @@ export class McpContext implements Context {
275328 throw new Error ( CLOSE_PAGE_ERROR ) ;
276329 }
277330 const page = this . getPageById ( pageId ) ;
331+ const browserContextName = this . #pageToBrowserContextName. get ( page ) ;
278332 await page . close ( { runBeforeUnload : false } ) ;
333+ this . #pageToBrowserContextName. delete ( page ) ;
334+
335+ if ( browserContextName ) {
336+ const ctx = this . #browserContexts. get ( browserContextName ) ;
337+ if ( ctx && ! ctx . closed ) {
338+ const remainingPages = await ctx . pages ( ) ;
339+ if ( remainingPages . length === 0 ) {
340+ await ctx . close ( ) ;
341+ this . #browserContexts. delete ( browserContextName ) ;
342+ }
343+ }
344+ }
279345 }
280346
281347 getNetworkRequestById ( reqid : number ) : HTTPRequest {
@@ -481,13 +547,40 @@ export class McpContext implements Context {
481547 }
482548 }
483549
484- /**
485- * Creates a snapshot of the pages.
486- */
487550 async createPagesSnapshot ( ) : Promise < Page [ ] > {
488- const allPages = await this . browser . pages (
551+ // Auto-discover BrowserContexts not in our mapping (e.g., externally
552+ // created incognito contexts) and assign generated names.
553+ const defaultCtx = this . browser . defaultBrowserContext ( ) ;
554+ const knownContexts = new Set ( this . #browserContexts. values ( ) ) ;
555+ for ( const ctx of this . browser . browserContexts ( ) ) {
556+ if ( ctx !== defaultCtx && ! ctx . closed && ! knownContexts . has ( ctx ) ) {
557+ this . #browserContexts. set (
558+ `browser-context-${ this . #nextBrowserContextId++ } ` ,
559+ ctx ,
560+ ) ;
561+ }
562+ }
563+
564+ const contextPages : Page [ ] = [ ] ;
565+ for ( const [ name , ctx ] of this . #browserContexts) {
566+ if ( ! ctx . closed ) {
567+ const pages = await ctx . pages ( ) ;
568+ for ( const page of pages ) {
569+ this . #pageToBrowserContextName. set ( page , name ) ;
570+ }
571+ contextPages . push ( ...pages ) ;
572+ }
573+ }
574+
575+ // browser.pages() returns pages from ALL contexts (default + incognito).
576+ // Filter out pages that belong to named browser contexts to avoid duplicates.
577+ const contextPagesSet = new Set ( contextPages ) ;
578+ const browserPages = await this . browser . pages (
489579 this . #options. experimentalIncludeAllPages ,
490580 ) ;
581+ const defaultPages = browserPages . filter ( p => ! contextPagesSet . has ( p ) ) ;
582+
583+ const allPages = [ ...defaultPages , ...contextPages ] ;
491584
492585 for ( const page of allPages ) {
493586 if ( ! this . #pageIdMap. has ( page ) ) {
@@ -496,8 +589,6 @@ export class McpContext implements Context {
496589 }
497590
498591 this . #pages = allPages . filter ( page => {
499- // If we allow debugging DevTools windows, return all pages.
500- // If we are in regular mode, the user should only see non-DevTools page.
501592 return (
502593 this . #options. experimentalDevToolsDebugging ||
503594 ! page . url ( ) . startsWith ( 'devtools://' )
@@ -518,9 +609,18 @@ export class McpContext implements Context {
518609
519610 async detectOpenDevToolsWindows ( ) {
520611 this . logger ( 'Detecting open DevTools windows' ) ;
521- const pages = await this . browser . pages (
612+ const contextPages : Page [ ] = [ ] ;
613+ for ( const ctx of this . #browserContexts. values ( ) ) {
614+ if ( ! ctx . closed ) {
615+ contextPages . push ( ...( await ctx . pages ( ) ) ) ;
616+ }
617+ }
618+ const contextPagesSet = new Set ( contextPages ) ;
619+ const browserPages = await this . browser . pages (
522620 this . #options. experimentalIncludeAllPages ,
523621 ) ;
622+ const defaultPages = browserPages . filter ( p => ! contextPagesSet . has ( p ) ) ;
623+ const pages = [ ...defaultPages , ...contextPages ] ;
524624 this . #pageToDevToolsPage = new Map < Page , Page > ( ) ;
525625 for ( const devToolsPage of pages ) {
526626 if ( devToolsPage . url ( ) . startsWith ( 'devtools://' ) ) {
@@ -552,6 +652,10 @@ export class McpContext implements Context {
552652 return this . #pages;
553653 }
554654
655+ getBrowserContextName ( page : Page ) : string | undefined {
656+ return this . #pageToBrowserContextName. get ( page ) ;
657+ }
658+
555659 getDevToolsPage ( page : Page ) : Page | undefined {
556660 return this . #pageToDevToolsPage. get ( page ) ;
557661 }
@@ -770,7 +874,8 @@ export class McpContext implements Context {
770874 * We need to ignore favicon request as they make our test flaky
771875 */
772876 async setUpNetworkCollectorForTesting ( ) {
773- this . #networkCollector = new NetworkCollector ( this . browser , collect => {
877+ const targetEmitter = asTargetEmitter ( this . browser ) ;
878+ this . #networkCollector = new NetworkCollector ( targetEmitter , collect => {
774879 return {
775880 request : req => {
776881 if ( req . url ( ) . includes ( 'favicon.ico' ) ) {
@@ -780,7 +885,8 @@ export class McpContext implements Context {
780885 } ,
781886 } as ListenerMap ;
782887 } ) ;
783- await this . #networkCollector. init ( await this . browser . pages ( ) ) ;
888+ const pages = await this . browser . pages ( ) ;
889+ await this . #networkCollector. init ( pages ) ;
784890 }
785891
786892 async installExtension ( extensionPath : string ) : Promise < string > {
0 commit comments