@@ -120,6 +120,8 @@ export class McpContext implements Context {
120120 null ;
121121 #focusedPagePerContext = new Map < BrowserContext , Page > ( ) ;
122122
123+ #requestPage?: Page ;
124+
123125 #nextPageId = 1 ;
124126
125127 #nextSnapshotId = 1 ;
@@ -190,8 +192,16 @@ export class McpContext implements Context {
190192 return context ;
191193 }
192194
195+ setRequestPage ( page ?: Page ) : void {
196+ this . #requestPage = page ;
197+ }
198+
199+ #resolveTargetPage( ) : Page {
200+ return this . #requestPage ?? this . getSelectedPage ( ) ;
201+ }
202+
193203 resolveCdpRequestId ( cdpRequestId : string ) : number | undefined {
194- const selectedPage = this . getSelectedPage ( ) ;
204+ const selectedPage = this . #resolveTargetPage ( ) ;
195205 if ( ! cdpRequestId ) {
196206 this . logger ( 'no network request' ) ;
197207 return ;
@@ -239,19 +249,19 @@ export class McpContext implements Context {
239249 }
240250
241251 getNetworkRequests ( includePreservedRequests ?: boolean ) : HTTPRequest [ ] {
242- const page = this . getSelectedPage ( ) ;
252+ const page = this . #resolveTargetPage ( ) ;
243253 return this . #networkCollector. getData ( page , includePreservedRequests ) ;
244254 }
245255
246256 getConsoleData (
247257 includePreservedMessages ?: boolean ,
248258 ) : Array < ConsoleMessage | Error | DevTools . AggregatedIssue | UncaughtError > {
249- const page = this . getSelectedPage ( ) ;
259+ const page = this . #resolveTargetPage ( ) ;
250260 return this . #consoleCollector. getData ( page , includePreservedMessages ) ;
251261 }
252262
253263 getDevToolsUniverse ( ) : TargetUniverse | null {
254- return this . #devtoolsUniverseManager. get ( this . getSelectedPage ( ) ) ;
264+ return this . #devtoolsUniverseManager. get ( this . #resolveTargetPage ( ) ) ;
255265 }
256266
257267 getConsoleMessageStableId (
@@ -263,7 +273,7 @@ export class McpContext implements Context {
263273 getConsoleMessageById (
264274 id : number ,
265275 ) : ConsoleMessage | Error | DevTools . AggregatedIssue | UncaughtError {
266- return this . #consoleCollector. getById ( this . getSelectedPage ( ) , id ) ;
276+ return this . #consoleCollector. getById ( this . #resolveTargetPage ( ) , id ) ;
267277 }
268278
269279 async newPage (
@@ -305,7 +315,7 @@ export class McpContext implements Context {
305315 }
306316
307317 getNetworkRequestById ( reqid : number ) : HTTPRequest {
308- return this . #networkCollector. getById ( this . getSelectedPage ( ) , reqid ) ;
318+ return this . #networkCollector. getById ( this . #resolveTargetPage ( ) , reqid ) ;
309319 }
310320
311321 async emulate (
@@ -422,27 +432,27 @@ export class McpContext implements Context {
422432 }
423433
424434 getNetworkConditions ( ) : string | null {
425- return this . #getSelectedMcpPage ( ) . networkConditions ;
435+ return this . #getMcpPage ( this . #resolveTargetPage ( ) ) . networkConditions ;
426436 }
427437
428438 getCpuThrottlingRate ( ) : number {
429- return this . #getSelectedMcpPage ( ) . cpuThrottlingRate ;
439+ return this . #getMcpPage ( this . #resolveTargetPage ( ) ) . cpuThrottlingRate ;
430440 }
431441
432442 getGeolocation ( ) : GeolocationOptions | null {
433- return this . #getSelectedMcpPage ( ) . geolocation ;
443+ return this . #getMcpPage ( this . #resolveTargetPage ( ) ) . geolocation ;
434444 }
435445
436446 getViewport ( ) : Viewport | null {
437- return this . #getSelectedMcpPage ( ) . viewport ;
447+ return this . #getMcpPage ( this . #resolveTargetPage ( ) ) . viewport ;
438448 }
439449
440450 getUserAgent ( ) : string | null {
441- return this . #getSelectedMcpPage ( ) . userAgent ;
451+ return this . #getMcpPage ( this . #resolveTargetPage ( ) ) . userAgent ;
442452 }
443453
444454 getColorScheme ( ) : 'dark' | 'light' | null {
445- return this . #getSelectedMcpPage ( ) . colorScheme ;
455+ return this . #getMcpPage ( this . #resolveTargetPage ( ) ) . colorScheme ;
446456 }
447457
448458 setIsRunningPerformanceTrace ( x : boolean ) : void {
@@ -468,7 +478,7 @@ export class McpContext implements Context {
468478 }
469479
470480 getDialog ( page ?: Page ) : Dialog | undefined {
471- const targetPage = page ?? this . #selectedPage;
481+ const targetPage = page ?? this . #requestPage ?? this . # selectedPage;
472482 if ( ! targetPage ) {
473483 return undefined ;
474484 }
@@ -530,6 +540,19 @@ export class McpContext implements Context {
530540 return this . #selectedPage === page ;
531541 }
532542
543+ assertPageIsFocused ( page : Page ) : void {
544+ const ctx = page . browserContext ( ) ;
545+ const focused = this . #focusedPagePerContext. get ( ctx ) ;
546+ if ( focused && focused !== page ) {
547+ const targetId = this . #mcpPages. get ( page ) ?. id ?? '?' ;
548+ const focusedId = this . #mcpPages. get ( focused ) ?. id ?? '?' ;
549+ throw new Error (
550+ `Page ${ targetId } is not the active page in its browser context (page ${ focusedId } is). ` +
551+ `Call select_page with pageId ${ targetId } first.` ,
552+ ) ;
553+ }
554+ }
555+
533556 selectPage ( newPage : Page ) : void {
534557 const ctx = newPage . browserContext ( ) ;
535558 const oldFocused = this . #focusedPagePerContext. get ( ctx ) ;
@@ -562,7 +585,7 @@ export class McpContext implements Context {
562585 }
563586
564587 getNavigationTimeout ( ) {
565- const page = this . getSelectedPage ( ) ;
588+ const page = this . #resolveTargetPage ( ) ;
566589 return page . getDefaultNavigationTimeout ( ) ;
567590 }
568591
@@ -579,12 +602,39 @@ export class McpContext implements Context {
579602 return undefined ;
580603 }
581604
582- assertUidOnSelectedPage ( uid : string ) : void {
583- for ( const [ page , mcpPage ] of this . #mcpPages. entries ( ) ) {
584- if ( mcpPage . textSnapshot ?. idToNode . has ( uid ) ) {
585- const ctx = page . browserContext ( ) ;
605+ async getElementByUid (
606+ uid : string ,
607+ page ?: Page ,
608+ ) : Promise < ElementHandle < Element > > {
609+ if ( page ) {
610+ // Scoped search: only look in the target page's snapshot.
611+ const mcpPage = this . #mcpPages. get ( page ) ;
612+ if ( ! mcpPage ?. textSnapshot ) {
613+ throw new Error (
614+ `No snapshot found for page ${ mcpPage ?. id ?? '?' } . Use ${ takeSnapshot . name } to capture one.` ,
615+ ) ;
616+ }
617+ const node = mcpPage . textSnapshot . idToNode . get ( uid ) ;
618+ if ( ! node ) {
619+ throw new Error (
620+ `Element uid "${ uid } " not found on page ${ mcpPage . id } .` ,
621+ ) ;
622+ }
623+ return this . #resolveElementHandle( node , uid ) ;
624+ }
625+
626+ // Cross-page search with context-focus validation.
627+ let anySnapshot = false ;
628+ for ( const [ searchPage , mcpPage ] of this . #mcpPages. entries ( ) ) {
629+ if ( ! mcpPage . textSnapshot ) {
630+ continue ;
631+ }
632+ anySnapshot = true ;
633+ const node = mcpPage . textSnapshot . idToNode . get ( uid ) ;
634+ if ( node ) {
635+ const ctx = searchPage . browserContext ( ) ;
586636 const contextSelectedPage = this . #focusedPagePerContext. get ( ctx ) ;
587- if ( contextSelectedPage !== page ) {
637+ if ( contextSelectedPage !== searchPage ) {
588638 const targetId = mcpPage . id ;
589639 const selectedId = contextSelectedPage
590640 ? this . #mcpPages. get ( contextSelectedPage ) ?. id
@@ -595,37 +645,10 @@ export class McpContext implements Context {
595645 ) ;
596646 }
597647 // Align global #selectedPage for waitForEventsAfterAction etc.
598- if ( this . #selectedPage !== page ) {
599- this . #selectedPage = page ;
600- }
601- return ;
602- }
603- }
604- throw new Error ( 'No such element found in any snapshot.' ) ;
605- }
606-
607- async getElementByUid ( uid : string ) : Promise < ElementHandle < Element > > {
608- let anySnapshot = false ;
609- // Search across all per-page snapshots for the uid.
610- for ( const mcpPage of this . #mcpPages. values ( ) ) {
611- if ( ! mcpPage . textSnapshot ) {
612- continue ;
613- }
614- anySnapshot = true ;
615- const node = mcpPage . textSnapshot . idToNode . get ( uid ) ;
616- if ( node ) {
617- const message = `Element with uid ${ uid } no longer exists on the page.` ;
618- try {
619- const handle = await node . elementHandle ( ) ;
620- if ( ! handle ) {
621- throw new Error ( message ) ;
622- }
623- return handle ;
624- } catch ( error ) {
625- throw new Error ( message , {
626- cause : error ,
627- } ) ;
648+ if ( this . #selectedPage !== searchPage ) {
649+ this . #selectedPage = searchPage ;
628650 }
651+ return this . #resolveElementHandle( node , uid ) ;
629652 }
630653 }
631654 if ( ! anySnapshot ) {
@@ -636,6 +659,24 @@ export class McpContext implements Context {
636659 throw new Error ( 'No such element found in any snapshot.' ) ;
637660 }
638661
662+ async #resolveElementHandle(
663+ node : TextSnapshotNode ,
664+ uid : string ,
665+ ) : Promise < ElementHandle < Element > > {
666+ const message = `Element with uid ${ uid } no longer exists on the page.` ;
667+ try {
668+ const handle = await node . elementHandle ( ) ;
669+ if ( ! handle ) {
670+ throw new Error ( message ) ;
671+ }
672+ return handle ;
673+ } catch ( error ) {
674+ throw new Error ( message , {
675+ cause : error ,
676+ } ) ;
677+ }
678+ }
679+
639680 async createPagesSnapshot ( ) : Promise < Page [ ] > {
640681 const { pages : allPages , isolatedContextNames} = await this . #getAllPages( ) ;
641682
@@ -648,6 +689,21 @@ export class McpContext implements Context {
648689 mcpPage . isolatedContextName = isolatedContextNames . get ( page ) ;
649690 }
650691
692+ // Prune orphaned #mcpPages entries (pages that no longer exist).
693+ const currentPages = new Set ( allPages ) ;
694+ for ( const [ page , mcpPage ] of this . #mcpPages) {
695+ if ( ! currentPages . has ( page ) ) {
696+ mcpPage . dispose ( ) ;
697+ this . #mcpPages. delete ( page ) ;
698+ }
699+ }
700+ // Prune stale #focusedPagePerContext entries.
701+ for ( const [ ctx , page ] of this . #focusedPagePerContext) {
702+ if ( ! currentPages . has ( page ) ) {
703+ this . #focusedPagePerContext. delete ( ctx ) ;
704+ }
705+ }
706+
651707 this . #pages = allPages . filter ( page => {
652708 return (
653709 this . #options. experimentalDevToolsDebugging ||
@@ -757,7 +813,7 @@ export class McpContext implements Context {
757813 async getDevToolsData ( ) : Promise < DevToolsData > {
758814 try {
759815 this . logger ( 'Getting DevTools UI data' ) ;
760- const selectedPage = this . getSelectedPage ( ) ;
816+ const selectedPage = this . #resolveTargetPage ( ) ;
761817 const devtoolsPage = this . getDevToolsPage ( selectedPage ) ;
762818 if ( ! devtoolsPage ) {
763819 this . logger ( 'No DevTools page detected' ) ;
0 commit comments