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.
This commit is contained in:
parent
7de63b5bab
commit
afaa8a7014
47
README.md
47
README.md
@ -160,6 +160,8 @@ Playwright MCP server supports following arguments. They can be provided in the
|
|||||||
vision, pdf.
|
vision, pdf.
|
||||||
--cdp-endpoint <endpoint> CDP endpoint to connect to.
|
--cdp-endpoint <endpoint> CDP endpoint to connect to.
|
||||||
--config <path> path to the configuration file.
|
--config <path> path to the configuration file.
|
||||||
|
--console-output-file <path> file path to write browser console output to
|
||||||
|
for debugging and monitoring.
|
||||||
--device <device> device to emulate, for example: "iPhone 15"
|
--device <device> device to emulate, for example: "iPhone 15"
|
||||||
--executable-path <path> path to the browser executable.
|
--executable-path <path> path to the browser executable.
|
||||||
--headless run browser in headless mode, headed by
|
--headless run browser in headless mode, headed by
|
||||||
@ -529,7 +531,7 @@ http.createServer(async (req, res) => {
|
|||||||
|
|
||||||
- **browser_click**
|
- **browser_click**
|
||||||
- Title: 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:
|
- Parameters:
|
||||||
- `element` (string): Human-readable element description used to obtain permission to interact with the element
|
- `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
|
- `ref` (string): Exact target element reference from the page snapshot
|
||||||
@ -575,6 +577,18 @@ http.createServer(async (req, res) => {
|
|||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
|
- **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**
|
||||||
|
|
||||||
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_console_messages**
|
- **browser_console_messages**
|
||||||
- Title: Get console messages
|
- Title: Get console messages
|
||||||
- Description: Returns all console messages
|
- Description: Returns all console messages
|
||||||
@ -585,7 +599,7 @@ http.createServer(async (req, res) => {
|
|||||||
|
|
||||||
- **browser_drag**
|
- **browser_drag**
|
||||||
- Title: Drag mouse
|
- 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:
|
- Parameters:
|
||||||
- `startElement` (string): Human-readable source element description used to obtain the permission to interact with the element
|
- `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
|
- `startRef` (string): Exact source element reference from the page snapshot
|
||||||
@ -597,7 +611,7 @@ http.createServer(async (req, res) => {
|
|||||||
|
|
||||||
- **browser_evaluate**
|
- **browser_evaluate**
|
||||||
- Title: Evaluate JavaScript
|
- 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:
|
- Parameters:
|
||||||
- `function` (string): () => { /* code */ } or (element) => { /* code */ } when element is provided
|
- `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
|
- `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**
|
- **browser_file_upload**
|
||||||
- Title: Upload files
|
- 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:
|
- Parameters:
|
||||||
- `paths` (array): The absolute paths to the files to upload. Can be a single file or multiple files.
|
- `paths` (array): The absolute paths to the files to upload. Can be a single file or multiple files.
|
||||||
- Read-only: **false**
|
- Read-only: **false**
|
||||||
@ -617,7 +631,7 @@ http.createServer(async (req, res) => {
|
|||||||
|
|
||||||
- **browser_handle_dialog**
|
- **browser_handle_dialog**
|
||||||
- Title: Handle a 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:
|
- Parameters:
|
||||||
- `accept` (boolean): Whether to accept the dialog.
|
- `accept` (boolean): Whether to accept the dialog.
|
||||||
- `promptText` (string, optional): The text of the prompt in case of a prompt 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**
|
- **browser_hover**
|
||||||
- Title: Hover mouse
|
- 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:
|
- Parameters:
|
||||||
- `element` (string): Human-readable element description used to obtain permission to interact with the element
|
- `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
|
- `ref` (string): Exact target element reference from the page snapshot
|
||||||
@ -673,7 +687,7 @@ http.createServer(async (req, res) => {
|
|||||||
|
|
||||||
- **browser_navigate**
|
- **browser_navigate**
|
||||||
- Title: Navigate to a URL
|
- 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:
|
- Parameters:
|
||||||
- `url` (string): The URL to navigate to
|
- `url` (string): The URL to navigate to
|
||||||
- Read-only: **false**
|
- Read-only: **false**
|
||||||
@ -706,7 +720,7 @@ http.createServer(async (req, res) => {
|
|||||||
|
|
||||||
- **browser_press_key**
|
- **browser_press_key**
|
||||||
- Title: Press a 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:
|
- Parameters:
|
||||||
- `key` (string): Name of the key to press or a character to generate, such as `ArrowLeft` or `a`
|
- `key` (string): Name of the key to press or a character to generate, such as `ArrowLeft` or `a`
|
||||||
- Read-only: **false**
|
- Read-only: **false**
|
||||||
@ -733,7 +747,7 @@ http.createServer(async (req, res) => {
|
|||||||
|
|
||||||
- **browser_select_option**
|
- **browser_select_option**
|
||||||
- Title: 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:
|
- Parameters:
|
||||||
- `element` (string): Human-readable element description used to obtain permission to interact with the element
|
- `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
|
- `ref` (string): Exact target element reference from the page snapshot
|
||||||
@ -744,7 +758,7 @@ http.createServer(async (req, res) => {
|
|||||||
|
|
||||||
- **browser_snapshot**
|
- **browser_snapshot**
|
||||||
- Title: Page 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
|
- Parameters: None
|
||||||
- Read-only: **true**
|
- Read-only: **true**
|
||||||
|
|
||||||
@ -770,20 +784,21 @@ http.createServer(async (req, res) => {
|
|||||||
|
|
||||||
- **browser_take_screenshot**
|
- **browser_take_screenshot**
|
||||||
- Title: Take a 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:
|
- Parameters:
|
||||||
- `raw` (boolean, optional): Whether to return without compression (in PNG format). Default is false, which returns a JPEG image.
|
- `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.
|
- `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.
|
- `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.
|
- `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.
|
- `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**
|
- Read-only: **true**
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_type**
|
- **browser_type**
|
||||||
- Title: Type text
|
- 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:
|
- Parameters:
|
||||||
- `element` (string): Human-readable element description used to obtain permission to interact with the element
|
- `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
|
- `ref` (string): Exact target element reference from the page snapshot
|
||||||
@ -805,7 +820,7 @@ http.createServer(async (req, res) => {
|
|||||||
|
|
||||||
- **browser_wait_for**
|
- **browser_wait_for**
|
||||||
- Title: 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:
|
- Parameters:
|
||||||
- `time` (number, optional): The time to wait in seconds
|
- `time` (number, optional): The time to wait in seconds
|
||||||
- `text` (string, optional): The text to wait for
|
- `text` (string, optional): The text to wait for
|
||||||
@ -821,7 +836,7 @@ http.createServer(async (req, res) => {
|
|||||||
|
|
||||||
- **browser_tab_close**
|
- **browser_tab_close**
|
||||||
- Title: Close a tab
|
- Title: Close a tab
|
||||||
- Description: Close a tab
|
- Description: Close a tab. Returns page snapshot after closing tab (configurable via browser_configure_snapshots).
|
||||||
- Parameters:
|
- Parameters:
|
||||||
- `index` (number, optional): The index of the tab to close. Closes current tab if not provided.
|
- `index` (number, optional): The index of the tab to close. Closes current tab if not provided.
|
||||||
- Read-only: **false**
|
- Read-only: **false**
|
||||||
@ -838,7 +853,7 @@ http.createServer(async (req, res) => {
|
|||||||
|
|
||||||
- **browser_tab_new**
|
- **browser_tab_new**
|
||||||
- Title: Open a new tab
|
- 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:
|
- Parameters:
|
||||||
- `url` (string, optional): The URL to navigate to in the new tab. If not provided, the new tab will be blank.
|
- `url` (string, optional): The URL to navigate to in the new tab. If not provided, the new tab will be blank.
|
||||||
- Read-only: **true**
|
- Read-only: **true**
|
||||||
@ -847,7 +862,7 @@ http.createServer(async (req, res) => {
|
|||||||
|
|
||||||
- **browser_tab_select**
|
- **browser_tab_select**
|
||||||
- Title: Select a tab
|
- 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:
|
- Parameters:
|
||||||
- `index` (number): The index of the tab to select
|
- `index` (number): The index of the tab to select
|
||||||
- Read-only: **true**
|
- Read-only: **true**
|
||||||
|
|||||||
193
console-capture-extension/background.js
Normal file
193
console-capture-extension/background.js
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
50
console-capture-extension/content.js
Normal file
50
console-capture-extension/content.js
Normal file
@ -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');
|
||||||
BIN
console-capture-extension/icon-128.png
Normal file
BIN
console-capture-extension/icon-128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
BIN
console-capture-extension/icon-16.png
Normal file
BIN
console-capture-extension/icon-16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 571 B |
BIN
console-capture-extension/icon-32.png
Normal file
BIN
console-capture-extension/icon-32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
console-capture-extension/icon-48.png
Normal file
BIN
console-capture-extension/icon-48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
37
console-capture-extension/manifest.json
Normal file
37
console-capture-extension/manifest.json
Normal file
@ -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": ["<all_urls>"],
|
||||||
|
"js": ["content.js"],
|
||||||
|
"run_at": "document_start"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"host_permissions": [
|
||||||
|
"<all_urls>"
|
||||||
|
],
|
||||||
|
|
||||||
|
"icons": {
|
||||||
|
"16": "icon-16.png",
|
||||||
|
"32": "icon-32.png",
|
||||||
|
"48": "icon-48.png",
|
||||||
|
"128": "icon-128.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -72,6 +72,12 @@ class BaseContextFactory implements BrowserContextFactory {
|
|||||||
testDebug(`create browser context (${this.name})`);
|
testDebug(`create browser context (${this.name})`);
|
||||||
const browser = await this._obtainBrowser();
|
const browser = await this._obtainBrowser();
|
||||||
const browserContext = await this._doCreateContext(browser, extensionPaths);
|
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) };
|
return { browserContext, close: () => this._closeBrowserContext(browserContext, browser) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -60,7 +60,6 @@ const defaultConfig: FullConfig = {
|
|||||||
browserName: 'chromium',
|
browserName: 'chromium',
|
||||||
isolated: true,
|
isolated: true,
|
||||||
launchOptions: {
|
launchOptions: {
|
||||||
channel: 'chrome',
|
|
||||||
headless: false,
|
headless: false,
|
||||||
chromiumSandbox: true,
|
chromiumSandbox: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -315,6 +315,11 @@ export class Context {
|
|||||||
|
|
||||||
const browserContext = await browser.newContext(contextOptions);
|
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 {
|
return {
|
||||||
browserContext,
|
browserContext,
|
||||||
close: async () => {
|
close: async () => {
|
||||||
@ -373,6 +378,7 @@ export class Context {
|
|||||||
timezone?: string;
|
timezone?: string;
|
||||||
colorScheme?: 'light' | 'dark' | 'no-preference';
|
colorScheme?: 'light' | 'dark' | 'no-preference';
|
||||||
permissions?: string[];
|
permissions?: string[];
|
||||||
|
offline?: boolean;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const currentConfig = { ...this.config };
|
const currentConfig = { ...this.config };
|
||||||
|
|
||||||
@ -428,6 +434,10 @@ export class Context {
|
|||||||
currentConfig.browser.contextOptions.permissions = changes.permissions;
|
currentConfig.browser.contextOptions.permissions = changes.permissions;
|
||||||
|
|
||||||
|
|
||||||
|
if (changes.offline !== undefined)
|
||||||
|
(currentConfig.browser as any).offline = changes.offline;
|
||||||
|
|
||||||
|
|
||||||
// Store the modified config
|
// Store the modified config
|
||||||
(this as any).config = currentConfig;
|
(this as any).config = currentConfig;
|
||||||
|
|
||||||
|
|||||||
215
src/tab.ts
215
src/tab.ts
@ -71,6 +71,12 @@ export class Tab extends EventEmitter<TabEventsInterface> {
|
|||||||
});
|
});
|
||||||
page.setDefaultNavigationTimeout(60000);
|
page.setDefaultNavigationTimeout(60000);
|
||||||
page.setDefaultTimeout(5000);
|
page.setDefaultTimeout(5000);
|
||||||
|
|
||||||
|
// Initialize service worker console capture
|
||||||
|
void this._initializeServiceWorkerConsoleCapture();
|
||||||
|
|
||||||
|
// Initialize extension-based console capture
|
||||||
|
void this._initializeExtensionConsoleCapture();
|
||||||
}
|
}
|
||||||
|
|
||||||
modalStates(): ModalState[] {
|
modalStates(): ModalState[] {
|
||||||
@ -160,6 +166,215 @@ export class Tab extends EventEmitter<TabEventsInterface> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
private _onClose() {
|
||||||
this._clearCollectedArtifacts();
|
this._clearCollectedArtifacts();
|
||||||
this._onPageClose(this);
|
this._onPageClose(this);
|
||||||
|
|||||||
@ -37,7 +37,8 @@ const configureSchema = z.object({
|
|||||||
locale: z.string().optional().describe('Browser locale (e.g., "en-US", "fr-FR", "ja-JP")'),
|
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")'),
|
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'),
|
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({});
|
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.')
|
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<typeof offlineModeSchema>, 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 [
|
export default [
|
||||||
defineTool({
|
defineTool({
|
||||||
capability: 'core',
|
capability: 'core',
|
||||||
@ -197,6 +235,14 @@ export default [
|
|||||||
changes.push(`permissions: ${params.permissions.join(', ')}`);
|
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) {
|
if (changes.length === 0) {
|
||||||
response.addResult('No configuration changes detected. Current settings remain the same.');
|
response.addResult('No configuration changes detected. Current settings remain the same.');
|
||||||
return;
|
return;
|
||||||
@ -213,6 +259,7 @@ export default [
|
|||||||
timezone: params.timezone,
|
timezone: params.timezone,
|
||||||
colorScheme: params.colorScheme,
|
colorScheme: params.colorScheme,
|
||||||
permissions: params.permissions,
|
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.`);
|
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` +
|
`Path: ${params.path}\n` +
|
||||||
`Manifest version: ${manifest.manifest_version || 'unknown'}\n\n` +
|
`Manifest version: ${manifest.manifest_version || 'unknown'}\n\n` +
|
||||||
`The browser has been restarted with the extension loaded.\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) {
|
} catch (error) {
|
||||||
@ -523,7 +575,12 @@ export default [
|
|||||||
`Version: ${extensionInfo.version}\n` +
|
`Version: ${extensionInfo.version}\n` +
|
||||||
`Downloaded to: ${extensionDir}\n\n` +
|
`Downloaded to: ${extensionDir}\n\n` +
|
||||||
`The browser has been restarted with the extension loaded.\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) {
|
} catch (error) {
|
||||||
@ -612,6 +669,7 @@ export default [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
offlineModeTest,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Helper functions for extension downloading
|
// Helper functions for extension downloading
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user