/** * Copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ export function debugLog(...args: unknown[]): void { const enabled = true; if (enabled) { // eslint-disable-next-line no-console console.log('[Extension]', ...args); } } type ProtocolCommand = { id: number; method: string; params?: any; }; type ProtocolResponse = { id?: number; method?: string; params?: any; result?: any; error?: string; }; export class RelayConnection { private _debuggee: chrome.debugger.Debuggee; private _ws: WebSocket; private _eventListener: (source: chrome.debugger.DebuggerSession, method: string, params: any) => void; private _detachListener: (source: chrome.debugger.Debuggee, reason: string) => void; constructor(ws: WebSocket, tabId: number) { this._debuggee = { tabId }; this._ws = ws; this._ws.onmessage = this._onMessage.bind(this); // Store listeners for cleanup this._eventListener = this._onDebuggerEvent.bind(this); this._detachListener = this._onDebuggerDetach.bind(this); chrome.debugger.onEvent.addListener(this._eventListener); chrome.debugger.onDetach.addListener(this._detachListener); } close(message: string): void { chrome.debugger.onEvent.removeListener(this._eventListener); chrome.debugger.onDetach.removeListener(this._detachListener); this._ws.close(1000, message); } private async _detachDebugger(): Promise { await chrome.debugger.detach(this._debuggee); } private _onDebuggerEvent(source: chrome.debugger.DebuggerSession, method: string, params: any): void { if (source.tabId !== this._debuggee.tabId) return; debugLog('Forwarding CDP event:', method, params); const sessionId = source.sessionId; this._sendMessage({ method: 'forwardCDPEvent', params: { sessionId, method, params, }, }); } private _onDebuggerDetach(source: chrome.debugger.Debuggee, reason: string): void { if (source.tabId !== this._debuggee.tabId) return; this._sendMessage({ method: 'detachedFromTab', params: { tabId: this._debuggee.tabId, reason, }, }); this._debuggee = { }; } private _onMessage(event: MessageEvent): void { this._onMessageAsync(event).catch(e => debugLog('Error handling message:', e)); } private async _onMessageAsync(event: MessageEvent): Promise { let message: ProtocolCommand; try { message = JSON.parse(event.data); } catch (error: any) { debugLog('Error parsing message:', error); this._sendError(-32700, `Error parsing message: ${error.message}`); return; } debugLog('Received message:', message); const response: ProtocolResponse = { id: message.id, }; try { response.result = await this._handleCommand(message); } catch (error: any) { debugLog('Error handling command:', error); response.error = error.message; } debugLog('Sending response:', response); this._sendMessage(response); } private async _handleCommand(message: ProtocolCommand): Promise { if (!this._debuggee.tabId) throw new Error('No tab is connected. Please go to the Playwright MCP extension and select the tab you want to connect to.'); if (message.method === 'attachToTab') { debugLog('Attaching debugger to tab:', this._debuggee); await chrome.debugger.attach(this._debuggee, '1.3'); const result: any = await chrome.debugger.sendCommand(this._debuggee, 'Target.getTargetInfo'); return { targetInfo: result?.targetInfo, }; } if (message.method === 'detachFromTab') { debugLog('Detaching debugger from tab:', this._debuggee); return await this._detachDebugger(); } if (message.method === 'forwardCDPCommand') { const { sessionId, method, params } = message.params; debugLog('CDP command:', method, params); const debuggerSession: chrome.debugger.DebuggerSession = { ...this._debuggee, sessionId, }; // Forward CDP command to chrome.debugger return await chrome.debugger.sendCommand( debuggerSession, method, params ); } } private _sendError(code: number, message: string): void { this._sendMessage({ error: { code, message, }, }); } private _sendMessage(message: any): void { this._ws.send(JSON.stringify(message)); } }