import puppeteer, { Browser, Page } from 'puppeteer-core';
import { findChromePath } from '../utils/index.js';
// Browser state
let browserInstance: Browser | null = null;
let currentPage: Page | null = null;
// Tool Schemas
export const BROWSER_SCHEMAS = {
launch_browser: {
type: 'object',
properties: {},
},
navigate: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'URL to navigate to (e.g., http://localhost:8081 for Metro)',
},
},
required: ['url'],
},
capture_screenshot: {
type: 'object',
properties: {
fullPage: {
type: 'boolean',
description: 'Capture full page or just viewport (default: true)',
default: true,
},
selector: {
type: 'string',
description: 'CSS selector to screenshot specific element',
},
},
},
click_element: {
type: 'object',
properties: {
selector: {
type: 'string',
description: 'CSS selector of element to click',
},
},
required: ['selector'],
},
type_text: {
type: 'object',
properties: {
selector: {
type: 'string',
description: 'CSS selector of input element',
},
text: {
type: 'string',
description: 'Text to type',
},
clear: {
type: 'boolean',
description: 'Clear existing text first (default: true)',
default: true,
},
},
required: ['selector', 'text'],
},
get_page_info: {
type: 'object',
properties: {},
},
get_dom_tree: {
type: 'object',
properties: {
selector: {
type: 'string',
description: 'CSS selector to get tree from (default: body)',
},
depth: {
type: 'number',
description: 'Maximum depth to traverse (default: 5)',
default: 5,
},
},
},
evaluate_javascript: {
type: 'object',
properties: {
code: {
type: 'string',
description: 'JavaScript code to evaluate in page context',
},
},
required: ['code'],
},
query_elements: {
type: 'object',
properties: {
selector: {
type: 'string',
description: 'CSS selector to find elements',
},
},
required: ['selector'],
},
get_element_info: {
type: 'object',
properties: {
selector: {
type: 'string',
description: 'CSS selector of element',
},
},
required: ['selector'],
},
close_browser: {
type: 'object',
properties: {},
},
open_metro_bundler: {
type: 'object',
properties: {
port: {
type: 'number',
description: 'Metro bundler port (default: 8081)',
default: 8081,
},
},
},
get_metro_logs: {
type: 'object',
properties: {},
},
reload_metro: {
type: 'object',
properties: {},
},
};
// Tool definitions for listing
export const BROWSER_TOOLS = [
{
name: 'launch_browser',
description: 'Launch browser instance (Chrome/Chromium)',
inputSchema: BROWSER_SCHEMAS.launch_browser,
},
{
name: 'navigate',
description: 'Navigate to URL (Metro bundler, Chrome DevTools, any web page)',
inputSchema: BROWSER_SCHEMAS.navigate,
},
{
name: 'capture_screenshot',
description: 'Capture screenshot of current page or specific element',
inputSchema: BROWSER_SCHEMAS.capture_screenshot,
},
{
name: 'click_element',
description: 'Click element by CSS selector',
inputSchema: BROWSER_SCHEMAS.click_element,
},
{
name: 'type_text',
description: 'Type text into input field',
inputSchema: BROWSER_SCHEMAS.type_text,
},
{
name: 'get_page_info',
description: 'Get current page URL, title, and viewport info',
inputSchema: BROWSER_SCHEMAS.get_page_info,
},
{
name: 'get_dom_tree',
description: 'Get HTML structure of page or element',
inputSchema: BROWSER_SCHEMAS.get_dom_tree,
},
{
name: 'evaluate_javascript',
description: 'Execute JavaScript code in page context',
inputSchema: BROWSER_SCHEMAS.evaluate_javascript,
},
{
name: 'query_elements',
description: 'Find all elements matching CSS selector',
inputSchema: BROWSER_SCHEMAS.query_elements,
},
{
name: 'get_element_info',
description: 'Get detailed info about specific element',
inputSchema: BROWSER_SCHEMAS.get_element_info,
},
{
name: 'close_browser',
description: 'Close browser and cleanup',
inputSchema: BROWSER_SCHEMAS.close_browser,
},
{
name: 'open_metro_bundler',
description: 'Quick shortcut to open React Native Metro bundler UI',
inputSchema: BROWSER_SCHEMAS.open_metro_bundler,
},
{
name: 'get_metro_logs',
description: 'Extract logs from Metro bundler UI',
inputSchema: BROWSER_SCHEMAS.get_metro_logs,
},
{
name: 'reload_metro',
description: 'Trigger reload in Metro bundler',
inputSchema: BROWSER_SCHEMAS.reload_metro,
},
];
// Browser lifecycle functions
export async function launchBrowser(): Promise<void> {
if (browserInstance) {
return;
}
const chromePath = findChromePath();
if (!chromePath) {
throw new Error('Chrome not found. Please install Google Chrome.');
}
browserInstance = await puppeteer.launch({
executablePath: chromePath,
headless: false,
defaultViewport: { width: 1280, height: 800 },
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
],
});
currentPage = await browserInstance.newPage();
console.error('Browser launched successfully');
console.error(` Chrome: ${chromePath}`);
}
export async function ensureBrowser(): Promise<Page> {
if (!currentPage || !browserInstance) {
await launchBrowser();
}
return currentPage!;
}
export async function closeBrowser(): Promise<void> {
if (browserInstance) {
await browserInstance.close();
browserInstance = null;
currentPage = null;
console.error('Browser closed');
}
}
// Navigation functions
export async function navigateToUrl(url: string): Promise<string> {
const page = await ensureBrowser();
try {
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
return `Navigated to: ${page.url()}`;
} catch (error) {
throw new Error(`Navigation failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Screenshot functions
export async function captureScreenshot(fullPage: boolean = true, selector?: string): Promise<string> {
const page = await ensureBrowser();
try {
let screenshot: Buffer;
if (selector) {
const element = await page.$(selector);
if (!element) {
throw new Error(`Element not found: ${selector}`);
}
screenshot = await element.screenshot();
} else {
screenshot = await page.screenshot({ fullPage });
}
return screenshot.toString('base64');
} catch (error) {
throw new Error(`Screenshot failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Interaction functions
export async function clickElement(selector: string): Promise<string> {
const page = await ensureBrowser();
try {
await page.waitForSelector(selector, { timeout: 5000 });
await page.click(selector);
return `Clicked: ${selector}`;
} catch (error) {
throw new Error(`Click failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
export async function typeText(selector: string, text: string, clear: boolean = true): Promise<string> {
const page = await ensureBrowser();
try {
await page.waitForSelector(selector, { timeout: 5000 });
if (clear) {
await page.click(selector, { clickCount: 3 });
await page.keyboard.press('Backspace');
}
await page.type(selector, text);
return `Typed "${text}" into: ${selector}`;
} catch (error) {
throw new Error(`Type failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Page info functions
export async function getPageInfo(): Promise<any> {
const page = await ensureBrowser();
try {
const url = page.url();
const title = await page.title();
const viewport = page.viewport();
return {
url,
title,
viewport,
};
} catch (error) {
throw new Error(`Get page info failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
export async function getDomTree(selector?: string, depth: number = 5): Promise<string> {
const page = await ensureBrowser();
try {
const html = await page.evaluate((sel, maxDepth) => {
const element = sel ? document.querySelector(sel) : document.body;
if (!element) return `Element not found: ${sel}`;
function getTree(node: Element, currentDepth: number): string {
if (currentDepth > maxDepth) return '...';
const tag = node.tagName.toLowerCase();
const id = node.id ? `#${node.id}` : '';
const classes = node.className ? `.${String(node.className).split(' ').join('.')}` : '';
const text = node.childNodes.length === 1 && node.childNodes[0].nodeType === 3
? node.textContent?.trim().substring(0, 50)
: '';
let result = `${' '.repeat(currentDepth)}<${tag}${id}${classes}>`;
if (text) result += ` ${text}`;
result += '\n';
if (currentDepth < maxDepth) {
Array.from(node.children).forEach(child => {
result += getTree(child, currentDepth + 1);
});
}
return result;
}
return getTree(element, 0);
}, selector, depth);
return html;
} catch (error) {
throw new Error(`Get DOM tree failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
export async function evaluateJavaScript(code: string): Promise<any> {
const page = await ensureBrowser();
try {
const result = await page.evaluate((codeStr) => {
return eval(codeStr);
}, code);
return result;
} catch (error) {
throw new Error(`JavaScript evaluation failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
export async function queryElements(selector: string): Promise<any[]> {
const page = await ensureBrowser();
try {
const elements = await page.evaluate((sel) => {
const els = Array.from(document.querySelectorAll(sel));
return els.map(el => ({
tag: el.tagName.toLowerCase(),
id: el.id || undefined,
className: el.className || undefined,
textContent: el.textContent?.trim().substring(0, 100),
}));
}, selector);
return elements;
} catch (error) {
throw new Error(`Query elements failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
export async function getElementInfo(selector: string): Promise<any> {
const page = await ensureBrowser();
try {
const info = await page.evaluate((sel) => {
const el = document.querySelector(sel);
if (!el) return null;
const rect = el.getBoundingClientRect();
const computed = window.getComputedStyle(el);
return {
tag: el.tagName.toLowerCase(),
id: el.id || undefined,
className: el.className || undefined,
textContent: el.textContent?.trim(),
innerHTML: el.innerHTML.substring(0, 200),
position: {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
},
styles: {
display: computed.display,
visibility: computed.visibility,
color: computed.color,
backgroundColor: computed.backgroundColor,
},
};
}, selector);
if (!info) {
throw new Error(`Element not found: ${selector}`);
}
return info;
} catch (error) {
throw new Error(`Get element info failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Metro bundler helpers
export async function openMetroBundler(port: number = 8081): Promise<string> {
const url = `http://localhost:${port}`;
await navigateToUrl(url);
return `Opened Metro bundler at: ${url}`;
}
export async function getMetroLogs(): Promise<string[]> {
const page = await ensureBrowser();
try {
const logs = await page.evaluate(() => {
const logElements = document.querySelectorAll('[data-testid="log-box"] pre, .log-box pre, pre');
return Array.from(logElements).map(el => el.textContent || '').filter(Boolean);
});
return logs;
} catch (error) {
throw new Error(`Get Metro logs failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
export async function reloadMetro(): Promise<string> {
const page = await ensureBrowser();
try {
// Try to find and click reload button
const selectors = [
'button:has-text("Reload")',
'[data-testid="reload-button"]',
'button[aria-label="Reload"]',
];
for (const selector of selectors) {
try {
await page.waitForSelector(selector, { timeout: 2000 });
await page.click(selector);
return 'Metro reloaded';
} catch {
continue;
}
}
// Fallback: keyboard shortcut
await page.keyboard.press('r');
return 'Metro reload triggered (keyboard shortcut)';
} catch (error) {
throw new Error(`Metro reload failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Tool handler
export async function handleBrowserTool(name: string, args: any): Promise<any> {
switch (name) {
case 'launch_browser': {
await launchBrowser();
return {
content: [
{
type: 'text',
text: 'Browser launched successfully',
},
],
};
}
case 'navigate': {
const { url } = args as { url: string };
const result = await navigateToUrl(url);
return {
content: [
{
type: 'text',
text: result,
},
],
};
}
case 'capture_screenshot': {
const { fullPage = true, selector } = args as {
fullPage?: boolean;
selector?: string;
};
const base64Image = await captureScreenshot(fullPage, selector);
const pageInfo = await getPageInfo();
return {
content: [
{
type: 'text',
text: `Screenshot captured from: ${pageInfo.url}`,
},
{
type: 'image',
data: base64Image,
mimeType: 'image/png',
},
],
};
}
case 'click_element': {
const { selector } = args as { selector: string };
const result = await clickElement(selector);
return {
content: [
{
type: 'text',
text: result,
},
],
};
}
case 'type_text': {
const { selector, text, clear = true } = args as {
selector: string;
text: string;
clear?: boolean;
};
const result = await typeText(selector, text, clear);
return {
content: [
{
type: 'text',
text: result,
},
],
};
}
case 'get_page_info': {
const info = await getPageInfo();
return {
content: [
{
type: 'text',
text: JSON.stringify(info, null, 2),
},
],
};
}
case 'get_dom_tree': {
const { selector, depth = 5 } = args as {
selector?: string;
depth?: number;
};
const tree = await getDomTree(selector, depth);
return {
content: [
{
type: 'text',
text: tree,
},
],
};
}
case 'evaluate_javascript': {
const { code } = args as { code: string };
const result = await evaluateJavaScript(code);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'query_elements': {
const { selector } = args as { selector: string };
const elements = await queryElements(selector);
return {
content: [
{
type: 'text',
text: JSON.stringify(elements, null, 2),
},
],
};
}
case 'get_element_info': {
const { selector } = args as { selector: string };
const info = await getElementInfo(selector);
return {
content: [
{
type: 'text',
text: JSON.stringify(info, null, 2),
},
],
};
}
case 'close_browser': {
await closeBrowser();
return {
content: [
{
type: 'text',
text: 'Browser closed successfully',
},
],
};
}
case 'open_metro_bundler': {
const { port = 8081 } = args as { port?: number };
const result = await openMetroBundler(port);
return {
content: [
{
type: 'text',
text: result,
},
],
};
}
case 'get_metro_logs': {
const logs = await getMetroLogs();
return {
content: [
{
type: 'text',
text: logs.length > 0 ? logs.join('\n\n') : 'No logs found',
},
],
};
}
case 'reload_metro': {
const result = await reloadMetro();
return {
content: [
{
type: 'text',
text: result,
},
],
};
}
default:
return null;
}
}
// Check if a tool name belongs to browser tools
export function isBrowserTool(name: string): boolean {
return BROWSER_TOOLS.some(tool => tool.name === name);
}