diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4eb28cd..2c28167 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,8 +48,6 @@ jobs: run: npx playwright install msedge - name: Build run: npm run build - - name: Build Chrome extension - run: npm run build:extension - name: Run tests run: npm test diff --git a/extension/icons/icon-128.png b/extension/icons/icon-128.png deleted file mode 100644 index c4bc8b0..0000000 Binary files a/extension/icons/icon-128.png and /dev/null differ diff --git a/extension/icons/icon-16.png b/extension/icons/icon-16.png deleted file mode 100644 index 0bab712..0000000 Binary files a/extension/icons/icon-16.png and /dev/null differ diff --git a/extension/icons/icon-32.png b/extension/icons/icon-32.png deleted file mode 100644 index 1f9a8cc..0000000 Binary files a/extension/icons/icon-32.png and /dev/null differ diff --git a/extension/icons/icon-48.png b/extension/icons/icon-48.png deleted file mode 100644 index ac23ef0..0000000 Binary files a/extension/icons/icon-48.png and /dev/null differ diff --git a/extension/manifest.json b/extension/manifest.json deleted file mode 100644 index d605e91..0000000 --- a/extension/manifest.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "manifest_version": 3, - "name": "Playwright MCP Bridge", - "version": "1.0.0", - "description": "Share browser tabs with Playwright MCP server through CDP bridge", - - "permissions": [ - "debugger", - "activeTab", - "tabs", - "storage" - ], - - "host_permissions": [ - "" - ], - - "background": { - "service_worker": "lib/background.js", - "type": "module" - }, - - "action": { - "default_title": "Share tab with Playwright MCP", - "default_popup": "popup.html", - "default_icon": { - "16": "icons/icon-16.png", - "32": "icons/icon-32.png", - "48": "icons/icon-48.png", - "128": "icons/icon-128.png" - } - }, - - "icons": { - "16": "icons/icon-16.png", - "32": "icons/icon-32.png", - "48": "icons/icon-48.png", - "128": "icons/icon-128.png" - } -} diff --git a/extension/popup.html b/extension/popup.html deleted file mode 100644 index 260ceac..0000000 --- a/extension/popup.html +++ /dev/null @@ -1,173 +0,0 @@ - - - - - - - - -
-

Playwright MCP Bridge

-
- -
- -
- - -
Enter the WebSocket URL of your MCP bridge server
-
- -
- -
- - - - diff --git a/extension/src/background.ts b/extension/src/background.ts deleted file mode 100644 index c6ad664..0000000 --- a/extension/src/background.ts +++ /dev/null @@ -1,199 +0,0 @@ -/** - * 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. - */ - -import { RelayConnection, debugLog } from './relayConnection.js'; - -/** - * Simple Chrome Extension that pumps CDP messages between chrome.debugger and WebSocket - */ - -type PopupMessage = { - type: 'getStatus' | 'connect' | 'disconnect'; - tabId: number; - bridgeUrl?: string; -}; - -type SendResponse = (response: any) => void; - -class TabShareExtension { - private activeConnections: Map; - - constructor() { - this.activeConnections = new Map(); // tabId -> connection - - // Remove page action click handler since we now use popup - chrome.tabs.onRemoved.addListener(this.onTabRemoved.bind(this)); - - // Handle messages from popup - chrome.runtime.onMessage.addListener(this.onMessage.bind(this)); - } - - /** - * Handle messages from popup - */ - onMessage(message: PopupMessage, sender: chrome.runtime.MessageSender, sendResponse: SendResponse): boolean { - switch (message.type) { - case 'getStatus': - this.getStatus(message.tabId, sendResponse); - return true; // Will respond asynchronously - - case 'connect': - this.connectTab(message.tabId, message.bridgeUrl!).then( - () => sendResponse({ success: true }), - (error: Error) => sendResponse({ success: false, error: error.message }) - ); - return true; // Will respond asynchronously - - case 'disconnect': - this.disconnectTab(message.tabId).then( - () => sendResponse({ success: true }), - (error: Error) => sendResponse({ success: false, error: error.message }) - ); - return true; // Will respond asynchronously - } - return false; - } - - /** - * Get connection status for popup - */ - getStatus(requestedTabId: number, sendResponse: SendResponse): void { - const isConnected = this.activeConnections.size > 0; - let activeTabId: number | null = null; - - if (isConnected) { - const [tabId] = this.activeConnections.entries().next().value as [number, RelayConnection]; - activeTabId = tabId; - - // Get tab info - chrome.tabs.get(tabId, tab => { - if (chrome.runtime.lastError) { - sendResponse({ - isConnected: false, - error: 'Active tab not found' - }); - } else { - sendResponse({ - isConnected: true, - activeTabId, - activeTabInfo: { - title: tab.title, - url: tab.url - } - }); - } - }); - } else { - sendResponse({ - isConnected: false, - activeTabId: null, - activeTabInfo: null - }); - } - } - - /** - * Connect a tab to the bridge server - */ - async connectTab(tabId: number, bridgeUrl: string): Promise { - try { - debugLog(`Connecting tab ${tabId} to bridge at ${bridgeUrl}`); - // Connect to bridge server - const socket = new WebSocket(bridgeUrl); - await new Promise((resolve, reject) => { - socket.onopen = () => resolve(); - socket.onerror = () => reject(new Error('WebSocket error')); - setTimeout(() => reject(new Error('Connection timeout')), 5000); - }); - - const info = this._createConnection(tabId, socket); - // Store connection - this.activeConnections.set(tabId, info); - - await this._updateUI(tabId, { text: '●', color: '#4CAF50', title: 'Disconnect from Playwright MCP' }); - debugLog(`Tab ${tabId} connected successfully`); - } catch (error: any) { - debugLog(`Failed to connect tab ${tabId}:`, error.message); - await this._cleanupConnection(tabId); - - // Show error to user - await this._updateUI(tabId, { text: '!', color: '#F44336', title: `Connection failed: ${error.message}` }); - - throw error; - } - } - - private async _updateUI(tabId: number, { text, color, title }: { text: string; color: string | null; title: string }): Promise { - await chrome.action.setBadgeText({ tabId, text }); - if (color) - await chrome.action.setBadgeBackgroundColor({ tabId, color }); - await chrome.action.setTitle({ tabId, title }); - } - - private _createConnection(tabId: number, socket: WebSocket): RelayConnection { - const connection = new RelayConnection(tabId, socket); - socket.onclose = () => { - debugLog(`WebSocket closed for tab ${tabId}`); - void this.disconnectTab(tabId); - }; - socket.onerror = error => { - debugLog(`WebSocket error for tab ${tabId}:`, error); - void this.disconnectTab(tabId); - }; - return connection; - } - - /** - * Disconnect a tab from the bridge - */ - async disconnectTab(tabId: number): Promise { - await this._cleanupConnection(tabId); - await this._updateUI(tabId, { text: '', color: null, title: 'Share tab with Playwright MCP' }); - debugLog(`Tab ${tabId} disconnected`); - } - - /** - * Clean up connection resources - */ - async _cleanupConnection(tabId: number): Promise { - const connection = this.activeConnections.get(tabId); - if (!connection) - return; - - this.activeConnections.delete(tabId); - - // Close WebSocket - connection.close(); - - // Detach debugger - try { - await connection.detachDebugger(); - } catch (error) { - // Ignore detach errors - might already be detached - debugLog('Error while detaching debugger:', error); - } - } - - /** - * Handle tab removal - */ - async onTabRemoved(tabId: number): Promise { - if (this.activeConnections.has(tabId)) - await this._cleanupConnection(tabId); - } -} - -new TabShareExtension(); diff --git a/extension/src/popup.ts b/extension/src/popup.ts deleted file mode 100644 index d378b5d..0000000 --- a/extension/src/popup.ts +++ /dev/null @@ -1,243 +0,0 @@ -/** - * 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. - */ - -class PopupController { - private currentTab: chrome.tabs.Tab | null; - private readonly bridgeUrlInput: HTMLInputElement; - private readonly connectBtn: HTMLButtonElement; - private readonly statusContainer: HTMLElement; - private readonly actionContainer: HTMLElement; - - constructor() { - this.currentTab = null; - this.bridgeUrlInput = document.getElementById('bridge-url') as HTMLInputElement; - this.connectBtn = document.getElementById('connect-btn') as HTMLButtonElement; - this.statusContainer = document.getElementById('status-container') as HTMLElement; - this.actionContainer = document.getElementById('action-container') as HTMLElement; - - void this.init(); - } - - async init(): Promise { - // Get current tab - const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); - this.currentTab = tab; - - // Load saved bridge URL - const result = await chrome.storage.sync.get(['bridgeUrl']); - const savedUrl = result.bridgeUrl || 'ws://localhost:9223/extension'; - if (this.bridgeUrlInput) { - this.bridgeUrlInput.value = savedUrl; - this.bridgeUrlInput.disabled = false; - } - - // Set up event listeners - if (this.bridgeUrlInput) - this.bridgeUrlInput.addEventListener('input', this.onUrlChange.bind(this)); - if (this.connectBtn) - this.connectBtn.addEventListener('click', this.onConnectClick.bind(this)); - - // Update UI based on current state - await this.updateUI(); - } - - async updateUI(): Promise { - if (!this.currentTab?.id) - return; - - // Get connection status from background script - const response = await chrome.runtime.sendMessage({ - type: 'getStatus', - tabId: this.currentTab.id - }); - - const { isConnected, activeTabId, activeTabInfo, error } = response as { - isConnected: boolean; - activeTabId: number | undefined; - activeTabInfo?: { title?: string; url?: string }; - error?: string; - }; - - if (!this.statusContainer || !this.actionContainer) - return; - - this.statusContainer.innerHTML = ''; - this.actionContainer.innerHTML = ''; - - if (error) { - this.showStatus('error', `Error: ${error}`); - this.showConnectButton(); - } else if (isConnected && activeTabId === this.currentTab.id) { - // Current tab is connected - this.showStatus('connected', 'This tab is currently shared with MCP server'); - this.showDisconnectButton(); - } else if (isConnected && activeTabId !== this.currentTab.id) { - // Another tab is connected - this.showStatus('warning', 'Another tab is already sharing the CDP session'); - this.showActiveTabInfo(activeTabInfo); - this.showFocusButton(activeTabId); - } else { - // No connection - this.showConnectButton(); - } - } - - showStatus(type: string, message: string): void { - if (!this.statusContainer) - return; - const statusDiv = document.createElement('div'); - statusDiv.className = `status ${type}`; - statusDiv.textContent = message; - this.statusContainer.appendChild(statusDiv); - } - - showConnectButton(): void { - if (!this.actionContainer) - return; - - this.actionContainer.innerHTML = ` - - `; - - const connectBtn = document.getElementById('connect-btn') as HTMLButtonElement | null; - if (connectBtn) { - connectBtn.addEventListener('click', this.onConnectClick.bind(this)); - - // Disable if URL is invalid - const isValidUrl = this.bridgeUrlInput ? this.isValidWebSocketUrl(this.bridgeUrlInput.value) : false; - connectBtn.disabled = !isValidUrl; - } - } - - showDisconnectButton(): void { - if (!this.actionContainer) - return; - - this.actionContainer.innerHTML = ` - - `; - - const disconnectBtn = document.getElementById('disconnect-btn') as HTMLButtonElement | null; - if (disconnectBtn) - disconnectBtn.addEventListener('click', this.onDisconnectClick.bind(this)); - } - - showActiveTabInfo(tabInfo?: { title?: string; url?: string }): void { - if (!tabInfo || !this.statusContainer) - return; - - const tabDiv = document.createElement('div'); - tabDiv.className = 'tab-info'; - tabDiv.innerHTML = ` -
${tabInfo.title || 'Unknown Tab'}
-
${tabInfo.url || ''}
- `; - this.statusContainer.appendChild(tabDiv); - } - - showFocusButton(activeTabId?: number): void { - if (!this.actionContainer) - return; - - this.actionContainer.innerHTML = ` - - `; - - const focusBtn = document.getElementById('focus-btn') as HTMLButtonElement | null; - if (focusBtn && activeTabId !== undefined) - focusBtn.addEventListener('click', () => this.onFocusClick(activeTabId)); - } - - onUrlChange(): void { - if (!this.bridgeUrlInput) - return; - - const isValid = this.isValidWebSocketUrl(this.bridgeUrlInput.value); - const connectBtn = document.getElementById('connect-btn') as HTMLButtonElement | null; - if (connectBtn) - connectBtn.disabled = !isValid; - - // Save URL to storage - if (isValid) - void chrome.storage.sync.set({ bridgeUrl: this.bridgeUrlInput.value }); - } - - async onConnectClick(): Promise { - if (!this.bridgeUrlInput || !this.currentTab?.id) - return; - - const url = this.bridgeUrlInput.value.trim(); - if (!this.isValidWebSocketUrl(url)) { - this.showStatus('error', 'Please enter a valid WebSocket URL'); - return; - } - - // Save URL to storage - await chrome.storage.sync.set({ bridgeUrl: url }); - - // Send connect message to background script - const response = await chrome.runtime.sendMessage({ - type: 'connect', - tabId: this.currentTab.id, - bridgeUrl: url - }); - - if (response.success) - await this.updateUI(); - else - this.showStatus('error', response.error || 'Failed to connect'); - } - - async onDisconnectClick(): Promise { - if (!this.currentTab?.id) - return; - - const response = await chrome.runtime.sendMessage({ - type: 'disconnect', - tabId: this.currentTab.id - }); - - if (response.success) - await this.updateUI(); - else - this.showStatus('error', response.error || 'Failed to disconnect'); - } - - async onFocusClick(activeTabId: number): Promise { - try { - await chrome.tabs.update(activeTabId, { active: true }); - window.close(); // Close popup after switching - } catch (error) { - this.showStatus('error', 'Failed to switch to tab'); - } - } - - isValidWebSocketUrl(url: string): boolean { - if (!url) - return false; - try { - const parsed = new URL(url); - return parsed.protocol === 'ws:' || parsed.protocol === 'wss:'; - } catch { - return false; - } - } -} - -// Initialize popup when DOM is loaded -document.addEventListener('DOMContentLoaded', () => { - new PopupController(); -}); diff --git a/extension/src/relayConnection.ts b/extension/src/relayConnection.ts deleted file mode 100644 index 8c7ef72..0000000 --- a/extension/src/relayConnection.ts +++ /dev/null @@ -1,166 +0,0 @@ -/** - * 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 _rootSessionId: string; - private _ws: WebSocket; - private _eventListener: (source: chrome.debugger.DebuggerSession, method: string, params: any) => void; - private _detachListener: (source: chrome.debugger.Debuggee, reason: string) => void; - - constructor(tabId: number, ws: WebSocket) { - this._debuggee = { tabId }; - this._rootSessionId = `pw-tab-${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 || 'Connection closed'); - } - - 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._rootSessionId; - 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, - }, - }); - } - - 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 (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 { - sessionId: this._rootSessionId, - 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 }; - // Pass session id, unless it's the root session. - if (sessionId && sessionId !== this._rootSessionId) - debuggerSession.sessionId = 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)); - } -} diff --git a/extension/tsconfig.json b/extension/tsconfig.json deleted file mode 100644 index 9fcde29..0000000 --- a/extension/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "esModuleInterop": true, - "moduleResolution": "node", - "strict": true, - "module": "ESNext", - "rootDir": "src", - "outDir": "./lib", - "resolveJsonModule": true, - }, - "include": [ - "src", - ], -} diff --git a/package.json b/package.json index 8db64d0..e43d357 100644 --- a/package.json +++ b/package.json @@ -17,18 +17,15 @@ "license": "Apache-2.0", "scripts": { "build": "tsc", - "build:extension": "tsc --project extension", "lint": "npm run update-readme && eslint . && tsc --noEmit", "update-readme": "node utils/update-readme.js", "watch": "tsc --watch", - "watch:extension": "tsc --watch --project extension", "test": "playwright test", "ctest": "playwright test --project=chrome", "ftest": "playwright test --project=firefox", "wtest": "playwright test --project=webkit", - "etest": "playwright test --project=chromium-extension", "run-server": "node lib/browserServer.js", - "clean": "rm -rf lib && rm -rf extension/lib", + "clean": "rm -rf lib", "npm-publish": "npm run clean && npm run build && npm run test && npm publish" }, "exports": { diff --git a/playwright.config.ts b/playwright.config.ts index 709e85d..9c8ba59 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -39,6 +39,5 @@ export default defineConfig({ }] : [], { name: 'firefox', use: { mcpBrowser: 'firefox' } }, { name: 'webkit', use: { mcpBrowser: 'webkit' } }, - { name: 'chromium-extension', use: { mcpBrowser: 'chromium', mcpMode: 'extension' } }, ], }); diff --git a/src/cdpRelay.ts b/src/cdpRelay.ts deleted file mode 100644 index a606733..0000000 --- a/src/cdpRelay.ts +++ /dev/null @@ -1,349 +0,0 @@ -/** - * 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. - */ - -/** - * Bridge Server - Standalone WebSocket server that bridges Playwright MCP and Chrome Extension - * - * Endpoints: - * - /cdp - Full CDP interface for Playwright MCP - * - /extension - Extension connection for chrome.debugger forwarding - */ - -/* eslint-disable no-console */ - -import { WebSocket, WebSocketServer } from 'ws'; -import http from 'node:http'; -import debug from 'debug'; -import { httpAddressToString } from './transport.js'; - -const debugLogger = debug('pw:mcp:relay'); - -const CDP_PATH = '/cdp'; -const EXTENSION_PATH = '/extension'; - -type CDPCommand = { - id: number; - sessionId?: string; - method: string; - params?: any; -}; - -type CDPResponse = { - id?: number; - sessionId?: string; - method?: string; - params?: any; - result?: any; - error?: { code?: number; message: string }; -}; - -export class CDPRelayServer { - private _wss: WebSocketServer; - private _playwrightSocket: WebSocket | null = null; - private _extensionConnection: ExtensionConnection | null = null; - private _connectionInfo: { - targetInfo: any; - // Page sessionId that should be used by this connection. - sessionId: string; - } | undefined; - - constructor(server: http.Server) { - this._wss = new WebSocketServer({ server }); - this._wss.on('connection', this._onConnection.bind(this)); - } - - stop(): void { - this._playwrightSocket?.close(); - this._extensionConnection?.close(); - } - - private _onConnection(ws: WebSocket, request: http.IncomingMessage): void { - const url = new URL(`http://localhost${request.url}`); - debugLogger(`New connection to ${url.pathname}`); - if (url.pathname === CDP_PATH) { - this._handlePlaywrightConnection(ws); - } else if (url.pathname === EXTENSION_PATH) { - this._handleExtensionConnection(ws); - } else { - debugLogger(`Invalid path: ${url.pathname}`); - ws.close(4004, 'Invalid path'); - } - } - - /** - * Handle Playwright MCP connection - provides full CDP interface - */ - private _handlePlaywrightConnection(ws: WebSocket): void { - if (this._playwrightSocket?.readyState === WebSocket.OPEN) { - debugLogger('Closing previous Playwright connection'); - this._playwrightSocket.close(1000, 'New connection established'); - } - this._playwrightSocket = ws; - debugLogger('Playwright MCP connected'); - ws.on('message', async data => { - try { - const message = JSON.parse(data.toString()); - await this._handlePlaywrightMessage(message); - } catch (error) { - debugLogger('Error parsing Playwright message:', error); - } - }); - ws.on('close', () => { - if (this._playwrightSocket === ws) { - void this._detachDebugger(); - this._playwrightSocket = null; - } - debugLogger('Playwright MCP disconnected'); - }); - ws.on('error', error => { - debugLogger('Playwright WebSocket error:', error); - }); - } - - private async _detachDebugger() { - this._connectionInfo = undefined; - await this._extensionConnection?.send('detachFromTab', {}); - } - - private _handleExtensionConnection(ws: WebSocket): void { - if (this._extensionConnection) - this._extensionConnection.close('New connection established'); - this._extensionConnection = new ExtensionConnection(ws); - this._extensionConnection.onclose = c => { - if (this._extensionConnection === c) - this._extensionConnection = null; - }; - this._extensionConnection.onmessage = this._handleExtensionMessage.bind(this); - } - - private _handleExtensionMessage(method: string, params: any) { - switch (method) { - case 'forwardCDPEvent': - this._sendToPlaywright({ - sessionId: params.sessionId, - method: params.method, - params: params.params - }); - break; - case 'detachedFromTab': - debugLogger('← Debugger detached from tab:', params); - this._connectionInfo = undefined; - this._extensionConnection?.close(); - this._extensionConnection = null; - break; - } - } - - private async _handlePlaywrightMessage(message: CDPCommand): Promise { - debugLogger('← Playwright:', `${message.method} (id=${message.id})`); - if (!this._extensionConnection) { - debugLogger('Extension not connected, sending error to Playwright'); - this._sendToPlaywright({ - id: message.id, - error: { message: 'Extension not connected' } - }); - return; - } - if (await this._interceptCDPCommand(message)) - return; - await this._forwardToExtension(message); - } - - private async _interceptCDPCommand(message: CDPCommand): Promise { - switch (message.method) { - case 'Browser.getVersion': { - this._sendToPlaywright({ - id: message.id, - result: { - protocolVersion: '1.3', - product: 'Chrome/Extension-Bridge', - userAgent: 'CDP-Bridge-Server/1.0.0', - } - }); - return true; - } - case 'Browser.setDownloadBehavior': { - this._sendToPlaywright({ - id: message.id - }); - return true; - } - case 'Target.setAutoAttach': { - // Simulate auto-attach behavior with real target info - if (!message.sessionId) { - this._connectionInfo = await this._extensionConnection!.send('attachToTab'); - debugLogger('Simulating auto-attach for target:', message); - this._sendToPlaywright({ - method: 'Target.attachedToTarget', - params: { - sessionId: this._connectionInfo!.sessionId, - targetInfo: { - ...this._connectionInfo!.targetInfo, - attached: true, - }, - waitingForDebugger: false - } - }); - this._sendToPlaywright({ - id: message.id - }); - } else { - await this._forwardToExtension(message); - } - return true; - } - case 'Target.getTargetInfo': { - debugLogger('Target.getTargetInfo', message); - this._sendToPlaywright({ - id: message.id, - result: this._connectionInfo?.targetInfo - }); - return true; - } - } - return false; - } - - private async _forwardToExtension(message: CDPCommand): Promise { - try { - if (!this._extensionConnection) - throw new Error('Extension not connected'); - const { id, sessionId, method, params } = message; - const result = await this._extensionConnection.send('forwardCDPCommand', { sessionId, method, params }); - this._sendToPlaywright({ id, sessionId, result }); - } catch (e) { - debugLogger('Error in the extension:', e); - this._sendToPlaywright({ - id: message.id, - sessionId: message.sessionId, - error: { message: (e as Error).message } - }); - } - } - - private _sendToPlaywright(message: CDPResponse): void { - debugLogger('→ Playwright:', `${message.method ?? `response(id=${message.id})`}`); - this._playwrightSocket?.send(JSON.stringify(message)); - } -} - -export async function startCDPRelayServer(httpServer: http.Server) { - const wsAddress = httpAddressToString(httpServer.address()).replace(/^http/, 'ws'); - const cdpRelayServer = new CDPRelayServer(httpServer); - process.on('exit', () => cdpRelayServer.stop()); - console.error(`CDP relay server started on ${wsAddress}${EXTENSION_PATH} - Connect to it using the browser extension.`); - const cdpEndpoint = `${wsAddress}${CDP_PATH}`; - return cdpEndpoint; -} - -// CLI usage -if (import.meta.url === `file://${process.argv[1]}`) { - const port = parseInt(process.argv[2], 10) || 9223; - const httpServer = http.createServer(); - await new Promise(resolve => httpServer.listen(port, resolve)); - const server = new CDPRelayServer(httpServer); - - console.error(`CDP Bridge Server listening on ws://localhost:${port}`); - console.error(`- Playwright MCP: ws://localhost:${port}${CDP_PATH}`); - console.error(`- Extension: ws://localhost:${port}${EXTENSION_PATH}`); - - process.on('SIGINT', () => { - debugLogger('\nShutting down bridge server...'); - server.stop(); - process.exit(0); - }); -} - -class ExtensionConnection { - private readonly _ws: WebSocket; - private readonly _callbacks = new Map void, reject: (e: Error) => void }>(); - private _lastId = 0; - - onmessage?: (method: string, params: any) => void; - onclose?: (self: ExtensionConnection) => void; - - constructor(ws: WebSocket) { - this._ws = ws; - this._ws.on('message', this._onMessage.bind(this)); - this._ws.on('close', this._onClose.bind(this)); - this._ws.on('error', this._onError.bind(this)); - } - - async send(method: string, params?: any, sessionId?: string): Promise { - if (this._ws.readyState !== WebSocket.OPEN) - throw new Error('WebSocket closed'); - const id = ++this._lastId; - this._ws.send(JSON.stringify({ id, method, params, sessionId })); - return new Promise((resolve, reject) => { - this._callbacks.set(id, { resolve, reject }); - }); - } - - close(message?: string) { - debugLogger('closing extension connection:', message); - this._ws.close(1000, message ?? 'Connection closed'); - this.onclose?.(this); - } - - private _onMessage(event: WebSocket.RawData) { - const eventData = event.toString(); - let parsedJson; - try { - parsedJson = JSON.parse(eventData); - } catch (e: any) { - debugLogger(` Closing websocket due to malformed JSON. eventData=${eventData} e=${e?.message}`); - this._ws.close(); - return; - } - try { - this._handleParsedMessage(parsedJson); - } catch (e: any) { - debugLogger(` Closing websocket due to failed onmessage callback. eventData=${eventData} e=${e?.message}`); - this._ws.close(); - } - } - - private _handleParsedMessage(object: any) { - if (object.id && this._callbacks.has(object.id)) { - const callback = this._callbacks.get(object.id)!; - this._callbacks.delete(object.id); - if (object.error) - callback.reject(new Error(object.error.message)); - else - callback.resolve(object.result); - } else if (object.id) { - debugLogger('← Extension: unexpected response', object); - } else { - this.onmessage?.(object.method, object.params); - } - } - - private _onClose(event: WebSocket.CloseEvent) { - debugLogger(` code=${event.code} reason=${event.reason}`); - this._dispose(); - } - - private _onError(event: WebSocket.ErrorEvent) { - debugLogger(` message=${event.message} type=${event.type} target=${event.target}`); - this._dispose(); - } - - private _dispose() { - for (const callback of this._callbacks.values()) - callback.reject(new Error('WebSocket closed')); - this._callbacks.clear(); - } -} diff --git a/src/config.ts b/src/config.ts index 1c4a6f8..d2cbd67 100644 --- a/src/config.ts +++ b/src/config.ts @@ -19,18 +19,10 @@ import os from 'os'; import path from 'path'; import { devices } from 'playwright'; -import type { Config as PublicConfig, ToolCapability } from '../config.js'; +import type { Config, ToolCapability } from '../config.js'; import type { BrowserContextOptions, LaunchOptions } from 'playwright'; import { sanitizeForFilePath } from './tools/utils.js'; -type Config = PublicConfig & { - /** - * TODO: Move to PublicConfig once we are ready to release this feature. - * Run server that is able to connect to the 'Playwright MCP' Chrome extension. - */ - extension?: boolean; -}; - export type CLIOptions = { allowedOrigins?: string[]; blockedOrigins?: string[]; @@ -58,7 +50,6 @@ export type CLIOptions = { userDataDir?: string; viewportSize?: string; vision?: boolean; - extension?: boolean; }; const defaultConfig: FullConfig = { @@ -108,13 +99,6 @@ export async function resolveCLIConfig(cliOptions: CLIOptions): Promise { let browserName: 'chromium' | 'firefox' | 'webkit' | undefined; let channel: string | undefined; @@ -160,8 +144,6 @@ export async function configFromCLIOptions(cliOptions: CLIOptions): Promise c.trim() as ToolCapability), vision: !!cliOptions.vision, - extension: !!cliOptions.extension, network: { allowedOrigins: cliOptions.allowedOrigins, blockedOrigins: cliOptions.blockedOrigins, diff --git a/src/connection.ts b/src/connection.ts index eff554d..a9508bb 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -22,14 +22,13 @@ import { Context } from './context.js'; import { snapshotTools, visionTools } from './tools.js'; import { packageJSON } from './package.js'; -import { FullConfig, validateConfig } from './config.js'; +import { FullConfig } from './config.js'; import type { BrowserContextFactory } from './browserContextFactory.js'; export function createConnection(config: FullConfig, browserContextFactory: BrowserContextFactory): Connection { const allTools = config.vision ? visionTools : snapshotTools; const tools = allTools.filter(tool => !config.capabilities || tool.capability === 'core' || config.capabilities.includes(tool.capability)); - validateConfig(config); const context = new Context(tools, config, browserContextFactory); const server = new McpServer({ name: 'Playwright', version: packageJSON.version }, { capabilities: { diff --git a/src/program.ts b/src/program.ts index 5a381d3..62109fa 100644 --- a/src/program.ts +++ b/src/program.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Option, program } from 'commander'; +import { program } from 'commander'; // @ts-ignore import { startTraceViewerServer } from 'playwright-core/lib/server'; @@ -22,7 +22,6 @@ import { startHttpServer, startHttpTransport, startStdioTransport } from './tran import { resolveCLIConfig } from './config.js'; import { Server } from './server.js'; import { packageJSON } from './package.js'; -import { startCDPRelayServer } from './cdpRelay.js'; program .version('Version ' + packageJSON.version) @@ -53,22 +52,15 @@ program .option('--user-data-dir ', 'path to the user data directory. If not specified, a temporary directory will be created.') .option('--viewport-size ', 'specify browser viewport size in pixels, for example "1280, 720"') .option('--vision', 'Run server that uses screenshots (Aria snapshots are used by default)') - .addOption(new Option('--extension', 'Allow connecting to a running browser instance (Edge/Chrome only). Requires the \'Playwright MCP\' browser extension to be installed.').hideHelp()) .action(async options => { const config = await resolveCLIConfig(options); const httpServer = config.server.port !== undefined ? await startHttpServer(config.server) : undefined; - if (config.extension) { - if (!httpServer) - throw new Error('--port parameter is required for extension mode'); - // Point CDP endpoint to the relay server. - config.browser.cdpEndpoint = await startCDPRelayServer(httpServer); - } const server = new Server(config); server.setupExitWatchdog(); if (httpServer) - await startHttpTransport(httpServer, server); + startHttpTransport(httpServer, server); else await startStdioTransport(server); diff --git a/src/resources/resource.ts b/src/resources/resource.ts deleted file mode 100644 index abe0e5b..0000000 --- a/src/resources/resource.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * 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. - */ - -import type { Context } from '../context.js'; - -export type ResourceSchema = { - uri: string; - name: string; - description?: string; - mimeType?: string; -}; - -export type ResourceResult = { - uri: string; - mimeType?: string; - text?: string; - blob?: string; -}; - -export type Resource = { - schema: ResourceSchema; - read: (context: Context, uri: string) => Promise; -}; diff --git a/tests/cdp.spec.ts b/tests/cdp.spec.ts index b29ddc0..32fb61c 100644 --- a/tests/cdp.spec.ts +++ b/tests/cdp.spec.ts @@ -19,8 +19,6 @@ import path from 'node:path'; import { spawnSync } from 'node:child_process'; import { test, expect } from './fixtures.js'; -test.skip(({ mcpMode }) => mcpMode === 'extension', 'Connecting to CDP server is not supported in combination with --extension'); - test('cdp server', async ({ cdpServer, startClient, server }) => { await cdpServer.start(); const { client } = await startClient({ args: [`--cdp-endpoint=${cdpServer.endpoint}`] }); diff --git a/tests/config.spec.ts b/tests/config.spec.ts index 8f12645..e378bc7 100644 --- a/tests/config.spec.ts +++ b/tests/config.spec.ts @@ -20,7 +20,6 @@ import { Config } from '../config.js'; import { test, expect } from './fixtures.js'; test('config user data dir', async ({ startClient, server, mcpMode }, testInfo) => { - test.skip(mcpMode === 'extension', 'Connecting to CDP server does not use user data dir'); server.setContent('/', ` Title Hello, world! @@ -47,7 +46,6 @@ test('config user data dir', async ({ startClient, server, mcpMode }, testInfo) test.describe(() => { test.use({ mcpBrowser: '' }); test('browserName', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-mcp/issues/458' } }, async ({ startClient, mcpMode }, testInfo) => { - test.skip(mcpMode === 'extension', 'Extension mode only supports Chromium'); const config: Config = { browser: { browserName: 'firefox', diff --git a/tests/device.spec.ts b/tests/device.spec.ts index 03dc5ee..ab8799d 100644 --- a/tests/device.spec.ts +++ b/tests/device.spec.ts @@ -17,7 +17,6 @@ import { test, expect } from './fixtures.js'; test('--device should work', async ({ startClient, server, mcpMode }) => { - test.skip(mcpMode === 'extension', 'Viewport is not supported when connecting via CDP. There we re-use the browser viewport.'); const { client } = await startClient({ args: ['--device', 'iPhone 15'], }); diff --git a/tests/extension.spec.ts b/tests/extension.spec.ts deleted file mode 100644 index a34dc54..0000000 --- a/tests/extension.spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * 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. - */ -import url from 'url'; -import path from 'path'; -import { spawnSync } from 'child_process'; - -import { test, expect } from './fixtures.js'; - -import { createConnection } from '@playwright/mcp'; - -test.skip(({ mcpMode }) => mcpMode !== 'extension'); - -test('does not allow --cdp-endpoint', async ({ startClient }) => { - await expect(createConnection({ - browser: { browserName: 'firefox' }, - ...({ extension: true }) - })).rejects.toThrow(/Extension mode is only supported for Chromium browsers/); -}); - -// NOTE: Can be removed when we drop Node.js 18 support and changed to import.meta.filename. -const __filename = url.fileURLToPath(import.meta.url); - -test('does not support --device', async () => { - const result = spawnSync('node', [ - path.join(__filename, '../../cli.js'), '--device=Pixel 5', '--extension', - ]); - expect(result.error).toBeUndefined(); - expect(result.status).toBe(1); - expect(result.stderr.toString()).toContain('Device emulation is not supported with extension mode.'); -}); diff --git a/tests/files.spec.ts b/tests/files.spec.ts index 73573ef..b45b5fa 100644 --- a/tests/files.spec.ts +++ b/tests/files.spec.ts @@ -95,7 +95,6 @@ The tool "browser_file_upload" can only be used when there is related modal stat }); test('clicking on download link emits download', async ({ startClient, server, mcpMode }, testInfo) => { - test.fixme(mcpMode === 'extension', 'Downloads are on the Browser CDP domain and not supported with --extension'); const { client } = await startClient({ config: { outputDir: testInfo.outputPath('output') }, }); @@ -120,7 +119,6 @@ test('clicking on download link emits download', async ({ startClient, server, m }); test('navigating to download link emits download', async ({ startClient, server, mcpBrowser, mcpMode }, testInfo) => { - test.fixme(mcpMode === 'extension', 'Downloads are on the Browser CDP domain and not supported with --extension'); const { client } = await startClient({ config: { outputDir: testInfo.outputPath('output') }, }); diff --git a/tests/fixtures.ts b/tests/fixtures.ts index 3e51a87..3c5fbad 100644 --- a/tests/fixtures.ts +++ b/tests/fixtures.ts @@ -17,16 +17,12 @@ import fs from 'fs'; import url from 'url'; import path from 'path'; -import net from 'net'; import { chromium } from 'playwright'; -import { fork } from 'child_process'; import { test as baseTest, expect as baseExpect } from '@playwright/test'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; -import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { TestServer } from './testserver/index.ts'; -import { ManualPromise } from '../src/manualPromise.js'; import type { Config } from '../config'; import type { BrowserContext } from 'playwright'; @@ -35,7 +31,7 @@ import type { Stream } from 'stream'; export type TestOptions = { mcpBrowser: string | undefined; - mcpMode: 'docker' | 'extension' | undefined; + mcpMode: 'docker' | undefined; }; type CDPServer = { @@ -52,7 +48,6 @@ type TestFixtures = { server: TestServer; httpsServer: TestServer; mcpHeadless: boolean; - startMcpExtension: (relayServerURL: string) => Promise; }; type WorkerFixtures = { @@ -71,7 +66,7 @@ export const test = baseTest.extend( await use(client); }, - startClient: async ({ mcpHeadless, mcpBrowser, mcpMode, startMcpExtension }, use, testInfo) => { + startClient: async ({ mcpHeadless, mcpBrowser, mcpMode }, use, testInfo) => { const userDataDir = mcpMode !== 'docker' ? testInfo.outputPath('user-data-dir') : undefined; const configDir = path.dirname(test.info().config.configFile!); let client: Client | undefined; @@ -95,7 +90,7 @@ export const test = baseTest.extend( } client = new Client({ name: options?.clientName ?? 'test', version: '1.0.0' }); - const { transport, stderr, relayServerURL } = await createTransport(args, mcpMode); + const { transport, stderr } = await createTransport(args, mcpMode); let stderrBuffer = ''; stderr?.on('data', data => { if (process.env.PWMCP_DEBUG) @@ -103,8 +98,6 @@ export const test = baseTest.extend( stderrBuffer += data.toString(); }); await client.connect(transport); - if (mcpMode === 'extension') - await startMcpExtension(relayServerURL!); await client.ping(); return { client, stderr: () => stderrBuffer }; }); @@ -147,38 +140,6 @@ export const test = baseTest.extend( mcpMode: [undefined, { option: true }], - startMcpExtension: async ({ mcpMode, mcpHeadless }, use) => { - let context: BrowserContext | undefined; - await use(async (relayServerURL: string) => { - if (mcpMode !== 'extension') - throw new Error('Must be running in MCP extension mode to use this fixture.'); - const cdpPort = await findFreePort(); - const pathToExtension = path.join(url.fileURLToPath(import.meta.url), '../../extension'); - context = await chromium.launchPersistentContext('', { - headless: mcpHeadless, - args: [ - `--disable-extensions-except=${pathToExtension}`, - `--load-extension=${pathToExtension}`, - '--enable-features=AllowContentInitiatedDataUrlNavigations', - ], - channel: 'chromium', - ...{ assistantMode: true, cdpPort }, - }); - const popupPage = await context.newPage(); - const page = context.pages()[0]; - await page.bringToFront(); - // Do not auto dismiss dialogs. - page.on('dialog', () => { }); - await expect.poll(() => context?.serviceWorkers()).toHaveLength(1); - // Connect to the relay server. - await popupPage.goto(new URL('/popup.html', context.serviceWorkers()[0].url()).toString()); - await popupPage.getByRole('textbox', { name: 'Bridge Server URL:' }).clear(); - await popupPage.getByRole('textbox', { name: 'Bridge Server URL:' }).fill(relayServerURL); - await popupPage.getByRole('button', { name: 'Share This Tab' }).click(); - }); - await context?.close(); - }, - _workerServers: [async ({ }, use, workerInfo) => { const port = 8907 + workerInfo.workerIndex * 4; const server = await TestServer.create(port); @@ -208,7 +169,6 @@ export const test = baseTest.extend( async function createTransport(args: string[], mcpMode: TestOptions['mcpMode']): Promise<{ transport: Transport, stderr: Stream | null, - relayServerURL?: string, }> { // NOTE: Can be removed when we drop Node.js 18 support and changed to import.meta.filename. const __filename = url.fileURLToPath(import.meta.url); @@ -223,42 +183,6 @@ async function createTransport(args: string[], mcpMode: TestOptions['mcpMode']): stderr: transport.stderr, }; } - if (mcpMode === 'extension') { - const relay = fork(path.join(__filename, '../../cli.js'), [...args, '--extension', '--port=0'], { - stdio: 'pipe' - }); - const cdpRelayServerReady = new ManualPromise(); - const sseEndpointPromise = new ManualPromise(); - let stderrBuffer = ''; - relay.stderr!.on('data', data => { - stderrBuffer += data.toString(); - const match = stderrBuffer.match(/Listening on (http:\/\/.*)/); - if (match) - sseEndpointPromise.resolve(match[1].toString()); - const extensionMatch = stderrBuffer.match(/CDP relay server started on (ws:\/\/.*\/extension)/); - if (extensionMatch) - cdpRelayServerReady.resolve(extensionMatch[1].toString()); - }); - relay.on('exit', () => { - sseEndpointPromise.reject(new Error(`Process exited`)); - cdpRelayServerReady.reject(new Error(`Process exited`)); - }); - const relayServerURL = await cdpRelayServerReady; - const sseEndpoint = await sseEndpointPromise; - - const transport = new SSEClientTransport(new URL(sseEndpoint)); - // We cannot just add transport.onclose here as Client.connect() overrides it. - const origClose = transport.close; - transport.close = async () => { - await origClose.call(transport); - relay.kill(); - }; - return { - transport, - stderr: relay.stderr!, - relayServerURL, - }; - } const transport = new StdioClientTransport({ command: 'node', @@ -332,17 +256,6 @@ export const expect = baseExpect.extend({ }, }); -async function findFreePort(): Promise { - return new Promise((resolve, reject) => { - const server = net.createServer(); - server.listen(0, () => { - const { port } = server.address() as net.AddressInfo; - server.close(() => resolve(port)); - }); - server.on('error', reject); - }); -} - export function formatOutput(output: string): string[] { return output.split('\n').map(line => line.replace(/^pw:mcp:test /, '').replace(/user data dir.*/, 'user data dir').trim()).filter(Boolean); } diff --git a/tests/launch.spec.ts b/tests/launch.spec.ts index 05c042c..09d09ae 100644 --- a/tests/launch.spec.ts +++ b/tests/launch.spec.ts @@ -18,8 +18,6 @@ import fs from 'fs'; import { test, expect, formatOutput } from './fixtures.js'; -test.skip(({ mcpMode }) => mcpMode === 'extension', 'launch scenarios are not supported with --extension - the browser is already launched'); - test('test reopen browser', async ({ startClient, server, mcpMode }) => { const { client, stderr } = await startClient(); await client.callTool({ diff --git a/tests/sse.spec.ts b/tests/sse.spec.ts index 14996c3..9e888a8 100644 --- a/tests/sse.spec.ts +++ b/tests/sse.spec.ts @@ -29,8 +29,6 @@ import type { Config } from '../config.d.ts'; // NOTE: Can be removed when we drop Node.js 18 support and changed to import.meta.filename. const __filename = url.fileURLToPath(import.meta.url); -baseTest.skip(({ mcpMode }) => mcpMode === 'extension', 'Extension tests run via SSE anyways'); - const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noPort?: boolean }) => Promise<{ url: URL, stderr: () => string }> }>({ serverEndpoint: async ({ mcpHeadless }, use, testInfo) => { let cp: ChildProcess | undefined; diff --git a/tests/tabs.spec.ts b/tests/tabs.spec.ts index 448e04a..6768d75 100644 --- a/tests/tabs.spec.ts +++ b/tests/tabs.spec.ts @@ -27,8 +27,6 @@ async function createTab(client: Client, title: string, body: string) { }); } -test.skip(({ mcpMode }) => mcpMode === 'extension', 'Multi-tab scenarios are not supported with --extension'); - test('list initial tabs', async ({ client }) => { expect(await client.callTool({ name: 'browser_tab_list', diff --git a/tests/trace.spec.ts b/tests/trace.spec.ts index a72b92f..ba4657d 100644 --- a/tests/trace.spec.ts +++ b/tests/trace.spec.ts @@ -20,8 +20,6 @@ import path from 'path'; import { test, expect } from './fixtures.js'; test('check that trace is saved', async ({ startClient, server, mcpMode }, testInfo) => { - test.fixme(mcpMode === 'extension', 'Tracing is not supported via CDP'); - const outputDir = testInfo.outputPath('output'); const { client } = await startClient({