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.    - + -## 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 = \` + +