From afaa8a7014eee3de8887b7c83be8e98cffe9c9cf Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Sun, 31 Aug 2025 16:28:43 -0600 Subject: [PATCH] feat: comprehensive console capture and offline mode support Major enhancements to browser automation and debugging capabilities: **Console Capture Features:** - Add console output file option (CLI, env var, session config) - Enhanced CDP console capture for service worker messages - Browser-level security warnings and mixed content errors - Network failure and loading error capture - All console contexts written to structured log files - Chrome extension for comprehensive console message interception **Offline Mode Support:** - Add browser_set_offline tool for DevTools-equivalent offline mode - Integrate offline mode into browser_configure tool - Support for testing network failure scenarios and service worker behavior **Extension Management:** - Improved extension installation messaging about session persistence - Console capture extension with debugger API access - Clear communication about extension lifecycle to MCP clients **Technical Implementation:** - CDP session management across multiple domains (Runtime, Network, Security, Log) - Service worker context console message interception - Browser context factory integration for offline mode - Pure Chromium configuration for optimal extension support All features provide MCP clients with powerful debugging capabilities equivalent to Chrome DevTools console and offline functionality. --- README.md | 47 ++++-- console-capture-extension/background.js | 193 +++++++++++++++++++++ console-capture-extension/content.js | 50 ++++++ console-capture-extension/icon-128.png | Bin 0 -> 6352 bytes console-capture-extension/icon-16.png | Bin 0 -> 571 bytes console-capture-extension/icon-32.png | Bin 0 -> 1258 bytes console-capture-extension/icon-48.png | Bin 0 -> 2043 bytes console-capture-extension/manifest.json | 37 ++++ src/browserContextFactory.ts | 6 + src/config.ts | 1 - src/context.ts | 10 ++ src/tab.ts | 215 ++++++++++++++++++++++++ src/tools/configure.ts | 64 ++++++- 13 files changed, 603 insertions(+), 20 deletions(-) create mode 100644 console-capture-extension/background.js create mode 100644 console-capture-extension/content.js create mode 100644 console-capture-extension/icon-128.png create mode 100644 console-capture-extension/icon-16.png create mode 100644 console-capture-extension/icon-32.png create mode 100644 console-capture-extension/icon-48.png create mode 100644 console-capture-extension/manifest.json diff --git a/README.md b/README.md index 0a38bd6..023c883 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,8 @@ Playwright MCP server supports following arguments. They can be provided in the vision, pdf. --cdp-endpoint CDP endpoint to connect to. --config path to the configuration file. + --console-output-file file path to write browser console output to + for debugging and monitoring. --device device to emulate, for example: "iPhone 15" --executable-path path to the browser executable. --headless run browser in headless mode, headed by @@ -529,7 +531,7 @@ http.createServer(async (req, res) => { - **browser_click** - Title: Click - - Description: Perform click on a web page. Returns page snapshot after click unless disabled with --no-snapshots. Large snapshots (>10k tokens) are truncated - use browser_snapshot for full capture. + - Description: Perform click on a web page. Returns page snapshot after click (configurable via browser_configure_snapshots). Use browser_snapshot for explicit full snapshots. - Parameters: - `element` (string): Human-readable element description used to obtain permission to interact with the element - `ref` (string): Exact target element reference from the page snapshot @@ -575,6 +577,18 @@ http.createServer(async (req, res) => { +- **browser_configure_snapshots** + - Title: Configure snapshot behavior + - Description: Configure how page snapshots are handled during the session. Control automatic snapshots, size limits, and differential modes. Changes take effect immediately for subsequent tool calls. + - Parameters: + - `includeSnapshots` (boolean, optional): Enable/disable automatic snapshots after interactive operations. When false, use browser_snapshot for explicit snapshots. + - `maxSnapshotTokens` (number, optional): Maximum tokens allowed in snapshots before truncation. Use 0 to disable truncation. + - `differentialSnapshots` (boolean, optional): Enable differential snapshots that show only changes since last snapshot instead of full page snapshots. + - `consoleOutputFile` (string, optional): File path to write browser console output to. Set to empty string to disable console file output. + - Read-only: **false** + + + - **browser_console_messages** - Title: Get console messages - Description: Returns all console messages @@ -585,7 +599,7 @@ http.createServer(async (req, res) => { - **browser_drag** - Title: Drag mouse - - Description: Perform drag and drop between two elements. Returns page snapshot after drag unless disabled with --no-snapshots. + - Description: Perform drag and drop between two elements. Returns page snapshot after drag (configurable via browser_configure_snapshots). - Parameters: - `startElement` (string): Human-readable source element description used to obtain the permission to interact with the element - `startRef` (string): Exact source element reference from the page snapshot @@ -597,7 +611,7 @@ http.createServer(async (req, res) => { - **browser_evaluate** - Title: Evaluate JavaScript - - Description: Evaluate JavaScript expression on page or element + - Description: Evaluate JavaScript expression on page or element. Returns page snapshot after evaluation (configurable via browser_configure_snapshots). - Parameters: - `function` (string): () => { /* code */ } or (element) => { /* code */ } when element is provided - `element` (string, optional): Human-readable element description used to obtain permission to interact with the element @@ -608,7 +622,7 @@ http.createServer(async (req, res) => { - **browser_file_upload** - Title: Upload files - - Description: Upload one or multiple files + - Description: Upload one or multiple files. Returns page snapshot after upload (configurable via browser_configure_snapshots). - Parameters: - `paths` (array): The absolute paths to the files to upload. Can be a single file or multiple files. - Read-only: **false** @@ -617,7 +631,7 @@ http.createServer(async (req, res) => { - **browser_handle_dialog** - Title: Handle a dialog - - Description: Handle a dialog + - Description: Handle a dialog. Returns page snapshot after handling dialog (configurable via browser_configure_snapshots). - Parameters: - `accept` (boolean): Whether to accept the dialog. - `promptText` (string, optional): The text of the prompt in case of a prompt dialog. @@ -627,7 +641,7 @@ http.createServer(async (req, res) => { - **browser_hover** - Title: Hover mouse - - Description: Hover over element on page. Returns page snapshot after hover unless disabled with --no-snapshots. + - Description: Hover over element on page. Returns page snapshot after hover (configurable via browser_configure_snapshots). - Parameters: - `element` (string): Human-readable element description used to obtain permission to interact with the element - `ref` (string): Exact target element reference from the page snapshot @@ -673,7 +687,7 @@ http.createServer(async (req, res) => { - **browser_navigate** - Title: Navigate to a URL - - Description: Navigate to a URL. Returns page snapshot after navigation unless disabled with --no-snapshots. + - Description: Navigate to a URL. Returns page snapshot after navigation (configurable via browser_configure_snapshots). - Parameters: - `url` (string): The URL to navigate to - Read-only: **false** @@ -706,7 +720,7 @@ http.createServer(async (req, res) => { - **browser_press_key** - Title: Press a key - - Description: Press a key on the keyboard. Returns page snapshot after keypress unless disabled with --no-snapshots. + - Description: Press a key on the keyboard. Returns page snapshot after keypress (configurable via browser_configure_snapshots). - Parameters: - `key` (string): Name of the key to press or a character to generate, such as `ArrowLeft` or `a` - Read-only: **false** @@ -733,7 +747,7 @@ http.createServer(async (req, res) => { - **browser_select_option** - Title: Select option - - Description: Select an option in a dropdown. Returns page snapshot after selection unless disabled with --no-snapshots. + - Description: Select an option in a dropdown. Returns page snapshot after selection (configurable via browser_configure_snapshots). - Parameters: - `element` (string): Human-readable element description used to obtain permission to interact with the element - `ref` (string): Exact target element reference from the page snapshot @@ -744,7 +758,7 @@ http.createServer(async (req, res) => { - **browser_snapshot** - Title: Page snapshot - - Description: Capture complete accessibility snapshot of the current page. Always returns full snapshot regardless of --no-snapshots or size limits. Better than screenshot for understanding page structure. + - Description: Capture complete accessibility snapshot of the current page. Always returns full snapshot regardless of session snapshot configuration. Better than screenshot for understanding page structure. - Parameters: None - Read-only: **true** @@ -770,20 +784,21 @@ http.createServer(async (req, res) => { - **browser_take_screenshot** - Title: Take a screenshot - - Description: Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions. + - Description: Take a screenshot of the current page. Images exceeding 8000 pixels in either dimension will be rejected unless allowLargeImages=true. You can't perform actions based on the screenshot, use browser_snapshot for actions. - Parameters: - `raw` (boolean, optional): Whether to return without compression (in PNG format). Default is false, which returns a JPEG image. - `filename` (string, optional): File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified. - `element` (string, optional): Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too. - `ref` (string, optional): Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too. - `fullPage` (boolean, optional): When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots. + - `allowLargeImages` (boolean, optional): Allow images with dimensions exceeding 8000 pixels (API limit). Default false - will error if image is too large to prevent API failures. - Read-only: **true** - **browser_type** - Title: Type text - - Description: Type text into editable element. Returns page snapshot after typing unless disabled with --no-snapshots. + - Description: Type text into editable element. Returns page snapshot after typing (configurable via browser_configure_snapshots). - Parameters: - `element` (string): Human-readable element description used to obtain permission to interact with the element - `ref` (string): Exact target element reference from the page snapshot @@ -805,7 +820,7 @@ http.createServer(async (req, res) => { - **browser_wait_for** - Title: Wait for - - Description: Wait for text to appear or disappear or a specified time to pass + - Description: Wait for text to appear or disappear or a specified time to pass. Returns page snapshot after waiting (configurable via browser_configure_snapshots). - Parameters: - `time` (number, optional): The time to wait in seconds - `text` (string, optional): The text to wait for @@ -821,7 +836,7 @@ http.createServer(async (req, res) => { - **browser_tab_close** - Title: Close a tab - - Description: Close a tab + - Description: Close a tab. Returns page snapshot after closing tab (configurable via browser_configure_snapshots). - Parameters: - `index` (number, optional): The index of the tab to close. Closes current tab if not provided. - Read-only: **false** @@ -838,7 +853,7 @@ http.createServer(async (req, res) => { - **browser_tab_new** - Title: Open a new tab - - Description: Open a new tab + - Description: Open a new tab. Returns page snapshot after opening tab (configurable via browser_configure_snapshots). - Parameters: - `url` (string, optional): The URL to navigate to in the new tab. If not provided, the new tab will be blank. - Read-only: **true** @@ -847,7 +862,7 @@ http.createServer(async (req, res) => { - **browser_tab_select** - Title: Select a tab - - Description: Select a tab by index + - Description: Select a tab by index. Returns page snapshot after selecting tab (configurable via browser_configure_snapshots). - Parameters: - `index` (number): The index of the tab to select - Read-only: **true** diff --git a/console-capture-extension/background.js b/console-capture-extension/background.js new file mode 100644 index 0000000..1facb6a --- /dev/null +++ b/console-capture-extension/background.js @@ -0,0 +1,193 @@ +// Background script for comprehensive console capture +console.log('Console Capture Extension: Background script loaded'); + +// Track active debug sessions +const debugSessions = new Map(); + +// Message storage for each tab +const tabConsoleMessages = new Map(); + +chrome.tabs.onCreated.addListener((tab) => { + if (tab.id) { + attachDebugger(tab.id); + } +}); + +chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { + if (changeInfo.status === 'loading' && tab.url && !tab.url.startsWith('chrome://')) { + attachDebugger(tabId); + } +}); + +chrome.tabs.onRemoved.addListener((tabId) => { + if (debugSessions.has(tabId)) { + try { + chrome.debugger.detach({ tabId }); + } catch (e) { + // Ignore errors when detaching + } + debugSessions.delete(tabId); + tabConsoleMessages.delete(tabId); + } +}); + +async function attachDebugger(tabId) { + try { + // Don't attach to chrome:// pages or if already attached + if (debugSessions.has(tabId)) { + return; + } + + // Attach debugger + await chrome.debugger.attach({ tabId }, '1.3'); + debugSessions.set(tabId, true); + + console.log(`Console Capture Extension: Attached to tab ${tabId}`); + + // Enable domains for comprehensive console capture + await chrome.debugger.sendCommand({ tabId }, 'Runtime.enable'); + await chrome.debugger.sendCommand({ tabId }, 'Log.enable'); + await chrome.debugger.sendCommand({ tabId }, 'Network.enable'); + await chrome.debugger.sendCommand({ tabId }, 'Security.enable'); + + // Initialize console messages array for this tab + if (!tabConsoleMessages.has(tabId)) { + tabConsoleMessages.set(tabId, []); + } + + } catch (error) { + console.log(`Console Capture Extension: Failed to attach to tab ${tabId}:`, error); + debugSessions.delete(tabId); + } +} + +// Listen for debugger events +chrome.debugger.onEvent.addListener((source, method, params) => { + const tabId = source.tabId; + if (!tabId || !debugSessions.has(tabId)) return; + + let consoleMessage = null; + + try { + switch (method) { + case 'Runtime.consoleAPICalled': + consoleMessage = { + type: params.type || 'log', + text: params.args?.map(arg => + arg.value !== undefined ? String(arg.value) : + arg.unserializableValue || '[object]' + ).join(' ') || '', + location: `runtime:${params.stackTrace?.callFrames?.[0]?.lineNumber || 0}`, + source: 'js-console', + timestamp: Date.now() + }; + break; + + case 'Runtime.exceptionThrown': + const exception = params.exceptionDetails; + consoleMessage = { + type: 'error', + text: exception?.text || exception?.exception?.description || 'Runtime Exception', + location: `runtime:${exception?.lineNumber || 0}`, + source: 'js-exception', + timestamp: Date.now() + }; + break; + + case 'Log.entryAdded': + const entry = params.entry; + if (entry && entry.text) { + consoleMessage = { + type: entry.level || 'info', + text: entry.text, + location: `browser-log:${entry.lineNumber || 0}`, + source: 'browser-log', + timestamp: Date.now() + }; + } + break; + + case 'Network.loadingFailed': + if (params.errorText) { + consoleMessage = { + type: 'error', + text: `Network Error: ${params.errorText} - ${params.blockedReason || 'Unknown reason'}`, + location: 'network-layer', + source: 'network-error', + timestamp: Date.now() + }; + } + break; + + case 'Security.securityStateChanged': + if (params.securityState === 'insecure' && params.explanations) { + for (const explanation of params.explanations) { + if (explanation.description && explanation.description.toLowerCase().includes('mixed content')) { + consoleMessage = { + type: 'error', + text: `Security Warning: ${explanation.description}`, + location: 'security-layer', + source: 'security-warning', + timestamp: Date.now() + }; + break; + } + } + } + break; + } + + if (consoleMessage) { + // Store the message + const messages = tabConsoleMessages.get(tabId) || []; + messages.push(consoleMessage); + tabConsoleMessages.set(tabId, messages); + + console.log(`Console Capture Extension: Captured message from tab ${tabId}:`, consoleMessage); + + // Send to content script for potential file writing + chrome.tabs.sendMessage(tabId, { + type: 'CONSOLE_MESSAGE', + message: consoleMessage + }).catch(() => { + // Ignore errors if content script not ready + }); + } + + } catch (error) { + console.log('Console Capture Extension: Error processing event:', error); + } +}); + +// Handle detach events +chrome.debugger.onDetach.addListener((source, reason) => { + const tabId = source.tabId; + if (tabId && debugSessions.has(tabId)) { + console.log(`Console Capture Extension: Detached from tab ${tabId}, reason: ${reason}`); + debugSessions.delete(tabId); + tabConsoleMessages.delete(tabId); + } +}); + +// API to get console messages for a tab +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.type === 'GET_CONSOLE_MESSAGES') { + const tabId = request.tabId || sender.tab?.id; + if (tabId) { + const messages = tabConsoleMessages.get(tabId) || []; + sendResponse({ messages }); + } else { + sendResponse({ messages: [] }); + } + return true; + } +}); + +// Initialize for existing tabs +chrome.tabs.query({}, (tabs) => { + for (const tab of tabs) { + if (tab.id && tab.url && !tab.url.startsWith('chrome://')) { + attachDebugger(tab.id); + } + } +}); \ No newline at end of file diff --git a/console-capture-extension/content.js b/console-capture-extension/content.js new file mode 100644 index 0000000..ee8a105 --- /dev/null +++ b/console-capture-extension/content.js @@ -0,0 +1,50 @@ +// Content script for console capture extension +console.log('Console Capture Extension: Content script loaded'); + +// Listen for console messages from background script +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.type === 'CONSOLE_MESSAGE') { + const message = request.message; + + // Forward to window for Playwright to access + window.postMessage({ + type: 'PLAYWRIGHT_CONSOLE_CAPTURE', + consoleMessage: message + }, '*'); + + console.log('Console Capture Extension: Forwarded message:', message); + } +}); + +// Also capture any window-level console messages that might be missed +const originalConsole = { + log: window.console.log, + warn: window.console.warn, + error: window.console.error, + info: window.console.info +}; + +function wrapConsoleMethod(method, level) { + return function(...args) { + // Call original method + originalConsole[method].apply(window.console, args); + + // Forward to Playwright + window.postMessage({ + type: 'PLAYWRIGHT_CONSOLE_CAPTURE', + consoleMessage: { + type: level, + text: args.map(arg => String(arg)).join(' '), + location: `content-script:${new Error().stack?.split('\n')[2]?.match(/:(\d+):/)?.[1] || 0}`, + source: 'content-wrapper', + timestamp: Date.now() + } + }, '*'); + }; +} + +// Wrap console methods +window.console.log = wrapConsoleMethod('log', 'log'); +window.console.warn = wrapConsoleMethod('warn', 'warning'); +window.console.error = wrapConsoleMethod('error', 'error'); +window.console.info = wrapConsoleMethod('info', 'info'); \ No newline at end of file diff --git a/console-capture-extension/icon-128.png b/console-capture-extension/icon-128.png new file mode 100644 index 0000000000000000000000000000000000000000..c4bc8b02508b40cec8a0d73465324a9612744172 GIT binary patch literal 6352 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 JPDHLkV1gAA4qyNP literal 0 HcmV?d00001 diff --git a/console-capture-extension/icon-32.png b/console-capture-extension/icon-32.png new file mode 100644 index 0000000000000000000000000000000000000000..1f9a8ccb89bdbccd6bdc1c7f945f72eed9b94017 GIT binary patch literal 1258 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 Z{soKhKGhw*_GbV9002ovPDHLkV1gda&R_rl literal 0 HcmV?d00001 diff --git a/console-capture-extension/manifest.json b/console-capture-extension/manifest.json new file mode 100644 index 0000000..9cffae8 --- /dev/null +++ b/console-capture-extension/manifest.json @@ -0,0 +1,37 @@ +{ + "manifest_version": 3, + "name": "Console Capture Extension", + "version": "1.0.0", + "description": "Captures comprehensive console messages including browser-level warnings and errors", + + "permissions": [ + "debugger", + "tabs", + "activeTab", + "storage" + ], + + "background": { + "service_worker": "background.js", + "type": "module" + }, + + "content_scripts": [ + { + "matches": [""], + "js": ["content.js"], + "run_at": "document_start" + } + ], + + "host_permissions": [ + "" + ], + + "icons": { + "16": "icon-16.png", + "32": "icon-32.png", + "48": "icon-48.png", + "128": "icon-128.png" + } +} \ No newline at end of file diff --git a/src/browserContextFactory.ts b/src/browserContextFactory.ts index a81ef50..2c7cc3f 100644 --- a/src/browserContextFactory.ts +++ b/src/browserContextFactory.ts @@ -72,6 +72,12 @@ class BaseContextFactory implements BrowserContextFactory { testDebug(`create browser context (${this.name})`); const browser = await this._obtainBrowser(); const browserContext = await this._doCreateContext(browser, extensionPaths); + + // Apply offline mode if configured + if ((this.browserConfig as any).offline !== undefined) { + await browserContext.setOffline((this.browserConfig as any).offline); + } + return { browserContext, close: () => this._closeBrowserContext(browserContext, browser) }; } diff --git a/src/config.ts b/src/config.ts index da46597..b4d878a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -60,7 +60,6 @@ const defaultConfig: FullConfig = { browserName: 'chromium', isolated: true, launchOptions: { - channel: 'chrome', headless: false, chromiumSandbox: true, }, diff --git a/src/context.ts b/src/context.ts index 400208a..040b4e9 100644 --- a/src/context.ts +++ b/src/context.ts @@ -315,6 +315,11 @@ export class Context { const browserContext = await browser.newContext(contextOptions); + // Apply offline mode if configured + if ((this.config as any).offline !== undefined) { + await browserContext.setOffline((this.config as any).offline); + } + return { browserContext, close: async () => { @@ -373,6 +378,7 @@ export class Context { timezone?: string; colorScheme?: 'light' | 'dark' | 'no-preference'; permissions?: string[]; + offline?: boolean; }): Promise { const currentConfig = { ...this.config }; @@ -428,6 +434,10 @@ export class Context { currentConfig.browser.contextOptions.permissions = changes.permissions; + if (changes.offline !== undefined) + (currentConfig.browser as any).offline = changes.offline; + + // Store the modified config (this as any).config = currentConfig; diff --git a/src/tab.ts b/src/tab.ts index 871e23d..c8fad2e 100644 --- a/src/tab.ts +++ b/src/tab.ts @@ -71,6 +71,12 @@ export class Tab extends EventEmitter { }); page.setDefaultNavigationTimeout(60000); page.setDefaultTimeout(5000); + + // Initialize service worker console capture + void this._initializeServiceWorkerConsoleCapture(); + + // Initialize extension-based console capture + void this._initializeExtensionConsoleCapture(); } modalStates(): ModalState[] { @@ -160,6 +166,215 @@ export class Tab extends EventEmitter { } } + private async _initializeServiceWorkerConsoleCapture() { + try { + // Only attempt CDP console capture for Chromium browsers + if (this.page.context().browser()?.browserType().name() !== 'chromium') + return; + + + const cdpSession = await this.page.context().newCDPSession(this.page); + + // Enable runtime domain for console API calls + await cdpSession.send('Runtime.enable'); + + // Enable network domain for network-related errors + await cdpSession.send('Network.enable'); + + // Enable security domain for mixed content warnings + await cdpSession.send('Security.enable'); + + // Enable log domain for browser log entries + await cdpSession.send('Log.enable'); + + // Listen for console API calls (includes service worker console messages) + cdpSession.on('Runtime.consoleAPICalled', (event: any) => { + this._handleServiceWorkerConsole(event); + }); + + // Listen for runtime exceptions (includes service worker errors) + cdpSession.on('Runtime.exceptionThrown', (event: any) => { + this._handleServiceWorkerException(event); + }); + + // Listen for network failed events + cdpSession.on('Network.loadingFailed', (event: any) => { + this._handleNetworkError(event); + }); + + // Listen for security state changes (mixed content) + cdpSession.on('Security.securityStateChanged', (event: any) => { + this._handleSecurityStateChange(event); + }); + + // Listen for log entries (browser-level logs) + cdpSession.on('Log.entryAdded', (event: any) => { + this._handleLogEntry(event); + }); + + } catch (error) { + // Silently handle CDP errors - not all contexts support CDP + logUnhandledError(error); + } + } + + private _handleServiceWorkerConsole(event: any) { + try { + // Check if this console event is from a service worker context + if (event.executionContextId && event.args && event.args.length > 0) { + const message = event.args.map((arg: any) => { + if (arg.value !== undefined) + return String(arg.value); + + if (arg.unserializableValue) + return arg.unserializableValue; + + if (arg.objectId) + return '[object]'; + + return ''; + }).join(' '); + + const location = `service-worker:${event.stackTrace?.callFrames?.[0]?.lineNumber || 0}`; + + const consoleMessage: ConsoleMessage = { + type: event.type || 'log', + text: message, + toString: () => `[${(event.type || 'log').toUpperCase()}] ${message} @ ${location}`, + }; + + this._handleConsoleMessage(consoleMessage); + } + } catch (error) { + logUnhandledError(error); + } + } + + private _handleServiceWorkerException(event: any) { + try { + const exception = event.exceptionDetails; + if (exception) { + const text = exception.text || exception.exception?.description || 'Service Worker Exception'; + const location = `service-worker:${exception.lineNumber || 0}`; + + const consoleMessage: ConsoleMessage = { + type: 'error', + text: text, + toString: () => `[ERROR] ${text} @ ${location}`, + }; + + this._handleConsoleMessage(consoleMessage); + } + } catch (error) { + logUnhandledError(error); + } + } + + private _handleNetworkError(event: any) { + try { + if (event.errorText && event.requestId) { + const consoleMessage: ConsoleMessage = { + type: 'error', + text: `Network Error: ${event.errorText} (${event.type || 'unknown'})`, + toString: () => `[NETWORK ERROR] ${event.errorText} @ ${event.type || 'network'}`, + }; + + this._handleConsoleMessage(consoleMessage); + } + } catch (error) { + logUnhandledError(error); + } + } + + private _handleSecurityStateChange(event: any) { + try { + if (event.securityState === 'insecure' && event.explanations) { + for (const explanation of event.explanations) { + if (explanation.description && explanation.description.includes('mixed content')) { + const consoleMessage: ConsoleMessage = { + type: 'error', + text: `Security Warning: ${explanation.description}`, + toString: () => `[SECURITY] ${explanation.description} @ security-layer`, + }; + + this._handleConsoleMessage(consoleMessage); + } + } + } + } catch (error) { + logUnhandledError(error); + } + } + + private _handleLogEntry(event: any) { + try { + const entry = event.entry; + if (entry && entry.text) { + const consoleMessage: ConsoleMessage = { + type: entry.level || 'info', + text: entry.text, + toString: () => `[${(entry.level || 'info').toUpperCase()}] ${entry.text} @ browser-log`, + }; + + this._handleConsoleMessage(consoleMessage); + } + } catch (error) { + logUnhandledError(error); + } + } + + private async _initializeExtensionConsoleCapture() { + try { + // Listen for console messages from the extension + await this.page.evaluate(() => { + window.addEventListener('message', (event) => { + if (event.data && event.data.type === 'PLAYWRIGHT_CONSOLE_CAPTURE') { + const message = event.data.consoleMessage; + + // Store the message in a global array for Playwright to access + if (!(window as any)._playwrightExtensionConsoleMessages) { + (window as any)._playwrightExtensionConsoleMessages = []; + } + (window as any)._playwrightExtensionConsoleMessages.push(message); + } + }); + }); + + // Poll for new extension console messages + setInterval(() => { + this._checkForExtensionConsoleMessages(); + }, 1000); + + } catch (error) { + logUnhandledError(error); + } + } + + private async _checkForExtensionConsoleMessages() { + try { + const newMessages = await this.page.evaluate(() => { + if (!(window as any)._playwrightExtensionConsoleMessages) { + return []; + } + const messages = (window as any)._playwrightExtensionConsoleMessages; + (window as any)._playwrightExtensionConsoleMessages = []; + return messages; + }); + + for (const message of newMessages) { + const consoleMessage: ConsoleMessage = { + type: message.type || 'log', + text: message.text || '', + toString: () => `[${(message.type || 'log').toUpperCase()}] ${message.text} @ ${message.location || message.source}`, + }; + + this._handleConsoleMessage(consoleMessage); + } + } catch (error) { + logUnhandledError(error); + } + } + private _onClose() { this._clearCollectedArtifacts(); this._onPageClose(this); diff --git a/src/tools/configure.ts b/src/tools/configure.ts index 4f9c51b..8f14901 100644 --- a/src/tools/configure.ts +++ b/src/tools/configure.ts @@ -37,7 +37,8 @@ const configureSchema = z.object({ locale: z.string().optional().describe('Browser locale (e.g., "en-US", "fr-FR", "ja-JP")'), timezone: z.string().optional().describe('Timezone ID (e.g., "America/New_York", "Europe/London", "Asia/Tokyo")'), colorScheme: z.enum(['light', 'dark', 'no-preference']).optional().describe('Preferred color scheme'), - permissions: z.array(z.string()).optional().describe('Permissions to grant (e.g., ["geolocation", "notifications", "camera", "microphone"])') + permissions: z.array(z.string()).optional().describe('Permissions to grant (e.g., ["geolocation", "notifications", "camera", "microphone"])'), + offline: z.boolean().optional().describe('Whether to emulate offline network conditions (equivalent to DevTools offline mode)') }); const listDevicesSchema = z.object({}); @@ -81,6 +82,43 @@ const configureSnapshotsSchema = z.object({ consoleOutputFile: z.string().optional().describe('File path to write browser console output to. Set to empty string to disable console file output.') }); +// Simple offline mode toggle for testing +const offlineModeSchema = z.object({ + offline: z.boolean().describe('Whether to enable offline mode (true) or online mode (false)') +}); + +const offlineModeTest = defineTool({ + capability: 'core', + schema: { + name: 'browser_set_offline', + title: 'Set browser offline mode', + description: 'Toggle browser offline mode on/off (equivalent to DevTools offline checkbox)', + inputSchema: offlineModeSchema, + type: 'destructive', + }, + handle: async (context: Context, params: z.output, response: Response) => { + try { + // Get current browser context + const tab = context.currentTab(); + if (!tab) { + throw new Error('No active browser tab. Navigate to a page first.'); + } + + const browserContext = tab.page.context(); + await browserContext.setOffline(params.offline); + + response.addResult( + `✅ Browser offline mode ${params.offline ? 'enabled' : 'disabled'}\n\n` + + `The browser will now ${params.offline ? 'block all network requests' : 'allow network requests'} ` + + `(equivalent to ${params.offline ? 'checking' : 'unchecking'} the offline checkbox in DevTools).` + ); + + } catch (error) { + throw new Error(`Failed to set offline mode: ${error}`); + } + }, +}); + export default [ defineTool({ capability: 'core', @@ -197,6 +235,14 @@ export default [ changes.push(`permissions: ${params.permissions.join(', ')}`); + if (params.offline !== undefined) { + const currentOffline = (currentConfig.browser as any).offline; + if (params.offline !== currentOffline) + changes.push(`offline mode: ${currentOffline ? 'enabled' : 'disabled'} → ${params.offline ? 'enabled' : 'disabled'}`); + + } + + if (changes.length === 0) { response.addResult('No configuration changes detected. Current settings remain the same.'); return; @@ -213,6 +259,7 @@ export default [ timezone: params.timezone, colorScheme: params.colorScheme, permissions: params.permissions, + offline: params.offline, }); response.addResult(`Browser configuration updated successfully:\n${changes.map(c => `• ${c}`).join('\n')}\n\nThe browser has been restarted with the new settings.`); @@ -398,7 +445,12 @@ export default [ `Path: ${params.path}\n` + `Manifest version: ${manifest.manifest_version || 'unknown'}\n\n` + `The browser has been restarted with the extension loaded.\n` + - `Use browser_list_extensions to see all installed extensions.` + `Use browser_list_extensions to see all installed extensions.\n\n` + + `⚠️ **Extension Persistence**: Extensions are session-based and will need to be reinstalled if:\n` + + `• The MCP client disconnects and reconnects\n` + + `• The browser context is restarted\n` + + `• You switch between isolated/persistent browser modes\n\n` + + `Extensions remain active for the current session only.` ); } catch (error) { @@ -523,7 +575,12 @@ export default [ `Version: ${extensionInfo.version}\n` + `Downloaded to: ${extensionDir}\n\n` + `The browser has been restarted with the extension loaded.\n` + - `Use browser_list_extensions to see all installed extensions.` + `Use browser_list_extensions to see all installed extensions.\n\n` + + `⚠️ **Extension Persistence**: Extensions are session-based and will need to be reinstalled if:\n` + + `• The MCP client disconnects and reconnects\n` + + `• The browser context is restarted\n` + + `• You switch between isolated/persistent browser modes\n\n` + + `Extensions remain active for the current session only.` ); } catch (error) { @@ -612,6 +669,7 @@ export default [ } }, }), + offlineModeTest, ]; // Helper functions for extension downloading