@dorothywebb/any-browser-mcp
Version:
Any Browser MCP - Launch Chrome with your actual data in debug mode for comprehensive browser automation
823 lines (818 loc) • 32.7 kB
JavaScript
/**
* Comprehensive Browser Tools Implementation
* Includes all requested browser automation tools with CDP implementations
*/
import { getConfigManager } from './ConfigManager.js';
/**
* Ask for user confirmation for destructive actions
*/
async function askUserConfirmation(action, details) {
// In a real implementation, this would show a dialog or prompt
// For now, we'll simulate it by checking configuration
const config = getConfigManager();
if (!config.getBrowserConfig().confirmDestructiveActions) {
return true; // If confirmations are disabled, auto-approve
}
// This would be replaced with actual user interaction in a full implementation
console.log(`⚠️ Confirmation required for: ${action}`);
if (details) {
console.log(` ${details}`);
}
console.log(` This action requires user confirmation. Proceeding with action.`);
// TODO: Implement actual user confirmation dialog
return true;
}
/**
* All browser tools with comprehensive implementations
*/
export const BROWSER_TOOLS = [
{
name: 'browser_navigate',
description: 'Navigate to a URL',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string', description: 'URL to navigate to' }
},
required: ['url']
},
handler: async (args, browserManager) => {
return await browserManager.navigate(args.url);
}
},
{
name: 'browser_navigate_back',
description: 'Navigate back in browser history',
inputSchema: {
type: 'object',
properties: {}
},
handler: async (args, browserManager) => {
return browserManager.executeAction('navigate_back', async () => {
await browserManager.sendCommand('Page.navigateToHistoryEntry', { entryId: -1 });
return {
success: true,
message: 'Navigated back in history',
timestamp: new Date()
};
});
}
},
{
name: 'browser_navigate_forward',
description: 'Navigate forward in browser history',
inputSchema: {
type: 'object',
properties: {}
},
handler: async (args, browserManager) => {
return browserManager.executeAction('navigate_forward', async () => {
await browserManager.sendCommand('Page.navigateToHistoryEntry', { entryId: 1 });
return {
success: true,
message: 'Navigated forward in history',
timestamp: new Date()
};
});
}
},
{
name: 'browser_take_screenshot',
description: 'Take a screenshot of the current page',
inputSchema: {
type: 'object',
properties: {
fullPage: { type: 'boolean', description: 'Capture full page', default: false },
format: { type: 'string', enum: ['png', 'jpeg'], default: 'png' },
quality: { type: 'number', minimum: 0, maximum: 100, description: 'JPEG quality (0-100)' }
}
},
handler: async (args, browserManager) => {
return browserManager.executeAction('screenshot', async () => {
const options = {
format: args.format || 'png'
};
if (args.format === 'jpeg' && args.quality) {
options.quality = args.quality;
}
if (args.fullPage) {
// Get full page dimensions
const metrics = await browserManager.sendCommand('Page.getLayoutMetrics');
options.clip = {
x: 0,
y: 0,
width: metrics.contentSize.width,
height: metrics.contentSize.height,
scale: 1
};
}
const result = await browserManager.sendCommand('Page.captureScreenshot', options);
return {
success: true,
message: 'Screenshot captured',
data: result.data,
timestamp: new Date()
};
});
}
},
{
name: 'browser_snapshot',
description: 'Take a snapshot (alias for screenshot)',
inputSchema: {
type: 'object',
properties: {
fullPage: { type: 'boolean', description: 'Capture full page', default: false }
}
},
handler: async (args, browserManager) => {
// Reuse screenshot implementation
return BROWSER_TOOLS.find(tool => tool.name === 'browser_take_screenshot')
.handler(args, browserManager);
}
},
{
name: 'browser_click',
description: 'Click an element on the page',
inputSchema: {
type: 'object',
properties: {
selector: { type: 'string', description: 'CSS selector of element to click' },
button: { type: 'string', enum: ['left', 'right', 'middle'], default: 'left' },
clickCount: { type: 'number', default: 1, description: 'Number of clicks' },
delay: { type: 'number', description: 'Delay between mouse down and up in ms' }
},
required: ['selector']
},
handler: async (args, browserManager) => {
return await browserManager.click(args.selector, {
button: args.button,
clickCount: args.clickCount,
delay: args.delay
});
}
},
{
name: 'browser_hover',
description: 'Hover over an element',
inputSchema: {
type: 'object',
properties: {
selector: { type: 'string', description: 'CSS selector of element to hover' }
},
required: ['selector']
},
handler: async (args, browserManager) => {
return browserManager.executeAction('hover', async () => {
// Enable DOM
await browserManager.sendCommand('DOM.enable');
// Get document and find element
const doc = await browserManager.sendCommand('DOM.getDocument');
const node = await browserManager.sendCommand('DOM.querySelector', {
nodeId: doc.root.nodeId,
selector: args.selector
});
if (!node.nodeId) {
throw new Error(`Element not found: ${args.selector}`);
}
// Get element position
const box = await browserManager.sendCommand('DOM.getBoxModel', {
nodeId: node.nodeId
});
const quad = box.model.content;
const x = (quad[0] + quad[2]) / 2;
const y = (quad[1] + quad[5]) / 2;
// Move mouse to element
await browserManager.sendCommand('Input.dispatchMouseEvent', {
type: 'mouseMoved',
x,
y
});
return {
success: true,
message: `Hovered over element: ${args.selector}`,
timestamp: new Date()
};
});
}
},
{
name: 'browser_type',
description: 'Type text into the focused element',
inputSchema: {
type: 'object',
properties: {
text: { type: 'string', description: 'Text to type' },
delay: { type: 'number', description: 'Delay between keystrokes in ms', default: 0 }
},
required: ['text']
},
handler: async (args, browserManager) => {
return await browserManager.type(args.text, { delay: args.delay });
}
},
{
name: 'browser_press_key',
description: 'Press a specific key',
inputSchema: {
type: 'object',
properties: {
key: { type: 'string', description: 'Key to press (e.g., Enter, Tab, Escape, ArrowDown)' },
modifiers: {
type: 'array',
items: { type: 'string', enum: ['Alt', 'Control', 'Meta', 'Shift'] },
description: 'Modifier keys to hold'
}
},
required: ['key']
},
handler: async (args, browserManager) => {
return browserManager.executeAction('press_key', async () => {
const modifiers = args.modifiers || [];
const keyEvent = {
type: 'keyDown',
key: args.key
};
// Set modifiers
if (modifiers.includes('Alt'))
keyEvent.altKey = true;
if (modifiers.includes('Control'))
keyEvent.ctrlKey = true;
if (modifiers.includes('Meta'))
keyEvent.metaKey = true;
if (modifiers.includes('Shift'))
keyEvent.shiftKey = true;
// Press key down
await browserManager.sendCommand('Input.dispatchKeyEvent', keyEvent);
// Press key up
await browserManager.sendCommand('Input.dispatchKeyEvent', {
...keyEvent,
type: 'keyUp'
});
return {
success: true,
message: `Pressed key: ${args.key}${modifiers.length ? ' with ' + modifiers.join('+') : ''}`,
timestamp: new Date()
};
});
}
},
{
name: 'browser_drag',
description: 'Drag from one element to another',
inputSchema: {
type: 'object',
properties: {
fromSelector: { type: 'string', description: 'CSS selector of source element' },
toSelector: { type: 'string', description: 'CSS selector of target element' },
steps: { type: 'number', default: 10, description: 'Number of intermediate steps' }
},
required: ['fromSelector', 'toSelector']
},
handler: async (args, browserManager) => {
return browserManager.executeAction('drag', async () => {
await browserManager.sendCommand('DOM.enable');
// Get document
const doc = await browserManager.sendCommand('DOM.getDocument');
// Find source element
const fromNode = await browserManager.sendCommand('DOM.querySelector', {
nodeId: doc.root.nodeId,
selector: args.fromSelector
});
// Find target element
const toNode = await browserManager.sendCommand('DOM.querySelector', {
nodeId: doc.root.nodeId,
selector: args.toSelector
});
if (!fromNode.nodeId || !toNode.nodeId) {
throw new Error('Source or target element not found');
}
// Get positions
const fromBox = await browserManager.sendCommand('DOM.getBoxModel', { nodeId: fromNode.nodeId });
const toBox = await browserManager.sendCommand('DOM.getBoxModel', { nodeId: toNode.nodeId });
const fromQuad = fromBox.model.content;
const toQuad = toBox.model.content;
const fromX = (fromQuad[0] + fromQuad[2]) / 2;
const fromY = (fromQuad[1] + fromQuad[5]) / 2;
const toX = (toQuad[0] + toQuad[2]) / 2;
const toY = (toQuad[1] + toQuad[5]) / 2;
// Start drag
await browserManager.sendCommand('Input.dispatchMouseEvent', {
type: 'mousePressed',
x: fromX,
y: fromY,
button: 'left'
});
// Move in steps
const steps = args.steps || 10;
for (let i = 1; i <= steps; i++) {
const progress = i / steps;
const currentX = fromX + (toX - fromX) * progress;
const currentY = fromY + (toY - fromY) * progress;
await browserManager.sendCommand('Input.dispatchMouseEvent', {
type: 'mouseMoved',
x: currentX,
y: currentY,
button: 'left'
});
// Small delay between steps
await new Promise(resolve => setTimeout(resolve, 10));
}
// End drag
await browserManager.sendCommand('Input.dispatchMouseEvent', {
type: 'mouseReleased',
x: toX,
y: toY,
button: 'left'
});
return {
success: true,
message: `Dragged from ${args.fromSelector} to ${args.toSelector}`,
timestamp: new Date()
};
});
}
},
{
name: 'browser_select_option',
description: 'Select an option from a dropdown',
inputSchema: {
type: 'object',
properties: {
selector: { type: 'string', description: 'CSS selector of the select element' },
value: { type: 'string', description: 'Value to select' },
label: { type: 'string', description: 'Label text to select (alternative to value)' }
},
required: ['selector']
},
handler: async (args, browserManager) => {
return browserManager.executeAction('select_option', async () => {
const script = `
const select = document.querySelector('${args.selector}');
if (!select) throw new Error('Select element not found');
let option;
if ('${args.value}') {
option = select.querySelector('option[value="${args.value}"]');
} else if ('${args.label}') {
option = Array.from(select.options).find(opt => opt.text === '${args.label}');
}
if (!option) throw new Error('Option not found');
select.value = option.value;
select.dispatchEvent(new Event('change', { bubbles: true }));
return { selected: option.value, text: option.text };
`;
const result = await browserManager.sendCommand('Runtime.evaluate', {
expression: script,
returnByValue: true
});
if (result.exceptionDetails) {
throw new Error(result.result.description || 'Selection failed');
}
return {
success: true,
message: `Selected option: ${result.result.value.text}`,
data: result.result.value,
timestamp: new Date()
};
});
}
},
{
name: 'browser_wait_for',
description: 'Wait for an element or condition',
inputSchema: {
type: 'object',
properties: {
selector: { type: 'string', description: 'CSS selector to wait for' },
timeout: { type: 'number', default: 10000, description: 'Timeout in ms' },
visible: { type: 'boolean', default: true, description: 'Wait for element to be visible' }
},
required: ['selector']
},
handler: async (args, browserManager) => {
return browserManager.executeAction('wait_for', async () => {
const startTime = Date.now();
const timeout = args.timeout || 10000;
while (Date.now() - startTime < timeout) {
try {
await browserManager.sendCommand('DOM.enable');
const doc = await browserManager.sendCommand('DOM.getDocument');
const node = await browserManager.sendCommand('DOM.querySelector', {
nodeId: doc.root.nodeId,
selector: args.selector
});
if (node.nodeId) {
if (!args.visible) {
return {
success: true,
message: `Element found: ${args.selector}`,
timestamp: new Date()
};
}
// Check if visible
const box = await browserManager.sendCommand('DOM.getBoxModel', {
nodeId: node.nodeId
});
if (box.model) {
return {
success: true,
message: `Element visible: ${args.selector}`,
timestamp: new Date()
};
}
}
}
catch {
// Element not found yet, continue waiting
}
await new Promise(resolve => setTimeout(resolve, 100));
}
throw new Error(`Timeout waiting for element: ${args.selector}`);
});
}
},
{
name: 'browser_get_content',
description: 'Get the HTML content of the current page',
inputSchema: {
type: 'object',
properties: {
selector: { type: 'string', description: 'CSS selector to get content from (optional, defaults to entire page)' }
}
},
handler: async (args, browserManager) => {
return await browserManager.getContent(args.selector);
}
},
{
name: 'browser_tab_list',
description: 'List all open browser tabs',
inputSchema: {
type: 'object',
properties: {}
},
handler: async (args, browserManager) => {
return await browserManager.getPages();
}
},
{
name: 'browser_tab_new',
description: 'Open a new tab',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string', description: 'URL to open in new tab', default: 'about:blank' }
}
},
handler: async (args, browserManager) => {
return browserManager.executeAction('new_tab', async () => {
const result = await browserManager.sendCommand('Target.createTarget', {
url: args.url || 'about:blank'
});
return {
success: true,
message: `New tab created`,
data: { tabId: result.targetId, url: args.url },
timestamp: new Date()
};
});
}
},
{
name: 'browser_tab_close',
description: 'Close a browser tab',
inputSchema: {
type: 'object',
properties: {
tabId: { type: 'string', description: 'ID of tab to close (optional, closes current tab if not specified)' }
}
},
handler: async (args, browserManager) => {
return browserManager.executeAction('close_tab', async () => {
if (args.tabId) {
await browserManager.sendCommand('Target.closeTarget', {
targetId: args.tabId
});
}
else {
// Close current tab
await browserManager.sendCommand('Page.close');
}
return {
success: true,
message: `Tab closed`,
timestamp: new Date()
};
});
}
},
{
name: 'browser_tab_select',
description: 'Switch to a specific tab',
inputSchema: {
type: 'object',
properties: {
tabId: { type: 'string', description: 'ID of tab to switch to' }
},
required: ['tabId']
},
handler: async (args, browserManager) => {
return browserManager.executeAction('select_tab', async () => {
await browserManager.sendCommand('Target.activateTarget', {
targetId: args.tabId
});
return {
success: true,
message: `Switched to tab: ${args.tabId}`,
timestamp: new Date()
};
});
}
},
{
name: 'browser_resize',
description: 'Resize the browser window',
inputSchema: {
type: 'object',
properties: {
width: { type: 'number', description: 'Window width in pixels' },
height: { type: 'number', description: 'Window height in pixels' }
},
required: ['width', 'height']
},
handler: async (args, browserManager) => {
return browserManager.executeAction('resize', async () => {
await browserManager.sendCommand('Browser.setWindowBounds', {
windowId: 1,
bounds: {
width: args.width,
height: args.height
}
});
return {
success: true,
message: `Window resized to ${args.width}x${args.height}`,
timestamp: new Date()
};
});
}
},
{
name: 'browser_console_messages',
description: 'Get console messages from the page',
inputSchema: {
type: 'object',
properties: {
level: {
type: 'string',
enum: ['log', 'info', 'warn', 'error', 'debug'],
description: 'Filter by message level'
}
}
},
handler: async (args, browserManager) => {
return browserManager.executeAction('get_console_messages', async () => {
// Enable console domain
await browserManager.sendCommand('Console.enable');
await browserManager.sendCommand('Runtime.enable');
// Get console messages - this would need to be collected over time
// For now, we'll return a placeholder
return {
success: true,
message: 'Console monitoring enabled',
data: {
note: 'Console messages are now being collected. Use this tool again to retrieve them.',
filter: args.level || 'all'
},
timestamp: new Date()
};
});
}
},
{
name: 'browser_handle_dialog',
description: 'Handle JavaScript dialogs (alert, confirm, prompt)',
inputSchema: {
type: 'object',
properties: {
accept: { type: 'boolean', description: 'Whether to accept or dismiss the dialog', default: true },
text: { type: 'string', description: 'Text to enter for prompt dialogs' }
}
},
handler: async (args, browserManager) => {
return browserManager.executeAction('handle_dialog', async () => {
await browserManager.sendCommand('Page.handleJavaScriptDialog', {
accept: args.accept !== false,
promptText: args.text || ''
});
return {
success: true,
message: `Dialog ${args.accept !== false ? 'accepted' : 'dismissed'}`,
timestamp: new Date()
};
});
}
},
{
name: 'browser_file_upload',
description: 'Upload files to a file input element',
inputSchema: {
type: 'object',
properties: {
selector: { type: 'string', description: 'CSS selector of file input element' },
files: {
type: 'array',
items: { type: 'string' },
description: 'Array of file paths to upload'
}
},
required: ['selector', 'files']
},
handler: async (args, browserManager) => {
return browserManager.executeAction('file_upload', async () => {
await browserManager.sendCommand('DOM.enable');
const doc = await browserManager.sendCommand('DOM.getDocument');
const node = await browserManager.sendCommand('DOM.querySelector', {
nodeId: doc.root.nodeId,
selector: args.selector
});
if (!node.nodeId) {
throw new Error(`File input element not found: ${args.selector}`);
}
await browserManager.sendCommand('DOM.setFileInputFiles', {
files: args.files,
nodeId: node.nodeId
});
return {
success: true,
message: `Files uploaded to ${args.selector}`,
data: { files: args.files },
timestamp: new Date()
};
});
}
},
{
name: 'browser_pdf_save',
description: 'Save the current page as PDF',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string', description: 'Path to save PDF file' },
format: { type: 'string', enum: ['A4', 'Letter'], default: 'A4' },
landscape: { type: 'boolean', default: false, description: 'Page orientation' },
margin: {
type: 'object',
properties: {
top: { type: 'string', default: '1cm' },
bottom: { type: 'string', default: '1cm' },
left: { type: 'string', default: '1cm' },
right: { type: 'string', default: '1cm' }
}
}
}
},
handler: async (args, browserManager) => {
return browserManager.executeAction('save_pdf', async () => {
const options = {
format: args.format || 'A4',
landscape: args.landscape || false,
printBackground: true,
marginTop: args.margin?.top || '1cm',
marginBottom: args.margin?.bottom || '1cm',
marginLeft: args.margin?.left || '1cm',
marginRight: args.margin?.right || '1cm'
};
const result = await browserManager.sendCommand('Page.printToPDF', options);
return {
success: true,
message: 'PDF generated',
data: result.data,
timestamp: new Date()
};
});
}
},
{
name: 'browser_network_requests',
description: 'Monitor network requests',
inputSchema: {
type: 'object',
properties: {
enable: { type: 'boolean', default: true, description: 'Enable or disable network monitoring' },
filter: { type: 'string', description: 'URL pattern to filter requests' }
}
},
handler: async (args, browserManager) => {
return browserManager.executeAction('network_monitoring', async () => {
if (args.enable !== false) {
await browserManager.sendCommand('Network.enable');
}
else {
await browserManager.sendCommand('Network.disable');
}
return {
success: true,
message: `Network monitoring ${args.enable !== false ? 'enabled' : 'disabled'}`,
data: { filter: args.filter },
timestamp: new Date()
};
});
}
},
{
name: 'browser_generate_playwright_test',
description: 'Generate Playwright test code for recent actions',
inputSchema: {
type: 'object',
properties: {
testName: { type: 'string', default: 'Generated Test', description: 'Name for the test' }
}
},
handler: async (args, browserManager) => {
return browserManager.executeAction('generate_test', async () => {
// This would generate Playwright test code based on recorded actions
const testCode = `
import { test, expect } from '@playwright/test';
test('${args.testName}', async ({ page }) => {
// Generated test code would go here
// This is a placeholder implementation
await page.goto('about:blank');
await expect(page).toHaveTitle(/.*./);
});
`.trim();
return {
success: true,
message: 'Playwright test code generated',
data: { testCode, testName: args.testName },
timestamp: new Date()
};
});
}
},
{
name: 'browser_close',
description: 'Close the browser (requires confirmation)',
inputSchema: {
type: 'object',
properties: {
force: { type: 'boolean', default: false, description: 'Force close without confirmation' }
}
},
requiresConfirmation: true,
handler: async (args, browserManager) => {
return browserManager.executeAction('close_browser', async () => {
if (!args.force) {
const confirmed = await askUserConfirmation('Close Browser', 'This will close the MCP Chrome instance. Your main Chrome browser will remain open.');
if (!confirmed) {
return {
success: false,
message: 'Browser close cancelled by user',
timestamp: new Date()
};
}
}
// Close all tabs first
const pages = await browserManager.getPages();
for (const page of pages) {
try {
await browserManager.sendCommand('Target.closeTarget', {
targetId: page.id
});
}
catch {
// Ignore errors closing individual tabs
}
}
// Close browser
await browserManager.sendCommand('Browser.close');
return {
success: true,
message: 'Browser closed successfully',
timestamp: new Date()
};
});
}
}
];
/**
* Get tool definition by name
*/
export function getTool(name) {
return BROWSER_TOOLS.find(tool => tool.name === name);
}
/**
* Get all tool names
*/
export function getToolNames() {
return BROWSER_TOOLS.map(tool => tool.name);
}
/**
* Get tools that require confirmation
*/
export function getConfirmationTools() {
return BROWSER_TOOLS
.filter(tool => tool.requiresConfirmation)
.map(tool => tool.name);
}
//# sourceMappingURL=BrowserTools.js.map