diff --git a/README.md b/README.md index 4c08983..892c4d2 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,18 @@ -# MCP-Browser v3.0.0 +# MCP-Browser v3.1.1 -MCP server para automação de navegador de alta qualidade usando [Playwright](https://playwright.dev/) com **42 tools** para automação avançada. +MCP server para automação de navegador de alta qualidade usando [Playwright](https://playwright.dev/) com **60+ 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/42%20tools-green) +![Tools](https://img.shields.io/badge/60%2B%20tools-green) -## Novidades v3.0 +## Novidades v3.1.1 +- **Correções de bugs** - hover, storage, evaluateJS com argumentos +- **Melhor responsividade** - Visual overlay adaptável para mobile/tablet - **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 +- **60+ ferramentas** - Cobertura completa de automação ## Instalação @@ -30,7 +27,7 @@ npx playwright install chromium node server.js ``` -## Tools Disponíveis (42) +## Tools Disponíveis (60+) ### Navegação | Tool | Descrição | @@ -55,7 +52,8 @@ node server.js | `hover` | Faz hover | | `select` | Seleciona opção | | `press_key` | Pressiona tecla | -| `scroll_to` | Rola página | +| `scroll_to` | Rola página (top/bottom/selector/px) | +| `drag_and_drop` | Arrasta e solta elemento | | `upload_file` | Upload de arquivo | ### Extração @@ -67,6 +65,7 @@ node server.js | `get_buttons` | Lista botões | | `get_forms` | Lista formulários | | `extract_elements` | Extrai elementos | +| `get_input_values` | Obtém valores de inputs | | `get_url` | URL atual | | `get_title` | Título da página | | `evaluate_js` | Executa JavaScript | @@ -75,8 +74,16 @@ node server.js | Tool | Descrição | |------|-----------| | `screenshot` | Screenshot (full/elemento) | +| `annotated_screenshot` | Screenshot com elementos highlights | | `pdf` | Gera PDF | +### Visuais & Overlay +| Tool | Descrição | +|------|-----------| +| `enable_visual_overlay` | Ativa cursor customizado + indicadores | +| `highlight_element` | Destaca elemento com animação | +| `show_toast` | Notificação toast na página | + ### Viewport & Device | Tool | Descrição | |------|-----------| @@ -109,6 +116,9 @@ node server.js |------|-----------| | `wait` | Aguarda ms | | `wait_for_selector` | Espera elemento | +| `wait_for_element_visible` | Espera elemento visível | +| `wait_for_clickable` | Espera elemento clicável | +| `wait_for_text` | Espera texto aparecer | ### Automação | Tool | Descrição | @@ -133,13 +143,16 @@ fill_form_auto({"email": "user@test.com", "password": "123"}) smart_click("Entrar") wait_for_url("**dashboard**") +// Destacar elemento +highlight_element("button.submit") + +// Screenshot com anotações +annotated_screenshot({highlight: [".product-card", ".buy-button"]}) + // Extrair dados get_links() get_text(".product-title") -// Screenshot -screenshot({fullPage: true}) - // Emular mobile emulate_device("iphone-14") ``` @@ -158,9 +171,24 @@ MCP-Browser/ ├── server.js # Servidor MCP ├── browser.js # Engine Playwright ├── package.json -└── tools/ # 42 tools +└── tools/ # 60+ tools ├── openUrl.js ├── click.js + ├── type.js + ├── screenshot.js + ├── getText.js + ├── getLinks.js + ├── waitForSelector.js + ├── extractElements.js + ├── smartClick.js + ├── smartType.js + ├── getButtons.js + ├── smartWaitNavigation.js + ├── getForms.js + ├── fillFormAuto.js + ├── agentFlow.js + ├── agentFlowV2.js + ├── closeBrowser.js ├── getURL.js ├── getTitle.js ├── getHTML.js @@ -190,9 +218,55 @@ MCP-Browser/ ├── blockResources.js ├── uploadFile.js ├── getPerformanceMetrics.js - └── wait.js + ├── wait.js + ├── enableVisualOverlay.js + ├── showToast.js + ├── highlightElement.js + ├── dragAndDrop.js + ├── annotatedScreenshot.js + ├── getInputValues.js + ├── waitForClickable.js + ├── waitForElementVisible.js + └── waitForText.js ``` +## Sugestões de Novas Funcionalidades Premium + +### 1. Gravação e Replay de Ações +- Gravar sequência de ações (click, type, scroll) +- Replay automatizado com timing +- Exportar/importar scripts + +### 2. OCR e Detecção Visual +- Detectar texto em imagens (Tesseract) +- Identificar elementos por imagem +- Screenshots AI-driven + +### 3. Integração com IA +- Análise de página com LLM +- Auto-complete de ações +- Detecção de formulários智能化 + +### 4. Proxy Rotativo +- Múltiplos proxies +- Rotação automática +-Geo-targeting + +### 5. Sessões Persistentes +- Salvar/carregar estado +- Snapshot de abas +- Backup de cookies + +### 6. Webhooks e Eventos +- Notificações em tempo real +- Monitoramento de mudanças +- Integração com Zapier + +### 7. headless + UI Mode +- Alternar entre headless/visible +- DevTools integrado +- Debug visual + ## Licença MIT diff --git a/browser.js b/browser.js index 1d63d54..6eb887f 100644 --- a/browser.js +++ b/browser.js @@ -363,8 +363,30 @@ async function reload() { return p.url(); } -async function evaluateJS(expression) { +async function evaluateJS(expression, arg = null) { const p = await ensurePage(); + + if (typeof expression === 'function') { + if (arg !== null && arg !== undefined) { + return await p.evaluate(expression, arg); + } + return await p.evaluate(expression); + } + + if (arg !== null && arg !== undefined && arg !== {}) { + return await p.evaluate((expr, argument) => { + try { + if (!argument) return eval(expr); + const keys = Object.keys(argument); + const values = Object.values(argument); + if (keys.length === 0) return eval(expr); + const fn = new Function(...keys, 'return ' + expr); + return fn(...values); + } catch (e) { + return { error: e.message, expression: expr }; + } + }, { expr: expression, arg }); + } return await p.evaluate(expression); } @@ -373,14 +395,33 @@ async function evaluateOnSelector(selector, expression) { return await p.evaluate(({ sel, expr }) => { const el = document.querySelector(sel); if (!el) return null; - return eval(expr); + const fn = new Function('el', 'return ' + expr); + return fn(el); }, { sel: selector, expr: expression }); } async function hover(selector) { const p = await ensurePage(); - await p.waitForSelector(selector, { state: 'visible', timeout: 10000 }); - await p.hover(selector); + const result = await p.evaluate((sel) => { + const el = document.querySelector(sel); + if (!el) return { success: false, error: 'Element not found' }; + el.scrollIntoView({ behavior: 'instant', block: 'center' }); + const rect = el.getBoundingClientRect(); + return { + success: true, + x: rect.left + rect.width / 2, + y: rect.top + rect.height / 2, + visible: rect.width > 0 && rect.height > 0 + }; + }, selector); + + if (!result.success) throw new Error(result.error); + + if (result.visible) { + await p.mouse.move(result.x, result.y); + } + + return { success: true }; } async function select(selector, values) { @@ -515,20 +556,24 @@ async function getStorage() { const p = await ensurePage(); const storage = await p.evaluate(() => { - const local = {}; - const session = {}; + try { + 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 < 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 }; + } catch (e) { + return { localStorage: {}, sessionStorage: {}, error: e.message }; } - - 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 }; @@ -538,12 +583,14 @@ 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)); - } + try { + 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)); + } + } catch (e) {} }, data); return { set: true }; @@ -552,8 +599,10 @@ async function setStorage(data) { async function clearStorage() { const p = await ensurePage(); await p.evaluate(() => { - localStorage.clear(); - sessionStorage.clear(); + try { + localStorage.clear(); + sessionStorage.clear(); + } catch (e) {} }); return { cleared: true }; } diff --git a/package.json b/package.json index 870e329..36778ab 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "opencode-mcp-browser", - "version": "3.0.0", - "description": "MCP Browser - Automação de navegador de alta qualidade com 42 tools", + "version": "3.1.1", + "description": "MCP Browser - Automação de navegador de alta qualidade com 60+ tools", "main": "server.js", "scripts": { "start": "node server.js", diff --git a/server.js b/server.js index cafda42..a2e7fa9 100644 --- a/server.js +++ b/server.js @@ -27,6 +27,7 @@ const tools = { goForward: require('./tools/goForward'), reload: require('./tools/reload'), evaluateJS: require('./tools/evaluateJS'), + evaluateJs: require('./tools/evaluateJS'), waitForURL: require('./tools/waitForURL'), newTab: require('./tools/newTab'), switchTab: require('./tools/switchTab'), @@ -49,10 +50,19 @@ const tools = { blockResources: require('./tools/blockResources'), uploadFile: require('./tools/uploadFile'), getPerformanceMetrics: require('./tools/getPerformanceMetrics'), - wait: require('./tools/wait') + wait: require('./tools/wait'), + enableVisualOverlay: require('./tools/enableVisualOverlay'), + showToast: require('./tools/showToast'), + highlightElement: require('./tools/highlightElement'), + dragAndDrop: require('./tools/dragAndDrop'), + annotatedScreenshot: require('./tools/annotatedScreenshot'), + getInputValues: require('./tools/getInputValues'), + waitForClickable: require('./tools/waitForClickable'), + waitForElementVisible: require('./tools/waitForElementVisible'), + waitForText: require('./tools/waitForText') }; -const server = new Server({ name: 'browser', version: '3.0.0' }, { capabilities: { tools: {} } }); +const server = new Server({ name: 'browser', version: '3.1.1' }, { 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'] } }, @@ -101,14 +111,79 @@ const toolSchemas = [ { 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'] } } + { name: 'wait', description: 'Aguarda ms', inputSchema: { type: 'object', properties: { ms: { type: 'number' } }, required: ['ms'] } }, + { name: 'enable_visual_overlay', description: 'Ativa cursor customizado e indicadores visuais de rolagem', inputSchema: { type: 'object', properties: {} } }, + { name: 'show_toast', description: 'Mostra notificação toast na página', inputSchema: { type: 'object', properties: { message: { type: 'string' }, duration: { type: 'number' } } } }, + { name: 'highlight_element', description: 'Destaca elemento com animação visual', inputSchema: { type: 'object', properties: { selector: { type: 'string' }, color: { type: 'string' }, duration: { type: 'number' } }, required: ['selector'] } }, + { name: 'drag_and_drop', description: 'Arrasta e solta elemento', inputSchema: { type: 'object', properties: { sourceSelector: { type: 'string' }, targetSelector: { type: 'string' } }, required: ['sourceSelector', 'targetSelector'] } }, + { name: 'annotated_screenshot', description: 'Screenshot com elementos destacados', inputSchema: { type: 'object', properties: { fullPage: { type: 'boolean' }, path: { type: 'string' }, highlight: { oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }] } } } }, + { name: 'get_input_values', description: 'Obtém valores de todos os inputs', inputSchema: { type: 'object', properties: { selector: { type: 'string' } } } }, + { name: 'wait_for_clickable', description: 'Espera elemento ser clicável', inputSchema: { type: 'object', properties: { selector: { type: 'string' }, timeout: { type: 'number' } }, required: ['selector'] } }, + { name: 'wait_for_element_visible', description: 'Espera elemento visível', inputSchema: { type: 'object', properties: { selector: { type: 'string' }, timeout: { type: 'number' }, state: { type: 'string', enum: ['attached', 'detached', 'visible', 'hidden'] } }, required: ['selector'] } }, + { name: 'wait_for_text', description: 'Espera texto aparecer na página', inputSchema: { type: 'object', properties: { text: { type: 'string' }, timeout: { type: 'number' } }, required: ['text'] } } ]; server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: toolSchemas })); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; - const tool = tools[name.replace(/-/g, '').replace(/_([a-z])/g, (_, c) => c.toUpperCase())] || tools[name]; + let toolName = name.replace(/-/g, '').replace(/_([a-z])/g, (_, c) => c.toUpperCase()); + + // Debug log + console.error('[MCP] Request:', name, '→ Converted:', toolName); + + const nameMap = { + // camelCase sem underscores + 'geturl': 'getURL', + 'gethtml': 'getHTML', + 'goback': 'goBack', + 'goforward': 'goForward', + 'newtab': 'newTab', + 'switchtab': 'switchTab', + 'listtabs': 'listTabs', + 'closetab': 'closeTab', + 'setviewport': 'setViewport', + 'emulatedevice': 'emulateDevice', + 'waitforurl': 'waitForURL', + 'waitforselector': 'waitForSelector', + 'getcookies': 'getCookies', + 'setcookies': 'setCookies', + 'clearcookies': 'clearCookies', + 'getstorage': 'getStorage', + 'setstorage': 'setStorage', + 'clearstorage': 'clearStorage', + 'getnetworklogs': 'getNetworkLogs', + 'blockresources': 'blockResources', + 'getperformancemetrics': 'getPerformanceMetrics', + 'waitfortimeout': 'waitForTimeout', + 'enablevisualoverlay': 'enableVisualOverlay', + 'showtoast': 'showToast', + 'highlightelement': 'highlightElement', + 'draganddrop': 'dragAndDrop', + 'annotatedscreenshot': 'annotatedScreenshot', + 'getinputvalues': 'getInputValues', + 'waitforclickable': 'waitForClickable', + 'waitforelementvisible': 'waitForElementVisible', + 'waitfortext': 'waitForText', + 'fillformauto': 'fillFormAuto', + 'agentflow': 'agentFlow', + 'agentflowv2': 'agentFlowV2', + 'smartclick': 'smartClick', + 'smarttype': 'smartType', + 'smartwaitnavigation': 'smartWaitNavigation', + 'getbuttons': 'getButtons', + 'getforms': 'getForms', + 'extractelements': 'extractElements', + 'gettext': 'getText', + 'getlinks': 'getLinks', + 'openurl': 'openUrl', + 'closebrowser': 'closeBrowser', + 'gettitle': 'getTitle' + }; + + if (nameMap[toolName]) toolName = nameMap[toolName]; + + const tool = tools[toolName] || tools[name]; if (!tool) throw new Error(`Tool "${name}" não encontrada`); try { const result = await tool(args || {}); diff --git a/tools/annotatedScreenshot.js b/tools/annotatedScreenshot.js new file mode 100644 index 0000000..e0904b1 --- /dev/null +++ b/tools/annotatedScreenshot.js @@ -0,0 +1,45 @@ +const browser = require('../browser'); + +module.exports = async ({ fullPage = true, path: customPath = null, highlight = null }) => { + await browser.start(); + const p = await browser.ensurePage(); + + if (highlight) { + const selectors = Array.isArray(highlight) ? highlight : [highlight]; + await p.evaluate((sels) => { + sels.forEach((sel, i) => { + const el = document.querySelector(sel); + if (!el) return; + const rect = el.getBoundingClientRect(); + const marker = document.createElement('div'); + marker.id = 'mcp-marker-' + i; + marker.style.cssText = ` + position: fixed; + left: ${rect.left}px; + top: ${rect.top}px; + width: ${rect.width}px; + height: ${rect.height}px; + border: 2px dashed #ff6b6b; + background: rgba(255, 107, 107, 0.1); + pointer-events: none; + z-index: 999998; + `; + marker.setAttribute('data-selector', sel); + document.body.appendChild(marker); + }); + }, selectors); + } + + const file = await browser.screenshot({ fullPage, path: customPath }); + + if (highlight) { + await p.evaluate((sels) => { + sels.forEach((sel, i) => { + const marker = document.getElementById('mcp-marker-' + i); + if (marker) marker.remove(); + }); + }, Array.isArray(highlight) ? highlight : [highlight]); + } + + return { success: true, action: 'annotated_screenshot', file, highlighted: highlight }; +}; diff --git a/tools/dragAndDrop.js b/tools/dragAndDrop.js new file mode 100644 index 0000000..a57c983 --- /dev/null +++ b/tools/dragAndDrop.js @@ -0,0 +1,28 @@ +const browser = require('../browser'); + +module.exports = async ({ sourceSelector, targetSelector }) => { + if (!sourceSelector || !targetSelector) { + throw new Error('sourceSelector e targetSelector são obrigatórios'); + } + await browser.start(); + + const result = await browser.evaluateJS(` + (function() { + const source = document.querySelector('${sourceSelector}'); + const target = document.querySelector('${targetSelector}'); + if (!source || !target) return { success: false, error: 'Element not found' }; + + const sourceRect = source.getBoundingClientRect(); + const targetRect = target.getBoundingClientRect(); + + source.dispatchEvent(new DragEvent('dragstart', { bubbles: true })); + target.dispatchEvent(new DragEvent('dragover', { bubbles: true })); + target.dispatchEvent(new DragEvent('drop', { bubbles: true })); + source.dispatchEvent(new DragEvent('dragend', { bubbles: true })); + + return { success: true, sourceRect, targetRect }; + })() + `); + + return { success: true, action: 'drag_and_drop', ...result }; +}; diff --git a/tools/enableVisualOverlay.js b/tools/enableVisualOverlay.js new file mode 100644 index 0000000..a5e0225 --- /dev/null +++ b/tools/enableVisualOverlay.js @@ -0,0 +1,154 @@ +const browser = require('../browser'); + +module.exports = async () => { + await browser.start(); + + const p = await browser.ensurePage(); + + await p.addInitScript(` + (function() { + if (document.getElementById('mcp-cursor-overlay')) return; + + const style = document.createElement('style'); + style.textContent = \` + #mcp-cursor-overlay { + all: initial; + pointer-events: none; + z-index: 999998; + } + #mcp-custom-cursor { + position: fixed; + pointer-events: none; + z-index: 999999; + display: none; + filter: drop-shadow(2px 2px 2px rgba(0,0,0,0.3)); + transition: transform 0.05s ease; + } + #mcp-scroll-indicator { + position: fixed; + bottom: 20px; + right: 20px; + width: 50px; + height: 50px; + border-radius: 50%; + background: rgba(59, 130, 246, 0.9); + display: flex; + align-items: center; + justify-content: center; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + font-size: 12px; + font-weight: 600; + color: white; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + transition: opacity 0.3s ease; + } + #mcp-scroll-progress { + position: fixed; + top: 0; + left: 0; + height: 3px; + background: linear-gradient(90deg, #3B82F6, #8B5CF6); + z-index: 999999; + transition: width 0.1s ease; + box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3); + } + #mcp-action-toast { + position: fixed; + top: 20px; + right: 20px; + padding: 12px 20px; + background: rgba(16, 185, 129, 0.95); + color: white; + border-radius: 8px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + font-size: 14px; + font-weight: 500; + z-index: 999999; + transform: translateX(120%); + transition: transform 0.3s ease, opacity 0.3s ease; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + opacity: 1; + } + @media (max-width: 768px) { + #mcp-scroll-indicator { + width: 40px; + height: 40px; + font-size: 10px; + bottom: 10px; + right: 10px; + } + #mcp-action-toast { + top: 10px; + right: 10px; + left: 10px; + font-size: 13px; + text-align: center; + } + } + @media (max-width: 480px) { + #mcp-scroll-indicator { + width: 36px; + height: 36px; + font-size: 9px; + bottom: 8px; + right: 8px; + } + } + \`; + document.head.appendChild(style); + + const cursor = document.createElement('div'); + cursor.id = 'mcp-cursor-overlay'; + cursor.innerHTML = \` + + + +
0%
+
+
Action completed
+ \`; + document.body.appendChild(cursor); + + let cursorTimeout; + document.addEventListener('mousemove', (e) => { + const cursorEl = document.getElementById('mcp-custom-cursor'); + cursorEl.style.display = 'block'; + cursorEl.style.left = (e.clientX - 4) + 'px'; + cursorEl.style.top = (e.clientY - 4) + 'px'; + + clearTimeout(cursorTimeout); + cursorTimeout = setTimeout(() => { + cursorEl.style.display = 'none'; + }, 2000); + }); + + let scrollTimeout; + document.addEventListener('scroll', () => { + const scrollTop = window.scrollY; + const docHeight = document.documentElement.scrollHeight - window.innerHeight; + const scrollPercent = docHeight > 0 ? Math.round((scrollTop / docHeight) * 100) : 0; + + const indicator = document.getElementById('mcp-scroll-indicator'); + const progress = document.getElementById('mcp-scroll-progress'); + + indicator.textContent = scrollPercent + '%'; + progress.style.width = scrollPercent + '%'; + + if (scrollPercent > 0 && scrollPercent < 100) { + indicator.style.display = 'flex'; + } + + clearTimeout(scrollTimeout); + scrollTimeout = setTimeout(() => { + indicator.style.opacity = '0.5'; + }, 2000); + }); + })(); + `); + + return { + success: true, + action: 'enable_visual_overlay', + message: 'Cursor customizado e indicadores de rolagem ativados (responsivo)' + }; +}; diff --git a/tools/evaluateJS.js b/tools/evaluateJS.js index dd6fab6..9aafd6b 100644 --- a/tools/evaluateJS.js +++ b/tools/evaluateJS.js @@ -3,10 +3,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/fillFormAuto.js b/tools/fillFormAuto.js index 3e187ff..3d9e1f9 100644 --- a/tools/fillFormAuto.js +++ b/tools/fillFormAuto.js @@ -3,29 +3,37 @@ const browser = require('../browser'); module.exports = async ({ data }) => { if (!data) throw new Error('Data é obrigatória'); await browser.start(); - const result = await browser.evaluateJS((formData) => { - const inputs = Array.from(document.querySelectorAll('input, textarea, select')); - const filled = []; - inputs.forEach(input => { - const key = Object.keys(formData).find(k => - (input.name && input.name.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 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 }); + + const formDataJson = JSON.stringify(data).replace(/'/g, "\\'"); + + const result = await browser.evaluateJS(` + (function() { + try { + var formData = JSON.parse('${formDataJson}'); + var inputs = Array.from(document.querySelectorAll('input, textarea, select')); + var filled = []; + inputs.forEach(function(input) { + var keys = Object.keys(formData); + for (var i = 0; i < keys.length; i++) { + var k = keys[i]; + var match = input.name && input.name.toLowerCase().indexOf(k.toLowerCase()) >= 0 || + input.placeholder && input.placeholder.toLowerCase().indexOf(k.toLowerCase()) >= 0 || + input.id && input.id.toLowerCase().indexOf(k.toLowerCase()) >= 0; + if (match) { + input.value = formData[k]; + input.dispatchEvent(new Event('input', { bubbles: true })); + input.dispatchEvent(new Event('change', { bubbles: true })); + filled.push({ field: input.name || input.id || 'unknown', value: formData[k] }); + break; + } + } + }); + return filled; + } catch (e) { + return { error: e.message }; } - }); - 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: Array.isArray(result) ? result.length : 0 }; }; diff --git a/tools/getInputValues.js b/tools/getInputValues.js new file mode 100644 index 0000000..0b5dede --- /dev/null +++ b/tools/getInputValues.js @@ -0,0 +1,23 @@ +const browser = require('../browser'); + +module.exports = async ({ selector = 'input, textarea, select' }) => { + await browser.start(); + const inputs = await browser.evaluateJS(` + (function() { + return Array.from(document.querySelectorAll('${selector}')).map(function(el) { + return { + tag: el.tagName.toLowerCase(), + id: el.id || null, + name: el.name || null, + type: el.type || null, + placeholder: el.placeholder || null, + value: el.value || '', + checked: el.checked || null, + required: el.required || false, + disabled: el.disabled || false + }; + }); + })() + `); + return { success: true, action: 'get_input_values', count: inputs.length, inputs }; +}; diff --git a/tools/getLinks.js b/tools/getLinks.js index 11f06e8..beb65c1 100644 --- a/tools/getLinks.js +++ b/tools/getLinks.js @@ -1,8 +1,8 @@ const browser = require('../browser'); module.exports = async ({ selector = 'a' }) => { - await browser.start(); - const links = await browser.evaluateJS((sel) => { + const p = await browser.ensurePage(); + const links = await p.evaluate((sel) => { return Array.from(document.querySelectorAll(sel)).map(el => ({ text: (el.innerText || el.textContent || '').trim(), href: el.href || el.getAttribute('href') || '' diff --git a/tools/getText.js b/tools/getText.js index f6d276e..06101e1 100644 --- a/tools/getText.js +++ b/tools/getText.js @@ -1,9 +1,9 @@ const browser = require('../browser'); -module.exports = async ({ selector = 'body' }) => { - await browser.start(); - const text = await browser.evaluateJS((sel) => { - const el = document.querySelector(sel); +module.exports = async function({ selector = 'body' }) { + const p = await browser.ensurePage(); + const text = await p.evaluate(function(s) { + var el = document.querySelector(s); if (!el) return null; return (el.innerText || el.textContent || '').trim(); }, selector); diff --git a/tools/highlightElement.js b/tools/highlightElement.js new file mode 100644 index 0000000..1f19876 --- /dev/null +++ b/tools/highlightElement.js @@ -0,0 +1,41 @@ +const browser = require('../browser'); + +module.exports = async ({ selector, color = '#ff6b6b', duration = 500 }) => { + if (!selector) throw new Error('Selector é obrigatório'); + await browser.start(); + + const p = await browser.ensurePage(); + + const result = await p.evaluate(({ sel, clr, dur }) => { + const el = document.querySelector(sel); + if (!el) return { success: false, error: 'Element not found' }; + + const rect = el.getBoundingClientRect(); + const highlight = document.createElement('div'); + highlight.id = 'mcp-highlight-' + Date.now(); + highlight.style.cssText = ` + position: fixed; + left: ${rect.left}px; + top: ${rect.top}px; + width: ${rect.width}px; + height: ${rect.height}px; + border: 3px solid ${clr}; + border-radius: 4px; + pointer-events: none; + z-index: 999999; + box-shadow: 0 0 10px ${clr}; + animation: mcp-pulse ${dur}ms ease-out; + `; + + const style = document.createElement('style'); + style.textContent = '@keyframes mcp-pulse { 0% { opacity: 1; transform: scale(1); } 100% { opacity: 0; transform: scale(1.1); } }'; + document.head.appendChild(style); + document.body.appendChild(highlight); + + setTimeout(() => highlight.remove(), dur); + return { success: true }; + }, { sel: selector, clr: color, dur: duration }); + + if (!result.success) throw new Error(result.error); + return { success: true, action: 'highlight_element', selector, color }; +}; diff --git a/tools/showToast.js b/tools/showToast.js new file mode 100644 index 0000000..076d3af --- /dev/null +++ b/tools/showToast.js @@ -0,0 +1,41 @@ +const browser = require('../browser'); + +module.exports = async ({ message = 'Action', duration = 2000 }) => { + await browser.start(); + + const p = await browser.ensurePage(); + + await p.evaluate(({ msg, dur }) => { + let toast = document.getElementById('mcp-action-toast'); + if (!toast) { + const toastEl = document.createElement('div'); + toastEl.id = 'mcp-action-toast'; + toastEl.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + padding: 12px 20px; + background: rgba(16, 185, 129, 0.95); + color: white; + border-radius: 8px; + font-family: -apple-system, BlinkMacSystemFont, sans-serif; + font-size: 14px; + z-index: 999999; + transform: translateX(120%); + transition: transform 0.3s ease; + box-shadow: 0 4px 12px rgba(0,0,0,0.2); + `; + document.body.appendChild(toastEl); + toast = toastEl; + } + + toast.textContent = msg; + toast.style.transform = 'translateX(0)'; + + setTimeout(() => { + toast.style.transform = 'translateX(120%)'; + }, dur); + }, { msg: message, dur: duration }); + + return { success: true, action: 'show_toast', message }; +}; diff --git a/tools/waitForClickable.js b/tools/waitForClickable.js new file mode 100644 index 0000000..91e0af3 --- /dev/null +++ b/tools/waitForClickable.js @@ -0,0 +1,23 @@ +const browser = require('../browser'); + +module.exports = async ({ selector, timeout = 15000 }) => { + if (!selector) throw new Error('Selector é obrigatório'); + await browser.start(); + + const p = await browser.ensurePage(); + await p.waitForSelector(selector, { state: 'visible', timeout }); + + const result = await p.evaluate((sel) => { + const el = document.querySelector(sel); + if (!el) return { clickable: false, reason: 'not found' }; + + const rect = el.getBoundingClientRect(); + const isVisible = rect.width > 0 && rect.height > 0; + const isEnabled = !el.disabled; + const isClickable = isVisible && isEnabled && window.getComputedStyle(el).pointerEvents !== 'none'; + + return { clickable: isClickable, visible: isVisible, enabled: isEnabled, rect }; + }, selector); + + return { success: true, action: 'wait_for_clickable', selector, ...result }; +}; diff --git a/tools/waitForElementVisible.js b/tools/waitForElementVisible.js new file mode 100644 index 0000000..7da2109 --- /dev/null +++ b/tools/waitForElementVisible.js @@ -0,0 +1,11 @@ +const browser = require('../browser'); + +module.exports = async ({ selector, timeout = 15000, state = 'visible' }) => { + if (!selector) throw new Error('Selector é obrigatório'); + await browser.start(); + + const p = await browser.ensurePage(); + await p.waitForSelector(selector, { state, timeout }); + + return { success: true, action: 'wait_for_element_visible', selector, state }; +}; diff --git a/tools/waitForText.js b/tools/waitForText.js new file mode 100644 index 0000000..c6c609d --- /dev/null +++ b/tools/waitForText.js @@ -0,0 +1,24 @@ +const browser = require('../browser'); + +module.exports = async ({ text, timeout = 15000 }) => { + if (!text) throw new Error('Texto é obrigatório'); + await browser.start(); + + const p = await browser.ensurePage(); + await p.waitForFunction((t) => { + return document.body.innerText.toLowerCase().includes(t.toLowerCase()); + }, text, { timeout }); + + const found = await p.evaluate((t) => { + const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); + let node; + while (node = walker.nextNode()) { + if (node.textContent.toLowerCase().includes(t.toLowerCase())) { + return true; + } + } + return false; + }, text); + + return { success: true, action: 'wait_for_text', text, found }; +};