@@ -15,10 +15,15 @@ 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 type { DevTools } from './third_party/index.js' ;
2024import type {
2125 Browser ,
26+ BrowserContext ,
2227 ConsoleMessage ,
2328 Debugger ,
2429 Dialog ,
@@ -74,7 +79,7 @@ interface EmulationSettings {
7479 viewport ?: Viewport | null ;
7580}
7681
77- interface McpContextOptions {
82+ export interface McpContextOptions {
7883 // Whether the DevTools windows are exposed as pages for debugging of DevTools.
7984 experimentalDevToolsDebugging : boolean ;
8085 // Whether all page-like targets are exposed as pages.
@@ -119,11 +124,17 @@ export class McpContext implements Context {
119124 browser : Browser ;
120125 logger : Debugger ;
121126
122- // The most recent page state.
127+ // Maps LLM-provided browserContext name → Puppeteer BrowserContext.
128+ #browserContexts = new Map < string , BrowserContext > ( ) ;
129+ // Reverse lookup: Page → browserContext name (for snapshot labeling).
130+ // WeakMap so closed pages are garbage-collected automatically.
131+ #pageToBrowserContextName = new WeakMap < Page , string > ( ) ;
132+ // Auto-generated name counter for when no name is provided.
133+ #nextBrowserContextId = 1 ;
134+
123135 #pages: Page [ ] = [ ] ;
124136 #pageToDevToolsPage = new Map < Page , Page > ( ) ;
125137 #selectedPage?: Page ;
126- // The most recent snapshot.
127138 #textSnapshot: TextSnapshot | null = null ;
128139 #networkCollector: NetworkCollector ;
129140 #consoleCollector: ConsoleCollector ;
@@ -158,9 +169,11 @@ export class McpContext implements Context {
158169 this . #locatorClass = locatorClass ;
159170 this . #options = options ;
160171
161- this . #networkCollector = new NetworkCollector ( this . browser ) ;
172+ const targetEmitter = asTargetEmitter ( this . browser ) ;
162173
163- this . #consoleCollector = new ConsoleCollector ( this . browser , collect => {
174+ this . #networkCollector = new NetworkCollector ( targetEmitter ) ;
175+
176+ this . #consoleCollector = new ConsoleCollector ( targetEmitter , collect => {
164177 return {
165178 console : event => {
166179 collect ( event ) ;
@@ -173,7 +186,7 @@ export class McpContext implements Context {
173186 } ,
174187 } as ListenerMap ;
175188 } ) ;
176- this . #devtoolsUniverseManager = new UniverseManager ( this . browser ) ;
189+ this . #devtoolsUniverseManager = new UniverseManager ( targetEmitter ) ;
177190 }
178191
179192 async #init( ) {
@@ -187,6 +200,12 @@ export class McpContext implements Context {
187200 this . #networkCollector. dispose ( ) ;
188201 this . #consoleCollector. dispose ( ) ;
189202 this . #devtoolsUniverseManager. dispose ( ) ;
203+ for ( const ctx of this . #browserContexts. values ( ) ) {
204+ if ( ! ctx . closed ) {
205+ void ctx . close ( ) . catch ( ( ) => { } ) ;
206+ }
207+ }
208+ this . #browserContexts. clear ( ) ;
190209 }
191210
192211 static async from (
@@ -269,8 +288,42 @@ export class McpContext implements Context {
269288 return this . #consoleCollector. getById ( this . getSelectedPage ( ) , id ) ;
270289 }
271290
272- async newPage ( background ?: boolean ) : Promise < Page > {
273- const page = await this . browser . newPage ( { background} ) ;
291+ async newPage (
292+ background ?: boolean ,
293+ browserContextName ?: string ,
294+ ) : Promise < Page > {
295+ let page : Page ;
296+ if ( browserContextName !== undefined ) {
297+ const isFirstBrowserContext = this . #browserContexts. size === 0 ;
298+ let ctx = this . #browserContexts. get ( browserContextName ) ;
299+ if ( ! ctx ) {
300+ ctx = await this . browser . createBrowserContext ( ) ;
301+ this . #browserContexts. set ( browserContextName , ctx ) ;
302+ }
303+ page = await ctx . newPage ( ) ;
304+ this . #pageToBrowserContextName. set ( page , browserContextName ) ;
305+
306+ // On the first browser context creation, close any leftover
307+ // about:blank pages from the default context. Chrome always opens
308+ // an initial about:blank tab that is no longer needed once named
309+ // browser contexts are in use. We only do this once to avoid
310+ // closing pages the LLM may have explicitly opened in the default
311+ // context later.
312+ if ( isFirstBrowserContext ) {
313+ const defaultPages = await this . browser . defaultBrowserContext ( ) . pages ( ) ;
314+ for ( const dp of defaultPages ) {
315+ if ( dp . url ( ) === 'about:blank' ) {
316+ try {
317+ await dp . close ( ) ;
318+ } catch {
319+ // Page may already be closed.
320+ }
321+ }
322+ }
323+ }
324+ } else {
325+ page = await this . browser . newPage ( { background} ) ;
326+ }
274327 await this . createPagesSnapshot ( ) ;
275328 this . selectPage ( page ) ;
276329 this . #networkCollector. addPage ( page ) ;
@@ -282,7 +335,20 @@ export class McpContext implements Context {
282335 throw new Error ( CLOSE_PAGE_ERROR ) ;
283336 }
284337 const page = this . getPageById ( pageId ) ;
338+ const browserContextName = this . #pageToBrowserContextName. get ( page ) ;
285339 await page . close ( { runBeforeUnload : false } ) ;
340+ this . #pageToBrowserContextName. delete ( page ) ;
341+
342+ if ( browserContextName ) {
343+ const ctx = this . #browserContexts. get ( browserContextName ) ;
344+ if ( ctx && ! ctx . closed ) {
345+ const remainingPages = await ctx . pages ( ) ;
346+ if ( remainingPages . length === 0 ) {
347+ await ctx . close ( ) ;
348+ this . #browserContexts. delete ( browserContextName ) ;
349+ }
350+ }
351+ }
286352 }
287353
288354 getNetworkRequestById ( reqid : number ) : HTTPRequest {
@@ -558,13 +624,40 @@ export class McpContext implements Context {
558624 }
559625 }
560626
561- /**
562- * Creates a snapshot of the pages.
563- */
564627 async createPagesSnapshot ( ) : Promise < Page [ ] > {
565- const allPages = await this . browser . pages (
628+ // Auto-discover BrowserContexts not in our mapping (e.g., externally
629+ // created incognito contexts) and assign generated names.
630+ const defaultCtx = this . browser . defaultBrowserContext ( ) ;
631+ const knownContexts = new Set ( this . #browserContexts. values ( ) ) ;
632+ for ( const ctx of this . browser . browserContexts ( ) ) {
633+ if ( ctx !== defaultCtx && ! ctx . closed && ! knownContexts . has ( ctx ) ) {
634+ this . #browserContexts. set (
635+ `browser-context-${ this . #nextBrowserContextId++ } ` ,
636+ ctx ,
637+ ) ;
638+ }
639+ }
640+
641+ const contextPages : Page [ ] = [ ] ;
642+ for ( const [ name , ctx ] of this . #browserContexts) {
643+ if ( ! ctx . closed ) {
644+ const pages = await ctx . pages ( ) ;
645+ for ( const page of pages ) {
646+ this . #pageToBrowserContextName. set ( page , name ) ;
647+ }
648+ contextPages . push ( ...pages ) ;
649+ }
650+ }
651+
652+ // browser.pages() returns pages from ALL contexts (default + incognito).
653+ // Filter out pages that belong to named browser contexts to avoid duplicates.
654+ const contextPagesSet = new Set ( contextPages ) ;
655+ const browserPages = await this . browser . pages (
566656 this . #options. experimentalIncludeAllPages ,
567657 ) ;
658+ const defaultPages = browserPages . filter ( p => ! contextPagesSet . has ( p ) ) ;
659+
660+ const allPages = [ ...defaultPages , ...contextPages ] ;
568661
569662 for ( const page of allPages ) {
570663 if ( ! this . #pageIdMap. has ( page ) ) {
@@ -573,8 +666,6 @@ export class McpContext implements Context {
573666 }
574667
575668 this . #pages = allPages . filter ( page => {
576- // If we allow debugging DevTools windows, return all pages.
577- // If we are in regular mode, the user should only see non-DevTools page.
578669 return (
579670 this . #options. experimentalDevToolsDebugging ||
580671 ! page . url ( ) . startsWith ( 'devtools://' )
@@ -595,9 +686,18 @@ export class McpContext implements Context {
595686
596687 async detectOpenDevToolsWindows ( ) {
597688 this . logger ( 'Detecting open DevTools windows' ) ;
598- const pages = await this . browser . pages (
689+ const contextPages : Page [ ] = [ ] ;
690+ for ( const ctx of this . #browserContexts. values ( ) ) {
691+ if ( ! ctx . closed ) {
692+ contextPages . push ( ...( await ctx . pages ( ) ) ) ;
693+ }
694+ }
695+ const contextPagesSet = new Set ( contextPages ) ;
696+ const browserPages = await this . browser . pages (
599697 this . #options. experimentalIncludeAllPages ,
600698 ) ;
699+ const defaultPages = browserPages . filter ( p => ! contextPagesSet . has ( p ) ) ;
700+ const pages = [ ...defaultPages , ...contextPages ] ;
601701 this . #pageToDevToolsPage = new Map < Page , Page > ( ) ;
602702 for ( const devToolsPage of pages ) {
603703 if ( devToolsPage . url ( ) . startsWith ( 'devtools://' ) ) {
@@ -629,6 +729,10 @@ export class McpContext implements Context {
629729 return this . #pages;
630730 }
631731
732+ getBrowserContextName ( page : Page ) : string | undefined {
733+ return this . #pageToBrowserContextName. get ( page ) ;
734+ }
735+
632736 getDevToolsPage ( page : Page ) : Page | undefined {
633737 return this . #pageToDevToolsPage. get ( page ) ;
634738 }
@@ -847,7 +951,8 @@ export class McpContext implements Context {
847951 * We need to ignore favicon request as they make our test flaky
848952 */
849953 async setUpNetworkCollectorForTesting ( ) {
850- this . #networkCollector = new NetworkCollector ( this . browser , collect => {
954+ const targetEmitter = asTargetEmitter ( this . browser ) ;
955+ this . #networkCollector = new NetworkCollector ( targetEmitter , collect => {
851956 return {
852957 request : req => {
853958 if ( req . url ( ) . includes ( 'favicon.ico' ) ) {
@@ -857,7 +962,8 @@ export class McpContext implements Context {
857962 } ,
858963 } as ListenerMap ;
859964 } ) ;
860- await this . #networkCollector. init ( await this . browser . pages ( ) ) ;
965+ const pages = await this . browser . pages ( ) ;
966+ await this . #networkCollector. init ( pages ) ;
861967 }
862968
863969 async installExtension ( extensionPath : string ) : Promise < string > {
0 commit comments