@@ -129,6 +129,8 @@ export class McpContext implements Context {
129129 null ;
130130 #focusedPagePerContext = new Map < BrowserContext , Page > ( ) ;
131131
132+ #requestPage?: Page ;
133+
132134 #nextPageId = 1 ;
133135
134136 #extensionServiceWorkerMap = new WeakMap < Target , string > ( ) ;
@@ -203,8 +205,16 @@ export class McpContext implements Context {
203205 return context ;
204206 }
205207
208+ setRequestPage ( page ?: Page ) : void {
209+ this . #requestPage = page ;
210+ }
211+
212+ #resolveTargetPage( ) : Page {
213+ return this . #requestPage ?? this . getSelectedPage ( ) ;
214+ }
215+
206216 resolveCdpRequestId ( cdpRequestId : string ) : number | undefined {
207- const selectedPage = this . getSelectedPage ( ) ;
217+ const selectedPage = this . #resolveTargetPage ( ) ;
208218 if ( ! cdpRequestId ) {
209219 this . logger ( 'no network request' ) ;
210220 return ;
@@ -252,19 +262,19 @@ export class McpContext implements Context {
252262 }
253263
254264 getNetworkRequests ( includePreservedRequests ?: boolean ) : HTTPRequest [ ] {
255- const page = this . getSelectedPage ( ) ;
265+ const page = this . #resolveTargetPage ( ) ;
256266 return this . #networkCollector. getData ( page , includePreservedRequests ) ;
257267 }
258268
259269 getConsoleData (
260270 includePreservedMessages ?: boolean ,
261271 ) : Array < ConsoleMessage | Error | DevTools . AggregatedIssue | UncaughtError > {
262- const page = this . getSelectedPage ( ) ;
272+ const page = this . #resolveTargetPage ( ) ;
263273 return this . #consoleCollector. getData ( page , includePreservedMessages ) ;
264274 }
265275
266276 getDevToolsUniverse ( ) : TargetUniverse | null {
267- return this . #devtoolsUniverseManager. get ( this . getSelectedPage ( ) ) ;
277+ return this . #devtoolsUniverseManager. get ( this . #resolveTargetPage ( ) ) ;
268278 }
269279
270280 getConsoleMessageStableId (
@@ -276,7 +286,7 @@ export class McpContext implements Context {
276286 getConsoleMessageById (
277287 id : number ,
278288 ) : ConsoleMessage | Error | DevTools . AggregatedIssue | UncaughtError {
279- return this . #consoleCollector. getById ( this . getSelectedPage ( ) , id ) ;
289+ return this . #consoleCollector. getById ( this . #resolveTargetPage ( ) , id ) ;
280290 }
281291
282292 async newPage (
@@ -318,7 +328,7 @@ export class McpContext implements Context {
318328 }
319329
320330 getNetworkRequestById ( reqid : number ) : HTTPRequest {
321- return this . #networkCollector. getById ( this . getSelectedPage ( ) , reqid ) ;
331+ return this . #networkCollector. getById ( this . #resolveTargetPage ( ) , reqid ) ;
322332 }
323333
324334 async emulate (
@@ -435,27 +445,27 @@ export class McpContext implements Context {
435445 }
436446
437447 getNetworkConditions ( ) : string | null {
438- return this . #getSelectedMcpPage ( ) . networkConditions ;
448+ return this . #getMcpPage ( this . #resolveTargetPage ( ) ) . networkConditions ;
439449 }
440450
441451 getCpuThrottlingRate ( ) : number {
442- return this . #getSelectedMcpPage ( ) . cpuThrottlingRate ;
452+ return this . #getMcpPage ( this . #resolveTargetPage ( ) ) . cpuThrottlingRate ;
443453 }
444454
445455 getGeolocation ( ) : GeolocationOptions | null {
446- return this . #getSelectedMcpPage ( ) . geolocation ;
456+ return this . #getMcpPage ( this . #resolveTargetPage ( ) ) . geolocation ;
447457 }
448458
449459 getViewport ( ) : Viewport | null {
450- return this . #getSelectedMcpPage ( ) . viewport ;
460+ return this . #getMcpPage ( this . #resolveTargetPage ( ) ) . viewport ;
451461 }
452462
453463 getUserAgent ( ) : string | null {
454- return this . #getSelectedMcpPage ( ) . userAgent ;
464+ return this . #getMcpPage ( this . #resolveTargetPage ( ) ) . userAgent ;
455465 }
456466
457467 getColorScheme ( ) : 'dark' | 'light' | null {
458- return this . #getSelectedMcpPage ( ) . colorScheme ;
468+ return this . #getMcpPage ( this . #resolveTargetPage ( ) ) . colorScheme ;
459469 }
460470
461471 setIsRunningPerformanceTrace ( x : boolean ) : void {
@@ -481,7 +491,7 @@ export class McpContext implements Context {
481491 }
482492
483493 getDialog ( page ?: Page ) : Dialog | undefined {
484- const targetPage = page ?? this . #selectedPage;
494+ const targetPage = page ?? this . #requestPage ?? this . # selectedPage;
485495 if ( ! targetPage ) {
486496 return undefined ;
487497 }
@@ -543,6 +553,19 @@ export class McpContext implements Context {
543553 return this . #selectedPage === page ;
544554 }
545555
556+ assertPageIsFocused ( page : Page ) : void {
557+ const ctx = page . browserContext ( ) ;
558+ const focused = this . #focusedPagePerContext. get ( ctx ) ;
559+ if ( focused && focused !== page ) {
560+ const targetId = this . #mcpPages. get ( page ) ?. id ?? '?' ;
561+ const focusedId = this . #mcpPages. get ( focused ) ?. id ?? '?' ;
562+ throw new Error (
563+ `Page ${ targetId } is not the active page in its browser context (page ${ focusedId } is). ` +
564+ `Call select_page with pageId ${ targetId } first.` ,
565+ ) ;
566+ }
567+ }
568+
546569 selectPage ( newPage : Page ) : void {
547570 const ctx = newPage . browserContext ( ) ;
548571 const oldFocused = this . #focusedPagePerContext. get ( ctx ) ;
@@ -575,7 +598,7 @@ export class McpContext implements Context {
575598 }
576599
577600 getNavigationTimeout ( ) {
578- const page = this . getSelectedPage ( ) ;
601+ const page = this . #resolveTargetPage ( ) ;
579602 return page . getDefaultNavigationTimeout ( ) ;
580603 }
581604
@@ -592,12 +615,39 @@ export class McpContext implements Context {
592615 return undefined ;
593616 }
594617
595- assertUidOnSelectedPage ( uid : string ) : void {
596- for ( const [ page , mcpPage ] of this . #mcpPages. entries ( ) ) {
597- if ( mcpPage . textSnapshot ?. idToNode . has ( uid ) ) {
598- const ctx = page . browserContext ( ) ;
618+ async getElementByUid (
619+ uid : string ,
620+ page ?: Page ,
621+ ) : Promise < ElementHandle < Element > > {
622+ if ( page ) {
623+ // Scoped search: only look in the target page's snapshot.
624+ const mcpPage = this . #mcpPages. get ( page ) ;
625+ if ( ! mcpPage ?. textSnapshot ) {
626+ throw new Error (
627+ `No snapshot found for page ${ mcpPage ?. id ?? '?' } . Use ${ takeSnapshot . name } to capture one.` ,
628+ ) ;
629+ }
630+ const node = mcpPage . textSnapshot . idToNode . get ( uid ) ;
631+ if ( ! node ) {
632+ throw new Error (
633+ `Element uid "${ uid } " not found on page ${ mcpPage . id } .` ,
634+ ) ;
635+ }
636+ return this . #resolveElementHandle( node , uid ) ;
637+ }
638+
639+ // Cross-page search with context-focus validation.
640+ let anySnapshot = false ;
641+ for ( const [ searchPage , mcpPage ] of this . #mcpPages. entries ( ) ) {
642+ if ( ! mcpPage . textSnapshot ) {
643+ continue ;
644+ }
645+ anySnapshot = true ;
646+ const node = mcpPage . textSnapshot . idToNode . get ( uid ) ;
647+ if ( node ) {
648+ const ctx = searchPage . browserContext ( ) ;
599649 const contextSelectedPage = this . #focusedPagePerContext. get ( ctx ) ;
600- if ( contextSelectedPage !== page ) {
650+ if ( contextSelectedPage !== searchPage ) {
601651 const targetId = mcpPage . id ;
602652 const selectedId = contextSelectedPage
603653 ? this . #mcpPages. get ( contextSelectedPage ) ?. id
@@ -608,37 +658,10 @@ export class McpContext implements Context {
608658 ) ;
609659 }
610660 // Align global #selectedPage for waitForEventsAfterAction etc.
611- if ( this . #selectedPage !== page ) {
612- this . #selectedPage = page ;
613- }
614- return ;
615- }
616- }
617- throw new Error ( 'No such element found in any snapshot.' ) ;
618- }
619-
620- async getElementByUid ( uid : string ) : Promise < ElementHandle < Element > > {
621- let anySnapshot = false ;
622- // Search across all per-page snapshots for the uid.
623- for ( const mcpPage of this . #mcpPages. values ( ) ) {
624- if ( ! mcpPage . textSnapshot ) {
625- continue ;
626- }
627- anySnapshot = true ;
628- const node = mcpPage . textSnapshot . idToNode . get ( uid ) ;
629- if ( node ) {
630- const message = `Element with uid ${ uid } no longer exists on the page.` ;
631- try {
632- const handle = await node . elementHandle ( ) ;
633- if ( ! handle ) {
634- throw new Error ( message ) ;
635- }
636- return handle ;
637- } catch ( error ) {
638- throw new Error ( message , {
639- cause : error ,
640- } ) ;
661+ if ( this . #selectedPage !== searchPage ) {
662+ this . #selectedPage = searchPage ;
641663 }
664+ return this . #resolveElementHandle( node , uid ) ;
642665 }
643666 }
644667 if ( ! anySnapshot ) {
@@ -684,6 +707,24 @@ export class McpContext implements Context {
684707 return this . #extensionServiceWorkers;
685708 }
686709
710+ async #resolveElementHandle(
711+ node : TextSnapshotNode ,
712+ uid : string ,
713+ ) : Promise < ElementHandle < Element > > {
714+ const message = `Element with uid ${ uid } no longer exists on the page.` ;
715+ try {
716+ const handle = await node . elementHandle ( ) ;
717+ if ( ! handle ) {
718+ throw new Error ( message ) ;
719+ }
720+ return handle ;
721+ } catch ( error ) {
722+ throw new Error ( message , {
723+ cause : error ,
724+ } ) ;
725+ }
726+ }
727+
687728 async createPagesSnapshot ( ) : Promise < Page [ ] > {
688729 const { pages : allPages , isolatedContextNames} = await this . #getAllPages( ) ;
689730
@@ -696,6 +737,21 @@ export class McpContext implements Context {
696737 mcpPage . isolatedContextName = isolatedContextNames . get ( page ) ;
697738 }
698739
740+ // Prune orphaned #mcpPages entries (pages that no longer exist).
741+ const currentPages = new Set ( allPages ) ;
742+ for ( const [ page , mcpPage ] of this . #mcpPages) {
743+ if ( ! currentPages . has ( page ) ) {
744+ mcpPage . dispose ( ) ;
745+ this . #mcpPages. delete ( page ) ;
746+ }
747+ }
748+ // Prune stale #focusedPagePerContext entries.
749+ for ( const [ ctx , page ] of this . #focusedPagePerContext) {
750+ if ( ! currentPages . has ( page ) ) {
751+ this . #focusedPagePerContext. delete ( ctx ) ;
752+ }
753+ }
754+
699755 this . #pages = allPages . filter ( page => {
700756 return (
701757 this . #options. experimentalDevToolsDebugging ||
@@ -815,7 +871,7 @@ export class McpContext implements Context {
815871 async getDevToolsData ( ) : Promise < DevToolsData > {
816872 try {
817873 this . logger ( 'Getting DevTools UI data' ) ;
818- const selectedPage = this . getSelectedPage ( ) ;
874+ const selectedPage = this . #resolveTargetPage ( ) ;
819875 const devtoolsPage = this . getDevToolsPage ( selectedPage ) ;
820876 if ( ! devtoolsPage ) {
821877 this . logger ( 'No DevTools page detected' ) ;
0 commit comments