@@ -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,9 @@ export class McpContext implements Context {
187194 this . #networkCollector. dispose ( ) ;
188195 this . #consoleCollector. dispose ( ) ;
189196 this . #devtoolsUniverseManager. dispose ( ) ;
197+ // Isolated contexts are intentionally not closed here.
198+ // Either the entire browser will be closed or we disconnect
199+ // without destroying browser state.
190200 }
191201
192202 static async from (
@@ -269,8 +279,41 @@ export class McpContext implements Context {
269279 return this . #consoleCollector. getById ( this . getSelectedPage ( ) , id ) ;
270280 }
271281
272- async newPage ( background ?: boolean ) : Promise < Page > {
273- const page = await this . browser . newPage ( { background} ) ;
282+ async newPage (
283+ background ?: boolean ,
284+ isolatedContextName ?: string ,
285+ ) : Promise < Page > {
286+ let page : Page ;
287+ if ( isolatedContextName !== undefined ) {
288+ const isFirstIsolatedContext = this . #isolatedContexts. size === 0 ;
289+ let ctx = this . #isolatedContexts. get ( isolatedContextName ) ;
290+ if ( ! ctx ) {
291+ ctx = await this . browser . createBrowserContext ( ) ;
292+ this . #isolatedContexts. set ( isolatedContextName , ctx ) ;
293+ }
294+ page = await ctx . newPage ( ) ;
295+ this . #pageToIsolatedContextName. set ( page , isolatedContextName ) ;
296+
297+ // On the first isolated context creation, close any leftover
298+ // about:blank pages from the default context. Chrome always opens
299+ // an initial about:blank tab that is no longer needed once isolated
300+ // contexts are in use. We only do this once to avoid closing pages
301+ // the LLM may have explicitly opened in the default context later.
302+ if ( isFirstIsolatedContext ) {
303+ const defaultPages = await this . browser . defaultBrowserContext ( ) . pages ( ) ;
304+ for ( const dp of defaultPages ) {
305+ if ( dp . url ( ) === 'about:blank' ) {
306+ try {
307+ await dp . close ( ) ;
308+ } catch {
309+ // Page may already be closed.
310+ }
311+ }
312+ }
313+ }
314+ } else {
315+ page = await this . browser . newPage ( { background} ) ;
316+ }
274317 await this . createPagesSnapshot ( ) ;
275318 this . selectPage ( page ) ;
276319 this . #networkCollector. addPage ( page ) ;
@@ -283,6 +326,7 @@ export class McpContext implements Context {
283326 }
284327 const page = this . getPageById ( pageId ) ;
285328 await page . close ( { runBeforeUnload : false } ) ;
329+ this . #pageToIsolatedContextName. delete ( page ) ;
286330 }
287331
288332 getNetworkRequestById ( reqid : number ) : HTTPRequest {
@@ -558,13 +602,8 @@ export class McpContext implements Context {
558602 }
559603 }
560604
561- /**
562- * Creates a snapshot of the pages.
563- */
564605 async createPagesSnapshot ( ) : Promise < Page [ ] > {
565- const allPages = await this . browser . pages (
566- this . #options. experimentalIncludeAllPages ,
567- ) ;
606+ const allPages = await this . #getAllPages( ) ;
568607
569608 for ( const page of allPages ) {
570609 if ( ! this . #pageIdMap. has ( page ) ) {
@@ -573,8 +612,6 @@ export class McpContext implements Context {
573612 }
574613
575614 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.
578615 return (
579616 this . #options. experimentalDevToolsDebugging ||
580617 ! page . url ( ) . startsWith ( 'devtools://' )
@@ -593,11 +630,44 @@ export class McpContext implements Context {
593630 return this . #pages;
594631 }
595632
596- async detectOpenDevToolsWindows ( ) {
597- this . logger ( 'Detecting open DevTools windows' ) ;
598- const pages = await this . browser . pages (
633+ async #getAllPages ( ) : Promise < Page [ ] > {
634+ const defaultCtx = this . browser . defaultBrowserContext ( ) ;
635+ const allPages = await this . browser . pages (
599636 this . #options. experimentalIncludeAllPages ,
600637 ) ;
638+
639+ // Build a reverse lookup from BrowserContext instance → name.
640+ const contextToName = new Map < BrowserContext , string > ( ) ;
641+ for ( const [ name , ctx ] of this . #isolatedContexts) {
642+ contextToName . set ( ctx , name ) ;
643+ }
644+
645+ // Auto-discover BrowserContexts not in our mapping (e.g., externally
646+ // created incognito contexts) and assign generated names.
647+ const knownContexts = new Set ( this . #isolatedContexts. values ( ) ) ;
648+ for ( const ctx of this . browser . browserContexts ( ) ) {
649+ if ( ctx !== defaultCtx && ! ctx . closed && ! knownContexts . has ( ctx ) ) {
650+ const name = `isolated-context-${ this . #nextIsolatedContextId++ } ` ;
651+ this . #isolatedContexts. set ( name , ctx ) ;
652+ contextToName . set ( ctx , name ) ;
653+ }
654+ }
655+
656+ // Use page.browserContext() to determine each page's context membership.
657+ for ( const page of allPages ) {
658+ const ctx = page . browserContext ( ) ;
659+ const name = contextToName . get ( ctx ) ;
660+ if ( name ) {
661+ this . #pageToIsolatedContextName. set ( page , name ) ;
662+ }
663+ }
664+
665+ return allPages ;
666+ }
667+
668+ async detectOpenDevToolsWindows ( ) {
669+ this . logger ( 'Detecting open DevTools windows' ) ;
670+ const pages = await this . #getAllPages( ) ;
601671 this . #pageToDevToolsPage = new Map < Page , Page > ( ) ;
602672 for ( const devToolsPage of pages ) {
603673 if ( devToolsPage . url ( ) . startsWith ( 'devtools://' ) ) {
@@ -629,6 +699,10 @@ export class McpContext implements Context {
629699 return this . #pages;
630700 }
631701
702+ getIsolatedContextName ( page : Page ) : string | undefined {
703+ return this . #pageToIsolatedContextName. get ( page ) ;
704+ }
705+
632706 getDevToolsPage ( page : Page ) : Page | undefined {
633707 return this . #pageToDevToolsPage. get ( page ) ;
634708 }
@@ -857,7 +931,8 @@ export class McpContext implements Context {
857931 } ,
858932 } as ListenerMap ;
859933 } ) ;
860- await this . #networkCollector. init ( await this . browser . pages ( ) ) ;
934+ const pages = await this . browser . pages ( ) ;
935+ await this . #networkCollector. init ( pages ) ;
861936 }
862937
863938 async installExtension ( extensionPath : string ) : Promise < string > {
0 commit comments