1919 *
2020 * Endpoints:
2121 * - /cdp/guid - Full CDP interface for Playwright MCP
22- * - /extension/guid - Extension connection that exposes a thin chrome.* RPC.
23- *
24- * The relay owns CDP session management: it asks the extension for the user's
25- * tab pick (extension.selectTab), then attaches the debugger and dispatches
26- * Target.attachedToTarget events to Playwright. Additional tabs are created
27- * either by Playwright (Target.createTarget → chrome.tabs.create) or by the
28- * controlled tabs themselves (chrome.tabs.onCreated event from the extension).
22+ * - /extension/guid - Extension connection for chrome.debugger forwarding
2923 */
3024
3125import { spawn } from 'child_process' ;
@@ -64,15 +58,6 @@ type CDPResponse = {
6458 error ?: { code ?: number ; message : string } ;
6559} ;
6660
67- type TabSession = {
68- tabId : number ;
69- sessionId : string ;
70- targetInfo : any ;
71- // Child CDP sessionIds (workers, oopifs, ...) belonging to this tab,
72- // tracked via Target.attachedToTarget / Target.detachedFromTarget events.
73- childSessions : Set < string > ;
74- } ;
75-
7661export class CDPRelayServer {
7762 private _wsHost : string ;
7863 private _browserChannel : string ;
@@ -83,10 +68,13 @@ export class CDPRelayServer {
8368 private _wss : WebSocketServer ;
8469 private _playwrightConnection : WebSocket | null = null ;
8570 private _extensionConnection : ExtensionConnection | null = null ;
86- private _extensionConnectionPromise ! : ManualPromise < void > ;
87-
88- private _tabSessions = new Map < number , TabSession > ( ) ;
71+ private _connectedTabInfo : {
72+ targetInfo : any ;
73+ // Page sessionId that should be used by this connection.
74+ sessionId : string ;
75+ } | undefined ;
8976 private _nextSessionId : number = 1 ;
77+ private _extensionConnectionPromise ! : ManualPromise < void > ;
9078
9179 constructor ( server : http . Server , browserChannel : string , userDataDir ?: string , executablePath ?: string ) {
9280 this . _wsHost = addressToString ( server . address ( ) , { protocol : 'ws' } ) ;
@@ -226,28 +214,12 @@ export class CDPRelayServer {
226214 }
227215
228216 private _resetExtensionConnection ( ) {
229- this . _tabSessions . clear ( ) ;
217+ this . _connectedTabInfo = undefined ;
230218 this . _extensionConnection = null ;
231219 this . _extensionConnectionPromise = new ManualPromise ( ) ;
232220 void this . _extensionConnectionPromise . catch ( logUnhandledError ) ;
233221 }
234222
235- private _findTabSessionBySessionId ( sessionId : string ) : TabSession | undefined {
236- for ( const session of this . _tabSessions . values ( ) ) {
237- if ( session . sessionId === sessionId )
238- return session ;
239- }
240- return undefined ;
241- }
242-
243- private _findTabSessionByChildSessionId ( childSessionId : string ) : TabSession | undefined {
244- for ( const session of this . _tabSessions . values ( ) ) {
245- if ( session . childSessions . has ( childSessionId ) )
246- return session ;
247- }
248- return undefined ;
249- }
250-
251223 private _closePlaywrightConnection ( reason : string ) {
252224 if ( this . _playwrightConnection ?. readyState === ws . OPEN )
253225 this . _playwrightConnection . close ( 1000 , reason ) ;
@@ -273,92 +245,17 @@ export class CDPRelayServer {
273245
274246 private _handleExtensionMessage < M extends keyof ExtensionEvents > ( method : M , params : ExtensionEvents [ M ] [ 'params' ] ) {
275247 switch ( method ) {
276- case 'chrome.debugger.onEvent' : {
277- const [ source , cdpMethod , cdpParams ] = params as ExtensionEvents [ 'chrome.debugger.onEvent' ] [ 'params' ] ;
278- if ( source . tabId === undefined )
279- return ;
280- const tabSession = this . _tabSessions . get ( source . tabId ) ;
281- if ( ! tabSession )
282- return ;
283- // Track child CDP sessions so we can route subsequent commands for
284- // them to the correct tab. Target.attachedToTarget introduces a new
285- // sessionId belonging to the same tab; Target.detachedFromTarget
286- // releases it.
287- const childSessionId = ( cdpParams as { sessionId ?: string } | undefined ) ?. sessionId ;
288- if ( cdpMethod === 'Target.attachedToTarget' && childSessionId )
289- tabSession . childSessions . add ( childSessionId ) ;
290- else if ( cdpMethod === 'Target.detachedFromTarget' && childSessionId )
291- tabSession . childSessions . delete ( childSessionId ) ;
292- // Top-level CDP events for the tab use the tab's relay sessionId.
293- // Child CDP sessions (workers, oopifs) keep their own sessionId.
294- const sessionId = source . sessionId || tabSession . sessionId ;
248+ case 'forwardCDPEvent' :
249+ const sessionId = params . sessionId || this . _connectedTabInfo ?. sessionId ;
295250 this . _sendToPlaywright ( {
296251 sessionId,
297- method : cdpMethod ,
298- params : cdpParams ,
252+ method : params . method ,
253+ params : params . params
299254 } ) ;
300255 break ;
301- }
302- case 'chrome.debugger.onDetach' : {
303- const [ source ] = params as ExtensionEvents [ 'chrome.debugger.onDetach' ] [ 'params' ] ;
304- if ( source . tabId !== undefined )
305- this . _detachTab ( source . tabId ) ;
306- break ;
307- }
308- case 'chrome.tabs.onCreated' : {
309- const [ tab ] = params as ExtensionEvents [ 'chrome.tabs.onCreated' ] [ 'params' ] ;
310- // A controlled tab opened a popup. Attach to it.
311- if ( tab . id !== undefined )
312- void this . _attachTab ( tab . id ) . catch ( logUnhandledError ) ;
313- break ;
314- }
315- case 'chrome.tabs.onRemoved' : {
316- const [ tabId ] = params as ExtensionEvents [ 'chrome.tabs.onRemoved' ] [ 'params' ] ;
317- this . _detachTab ( tabId ) ;
318- break ;
319- }
320256 }
321257 }
322258
323- private async _attachTab ( tabId : number ) : Promise < TabSession > {
324- const existing = this . _tabSessions . get ( tabId ) ;
325- if ( existing )
326- return existing ;
327- if ( ! this . _extensionConnection )
328- throw new Error ( 'Extension not connected' ) ;
329- await this . _extensionConnection . send ( 'chrome.debugger.attach' , [ { tabId } , '1.3' ] ) ;
330- const result = await this . _extensionConnection . send ( 'chrome.debugger.sendCommand' , [
331- { tabId } ,
332- 'Target.getTargetInfo' ,
333- ] ) ;
334- const targetInfo = result ?. targetInfo ;
335- const sessionId = `pw-tab-${ this . _nextSessionId ++ } ` ;
336- const tabSession : TabSession = { tabId, sessionId, targetInfo, childSessions : new Set ( ) } ;
337- this . _tabSessions . set ( tabId , tabSession ) ;
338- debugLogger ( `Attached tab ${ tabId } as session ${ sessionId } ` ) ;
339- this . _sendToPlaywright ( {
340- method : 'Target.attachedToTarget' ,
341- params : {
342- sessionId,
343- targetInfo : { ...targetInfo , attached : true } ,
344- waitingForDebugger : false ,
345- } ,
346- } ) ;
347- return tabSession ;
348- }
349-
350- private _detachTab ( tabId : number ) : void {
351- const tabSession = this . _tabSessions . get ( tabId ) ;
352- if ( ! tabSession )
353- return ;
354- this . _tabSessions . delete ( tabId ) ;
355- debugLogger ( `Detached tab ${ tabId } (session ${ tabSession . sessionId } )` ) ;
356- this . _sendToPlaywright ( {
357- method : 'Target.detachedFromTarget' ,
358- params : { sessionId : tabSession . sessionId } ,
359- } ) ;
360- }
361-
362259 private async _handlePlaywrightMessage ( message : CDPCommand ) : Promise < void > {
363260 debugLogger ( '← Playwright:' , `${ message . method } (id=${ message . id } )` ) ;
364261 const { id, sessionId, method, params } = message ;
@@ -391,53 +288,40 @@ export class CDPRelayServer {
391288 // Forward child session handling.
392289 if ( sessionId )
393290 break ;
394- // Ask the user to pick the initial tab via the connect UI, then attach.
395- if ( ! this . _extensionConnection )
396- throw new Error ( 'Extension not connected' ) ;
397- const { tabId } = await this . _extensionConnection . send ( 'extension.selectTab' , [ ] ) ;
398- await this . _attachTab ( tabId ) ;
291+ // Simulate auto-attach behavior with real target info
292+ const { targetInfo } = await this . _extensionConnection ! . send ( 'attachToTab' , { } ) ;
293+ this . _connectedTabInfo = {
294+ targetInfo,
295+ sessionId : `pw-tab-${ this . _nextSessionId ++ } ` ,
296+ } ;
297+ debugLogger ( 'Simulating auto-attach' ) ;
298+ this . _sendToPlaywright ( {
299+ method : 'Target.attachedToTarget' ,
300+ params : {
301+ sessionId : this . _connectedTabInfo . sessionId ,
302+ targetInfo : {
303+ ...this . _connectedTabInfo . targetInfo ,
304+ attached : true ,
305+ } ,
306+ waitingForDebugger : false
307+ }
308+ } ) ;
399309 return { } ;
400310 }
401- case 'Target.createTarget' : {
402- if ( ! this . _extensionConnection )
403- throw new Error ( 'Extension not connected' ) ;
404- const tab = await this . _extensionConnection . send ( 'chrome.tabs.create' , [ { url : params ?. url } ] ) ;
405- if ( tab ?. id === undefined )
406- throw new Error ( 'Failed to create tab' ) ;
407- const tabSession = await this . _attachTab ( tab . id ) ;
408- return { targetId : tabSession . targetInfo ?. targetId } ;
409- }
410311 case 'Target.getTargetInfo' : {
411- if ( ! sessionId )
412- return undefined ;
413- return this . _findTabSessionBySessionId ( sessionId ) ?. targetInfo ;
312+ return this . _connectedTabInfo ?. targetInfo ;
414313 }
415314 }
416- if ( ! sessionId )
417- throw new Error ( `Unsupported command without sessionId: ${ method } ` ) ;
418315 return await this . _forwardToExtension ( method , params , sessionId ) ;
419316 }
420317
421- private async _forwardToExtension ( method : string , params : any , sessionId : string ) : Promise < any > {
318+ private async _forwardToExtension ( method : string , params : any , sessionId : string | undefined ) : Promise < any > {
422319 if ( ! this . _extensionConnection )
423320 throw new Error ( 'Extension not connected' ) ;
424- // Resolve the sessionId to a tab session. Two cases:
425- // 1. sessionId is a relay-level tab session (pw-tab-N) → strip and route by tabId.
426- // 2. sessionId is a child CDP session (worker, oopif) → route to its owning tab,
427- // keep the sessionId so the extension forwards it to chrome.debugger.
428- let tabSession = this . _findTabSessionBySessionId ( sessionId ) ;
429- let cdpSessionId : string | undefined ;
430- if ( ! tabSession ) {
431- tabSession = this . _findTabSessionByChildSessionId ( sessionId ) ;
432- cdpSessionId = sessionId ;
433- }
434- if ( ! tabSession )
435- throw new Error ( `No tab found for sessionId: ${ sessionId } ` ) ;
436- return await this . _extensionConnection . send ( 'chrome.debugger.sendCommand' , [
437- { tabId : tabSession . tabId , sessionId : cdpSessionId } ,
438- method ,
439- params ,
440- ] ) ;
321+ // Top level sessionId is only passed between the relay and the client.
322+ if ( this . _connectedTabInfo ?. sessionId === sessionId )
323+ sessionId = undefined ;
324+ return await this . _extensionConnection . send ( 'forwardCDPCommand' , { sessionId, method, params } ) ;
441325 }
442326
443327 private _sendToPlaywright ( message : CDPResponse ) : void {
@@ -469,7 +353,7 @@ class ExtensionConnection {
469353 this . _ws . on ( 'error' , this . _onError . bind ( this ) ) ;
470354 }
471355
472- async send < M extends keyof ExtensionCommand > ( method : M , params : ExtensionCommand [ M ] [ 'params' ] ) : Promise < ExtensionCommand [ M ] [ 'result' ] > {
356+ async send < M extends keyof ExtensionCommand > ( method : M , params : ExtensionCommand [ M ] [ 'params' ] ) : Promise < any > {
473357 if ( this . _ws . readyState !== ws . OPEN )
474358 throw new Error ( `Unexpected WebSocket state: ${ this . _ws . readyState } ` ) ;
475359 const id = ++ this . _lastId ;
0 commit comments