@@ -21,10 +21,16 @@ import path from 'node:path';
2121import { listPages } from './tools/pages.js' ;
2222
2323export interface TextSnapshotNode extends SerializedAXNode {
24- id : number ;
24+ id : string ;
2525 children : TextSnapshotNode [ ] ;
2626}
2727
28+ export interface TextSnapshot {
29+ root : TextSnapshotNode ;
30+ idToNode : Map < string , TextSnapshotNode > ;
31+ snapshotId : string ;
32+ }
33+
2834export class McpContext implements Context {
2935 browser : Browser ;
3036 logger : Debugger ;
@@ -33,8 +39,7 @@ export class McpContext implements Context {
3339 #pages: Page [ ] = [ ] ;
3440 #selectedPageIdx = 0 ;
3541 // The most recent snapshot.
36- #textSnapshot: TextSnapshotNode | null = null ;
37- #idToNodeMap = new Map < number , TextSnapshotNode > ( ) ;
42+ #textSnapshot: TextSnapshot | null = null ;
3843 #networkCollector: NetworkCollector ;
3944 #consoleCollector: PageCollector < ConsoleMessage | Error > ;
4045
@@ -43,6 +48,8 @@ export class McpContext implements Context {
4348 #cpuThrottlingRate = 1 ;
4449 #dialog?: Dialog ;
4550
51+ #nextSnapshotId = 1 ;
52+
4653 private constructor ( browser : Browser , logger : Debugger ) {
4754 this . browser = browser ;
4855 this . logger = logger ;
@@ -192,11 +199,19 @@ export class McpContext implements Context {
192199 newPage . setDefaultNavigationTimeout ( 10_000 ) ;
193200 }
194201
195- async getElementByUid ( uid : number ) : Promise < ElementHandle < Element > > {
196- if ( ! this . #idToNodeMap . size ) {
202+ async getElementByUid ( uid : string ) : Promise < ElementHandle < Element > > {
203+ if ( ! this . #textSnapshot ?. idToNode . size ) {
197204 throw new Error ( 'No snapshot found. Use browser_snapshot to capture one' ) ;
198205 }
199- const node = this . #idToNodeMap. get ( uid ) ;
206+ const [ snapshotId ] = uid . split ( '_' ) ;
207+
208+ if ( this . #textSnapshot. snapshotId !== snapshotId ) {
209+ throw new Error (
210+ 'This uid is coming from a stale snapshot. Call take_snapshot to get a fresh snapshot.' ,
211+ ) ;
212+ }
213+
214+ const node = this . #textSnapshot?. idToNode . get ( uid ) ;
200215 if ( ! node ) {
201216 throw new Error ( 'No such element found in the snapshot' ) ;
202217 }
@@ -222,35 +237,38 @@ export class McpContext implements Context {
222237 /**
223238 * Creates a text snapshot of a page.
224239 */
225- async createTextSnapshot ( ) : Promise < TextSnapshotNode | null > {
240+ async createTextSnapshot ( ) : Promise < void > {
226241 const page = this . getSelectedPage ( ) ;
227242 const rootNode = await page . accessibility . snapshot ( ) ;
228243 if ( ! rootNode ) {
229- return null ;
244+ return ;
230245 }
231246
232247 // Iterate through the whole accessibility node tree and assign node ids that
233248 // will be used for the tree serialization and mapping ids back to nodes.
234249 let idCounter = 0 ;
235- this . #idToNodeMap . clear ( ) ;
250+ const idToNode = new Map < string , TextSnapshotNode > ( ) ;
236251 const assignIds = ( node : SerializedAXNode ) : TextSnapshotNode => {
237252 const nodeWithId : TextSnapshotNode = {
238253 ...node ,
239- id : idCounter ++ ,
254+ id : ` ${ this . #nextSnapshotId } _ ${ idCounter ++ } ` ,
240255 children : node . children
241256 ? node . children . map ( child => assignIds ( child ) )
242257 : [ ] ,
243258 } ;
244- this . #idToNodeMap . set ( nodeWithId . id , nodeWithId ) ;
259+ idToNode . set ( nodeWithId . id , nodeWithId ) ;
245260 return nodeWithId ;
246261 } ;
247262
248263 const rootNodeWithId = assignIds ( rootNode ) ;
249- this . #textSnapshot = rootNodeWithId ;
250- return rootNodeWithId ;
264+ this . #textSnapshot = {
265+ root : rootNodeWithId ,
266+ snapshotId : String ( this . #nextSnapshotId++ ) ,
267+ idToNode,
268+ } ;
251269 }
252270
253- getTextSnapshot ( ) : TextSnapshotNode | null {
271+ getTextSnapshot ( ) : TextSnapshot | null {
254272 return this . #textSnapshot;
255273 }
256274
0 commit comments