@@ -19,6 +19,7 @@ import {NetworkCollector, ConsoleCollector} from './PageCollector.js';
1919import type { DevTools } from './third_party/index.js' ;
2020import type {
2121 Browser ,
22+ BrowserContext ,
2223 ConsoleMessage ,
2324 Debugger ,
2425 Dialog ,
@@ -74,7 +75,7 @@ interface EmulationSettings {
7475 viewport ?: Viewport | null ;
7576}
7677
77- interface McpContextOptions {
78+ export interface McpContextOptions {
7879 // Whether the DevTools windows are exposed as pages for debugging of DevTools.
7980 experimentalDevToolsDebugging : boolean ;
8081 // Whether all page-like targets are exposed as pages.
@@ -119,11 +120,17 @@ export class McpContext implements Context {
119120 browser : Browser ;
120121 logger : Debugger ;
121122
122- // The most recent page state.
123+ // Maps LLM-provided isolatedContext name → Puppeteer BrowserContext.
124+ #isolatedContexts = new Map < string , BrowserContext > ( ) ;
125+ // Reverse lookup: Page → isolatedContext name (for snapshot labeling).
126+ // WeakMap so closed pages are garbage-collected automatically.
127+ #pageToIsolatedContextName = new WeakMap < Page , string > ( ) ;
128+ // Auto-generated name counter for when no name is provided.
129+ #nextIsolatedContextId = 1 ;
130+
123131 #pages: Page [ ] = [ ] ;
124132 #pageToDevToolsPage = new Map < Page , Page > ( ) ;
125133 #selectedPage?: Page ;
126- // The most recent snapshot.
127134 #textSnapshot: TextSnapshot | null = null ;
128135 #networkCollector: NetworkCollector ;
129136 #consoleCollector: ConsoleCollector ;
@@ -187,6 +194,14 @@ export class McpContext implements Context {
187194 this . #networkCollector. dispose ( ) ;
188195 this . #consoleCollector. dispose ( ) ;
189196 this . #devtoolsUniverseManager. dispose ( ) ;
197+ for ( const ctx of this . #isolatedContexts. values ( ) ) {
198+ if ( ! ctx . closed ) {
199+ void ctx . close ( ) . catch ( ( ) => {
200+ // Swallow errors during cleanup.
201+ } ) ;
202+ }
203+ }
204+ this . #isolatedContexts. clear ( ) ;
190205 }
191206
192207 static async from (
@@ -269,8 +284,41 @@ export class McpContext implements Context {
269284 return this . #consoleCollector. getById ( this . getSelectedPage ( ) , id ) ;
270285 }
271286
272- async newPage ( background ?: boolean ) : Promise < Page > {
273- const page = await this . browser . newPage ( { background} ) ;
287+ async newPage (
288+ background ?: boolean ,
289+ isolatedContextName ?: string ,
290+ ) : Promise < Page > {
291+ let page : Page ;
292+ if ( isolatedContextName !== undefined ) {
293+ const isFirstIsolatedContext = this . #isolatedContexts. size === 0 ;
294+ let ctx = this . #isolatedContexts. get ( isolatedContextName ) ;
295+ if ( ! ctx ) {
296+ ctx = await this . browser . createBrowserContext ( ) ;
297+ this . #isolatedContexts. set ( isolatedContextName , ctx ) ;
298+ }
299+ page = await ctx . newPage ( ) ;
300+ this . #pageToIsolatedContextName. set ( page , isolatedContextName ) ;
301+
302+ // On the first isolated context creation, close any leftover
303+ // about:blank pages from the default context. Chrome always opens
304+ // an initial about:blank tab that is no longer needed once isolated
305+ // contexts are in use. We only do this once to avoid closing pages
306+ // the LLM may have explicitly opened in the default context later.
307+ if ( isFirstIsolatedContext ) {
308+ const defaultPages = await this . browser . defaultBrowserContext ( ) . pages ( ) ;
309+ for ( const dp of defaultPages ) {
310+ if ( dp . url ( ) === 'about:blank' ) {
311+ try {
312+ await dp . close ( ) ;
313+ } catch {
314+ // Page may already be closed.
315+ }
316+ }
317+ }
318+ }
319+ } else {
320+ page = await this . browser . newPage ( { background} ) ;
321+ }
274322 await this . createPagesSnapshot ( ) ;
275323 this . selectPage ( page ) ;
276324 this . #networkCollector. addPage ( page ) ;
@@ -282,7 +330,20 @@ export class McpContext implements Context {
282330 throw new Error ( CLOSE_PAGE_ERROR ) ;
283331 }
284332 const page = this . getPageById ( pageId ) ;
333+ const isolatedContextName = this . #pageToIsolatedContextName. get ( page ) ;
285334 await page . close ( { runBeforeUnload : false } ) ;
335+ this . #pageToIsolatedContextName. delete ( page ) ;
336+
337+ if ( isolatedContextName ) {
338+ const ctx = this . #isolatedContexts. get ( isolatedContextName ) ;
339+ if ( ctx && ! ctx . closed ) {
340+ const remainingPages = await ctx . pages ( ) ;
341+ if ( remainingPages . length === 0 ) {
342+ await ctx . close ( ) ;
343+ this . #isolatedContexts. delete ( isolatedContextName ) ;
344+ }
345+ }
346+ }
286347 }
287348
288349 getNetworkRequestById ( reqid : number ) : HTTPRequest {
@@ -558,13 +619,8 @@ export class McpContext implements Context {
558619 }
559620 }
560621
561- /**
562- * Creates a snapshot of the pages.
563- */
564622 async createPagesSnapshot ( ) : Promise < Page [ ] > {
565- const allPages = await this . browser . pages (
566- this . #options. experimentalIncludeAllPages ,
567- ) ;
623+ const allPages = await this . #getAllPages( ) ;
568624
569625 for ( const page of allPages ) {
570626 if ( ! this . #pageIdMap. has ( page ) ) {
@@ -573,8 +629,6 @@ export class McpContext implements Context {
573629 }
574630
575631 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.
578632 return (
579633 this . #options. experimentalDevToolsDebugging ||
580634 ! page . url ( ) . startsWith ( 'devtools://' )
@@ -593,11 +647,44 @@ export class McpContext implements Context {
593647 return this . #pages;
594648 }
595649
596- async detectOpenDevToolsWindows ( ) {
597- this . logger ( 'Detecting open DevTools windows' ) ;
598- const pages = await this . browser . pages (
650+ async #getAllPages ( ) : Promise < Page [ ] > {
651+ const defaultCtx = this . browser . defaultBrowserContext ( ) ;
652+ const allPages = await this . browser . pages (
599653 this . #options. experimentalIncludeAllPages ,
600654 ) ;
655+
656+ // Build a reverse lookup from BrowserContext instance → name.
657+ const contextToName = new Map < BrowserContext , string > ( ) ;
658+ for ( const [ name , ctx ] of this . #isolatedContexts) {
659+ contextToName . set ( ctx , name ) ;
660+ }
661+
662+ // Auto-discover BrowserContexts not in our mapping (e.g., externally
663+ // created incognito contexts) and assign generated names.
664+ const knownContexts = new Set ( this . #isolatedContexts. values ( ) ) ;
665+ for ( const ctx of this . browser . browserContexts ( ) ) {
666+ if ( ctx !== defaultCtx && ! ctx . closed && ! knownContexts . has ( ctx ) ) {
667+ const name = `isolated-context-${ this . #nextIsolatedContextId++ } ` ;
668+ this . #isolatedContexts. set ( name , ctx ) ;
669+ contextToName . set ( ctx , name ) ;
670+ }
671+ }
672+
673+ // Use page.browserContext() to determine each page's context membership.
674+ for ( const page of allPages ) {
675+ const ctx = page . browserContext ( ) ;
676+ const name = contextToName . get ( ctx ) ;
677+ if ( name ) {
678+ this . #pageToIsolatedContextName. set ( page , name ) ;
679+ }
680+ }
681+
682+ return allPages ;
683+ }
684+
685+ async detectOpenDevToolsWindows ( ) {
686+ this . logger ( 'Detecting open DevTools windows' ) ;
687+ const pages = await this . #getAllPages( ) ;
601688 this . #pageToDevToolsPage = new Map < Page , Page > ( ) ;
602689 for ( const devToolsPage of pages ) {
603690 if ( devToolsPage . url ( ) . startsWith ( 'devtools://' ) ) {
@@ -629,6 +716,10 @@ export class McpContext implements Context {
629716 return this . #pages;
630717 }
631718
719+ getIsolatedContextName ( page : Page ) : string | undefined {
720+ return this . #pageToIsolatedContextName. get ( page ) ;
721+ }
722+
632723 getDevToolsPage ( page : Page ) : Page | undefined {
633724 return this . #pageToDevToolsPage. get ( page ) ;
634725 }
@@ -857,7 +948,8 @@ export class McpContext implements Context {
857948 } ,
858949 } as ListenerMap ;
859950 } ) ;
860- await this . #networkCollector. init ( await this . browser . pages ( ) ) ;
951+ const pages = await this . browser . pages ( ) ;
952+ await this . #networkCollector. init ( pages ) ;
861953 }
862954
863955 async installExtension ( extensionPath : string ) : Promise < string > {
0 commit comments