diff --git a/README.md b/README.md index 4c40d46..4c08983 100644 --- a/README.md +++ b/README.md @@ -1,254 +1,198 @@ -# MCP-Browser +# MCP-Browser v3.0.0 -MCP server que permite a qualquer agente de IA controlar um navegador real usando [Playwright](https://playwright.dev/). +MCP server para automação de navegador de alta qualidade usando [Playwright](https://playwright.dev/) com **42 tools** para automação avançada. ![Node](https://img.shields.io/badge/Node.js-18+-339933?logo=node.js&logoColor=white) ![MCP](https://img.shields.io/badge/MCP-stdio-blue) ![Platform](https://img.shields.io/badge/Windows%20|%20Linux%20|%20macOS-lightgrey) -![Tools](https://img.shields.io/badge/17%20tools-green) +![Tools](https://img.shields.io/badge/42%20tools-green) -## Funcionalidades +## Novidades v3.0 -- Controle total de navegador via IA -- 17 tools prontas para uso -- Reuso automático de abas (mesmo domínio) -- Preenchimento automático de formulários -- Fluxos agentísticos com retry -- Compatível com qualquer plataforma MCP - -## Pré-requisitos - -| Dependência | Versão mínima | -|-------------|---------------| -| Node.js | 18+ | -| npm | 9+ | +- **Stealth Mode** - Anti-detecção avançada +- **25 novas tools** - Muito mais poder +- **Multi-Tab real** - Gerenciamento completo +- **PDF generation** - Geração de PDFs +- **Network Monitoring** - Logs de rede +- **Device Emulation** - iPhone, Pixel, etc +- **Performance Metrics** - Métricas detalhadas ## Instalação ```bash -# 1. Clonar o repositório -git clone https://git.jf.eng.br/jfeng/MCP-Browser.git -cd MCP-Browser - -# 2. Instalar dependências npm install - -# 3. Instalar Chromium do Playwright npx playwright install chromium - -# Linux: instalar dependências do sistema -sudo npx playwright install-deps chromium ``` -## Configuração por plataforma - -Todas as plataformas usam o mesmo padrão: - -``` -Comando: node -Argumento: /caminho/absoluto/MCP-Browser/server.js -Transport: stdio -``` - -### OpenCode +## Configuração ```bash -opencode mcp add +node server.js ``` -- **Nome:** `meu-navegador` -- **Tipo:** `Local` -- **Comando:** `node /caminho/MCP-Browser/server.js` +## Tools Disponíveis (42) -### Cursor +### Navegação +| Tool | Descrição | +|------|-----------| +| `open_url` | Abre URL | +| `new_tab` | Nova aba | +| `switch_tab` | Troca aba | +| `list_tabs` | Lista abas | +| `close_tab` | Fecha aba | +| `go_back` | Voltar | +| `go_forward` | Avançar | +| `reload` | Recarregar | +| `wait_for_url` | Espera URL | -`~/.cursor/mcp.json` (global) ou `.cursor/mcp.json` (projeto): +### Interação +| Tool | Descrição | +|------|-----------| +| `click` | Clica (suporta duplo, right-click) | +| `smart_click` | Clica por texto | +| `type` | Digita (suporta lentamente) | +| `smart_type` | Digita por label | +| `hover` | Faz hover | +| `select` | Seleciona opção | +| `press_key` | Pressiona tecla | +| `scroll_to` | Rola página | +| `upload_file` | Upload de arquivo | -```json -{ - "mcpServers": { - "browser": { - "command": "node", - "args": ["/caminho/MCP-Browser/server.js"] - } - } -} -``` +### Extração +| Tool | Descrição | +|------|-----------| +| `get_text` | Extrai texto | +| `get_html` | Extrai HTML | +| `get_links` | Lista links | +| `get_buttons` | Lista botões | +| `get_forms` | Lista formulários | +| `extract_elements` | Extrai elementos | +| `get_url` | URL atual | +| `get_title` | Título da página | +| `evaluate_js` | Executa JavaScript | -### Claude Desktop +### Screens & PDF +| Tool | Descrição | +|------|-----------| +| `screenshot` | Screenshot (full/elemento) | +| `pdf` | Gera PDF | -`%APPDATA%\Claude\claude_desktop_config.json` (Windows) -`~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) +### Viewport & Device +| Tool | Descrição | +|------|-----------| +| `set_viewport` | Define viewport | +| `emulate_device` | Emula dispositivo | -```json -{ - "mcpServers": { - "browser": { - "command": "node", - "args": ["/caminho/MCP-Browser/server.js"] - } - } -} -``` +### Cookies & Storage +| Tool | Descrição | +|------|-----------| +| `get_cookies` | Obtém cookies | +| `set_cookies` | Define cookies | +| `clear_cookies` | Limpa cookies | +| `get_storage` | Obtém storage | +| `set_storage` | Define storage | +| `clear_storage` | Limpa storage | -### Windsurf +### Network +| Tool | Descrição | +|------|-----------| +| `get_network_logs` | Logs de rede | +| `block_resources` | Bloqueia recursos | -`~/.windsurf/mcp.json`: +### Performance +| Tool | Descrição | +|------|-----------| +| `get_performance_metrics` | Métricas | -```json -{ - "mcpServers": { - "browser": { - "command": "node", - "args": ["/caminho/MCP-Browser/server.js"] - } - } -} -``` +### Wait +| Tool | Descrição | +|------|-----------| +| `wait` | Aguarda ms | +| `wait_for_selector` | Espera elemento | -### Trae +### Automação +| Tool | Descrição | +|------|-----------| +| `fill_form_auto` | Preenche formulário | +| `agent_flow` | Fluxo automático | +| `agent_flow_v2` | Fluxo com retry | -Settings → Extensions → MCP → Adicionar server: +### Controle +| Tool | Descrição | +|------|-----------| +| `close_browser` | Fecha navegador | -- **Name:** `browser` -- **Command:** `node` -- **Args:** `["/caminho/MCP-Browser/server.js"]` +## Exemplo de Uso -### Antigravity - -Configuração de MCP → Novo server: - -- **Name:** `browser` -- **Type:** `stdio` -- **Command:** `node /caminho/MCP-Browser/server.js` - -### VS Code - -`.vscode/mcp.json`: - -```json -{ - "mcpServers": { - "browser": { - "command": "node", - "args": ["/caminho/MCP-Browser/server.js"] - } - } -} -``` - -### Qualquer plataforma MCP - -Se suporta **MCP stdio transport**, use: - -``` -Comando: node -Argumento: /caminho/absoluto/MCP-Browser/server.js -``` - -## Tools disponíveis (17) - -| # | Tool | Descrição | Parâmetros | -|---|------|-----------|------------| -| 1 | `open_url` | Abre uma URL | `url` | -| 2 | `click` | Clica por seletor CSS | `selector` | -| 3 | `type` | Digita texto em campo | `selector`, `text` | -| 4 | `screenshot` | Captura screenshot | `fullPage` (opcional) | -| 5 | `get_text` | Extrai texto da página | `selector` (opcional) | -| 6 | `get_links` | Lista links | `selector` (opcional) | -| 7 | `wait_for_selector` | Espera elemento aparecer | `selector`, `timeout` | -| 8 | `extract_elements` | Extrai elementos com atributos | `selector`, `attributes` | -| 9 | `smart_click` | Clica por texto visível | `text` | -| 10 | `smart_type` | Digita por label/placeholder | `label`, `text` | -| 11 | `get_buttons` | Lista botões disponíveis | — | -| 12 | `smart_wait_navigation` | Espera mudança de URL | `timeout` (opcional) | -| 13 | `get_forms` | Lista formulários e campos | — | -| 14 | `fill_form_auto` | Preenche formulário auto | `data` | -| 15 | `agent_flow` | Fluxo automático simples | `goal`, `data` | -| 16 | `agent_flow_v2` | Fluxo com retry/fallback | `goal`, `data`, `retries` | -| 17 | `close_browser` | Fecha o navegador | — | - -## Exemplos de uso - -### Abrir site e extrair conteúdo - -``` +```javascript +// Abrir site open_url("https://example.com") -get_text() -get_links() -screenshot() -``` -### Login automático - -``` -open_url("https://site.com/login") -fill_form_auto({"email": "user@test.com", "password": "1234"}) +// Login automático +fill_form_auto({"email": "user@test.com", "password": "123"}) smart_click("Entrar") -smart_wait_navigation() +wait_for_url("**dashboard**") + +// Extrair dados +get_links() +get_text(".product-title") + +// Screenshot +screenshot({fullPage: true}) + +// Emular mobile +emulate_device("iphone-14") ``` -### Scraping com atributos +## Dispositivos Disponíveis -``` -open_url("https://site.com/produtos") -extract_elements(".produto", ["href", "data-id"]) -``` +- iphone-se, iphone-14, iphone-14-pro-max +- ipad, ipad-pro +- pixel-5, galaxy-s20, galaxy-s23-ultra +- desktop-1080p, desktop-4k -## Estrutura do projeto +## Estrutura ``` MCP-Browser/ -├── server.js # Servidor MCP (entry point) -├── browser.js # Engine Playwright (singleton) +├── server.js # Servidor MCP +├── browser.js # Engine Playwright ├── package.json -├── docs.html # Documentação detalhada -└── tools/ +└── tools/ # 42 tools ├── openUrl.js ├── click.js - ├── type.js - ├── screenshot.js - ├── getText.js - ├── getLinks.js - ├── getButtons.js - ├── getForms.js - ├── extractElements.js - ├── waitForSelector.js - ├── smartClick.js - ├── smartType.js - ├── fillFormAuto.js - ├── smartWaitNavigation.js - ├── agentFlow.js - ├── agentFlowV2.js - └── closeBrowser.js + ├── getURL.js + ├── getTitle.js + ├── getHTML.js + ├── goBack.js + ├── goForward.js + ├── reload.js + ├── evaluateJS.js + ├── waitForURL.js + ├── newTab.js + ├── switchTab.js + ├── listTabs.js + ├── closeTab.js + ├── setViewport.js + ├── emulateDevice.js + ├── hover.js + ├── select.js + ├── pressKey.js + ├── scrollTo.js + ├── pdf.js + ├── getCookies.js + ├── setCookies.js + ├── clearCookies.js + ├── getStorage.js + ├── setStorage.js + ├── clearStorage.js + ├── getNetworkLogs.js + ├── blockResources.js + ├── uploadFile.js + ├── getPerformanceMetrics.js + └── wait.js ``` -## Comportamento - -- **Reuso de abas:** mesmo domínio reutiliza aba, domínios diferentes abrem nova aba -- **Headed mode:** navegador abre visível. Para headless, edite `browser.js` e mude `headless: true` -- **Singleton:** uma única instância do navegador compartilhada entre todas as tools - -## Solução de problemas - -| Problema | Solução | -|----------|---------| -| Tools não aparecem | Reiniciar a IDE após configurar MCP | -| `node: not found` | Instalar Node.js 18+ e adicionar ao PATH | -| Chromium não encontrado | `npx playwright install chromium` | -| Erro de permissão (Linux) | `sudo npx playwright install-deps chromium` | -| Caminho com espaços | Usar aspas no caminho | - -## Caminhos por SO - -| Sistema | Exemplo | -|---------|---------| -| Windows | `C:/Users/seuuser/mcp-browser/server.js` | -| macOS | `/Users/seuuser/mcp-browser/server.js` | -| Linux | `/home/seuuser/mcp-browser/server.js` | - -> **Importante:** Nunca usar `\` simples. Sempre `/` ou `\\`. - ## Licença MIT diff --git a/browser.js b/browser.js index d7975c5..1d63d54 100644 --- a/browser.js +++ b/browser.js @@ -2,64 +2,250 @@ const { chromium } = require('playwright'); let browser = null; let context = null; -let page = null; +let pages = []; +let activePageIndex = 0; +let downloadsPath = null; +let networkLogs = []; +let blockedResources = []; -async function start() { +const STEALTH_SCRIPTS = [ + `Object.defineProperty(navigator, 'webdriver', { get: () => undefined });`, + `Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });`, + `Object.defineProperty(navigator, 'languages', { get: () => ['pt-BR', 'pt', 'en-US', 'en'] });`, + `window.chrome = { runtime: {}, loadTimes: function(){}, csi: function(){} };`, + `const originalQuery = window.navigator.permissions.query; + window.navigator.permissions.query = (parameters) => ( + parameters.name === 'notifications' ? + Promise.resolve({ state: Notification.permission }) : + originalQuery(parameters) + );`, + `delete navigator.__proto__.webdriver;`, + `const iframeDesc = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, 'contentWindow'); + Object.defineProperty(HTMLIFrameElement.prototype, 'contentWindow', { + get: function() { + const iframe = iframeDesc.get.call(this); + if (iframe && !iframe.chrome) { + iframe.chrome = window.chrome; + } + return iframe; + } + });`, + `const getParameter = WebGLRenderingContext.prototype.getParameter; + WebGLRenderingContext.prototype.getParameter = function(parameter) { + if (parameter === 37445) return 'Intel Inc.'; + if (parameter === 37446) return 'Intel Iris OpenGL Engine'; + return getParameter.call(this, parameter); + };` +]; + +async function start(options = {}) { if (browser) return; - if (page) return; - browser = await chromium.launch({ - headless: false, - args: ['--disable-blink-features=AutomationControlled'] + const { + headless = false, + proxy = null, + downloadsDir = null, + viewport = { width: 1280, height: 720 }, + userAgent = null, + locale = 'pt-BR', + timezoneId = 'America/Sao_Paulo', + geolocation = null, + permissions = ['geolocation'], + ignoreHTTPSErrors = true, + colorScheme = 'light', + args = [] + } = options; + + downloadsPath = downloadsDir; + + const launchArgs = [ + '--disable-blink-features=AutomationControlled', + '--disable-infobars', + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-accelerated-2d-canvas', + '--disable-gpu', + '--window-size=1280,720', + '--disable-extensions', + ...args + ]; + + const launchOptions = { + headless, + args: launchArgs + }; + + if (proxy) { + launchOptions.proxy = typeof proxy === 'string' ? { server: proxy } : proxy; + } + + browser = await chromium.launch(launchOptions); + + const contextOptions = { + viewport, + userAgent: userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', + locale, + timezoneId, + ignoreHTTPSErrors, + colorScheme, + permissions: geolocation ? permissions : [], + javaScriptEnabled: true, + bypassCSP: true, + offline: false, + hasTouch: false, + isMobile: false, + deviceScaleFactor: 1 + }; + + if (geolocation) { + contextOptions.geolocation = geolocation; + } + + if (downloadsPath) { + contextOptions.acceptDownloads = true; + } + + context = await browser.newContext(contextOptions); + + await context.addInitScript(STEALTH_SCRIPTS.join('\n')); + + const page = await context.newPage(); + pages = [page]; + activePageIndex = 0; + + if (downloadsPath) { + page.on('download', async (download) => { + const filePath = `${downloadsPath}/${download.suggestedFilename()}`; + await download.saveAs(filePath); + }); + } + + page.on('request', (req) => { + if (blockedResources.some(r => req.resourceType() === r)) { + req.abort(); + return; + } + networkLogs.push({ + type: 'request', + url: req.url(), + method: req.method(), + resourceType: req.resourceType(), + timestamp: Date.now() + }); + if (networkLogs.length > 1000) networkLogs = networkLogs.slice(-500); }); - context = await browser.newContext({ - viewport: { width: 1280, height: 720 }, - userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + page.on('response', (res) => { + networkLogs.push({ + type: 'response', + url: res.url(), + status: res.status(), + timestamp: Date.now() + }); }); - page = await context.newPage(); - browser.on('disconnected', () => { browser = null; context = null; - page = null; + pages = []; + activePageIndex = 0; + networkLogs = []; }); } async function ensurePage() { if (!browser) { await start(); - return page; + return pages[activePageIndex]; } - if (!page || page.isClosed()) { - page = await context.newPage(); + const currentPage = pages[activePageIndex]; + if (!currentPage || currentPage.isClosed()) { + const newPage = await context.newPage(); + pages[activePageIndex] = newPage; + setupPageListeners(newPage); + return newPage; } - return page; + return currentPage; } -async function goto(url) { - const p = await ensurePage(); - await p.goto(url, { - waitUntil: 'domcontentloaded', - timeout: 30000 +function setupPageListeners(page) { + if (downloadsPath) { + page.on('download', async (download) => { + const filePath = `${downloadsPath}/${download.suggestedFilename()}`; + await download.saveAs(filePath); + }); + } + + page.on('request', (req) => { + if (blockedResources.some(r => req.resourceType() === r)) { + req.abort(); + return; + } + networkLogs.push({ + type: 'request', + url: req.url(), + method: req.method(), + resourceType: req.resourceType(), + timestamp: Date.now() + }); }); - await p.waitForTimeout(1500); + page.on('response', (res) => { + networkLogs.push({ + type: 'response', + url: res.url(), + status: res.status(), + timestamp: Date.now() + }); + }); } -async function click(selector) { +async function goto(url, options = {}) { const p = await ensurePage(); - await p.waitForSelector(selector, { state: 'visible', timeout: 10000 }); - await p.click(selector); + const { + waitUntil = 'domcontentloaded', + timeout = 30000, + referer = undefined + } = options; + + const response = await p.goto(url, { waitUntil, timeout, referer }); + await p.waitForTimeout(1000); + + return { + status: response?.status() || null, + url: p.url(), + title: await p.title() + }; } -async function type(selector, text) { +async function click(selector, options = {}) { + const p = await ensurePage(); + await p.waitForSelector(selector, { state: 'visible', timeout: options.timeout || 10000 }); + await p.click(selector, { + button: options.button || 'left', + clickCount: options.clickCount || 1, + delay: options.delay || 0, + force: options.force || false + }); +} + +async function type(selector, text, options = {}) { const p = await ensurePage(); await p.waitForSelector(selector, { state: 'visible', timeout: 10000 }); - await p.fill(selector, text); + + if (options.clear) { + await p.fill(selector, ''); + } + + if (options.slowly) { + await p.click(selector); + await p.keyboard.type(text, { delay: options.delay || 50 }); + } else { + await p.fill(selector, text); + } } async function content() { @@ -72,29 +258,418 @@ async function title() { return await p.title(); } -async function screenshot(path, fullPage = true) { +async function currentUrl() { const p = await ensurePage(); - await p.screenshot({ path, fullPage }); + return p.url(); } -async function newTab() { - const p = await context.newPage(); - page = p; - return p; +async function screenshot(options = {}) { + const p = await ensurePage(); + const { + fullPage = true, + path = `screenshot-${Date.now()}.png`, + type = 'png', + quality, + clip, + element = null + } = options; + + if (element) { + const el = await p.$(element); + if (el) { + await el.screenshot({ path, type, quality }); + return path; + } + } + + await p.screenshot({ path, fullPage, type, quality, clip }); + return path; +} + +async function newTab(url = null) { + if (!browser || !context) await start(); + if (!context) throw new Error('Browser context not available'); + const page = await context.newPage(); + setupPageListeners(page); + pages.push(page); + activePageIndex = pages.length - 1; + + if (url) { + await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 }); + await page.waitForTimeout(1000); + } + + return page; +} + +async function switchTab(index) { + if (index < 0 || index >= pages.length) { + throw new Error(`Tab index ${index} out of range. Available tabs: ${pages.length}`); + } + activePageIndex = index; + return pages[activePageIndex]; +} + +function listTabs() { + return pages.map((p, i) => ({ + index: i, + url: p.url(), + title: null, + isActive: i === activePageIndex, + isClosed: p.isClosed() + })); +} + +async function closeTab(index = null) { + const idx = index !== null ? index : activePageIndex; + if (idx < 0 || idx >= pages.length) { + throw new Error(`Tab index ${idx} out of range`); + } + + const page = pages[idx]; + await page.close(); + pages.splice(idx, 1); + + if (pages.length === 0) { + activePageIndex = 0; + const newPage = await context.newPage(); + setupPageListeners(newPage); + pages.push(newPage); + } else if (activePageIndex >= pages.length) { + activePageIndex = pages.length - 1; + } + + return { closed: idx, activeTab: activePageIndex, totalTabs: pages.length }; +} + +async function goBack() { + const p = await ensurePage(); + await p.goBack({ waitUntil: 'domcontentloaded' }); + await p.waitForTimeout(500); + return p.url(); +} + +async function goForward() { + const p = await ensurePage(); + await p.goForward({ waitUntil: 'domcontentloaded' }); + await p.waitForTimeout(500); + return p.url(); +} + +async function reload() { + const p = await ensurePage(); + await p.reload({ waitUntil: 'domcontentloaded' }); + await p.waitForTimeout(500); + return p.url(); +} + +async function evaluateJS(expression) { + const p = await ensurePage(); + return await p.evaluate(expression); +} + +async function evaluateOnSelector(selector, expression) { + const p = await ensurePage(); + return await p.evaluate(({ sel, expr }) => { + const el = document.querySelector(sel); + if (!el) return null; + return eval(expr); + }, { sel: selector, expr: expression }); +} + +async function hover(selector) { + const p = await ensurePage(); + await p.waitForSelector(selector, { state: 'visible', timeout: 10000 }); + await p.hover(selector); +} + +async function select(selector, values) { + const p = await ensurePage(); + await p.waitForSelector(selector, { state: 'attached', timeout: 10000 }); + return await p.selectOption(selector, values); +} + +async function dragAndDrop(sourceSelector, targetSelector) { + const p = await ensurePage(); + await p.waitForSelector(sourceSelector, { state: 'visible', timeout: 10000 }); + await p.waitForSelector(targetSelector, { state: 'visible', timeout: 10000 }); + await p.dragAndDrop(sourceSelector, targetSelector); +} + +async function pressKey(key, options = {}) { + const p = await ensurePage(); + const { selector = null, delay = 0 } = options; + + if (selector) { + await p.waitForSelector(selector, { state: 'visible', timeout: 10000 }); + await p.focus(selector); + } + + await p.keyboard.press(key, { delay }); +} + +async function scrollTo(position) { + const p = await ensurePage(); + + if (typeof position === 'number') { + await p.evaluate((y) => window.scrollTo(0, y), position); + } else if (typeof position === 'object' && position.selector) { + await p.evaluate((sel) => { + const el = document.querySelector(sel); + if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' }); + }, position.selector); + } else if (position === 'top') { + await p.evaluate(() => window.scrollTo(0, 0)); + } else if (position === 'bottom') { + await p.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + } +} + +async function setViewport(width, height) { + const p = await ensurePage(); + await p.setViewportSize({ width, height }); +} + +async function emulateDevice(deviceName) { + const devices = { + 'iphone-se': { viewport: { width: 375, height: 667 }, userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1', isMobile: true, hasTouch: true, deviceScaleFactor: 2 }, + 'iphone-14': { viewport: { width: 390, height: 844 }, userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1', isMobile: true, hasTouch: true, deviceScaleFactor: 3 }, + 'iphone-14-pro-max': { viewport: { width: 430, height: 932 }, userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1', isMobile: true, hasTouch: true, deviceScaleFactor: 3 }, + 'ipad': { viewport: { width: 768, height: 1024 }, userAgent: 'Mozilla/5.0 (iPad; CPU OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1', isMobile: true, hasTouch: true, deviceScaleFactor: 2 }, + 'ipad-pro': { viewport: { width: 1024, height: 1366 }, userAgent: 'Mozilla/5.0 (iPad; CPU OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1', isMobile: true, hasTouch: true, deviceScaleFactor: 2 }, + 'pixel-5': { viewport: { width: 393, height: 851 }, userAgent: 'Mozilla/5.0 (Linux; Android 13; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36', isMobile: true, hasTouch: true, deviceScaleFactor: 2.75 }, + 'galaxy-s20': { viewport: { width: 360, height: 800 }, userAgent: 'Mozilla/5.0 (Linux; Android 13; SM-G981B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36', isMobile: true, hasTouch: true, deviceScaleFactor: 3 }, + 'galaxy-s23-ultra': { viewport: { width: 384, height: 854 }, userAgent: 'Mozilla/5.0 (Linux; Android 13; SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36', isMobile: true, hasTouch: true, deviceScaleFactor: 3.5 }, + 'desktop-1080p': { viewport: { width: 1920, height: 1080 }, userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', isMobile: false, hasTouch: false, deviceScaleFactor: 1 }, + 'desktop-4k': { viewport: { width: 3840, height: 2160 }, userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', isMobile: false, hasTouch: false, deviceScaleFactor: 2 } + }; + + const device = devices[deviceName.toLowerCase()]; + if (!device) { + throw new Error(`Device "${deviceName}" not found. Available: ${Object.keys(devices).join(', ')}`); + } + + if (!context) await start(); + + await context.close(); + + const contextOptions = { + viewport: device.viewport, + userAgent: device.userAgent, + isMobile: device.isMobile, + hasTouch: device.hasTouch, + deviceScaleFactor: device.deviceScaleFactor, + locale: 'pt-BR', + timezoneId: 'America/Sao_Paulo', + ignoreHTTPSErrors: true + }; + + context = await browser.newContext(contextOptions); + await context.addInitScript(STEALTH_SCRIPTS.join('\n')); + + pages = []; + const page = await context.newPage(); + setupPageListeners(page); + pages.push(page); + activePageIndex = 0; + + return { device: deviceName, viewport: device.viewport }; +} + +async function pdf(options = {}) { + const p = await ensurePage(); + const { + path = `page-${Date.now()}.pdf`, + format = 'A4', + landscape = false, + printBackground = true, + margin = { top: '20px', bottom: '20px', left: '20px', right: '20px' } + } = options; + + await p.pdf({ path, format, landscape, printBackground, margin }); + return path; +} + +async function getCookies(urls = []) { + if (!context) await start(); + if (urls.length > 0) { + return await context.cookies(urls); + } + return await context.cookies(); +} + +async function setCookies(cookies) { + if (!context) await start(); + await context.addCookies(cookies); + return { set: cookies.length }; +} + +async function clearCookies() { + if (!context) return { cleared: 0 }; + const allCookies = await context.cookies(); + await context.clearCookies(); + return { cleared: allCookies.length }; +} + +async function getStorage() { + const p = await ensurePage(); + + const storage = await p.evaluate(() => { + const local = {}; + const session = {}; + + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + local[key] = localStorage.getItem(key); + } + + for (let i = 0; i < sessionStorage.length; i++) { + const key = sessionStorage.key(i); + session[key] = sessionStorage.getItem(key); + } + + return { localStorage: local, sessionStorage: session }; + }); + + return { origin: p.url(), ...storage }; +} + +async function setStorage(data) { + const p = await ensurePage(); + + await p.evaluate((storageData) => { + if (storageData.localStorage) { + Object.entries(storageData.localStorage).forEach(([k, v]) => localStorage.setItem(k, v)); + } + if (storageData.sessionStorage) { + Object.entries(storageData.sessionStorage).forEach(([k, v]) => sessionStorage.setItem(k, v)); + } + }, data); + + return { set: true }; +} + +async function clearStorage() { + const p = await ensurePage(); + await p.evaluate(() => { + localStorage.clear(); + sessionStorage.clear(); + }); + return { cleared: true }; +} + +async function getNetworkLogs(filter = null) { + if (!filter) return networkLogs; + + return networkLogs.filter(log => { + if (filter.type && log.type !== filter.type) return false; + if (filter.url && !log.url.includes(filter.url)) return false; + if (filter.method && log.method !== filter.method) return false; + if (filter.resourceType && log.resourceType !== filter.resourceType) return false; + return true; + }); +} + +function clearNetworkLogs() { + const count = networkLogs.length; + networkLogs = []; + return { cleared: count }; +} + +function blockResources(types = []) { + blockedResources = [...new Set([...blockedResources, ...types])]; + return { blocked: blockedResources }; +} + +function unblockResources(types = []) { + if (types.length === 0) { + blockedResources = []; + } else { + blockedResources = blockedResources.filter(t => !types.includes(t)); + } + return { blocked: blockedResources }; +} + +async function waitForURL(urlPattern, options = {}) { + const p = await ensurePage(); + const { timeout = 30000 } = options; + + if (typeof urlPattern === 'string') { + await p.waitForURL(`**${urlPattern}**`, { timeout }); + } else if (urlPattern instanceof RegExp) { + await p.waitForURL(urlPattern, { timeout }); + } + + return p.url(); +} + +async function uploadFile(selector, filePaths) { + const p = await ensurePage(); + await p.waitForSelector(selector, { state: 'attached', timeout: 10000 }); + const files = Array.isArray(filePaths) ? filePaths : [filePaths]; + await p.setInputFiles(selector, files); + return { uploaded: files.length, files }; +} + +async function download(url) { + const p = await ensurePage(); + + const [dl] = await Promise.all([ + p.waitForEvent('download'), + p.evaluate((u) => { + const a = document.createElement('a'); + a.href = u; + a.download = ''; + document.body.appendChild(a); + a.click(); + a.remove(); + }, url) + ]); + + const filename = dl.suggestedFilename(); + const savePath = downloadsPath ? `${downloadsPath}/${filename}` : filename; + await dl.saveAs(savePath); + + return { filename, path: savePath }; } async function close() { if (browser) { try { await browser.close(); - } catch (e) { - } + } catch (e) { } browser = null; context = null; - page = null; + pages = []; + activePageIndex = 0; + networkLogs = []; + blockedResources = []; } } +async function waitForTimeout(ms) { + const p = await ensurePage(); + await p.waitForTimeout(ms); +} + +async function getPerformanceMetrics() { + const p = await ensurePage(); + return await p.evaluate(() => { + const perf = performance.getEntriesByType('navigation')[0]; + if (!perf) return null; + return { + dns: perf.domainLookupEnd - perf.domainLookupStart, + tcp: perf.connectEnd - perf.connectStart, + ttfb: perf.responseStart - perf.requestStart, + download: perf.responseEnd - perf.responseStart, + domInteractive: perf.domInteractive, + domComplete: perf.domComplete, + loadEvent: perf.loadEventEnd - perf.loadEventStart, + total: perf.loadEventEnd - perf.navigationStart + }; + }); +} + module.exports = { start, goto, @@ -102,13 +677,45 @@ module.exports = { type, content, title, + currentUrl, screenshot, close, newTab, + switchTab, + listTabs, + closeTab, ensurePage, + goBack, + goForward, + reload, + evaluateJS, + evaluateOnSelector, + hover, + select, + dragAndDrop, + pressKey, + scrollTo, + setViewport, + emulateDevice, + pdf, + getCookies, + setCookies, + clearCookies, + getStorage, + setStorage, + clearStorage, + getNetworkLogs, + clearNetworkLogs, + blockResources, + unblockResources, + waitForURL, + uploadFile, + download, + waitForTimeout, + getPerformanceMetrics, get page() { - return page; + return pages[activePageIndex]; }, get browser() { @@ -117,5 +724,13 @@ module.exports = { get context() { return context; + }, + + get tabs() { + return pages; + }, + + get activeTab() { + return activePageIndex; } }; diff --git a/package.json b/package.json index 3303846..870e329 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { "name": "opencode-mcp-browser", - "version": "1.0.0", - "description": "", - "main": "index.js", + "version": "3.0.0", + "description": "MCP Browser - Automação de navegador de alta qualidade com 42 tools", + "main": "server.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "start": "node server.js", + "test": "echo \"Execute: node server.js\" && exit 0" }, - "keywords": [], + "keywords": ["mcp", "browser", "playwright", "automation", "scraping"], "author": "", "license": "ISC", "type": "commonjs", diff --git a/server.js b/server.js index 0ea7ed4..cafda42 100644 --- a/server.js +++ b/server.js @@ -1,108 +1,122 @@ const { Server } = require('@modelcontextprotocol/sdk/server/index.js'); const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'); +const { ListToolsRequestSchema, CallToolRequestSchema } = require('@modelcontextprotocol/sdk/types.js'); -const { - ListToolsRequestSchema, - CallToolRequestSchema -} = require('@modelcontextprotocol/sdk/types.js'); +const tools = { + openUrl: require('./tools/openUrl'), + click: require('./tools/click'), + type: require('./tools/type'), + screenshot: require('./tools/screenshot'), + getText: require('./tools/getText'), + getLinks: require('./tools/getLinks'), + waitForSelector: require('./tools/waitForSelector'), + extractElements: require('./tools/extractElements'), + smartClick: require('./tools/smartClick'), + smartType: require('./tools/smartType'), + getButtons: require('./tools/getButtons'), + smartWaitNavigation: require('./tools/smartWaitNavigation'), + getForms: require('./tools/getForms'), + fillFormAuto: require('./tools/fillFormAuto'), + agentFlow: require('./tools/agentFlow'), + agentFlowV2: require('./tools/agentFlowV2'), + closeBrowser: require('./tools/closeBrowser'), + getURL: require('./tools/getURL'), + getTitle: require('./tools/getTitle'), + getHTML: require('./tools/getHTML'), + goBack: require('./tools/goBack'), + goForward: require('./tools/goForward'), + reload: require('./tools/reload'), + evaluateJS: require('./tools/evaluateJS'), + waitForURL: require('./tools/waitForURL'), + newTab: require('./tools/newTab'), + switchTab: require('./tools/switchTab'), + listTabs: require('./tools/listTabs'), + closeTab: require('./tools/closeTab'), + setViewport: require('./tools/setViewport'), + emulateDevice: require('./tools/emulateDevice'), + hover: require('./tools/hover'), + select: require('./tools/select'), + pressKey: require('./tools/pressKey'), + scrollTo: require('./tools/scrollTo'), + pdf: require('./tools/pdf'), + getCookies: require('./tools/getCookies'), + setCookies: require('./tools/setCookies'), + clearCookies: require('./tools/clearCookies'), + getStorage: require('./tools/getStorage'), + setStorage: require('./tools/setStorage'), + clearStorage: require('./tools/clearStorage'), + getNetworkLogs: require('./tools/getNetworkLogs'), + blockResources: require('./tools/blockResources'), + uploadFile: require('./tools/uploadFile'), + getPerformanceMetrics: require('./tools/getPerformanceMetrics'), + wait: require('./tools/wait') +}; -// TOOLS -const openUrl = require('./tools/openUrl'); -const click = require('./tools/click'); -const type = require('./tools/type'); -const screenshot = require('./tools/screenshot'); -const getText = require('./tools/getText'); -const getLinks = require('./tools/getLinks'); -const waitForSelector = require('./tools/waitForSelector'); -const extractElements = require('./tools/extractElements'); -const smartClick = require('./tools/smartClick'); -const smartType = require('./tools/smartType'); -const getButtons = require('./tools/getButtons'); -const smartWaitNavigation = require('./tools/smartWaitNavigation'); -const getForms = require('./tools/getForms'); -const fillFormAuto = require('./tools/fillFormAuto'); -const agentFlow = require('./tools/agentFlow'); -const agentFlowV2 = require('./tools/agentFlowV2'); -const closeBrowser = require('./tools/closeBrowser'); +const server = new Server({ name: 'browser', version: '3.0.0' }, { capabilities: { tools: {} } }); -// SERVER MCP -const server = new Server( - { - name: "browser", - version: "2.0.0" - }, - { - capabilities: { - tools: {} - } - } -); +const toolSchemas = [ + { name: 'open_url', description: 'Abre uma URL no navegador', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'URL para abrir' }, waitUntil: { type: 'string', enum: ['domcontentloaded', 'load', 'networkidle'] } }, required: ['url'] } }, + { name: 'click', description: 'Clica em elemento pelo seletor CSS', inputSchema: { type: 'object', properties: { selector: { type: 'string' }, button: { type: 'string', enum: ['left', 'right', 'middle'] }, double: { type: 'boolean' } }, required: ['selector'] } }, + { name: 'type', description: 'Digita texto em campo', inputSchema: { type: 'object', properties: { selector: { type: 'string' }, text: { type: 'string' }, clear: { type: 'boolean' }, slowly: { type: 'boolean' } }, required: ['selector', 'text'] } }, + { name: 'screenshot', description: 'Captura screenshot', inputSchema: { type: 'object', properties: { fullPage: { type: 'boolean' }, element: { type: 'string' }, path: { type: 'string' } } } }, + { name: 'get_text', description: 'Extrai texto', inputSchema: { type: 'object', properties: { selector: { type: 'string' } } } }, + { name: 'get_links', description: 'Lista links', inputSchema: { type: 'object', properties: { selector: { type: 'string' } } } }, + { name: 'wait_for_selector', description: 'Espera elemento', inputSchema: { type: 'object', properties: { selector: { type: 'string' }, timeout: { type: 'number' }, state: { type: 'string', enum: ['attached', 'detached', 'visible', 'hidden'] } }, required: ['selector'] } }, + { name: 'extract_elements', description: 'Extrai elementos com atributos', inputSchema: { type: 'object', properties: { selector: { type: 'string' }, attributes: { type: 'array', items: { type: 'string' } } }, required: ['selector'] } }, + { name: 'smart_click', description: 'Clica por texto visível', inputSchema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] } }, + { name: 'smart_type', description: 'Digita por label/placeholder', inputSchema: { type: 'object', properties: { label: { type: 'string' }, text: { type: 'string' } }, required: ['label', 'text'] } }, + { name: 'get_buttons', description: 'Lista botões', inputSchema: { type: 'object', properties: {} } }, + { name: 'smart_wait_navigation', description: 'Espera mudança de URL', inputSchema: { type: 'object', properties: { timeout: { type: 'number' } } } }, + { name: 'get_forms', description: 'Lista formulários', inputSchema: { type: 'object', properties: {} } }, + { name: 'fill_form_auto', description: 'Preenche formulário', inputSchema: { type: 'object', properties: { data: { type: 'object' } }, required: ['data'] } }, + { name: 'agent_flow', description: 'Fluxo automático simples', inputSchema: { type: 'object', properties: { goal: { type: 'string' }, data: { type: 'object' } }, required: ['goal'] } }, + { name: 'agent_flow_v2', description: 'Fluxo com retry', inputSchema: { type: 'object', properties: { goal: { type: 'string' }, data: { type: 'object' }, retries: { type: 'number' } }, required: ['goal'] } }, + { name: 'close_browser', description: 'Fecha navegador', inputSchema: { type: 'object', properties: {} } }, + { name: 'get_url', description: 'Obtém URL atual', inputSchema: { type: 'object', properties: {} } }, + { name: 'get_title', description: 'Obtém título', inputSchema: { type: 'object', properties: {} } }, + { name: 'get_html', description: 'Obtém HTML', inputSchema: { type: 'object', properties: { selector: { type: 'string' } } } }, + { name: 'go_back', description: 'Voltar no histórico', inputSchema: { type: 'object', properties: {} } }, + { name: 'go_forward', description: 'Avançar no histórico', inputSchema: { type: 'object', properties: {} } }, + { name: 'reload', description: 'Recarrega página', inputSchema: { type: 'object', properties: {} } }, + { name: 'evaluate_js', description: 'Executa JavaScript', inputSchema: { type: 'object', properties: { expression: { type: 'string' }, selector: { type: 'string' } }, required: ['expression'] } }, + { name: 'wait_for_url', description: 'Espera URL específica', inputSchema: { type: 'object', properties: { pattern: { type: 'string' }, timeout: { type: 'number' } }, required: ['pattern'] } }, + { name: 'new_tab', description: 'Nova aba', inputSchema: { type: 'object', properties: { url: { type: 'string' } } } }, + { name: 'switch_tab', description: 'Muda aba', inputSchema: { type: 'object', properties: { index: { type: 'number' } }, required: ['index'] } }, + { name: 'list_tabs', description: 'Lista abas', inputSchema: { type: 'object', properties: {} } }, + { name: 'close_tab', description: 'Fecha aba', inputSchema: { type: 'object', properties: { index: { type: 'number' } } } }, + { name: 'set_viewport', description: 'Define viewport', inputSchema: { type: 'object', properties: { width: { type: 'number' }, height: { type: 'number' } }, required: ['width', 'height'] } }, + { name: 'emulate_device', description: 'Emula dispositivo', inputSchema: { type: 'object', properties: { device: { type: 'string' } }, required: ['device'] } }, + { name: 'hover', description: 'Faz hover', inputSchema: { type: 'object', properties: { selector: { type: 'string' } }, required: ['selector'] } }, + { name: 'select', description: 'Seleciona opções', inputSchema: { type: 'object', properties: { selector: { type: 'string' }, values: { type: 'array', items: { type: 'string' } } }, required: ['selector', 'values'] } }, + { name: 'press_key', description: 'Pressiona tecla', inputSchema: { type: 'object', properties: { key: { type: 'string' }, selector: { type: 'string' } }, required: ['key'] } }, + { name: 'scroll_to', description: 'Rola página', inputSchema: { type: 'object', properties: { position: { oneOf: [{ type: 'number' }, { type: 'string', enum: ['top', 'bottom'] }, { type: 'object', properties: { selector: { type: 'string' } } }] } } } }, + { name: 'pdf', description: 'Gera PDF', inputSchema: { type: 'object', properties: { path: { type: 'string' }, format: { type: 'string' }, landscape: { type: 'boolean' } } } }, + { name: 'get_cookies', description: 'Obtém cookies', inputSchema: { type: 'object', properties: { urls: { type: 'array', items: { type: 'string' } } } } }, + { name: 'set_cookies', description: 'Define cookies', inputSchema: { type: 'object', properties: { cookies: { type: 'array' } }, required: ['cookies'] } }, + { name: 'clear_cookies', description: 'Limpa cookies', inputSchema: { type: 'object', properties: {} } }, + { name: 'get_storage', description: 'Obtém storage', inputSchema: { type: 'object', properties: {} } }, + { name: 'set_storage', description: 'Define storage', inputSchema: { type: 'object', properties: { localStorage: { type: 'object' }, sessionStorage: { type: 'object' } } } }, + { name: 'clear_storage', description: 'Limpa storage', inputSchema: { type: 'object', properties: {} } }, + { name: 'get_network_logs', description: 'Logs de rede', inputSchema: { type: 'object', properties: { type: { type: 'string' }, url: { type: 'string' } } } }, + { name: 'block_resources', description: 'Bloqueia recursos', inputSchema: { type: 'object', properties: { types: { type: 'array', items: { type: 'string' } } }, required: ['types'] } }, + { name: 'upload_file', description: 'Upload de arquivo', inputSchema: { type: 'object', properties: { selector: { type: 'string' }, filePaths: { oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }] } }, required: ['selector', 'filePaths'] } }, + { name: 'get_performance_metrics', description: 'Métricas de performance', inputSchema: { type: 'object', properties: {} } }, + { name: 'wait', description: 'Aguarda ms', inputSchema: { type: 'object', properties: { ms: { type: 'number' } }, required: ['ms'] } } +]; -// LISTAR TOOLS -server.setRequestHandler(ListToolsRequestSchema, async () => { - return { - tools: [ - { name: "open_url", description: "Abre uma URL", inputSchema: { type: "object", properties: { url: { type: "string" } }, required: ["url"] } }, - { name: "click", description: "Clica por seletor", inputSchema: { type: "object", properties: { selector: { type: "string" } }, required: ["selector"] } }, - { name: "type", description: "Digita texto", inputSchema: { type: "object", properties: { selector: { type: "string" }, text: { type: "string" } }, required: ["selector", "text"] } }, - { name: "screenshot", description: "Screenshot", inputSchema: { type: "object", properties: { fullPage: { type: "boolean" } } } }, - { name: "get_text", description: "Extrai texto", inputSchema: { type: "object", properties: { selector: { type: "string" } } } }, - { name: "get_links", description: "Lista links", inputSchema: { type: "object", properties: { selector: { type: "string" } } } }, - { name: "wait_for_selector", description: "Espera elemento", inputSchema: { type: "object", properties: { selector: { type: "string" }, timeout: { type: "number" } }, required: ["selector"] } }, - { name: "extract_elements", description: "Extrai elementos", inputSchema: { type: "object", properties: { selector: { type: "string" }, attributes: { type: "array", items: { type: "string" } } }, required: ["selector"] } }, - { name: "smart_click", description: "Clica por texto visível", inputSchema: { type: "object", properties: { text: { type: "string" } }, required: ["text"] } }, - { name: "smart_type", description: "Digita por label/placeholder", inputSchema: { type: "object", properties: { label: { type: "string" }, text: { type: "string" } }, required: ["label", "text"] } }, - { name: "get_buttons", description: "Lista botões disponíveis", inputSchema: { type: "object", properties: {} } }, - { name: "smart_wait_navigation", description: "Espera mudança de página", inputSchema: { type: "object", properties: { timeout: { type: "number" } } } }, - { name: "get_forms", description: "Lista formulários da página", inputSchema: { type: "object", properties: {} } }, - { name: "fill_form_auto", description: "Preenche formulário automaticamente", inputSchema: { type: "object", properties: { data: { type: "object" } }, required: ["data"] } }, - { name: "agent_flow", description: "Fluxo automático simples", inputSchema: { type: "object", properties: { goal: { type: "string" }, data: { type: "object" } }, required: ["goal"] } }, - { name: "agent_flow_v2", description: "Fluxo automático avançado com retry e fallback", inputSchema: { type: "object", properties: { goal: { type: "string" }, data: { type: "object" }, retries: { type: "number" } }, required: ["goal"] } }, - { name: "close_browser", description: "Fecha o navegador", inputSchema: { type: "object", properties: {} } } - ] - }; -}); +server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: toolSchemas })); -// EXECUTAR TOOL server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; - - const tools = { - open_url: openUrl, - click, - type, - screenshot, - get_text: getText, - get_links: getLinks, - wait_for_selector: waitForSelector, - extract_elements: extractElements, - smart_click: smartClick, - smart_type: smartType, - get_buttons: getButtons, - smart_wait_navigation: smartWaitNavigation, - get_forms: getForms, - fill_form_auto: fillFormAuto, - agent_flow: agentFlow, - agent_flow_v2: agentFlowV2, - close_browser: closeBrowser - }; - - if (!tools[name]) { - throw new Error("Tool não encontrada"); + const tool = tools[name.replace(/-/g, '').replace(/_([a-z])/g, (_, c) => c.toUpperCase())] || tools[name]; + if (!tool) throw new Error(`Tool "${name}" não encontrada`); + try { + const result = await tool(args || {}); + return { content: [{ type: 'text', text: JSON.stringify(result) }] }; + } catch (error) { + return { content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }], isError: true }; } - - const result = await tools[name](args || {}); - - return { - content: [ - { - type: "text", - text: JSON.stringify(result) - } - ] - }; }); -// START SERVER const transport = new StdioServerTransport(); -server.connect(transport); \ No newline at end of file +server.connect(transport).catch(console.error); diff --git a/tools/blockResources.js b/tools/blockResources.js new file mode 100644 index 0000000..3c523f1 --- /dev/null +++ b/tools/blockResources.js @@ -0,0 +1,8 @@ +const browser = require('../browser'); + +module.exports = async ({ types }) => { + if (!types || !Array.isArray(types)) throw new Error('Types array é obrigatório'); + await browser.start(); + const result = browser.blockResources(types); + return { success: true, action: 'block_resources', ...result }; +}; diff --git a/tools/clearCookies.js b/tools/clearCookies.js new file mode 100644 index 0000000..5766d32 --- /dev/null +++ b/tools/clearCookies.js @@ -0,0 +1,7 @@ +const browser = require('../browser'); + +module.exports = async () => { + await browser.start(); + const result = await browser.clearCookies(); + return { success: true, action: 'clear_cookies', ...result }; +}; diff --git a/tools/clearStorage.js b/tools/clearStorage.js new file mode 100644 index 0000000..7c141ad --- /dev/null +++ b/tools/clearStorage.js @@ -0,0 +1,7 @@ +const browser = require('../browser'); + +module.exports = async () => { + await browser.start(); + await browser.clearStorage(); + return { success: true, action: 'clear_storage', cleared: true }; +}; diff --git a/tools/click.js b/tools/click.js index ff25473..ff132b5 100644 --- a/tools/click.js +++ b/tools/click.js @@ -1,19 +1,8 @@ const browser = require('../browser'); -module.exports = async ({ selector }) => { - if (!selector) { - throw new Error('Selector é obrigatório'); - } - +module.exports = async ({ selector, button = 'left', double = false }) => { + if (!selector) throw new Error('Selector é obrigatório'); await browser.start(); - const p = await browser.ensurePage(); - - await p.waitForSelector(selector, { state: 'visible', timeout: 10000 }); - await p.click(selector); - - return { - success: true, - action: "click", - selector - }; + await browser.click(selector, { button, clickCount: double ? 2 : 1 }); + return { success: true, action: 'click', selector, button, double }; }; diff --git a/tools/closeTab.js b/tools/closeTab.js new file mode 100644 index 0000000..9473113 --- /dev/null +++ b/tools/closeTab.js @@ -0,0 +1,7 @@ +const browser = require('../browser'); + +module.exports = async ({ index = null }) => { + await browser.start(); + const result = await browser.closeTab(index); + return { success: true, action: 'close_tab', ...result }; +}; diff --git a/tools/emulateDevice.js b/tools/emulateDevice.js new file mode 100644 index 0000000..0d0f2e4 --- /dev/null +++ b/tools/emulateDevice.js @@ -0,0 +1,8 @@ +const browser = require('../browser'); + +module.exports = async ({ device }) => { + if (!device) throw new Error('Device é obrigatório'); + await browser.start(); + const result = await browser.emulateDevice(device); + return { success: true, action: 'emulate_device', ...result }; +}; diff --git a/tools/evaluateJS.js b/tools/evaluateJS.js new file mode 100644 index 0000000..dd6fab6 --- /dev/null +++ b/tools/evaluateJS.js @@ -0,0 +1,12 @@ +const browser = require('../browser'); + +module.exports = async ({ expression, selector = null }) => { + if (!expression) throw new Error('Expression é obrigatória'); + await browser.start(); + if (selector) { + const result = await browser.evaluateOnSelector(selector, expression); + return { success: true, action: 'evaluate_js', selector, result }; + } + const result = await browser.evaluateJS(expression); + return { success: true, action: 'evaluate_js', result }; +}; diff --git a/tools/extractElements.js b/tools/extractElements.js index f93a3dd..df78f08 100644 --- a/tools/extractElements.js +++ b/tools/extractElements.js @@ -1,36 +1,15 @@ const browser = require('../browser'); module.exports = async ({ selector, attributes = [] }) => { - if (!selector) { - throw new Error('Selector é obrigatório'); - } - + if (!selector) throw new Error('Selector é obrigatório'); await browser.start(); - const p = await browser.ensurePage(); - - const attrsStr = JSON.stringify(Array.isArray(attributes) ? attributes : []); - - const elements = await p.evaluate((sel) => { - const attrs = JSON.parse(sel.__attrs); - const els = Array.from(document.querySelectorAll(sel.__sel)); - - return els.map(el => { - const data = { - text: (el.innerText || el.textContent || '').trim() - }; - - for (let i = 0; i < attrs.length; i++) { - data[attrs[i]] = el.getAttribute(attrs[i]); - } - + const attrs = Array.isArray(attributes) ? attributes : []; + const elements = await browser.evaluateJS(({ sel, attrs: a }) => { + return Array.from(document.querySelectorAll(sel)).map(el => { + const data = { text: (el.innerText || el.textContent || '').trim() }; + a.forEach(attr => data[attr] = el.getAttribute(attr)); return data; }); - }, { __sel: selector, __attrs: attrsStr }); - - return { - success: true, - action: "extract_elements", - count: elements.length, - elements - }; + }, { sel: selector, attrs }); + return { success: true, action: 'extract_elements', count: elements.length, elements }; }; diff --git a/tools/fillFormAuto.js b/tools/fillFormAuto.js index df5ec95..3e187ff 100644 --- a/tools/fillFormAuto.js +++ b/tools/fillFormAuto.js @@ -1,62 +1,31 @@ const browser = require('../browser'); module.exports = async ({ data }) => { - if (!data) { - throw new Error('Data é obrigatória'); - } - + if (!data) throw new Error('Data é obrigatória'); await browser.start(); - const p = await browser.ensurePage(); - - const result = await p.evaluate((formData) => { + const result = await browser.evaluateJS((formData) => { const inputs = Array.from(document.querySelectorAll('input, textarea, select')); - - let filled = []; - + const filled = []; inputs.forEach(input => { const key = Object.keys(formData).find(k => (input.name && input.name.toLowerCase().includes(k.toLowerCase())) || - (input.placeholder && input.placeholder.toLowerCase().includes(k.toLowerCase())) || - (input.id && input.id.toLowerCase().includes(k.toLowerCase())) || + (input.placeholder || '').toLowerCase().includes(k.toLowerCase()) || + (input.id || '').toLowerCase().includes(k.toLowerCase()) || (input.getAttribute('aria-label') || '').toLowerCase().includes(k.toLowerCase()) ); - if (key) { const value = formData[key]; - - const nativeInputValueSetter = Object.getOwnPropertyDescriptor( - window.HTMLInputElement.prototype, 'value' - )?.set; - - const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor( - window.HTMLTextAreaElement.prototype, 'value' - )?.set; - - if (input.tagName === 'INPUT' && nativeInputValueSetter) { - nativeInputValueSetter.call(input, value); - } else if (input.tagName === 'TEXTAREA' && nativeTextAreaValueSetter) { - nativeTextAreaValueSetter.call(input, value); - } else { - input.value = value; - } - + const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set; + const nativeTextAreaSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set; + if (input.tagName === 'INPUT' && nativeSetter) nativeSetter.call(input, value); + else if (input.tagName === 'TEXTAREA' && nativeTextAreaSetter) nativeTextAreaSetter.call(input, value); + else input.value = value; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); - - filled.push({ - field: input.name || input.id || input.placeholder || 'unknown', - value: value - }); + filled.push({ field: input.name || input.id || input.placeholder || 'unknown', value }); } }); - return filled; }, data); - - return { - success: true, - action: "fill_form_auto", - filled: result, - count: result.length - }; + return { success: true, action: 'fill_form_auto', filled: result, count: result.length }; }; diff --git a/tools/getButtons.js b/tools/getButtons.js index 9ad9711..284adae 100644 --- a/tools/getButtons.js +++ b/tools/getButtons.js @@ -2,22 +2,10 @@ const browser = require('../browser'); module.exports = async () => { await browser.start(); - const p = await browser.ensurePage(); - - const buttons = await p.evaluate(() => { - const elements = Array.from(document.querySelectorAll('button, input[type="submit"], input[type="button"], [role="button"], a.btn, a.button')); - - return elements.map(el => ({ - text: (el.innerText || el.value || el.getAttribute('aria-label') || '').trim(), - type: el.tagName.toLowerCase(), - id: el.id || null - })).filter(b => b.text !== ''); + const buttons = await browser.evaluateJS(() => { + return Array.from(document.querySelectorAll('button, input[type="submit"], input[type="button"], [role="button"], a.btn, a.button')) + .map(el => ({ text: (el.innerText || el.value || el.getAttribute('aria-label') || '').trim(), type: el.tagName.toLowerCase(), id: el.id || null })) + .filter(b => b.text !== ''); }); - - return { - success: true, - action: "get_buttons", - count: buttons.length, - buttons - }; + return { success: true, action: 'get_buttons', count: buttons.length, buttons }; }; diff --git a/tools/getCookies.js b/tools/getCookies.js new file mode 100644 index 0000000..255b80a --- /dev/null +++ b/tools/getCookies.js @@ -0,0 +1,7 @@ +const browser = require('../browser'); + +module.exports = async ({ urls = [] }) => { + await browser.start(); + const cookies = await browser.getCookies(urls); + return { success: true, action: 'get_cookies', count: cookies.length, cookies }; +}; diff --git a/tools/getForms.js b/tools/getForms.js index f000d72..8aa8c40 100644 --- a/tools/getForms.js +++ b/tools/getForms.js @@ -2,31 +2,14 @@ const browser = require('../browser'); module.exports = async () => { await browser.start(); - const p = await browser.ensurePage(); - - const forms = await p.evaluate(() => { - const formElements = Array.from(document.querySelectorAll('form')); - - return formElements.map(form => { - const inputs = Array.from(form.querySelectorAll('input, textarea, select')); - - return { - action: form.action || null, - method: form.method || 'GET', - fields: inputs.map(input => ({ - name: input.name || null, - type: input.type || input.tagName.toLowerCase(), - placeholder: input.placeholder || '', - id: input.id || null - })) - }; - }); + const forms = await browser.evaluateJS(() => { + return Array.from(document.querySelectorAll('form')).map(form => ({ + action: form.action || null, + method: form.method || 'GET', + fields: Array.from(form.querySelectorAll('input, textarea, select')).map(input => ({ + name: input.name || null, type: input.type || input.tagName.toLowerCase(), placeholder: input.placeholder || '', id: input.id || null + })) + })); }); - - return { - success: true, - action: "get_forms", - count: forms.length, - forms - }; + return { success: true, action: 'get_forms', count: forms.length, forms }; }; diff --git a/tools/getHTML.js b/tools/getHTML.js new file mode 100644 index 0000000..da3558a --- /dev/null +++ b/tools/getHTML.js @@ -0,0 +1,11 @@ +const browser = require('../browser'); + +module.exports = async ({ selector = null }) => { + await browser.start(); + if (selector) { + const html = await browser.evaluateJS((sel) => document.querySelector(sel)?.outerHTML || null, selector); + return { success: true, action: 'get_html', selector, html }; + } + const html = await browser.content(); + return { success: true, action: 'get_html', html: html.substring(0, 50000) }; +}; diff --git a/tools/getLinks.js b/tools/getLinks.js index eb18554..11f06e8 100644 --- a/tools/getLinks.js +++ b/tools/getLinks.js @@ -2,21 +2,11 @@ const browser = require('../browser'); module.exports = async ({ selector = 'a' }) => { await browser.start(); - const p = await browser.ensurePage(); - - const links = await p.evaluate((sel) => { - const elements = Array.from(document.querySelectorAll(sel)); - - return elements.map(el => ({ + const links = await browser.evaluateJS((sel) => { + return Array.from(document.querySelectorAll(sel)).map(el => ({ text: (el.innerText || el.textContent || '').trim(), href: el.href || el.getAttribute('href') || '' - })).filter(link => link.href && link.href !== ''); + })).filter(link => link.href); }, selector); - - return { - success: true, - action: "get_links", - count: links.length, - links - }; + return { success: true, action: 'get_links', count: links.length, links }; }; diff --git a/tools/getNetworkLogs.js b/tools/getNetworkLogs.js new file mode 100644 index 0000000..2a90b56 --- /dev/null +++ b/tools/getNetworkLogs.js @@ -0,0 +1,11 @@ +const browser = require('../browser'); + +module.exports = async ({ type = null, url = null, method = null }) => { + await browser.start(); + const filter = {}; + if (type) filter.type = type; + if (url) filter.url = url; + if (method) filter.method = method; + const logs = Object.keys(filter).length > 0 ? await browser.getNetworkLogs(filter) : await browser.getNetworkLogs(); + return { success: true, action: 'get_network_logs', count: logs.length, logs }; +}; diff --git a/tools/getPerformanceMetrics.js b/tools/getPerformanceMetrics.js new file mode 100644 index 0000000..66f0351 --- /dev/null +++ b/tools/getPerformanceMetrics.js @@ -0,0 +1,7 @@ +const browser = require('../browser'); + +module.exports = async () => { + await browser.start(); + const metrics = await browser.getPerformanceMetrics(); + return { success: true, action: 'get_performance_metrics', metrics }; +}; diff --git a/tools/getStorage.js b/tools/getStorage.js new file mode 100644 index 0000000..d557acc --- /dev/null +++ b/tools/getStorage.js @@ -0,0 +1,7 @@ +const browser = require('../browser'); + +module.exports = async () => { + await browser.start(); + const storage = await browser.getStorage(); + return { success: true, action: 'get_storage', ...storage }; +}; diff --git a/tools/getText.js b/tools/getText.js index 0bed5b9..f6d276e 100644 --- a/tools/getText.js +++ b/tools/getText.js @@ -2,20 +2,10 @@ const browser = require('../browser'); module.exports = async ({ selector = 'body' }) => { await browser.start(); - const p = await browser.ensurePage(); - - const text = await p.evaluate((sel) => { + const text = await browser.evaluateJS((sel) => { const el = document.querySelector(sel); if (!el) return null; - - const t = el.innerText || el.textContent || ''; - return t.trim() || null; + return (el.innerText || el.textContent || '').trim(); }, selector); - - return { - success: true, - action: "get_text", - selector, - text - }; + return { success: true, action: 'get_text', selector, text }; }; diff --git a/tools/getTitle.js b/tools/getTitle.js new file mode 100644 index 0000000..111d422 --- /dev/null +++ b/tools/getTitle.js @@ -0,0 +1,7 @@ +const browser = require('../browser'); + +module.exports = async () => { + await browser.start(); + const title = await browser.title(); + return { success: true, action: 'get_title', title }; +}; diff --git a/tools/getURL.js b/tools/getURL.js new file mode 100644 index 0000000..2bb1974 --- /dev/null +++ b/tools/getURL.js @@ -0,0 +1,7 @@ +const browser = require('../browser'); + +module.exports = async () => { + await browser.start(); + const url = await browser.currentUrl(); + return { success: true, action: 'get_url', url }; +}; diff --git a/tools/goBack.js b/tools/goBack.js new file mode 100644 index 0000000..3d55828 --- /dev/null +++ b/tools/goBack.js @@ -0,0 +1,7 @@ +const browser = require('../browser'); + +module.exports = async () => { + await browser.start(); + const url = await browser.goBack(); + return { success: true, action: 'go_back', url }; +}; diff --git a/tools/goForward.js b/tools/goForward.js new file mode 100644 index 0000000..5b97e6b --- /dev/null +++ b/tools/goForward.js @@ -0,0 +1,7 @@ +const browser = require('../browser'); + +module.exports = async () => { + await browser.start(); + const url = await browser.goForward(); + return { success: true, action: 'go_forward', url }; +}; diff --git a/tools/hover.js b/tools/hover.js new file mode 100644 index 0000000..d6fb594 --- /dev/null +++ b/tools/hover.js @@ -0,0 +1,8 @@ +const browser = require('../browser'); + +module.exports = async ({ selector }) => { + if (!selector) throw new Error('Selector é obrigatório'); + await browser.start(); + await browser.hover(selector); + return { success: true, action: 'hover', selector }; +}; diff --git a/tools/listTabs.js b/tools/listTabs.js new file mode 100644 index 0000000..5d7144d --- /dev/null +++ b/tools/listTabs.js @@ -0,0 +1,16 @@ +const browser = require('../browser'); + +module.exports = async () => { + await browser.start(); + const tabs = browser.listTabs(); + const tabsWithTitle = await Promise.all(tabs.map(async (t, i) => { + if (!t.isClosed) { + try { + const p = browser.tabs[i]; + t.title = await p.title(); + } catch (e) { t.title = null; } + } + return t; + })); + return { success: true, action: 'list_tabs', tabs: tabsWithTitle }; +}; diff --git a/tools/newTab.js b/tools/newTab.js new file mode 100644 index 0000000..68aab41 --- /dev/null +++ b/tools/newTab.js @@ -0,0 +1,10 @@ +const browser = require('../browser'); + +module.exports = async ({ url = null }) => { + await browser.start(); + await browser.newTab(url); + const p = browser.page; + const finalUrl = url || p.url(); + const title = await p.title(); + return { success: true, action: 'new_tab', url: finalUrl, title }; +}; diff --git a/tools/openUrl.js b/tools/openUrl.js index 6adcafc..f7e2224 100644 --- a/tools/openUrl.js +++ b/tools/openUrl.js @@ -1,56 +1,24 @@ const browser = require('../browser'); -module.exports = async ({ url }) => { - if (!url) { - throw new Error('URL é obrigatória'); - } - +module.exports = async ({ url, waitUntil = 'domcontentloaded' }) => { + if (!url) throw new Error('URL é obrigatória'); await browser.start(); const p = await browser.ensurePage(); - const currentUrl = p.url(); - + if (currentUrl && currentUrl !== 'about:blank') { - if (currentUrl.includes(new URL(url).hostname)) { - await p.goto(url, { - waitUntil: 'domcontentloaded', - timeout: 30000 - }); - await p.waitForTimeout(1500); - - return { - success: true, - url, - title: await p.title(), - tab: 'reused' - }; - } - - const newPage = await browser.newTab(); - await newPage.goto(url, { - waitUntil: 'domcontentloaded', - timeout: 30000 - }); - await newPage.waitForTimeout(1500); - - return { - success: true, - url, - title: await newPage.title(), - tab: 'new' - }; + try { + const targetUrl = new URL(url); + if (currentUrl.includes(targetUrl.hostname)) { + const result = await browser.goto(url, { waitUntil }); + return { success: true, ...result, tab: 'reused' }; + } + } catch (e) {} + await browser.newTab(url); + const newPage = browser.page; + return { success: true, url, title: await newPage.title(), tab: 'new' }; } - - await p.goto(url, { - waitUntil: 'domcontentloaded', - timeout: 30000 - }); - await p.waitForTimeout(1500); - - return { - success: true, - url, - title: await p.title(), - tab: 'existing' - }; + + const result = await browser.goto(url, { waitUntil }); + return { success: true, ...result, tab: 'existing' }; }; diff --git a/tools/pdf.js b/tools/pdf.js new file mode 100644 index 0000000..9a8bf7d --- /dev/null +++ b/tools/pdf.js @@ -0,0 +1,7 @@ +const browser = require('../browser'); + +module.exports = async ({ path = null, format = 'A4', landscape = false }) => { + await browser.start(); + const file = await browser.pdf({ path, format, landscape }); + return { success: true, action: 'pdf', file }; +}; diff --git a/tools/pressKey.js b/tools/pressKey.js new file mode 100644 index 0000000..d7931b6 --- /dev/null +++ b/tools/pressKey.js @@ -0,0 +1,8 @@ +const browser = require('../browser'); + +module.exports = async ({ key, selector = null }) => { + if (!key) throw new Error('Key é obrigatória'); + await browser.start(); + await browser.pressKey(key, { selector }); + return { success: true, action: 'press_key', key, selector }; +}; diff --git a/tools/reload.js b/tools/reload.js new file mode 100644 index 0000000..7769ee1 --- /dev/null +++ b/tools/reload.js @@ -0,0 +1,7 @@ +const browser = require('../browser'); + +module.exports = async () => { + await browser.start(); + const url = await browser.reload(); + return { success: true, action: 'reload', url }; +}; diff --git a/tools/screenshot.js b/tools/screenshot.js index 0bb7d5f..1f421fc 100644 --- a/tools/screenshot.js +++ b/tools/screenshot.js @@ -1,16 +1,7 @@ const browser = require('../browser'); -module.exports = async ({ fullPage = true }) => { +module.exports = async ({ fullPage = true, element = null, path: customPath = null }) => { await browser.start(); - const p = await browser.ensurePage(); - - const path = `screenshot-${Date.now()}.png`; - - await p.screenshot({ path, fullPage }); - - return { - success: true, - action: "screenshot", - file: path - }; + const file = await browser.screenshot({ fullPage, path: customPath, element }); + return { success: true, action: 'screenshot', file }; }; diff --git a/tools/scrollTo.js b/tools/scrollTo.js new file mode 100644 index 0000000..e132b95 --- /dev/null +++ b/tools/scrollTo.js @@ -0,0 +1,8 @@ +const browser = require('../browser'); + +module.exports = async ({ position }) => { + if (!position) throw new Error('Position é obrigatória'); + await browser.start(); + await browser.scrollTo(position); + return { success: true, action: 'scroll_to', position }; +}; diff --git a/tools/select.js b/tools/select.js new file mode 100644 index 0000000..eec2cf9 --- /dev/null +++ b/tools/select.js @@ -0,0 +1,8 @@ +const browser = require('../browser'); + +module.exports = async ({ selector, values }) => { + if (!selector || !values) throw new Error('Selector e values são obrigatórios'); + await browser.start(); + const result = await browser.select(selector, values); + return { success: true, action: 'select', selector, selected: result }; +}; diff --git a/tools/setCookies.js b/tools/setCookies.js new file mode 100644 index 0000000..1465ef2 --- /dev/null +++ b/tools/setCookies.js @@ -0,0 +1,8 @@ +const browser = require('../browser'); + +module.exports = async ({ cookies }) => { + if (!cookies || !Array.isArray(cookies)) throw new Error('Cookies array é obrigatório'); + await browser.start(); + const result = await browser.setCookies(cookies); + return { success: true, action: 'set_cookies', ...result }; +}; diff --git a/tools/setStorage.js b/tools/setStorage.js new file mode 100644 index 0000000..c24758c --- /dev/null +++ b/tools/setStorage.js @@ -0,0 +1,10 @@ +const browser = require('../browser'); + +module.exports = async ({ localStorage = {}, sessionStorage = {} }) => { + await browser.start(); + const data = {}; + if (Object.keys(localStorage).length > 0) data.localStorage = localStorage; + if (Object.keys(sessionStorage).length > 0) data.sessionStorage = sessionStorage; + await browser.setStorage(data); + return { success: true, action: 'set_storage', set: true }; +}; diff --git a/tools/setViewport.js b/tools/setViewport.js new file mode 100644 index 0000000..4e042bc --- /dev/null +++ b/tools/setViewport.js @@ -0,0 +1,8 @@ +const browser = require('../browser'); + +module.exports = async ({ width, height }) => { + if (width === undefined || height === undefined) throw new Error('Width e height são obrigatórios'); + await browser.start(); + await browser.setViewport(width, height); + return { success: true, action: 'set_viewport', width, height }; +}; diff --git a/tools/smartClick.js b/tools/smartClick.js index 5c66da4..81fa6a0 100644 --- a/tools/smartClick.js +++ b/tools/smartClick.js @@ -1,54 +1,27 @@ const browser = require('../browser'); module.exports = async ({ text }) => { - if (!text) { - throw new Error('Texto é obrigatório'); - } - + if (!text) throw new Error('Texto é obrigatório'); await browser.start(); const p = await browser.ensurePage(); - + const clicked = await p.evaluate((targetText) => { - const elements = Array.from(document.querySelectorAll( - 'a, button, input[type="submit"], input[type="button"], [role="button"], [onclick]' - )); - + const elements = Array.from(document.querySelectorAll('a, button, input[type="submit"], input[type="button"], [role="button"], [onclick]')); const el = elements.find(e => { const txt = (e.innerText || e.textContent || e.value || e.getAttribute('aria-label') || '').trim().toLowerCase(); return txt.includes(targetText.toLowerCase()); }); - if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); el.click(); - - if (el.tagName === 'A' && el.href) { - return { clicked: true, href: el.href }; - } - - return { clicked: true, href: null }; + return { clicked: true, href: el.tagName === 'A' ? el.href : null }; } - return { clicked: false, href: null }; }, text); - if (!clicked.clicked) { - throw new Error(`Elemento não encontrado pelo texto: "${text}"`); - } - - if (clicked.href) { - try { - await p.waitForNavigation({ timeout: 5000, waitUntil: 'domcontentloaded' }); - } catch (e) { - } - } else { - await p.waitForTimeout(1000); - } - - return { - success: true, - action: "smart_click", - text, - navigatedTo: clicked.href || null - }; + if (!clicked.clicked) throw new Error(`Elemento não encontrado: "${text}"`); + if (clicked.href) { try { await p.waitForNavigation({ timeout: 5000, waitUntil: 'domcontentloaded' }); } catch (e) {} } + else await p.waitForTimeout(1000); + + return { success: true, action: 'smart_click', text, navigatedTo: clicked.href }; }; diff --git a/tools/smartType.js b/tools/smartType.js index 2438339..bd9fb02 100644 --- a/tools/smartType.js +++ b/tools/smartType.js @@ -1,84 +1,33 @@ const browser = require('../browser'); module.exports = async ({ label, text }) => { - if (!label || !text) { - throw new Error('Label e text são obrigatórios'); - } - + if (!label || !text) throw new Error('Label e text são obrigatórios'); await browser.start(); - const p = await browser.ensurePage(); - - const success = await p.evaluate(({ labelText, inputText }) => { - function setInputValue(input, value) { - const nativeSetter = Object.getOwnPropertyDescriptor( - window.HTMLInputElement.prototype, 'value' - )?.set; - - const nativeTextAreaSetter = Object.getOwnPropertyDescriptor( - window.HTMLTextAreaElement.prototype, 'value' - )?.set; - - if (input.tagName === 'INPUT' && nativeSetter) { - nativeSetter.call(input, value); - } else if (input.tagName === 'TEXTAREA' && nativeTextAreaSetter) { - nativeTextAreaSetter.call(input, value); - } else { - input.value = value; - } - + const success = await browser.evaluateJS(({ labelText, inputText }) => { + const setInput = (input, value) => { + const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set; + const nativeTextAreaSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set; + if (input.tagName === 'INPUT' && nativeSetter) nativeSetter.call(input, value); + else if (input.tagName === 'TEXTAREA' && nativeTextAreaSetter) nativeTextAreaSetter.call(input, value); + else input.value = value; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); - } - - const labels = Array.from(document.querySelectorAll('label')); - - const foundLabel = labels.find(l => - (l.innerText || l.textContent || '').toLowerCase().includes(labelText.toLowerCase()) - ); - + }; + const foundLabel = Array.from(document.querySelectorAll('label')).find(l => (l.innerText || l.textContent || '').toLowerCase().includes(labelText.toLowerCase())); if (foundLabel) { const forAttr = foundLabel.getAttribute('for'); - - if (forAttr) { - const input = document.getElementById(forAttr); - if (input) { - setInputValue(input, inputText); - return true; - } - } - - const input = foundLabel.querySelector('input, textarea'); - if (input) { - setInputValue(input, inputText); - return true; - } + if (forAttr) { const input = document.getElementById(forAttr); if (input) { setInput(input, inputText); return true; } } + const input = foundLabel.querySelector('input, textarea'); if (input) { setInput(input, inputText); return true; } } - - const inputs = Array.from(document.querySelectorAll('input, textarea')); - - const foundInput = inputs.find(i => + const foundInput = Array.from(document.querySelectorAll('input, textarea')).find(i => (i.placeholder || '').toLowerCase().includes(labelText.toLowerCase()) || (i.name || '').toLowerCase().includes(labelText.toLowerCase()) || (i.id || '').toLowerCase().includes(labelText.toLowerCase()) || (i.getAttribute('aria-label') || '').toLowerCase().includes(labelText.toLowerCase()) ); - - if (foundInput) { - setInputValue(foundInput, inputText); - return true; - } - + if (foundInput) { setInput(foundInput, inputText); return true; } return false; }, { labelText: label, inputText: text }); - - if (!success) { - throw new Error('Campo não encontrado'); - } - - return { - success: true, - action: "smart_type", - label, - text - }; + if (!success) throw new Error('Campo não encontrado'); + return { success: true, action: 'smart_type', label, text }; }; diff --git a/tools/smartWaitNavigation.js b/tools/smartWaitNavigation.js index 1323fc5..27d067e 100644 --- a/tools/smartWaitNavigation.js +++ b/tools/smartWaitNavigation.js @@ -2,25 +2,9 @@ const browser = require('../browser'); module.exports = async ({ timeout = 15000 }) => { await browser.start(); + const oldUrl = await browser.currentUrl(); const p = await browser.ensurePage(); - - const oldUrl = p.url(); - - try { - await Promise.race([ - p.waitForNavigation({ timeout, waitUntil: 'domcontentloaded' }), - p.waitForLoadState('domcontentloaded', { timeout }) - ]); - } catch (e) { - } - - const newUrl = p.url(); - - return { - success: true, - action: "smart_wait_navigation", - oldUrl, - newUrl, - changed: oldUrl !== newUrl - }; + try { await Promise.race([p.waitForNavigation({ timeout, waitUntil: 'domcontentloaded' }), p.waitForLoadState('domcontentloaded', { timeout })]); } catch (e) {} + const newUrl = await browser.currentUrl(); + return { success: true, action: 'smart_wait_navigation', oldUrl, newUrl, changed: oldUrl !== newUrl }; }; diff --git a/tools/switchTab.js b/tools/switchTab.js new file mode 100644 index 0000000..e1c07a2 --- /dev/null +++ b/tools/switchTab.js @@ -0,0 +1,10 @@ +const browser = require('../browser'); + +module.exports = async ({ index }) => { + if (index === undefined) throw new Error('Index é obrigatório'); + await browser.start(); + await browser.switchTab(index); + const url = await browser.currentUrl(); + const title = await browser.title(); + return { success: true, action: 'switch_tab', index, url, title }; +}; diff --git a/tools/type.js b/tools/type.js index cfb9ead..89205f3 100644 --- a/tools/type.js +++ b/tools/type.js @@ -1,20 +1,8 @@ const browser = require('../browser'); -module.exports = async ({ selector, text }) => { - if (!selector || !text) { - throw new Error('Selector e text são obrigatórios'); - } - +module.exports = async ({ selector, text, clear = false, slowly = false }) => { + if (!selector || !text) throw new Error('Selector e text são obrigatórios'); await browser.start(); - const p = await browser.ensurePage(); - - await p.waitForSelector(selector, { state: 'visible', timeout: 10000 }); - await p.fill(selector, text); - - return { - success: true, - action: "type", - selector, - text - }; + await browser.type(selector, text, { clear, slowly }); + return { success: true, action: 'type', selector, text, clear, slowly }; }; diff --git a/tools/uploadFile.js b/tools/uploadFile.js new file mode 100644 index 0000000..cae2381 --- /dev/null +++ b/tools/uploadFile.js @@ -0,0 +1,9 @@ +const browser = require('../browser'); + +module.exports = async ({ selector, filePaths }) => { + if (!selector) throw new Error('Selector é obrigatório'); + if (!filePaths) throw new Error('FilePaths é obrigatório'); + await browser.start(); + const result = await browser.uploadFile(selector, filePaths); + return { success: true, action: 'upload_file', ...result }; +}; diff --git a/tools/wait.js b/tools/wait.js new file mode 100644 index 0000000..acd4836 --- /dev/null +++ b/tools/wait.js @@ -0,0 +1,8 @@ +const browser = require('../browser'); + +module.exports = async ({ ms }) => { + if (!ms || typeof ms !== 'number') throw new Error('MS é obrigatório e deve ser número'); + await browser.start(); + await browser.waitForTimeout(ms); + return { success: true, action: 'wait', ms }; +}; diff --git a/tools/waitForSelector.js b/tools/waitForSelector.js index 1349c7f..b47d411 100644 --- a/tools/waitForSelector.js +++ b/tools/waitForSelector.js @@ -1,22 +1,9 @@ const browser = require('../browser'); -module.exports = async ({ selector, timeout = 15000 }) => { - if (!selector) { - throw new Error('Selector é obrigatório'); - } - +module.exports = async ({ selector, timeout = 15000, state = 'attached' }) => { + if (!selector) throw new Error('Selector é obrigatório'); await browser.start(); const p = await browser.ensurePage(); - - const el = await p.waitForSelector(selector, { - timeout, - state: 'attached' - }); - - return { - success: true, - action: "wait_for_selector", - selector, - found: !!el - }; + const el = await p.waitForSelector(selector, { timeout, state }); + return { success: true, action: 'wait_for_selector', selector, found: !!el }; }; diff --git a/tools/waitForURL.js b/tools/waitForURL.js new file mode 100644 index 0000000..7b809c1 --- /dev/null +++ b/tools/waitForURL.js @@ -0,0 +1,8 @@ +const browser = require('../browser'); + +module.exports = async ({ pattern, timeout = 30000 }) => { + if (!pattern) throw new Error('Pattern é obrigatório'); + await browser.start(); + const url = await browser.waitForURL(pattern, { timeout }); + return { success: true, action: 'wait_for_url', pattern, url }; +};